Compare commits
59 Commits
v1.0.0-bet
...
fix-web
| Author | SHA1 | Date | |
|---|---|---|---|
| 0faa8694f8 | |||
| 5ddae637f6 | |||
| 0ae38d38d4 | |||
| 4253a6df3e | |||
| 3a95456917 | |||
| a8bdd49072 | |||
| fd32b8c0ce | |||
| 8d4d8184e6 | |||
| e85974497b | |||
| da7167013f | |||
| 2ff83079b8 | |||
| b349656c56 | |||
| 8bbb472103 | |||
| dfda7ff157 | |||
| f46f15e10c | |||
| 2f15615d7b | |||
| 9addbd49f4 | |||
| b518c729f6 | |||
| 4b57a1d2a6 | |||
| 5ea6cdb34f | |||
| 1f04a824d7 | |||
| c78e0a5163 | |||
| 5d4c8634ef | |||
| 4efeec08c4 | |||
| 2e451aae12 | |||
| cdfcfdd782 | |||
| 9817dd29a6 | |||
| f066cb8223 | |||
| 6d60726f27 | |||
| 80186a79e5 | |||
|
|
cffcb4711d | ||
|
|
44484d9992 | ||
|
|
15ac8d6775 | ||
|
|
6f0cefed33 | ||
|
|
c239e351b8 | ||
|
|
9ff18aae35 | ||
|
|
6fef2ae1c6 | ||
|
|
c2a723faa8 | ||
|
|
6b872527ad | ||
|
|
d9b16aea9a | ||
|
|
8345def015 | ||
|
|
b0ba2549d7 | ||
|
|
c8034642c5 | ||
|
|
8205c69e19 | ||
|
|
d36364d60d | ||
|
|
99363475e0 | ||
|
|
a52e1877fe | ||
|
|
b41de00c95 | ||
|
|
94cbb4aa0e | ||
|
|
40e7ca368b | ||
|
|
b91697defc | ||
|
|
1d57a2deab | ||
|
|
aa26c9ccba | ||
|
|
ff61c28a76 | ||
|
|
069eae8145 | ||
|
|
ccbcc644db | ||
|
|
15c5ce7677 | ||
|
|
40d726c986 | ||
|
|
fe4eb7df74 |
10
.github/workflows/release.yml
vendored
10
.github/workflows/release.yml
vendored
@@ -45,7 +45,7 @@ jobs:
|
|||||||
# Windows and macOS builds using cargo-packager
|
# Windows and macOS builds using cargo-packager
|
||||||
- name: Build with cargo-packager (Windows/macOS)
|
- name: Build with cargo-packager (Windows/macOS)
|
||||||
if: runner.os != 'Linux'
|
if: runner.os != 'Linux'
|
||||||
working-directory: crates/coop
|
working-directory: desktop
|
||||||
run: |
|
run: |
|
||||||
cargo install cargo-packager --locked
|
cargo install cargo-packager --locked
|
||||||
cargo packager --release
|
cargo packager --release
|
||||||
@@ -154,13 +154,13 @@ jobs:
|
|||||||
|
|
||||||
- name: Create draft release
|
- name: Create draft release
|
||||||
id: create_release
|
id: create_release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: akkuman/gitea-release-action@v1
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ steps.version.outputs.tag }}
|
server_url: "https://git.reya.su/"
|
||||||
name: ${{ steps.version.outputs.tag }}
|
repository: "reya/coop"
|
||||||
|
token: ${{ secrets.GITEA_TOKEN }}
|
||||||
draft: true
|
draft: true
|
||||||
prerelease: false
|
prerelease: false
|
||||||
generate_release_notes: true
|
|
||||||
files: |
|
files: |
|
||||||
artifacts/**/*
|
artifacts/**/*
|
||||||
|
|
||||||
|
|||||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -19,5 +19,8 @@ dist/
|
|||||||
|
|
||||||
# Useless stuffs
|
# Useless stuffs
|
||||||
.DS_Store
|
.DS_Store
|
||||||
# Added by goreleaser init:
|
|
||||||
.intentionally-empty-file.o
|
.cargo/
|
||||||
|
vendor/
|
||||||
|
wasm/
|
||||||
|
node_modules/
|
||||||
|
|||||||
3911
Cargo.lock
generated
3911
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
26
Cargo.toml
26
Cargo.toml
@@ -1,17 +1,17 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = ["crates/*"]
|
members = ["crates/*", "desktop", "web"]
|
||||||
default-members = ["crates/coop"]
|
default-members = ["desktop"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "1.0.0-beta"
|
version = "1.0.0-beta5"
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
# GPUI
|
# GPUI
|
||||||
gpui = { git = "https://github.com/zed-industries/zed" }
|
gpui = { git = "https://github.com/zed-industries/zed" }
|
||||||
gpui_platform = { git = "https://github.com/zed-industries/zed", features = ["font-kit", "x11", "wayland", "runtime_shaders"] }
|
gpui_platform = { git = "https://github.com/zed-industries/zed", features = ["font-kit", "x11", "wayland"] }
|
||||||
gpui_linux = { git = "https://github.com/zed-industries/zed" }
|
gpui_linux = { git = "https://github.com/zed-industries/zed" }
|
||||||
gpui_windows = { git = "https://github.com/zed-industries/zed" }
|
gpui_windows = { git = "https://github.com/zed-industries/zed" }
|
||||||
gpui_macos = { git = "https://github.com/zed-industries/zed" }
|
gpui_macos = { git = "https://github.com/zed-industries/zed" }
|
||||||
@@ -20,10 +20,12 @@ reqwest_client = { git = "https://github.com/zed-industries/zed" }
|
|||||||
|
|
||||||
# Nostr
|
# Nostr
|
||||||
nostr-lmdb = { git = "https://github.com/rust-nostr/nostr" }
|
nostr-lmdb = { git = "https://github.com/rust-nostr/nostr" }
|
||||||
nostr-connect = { git = "https://github.com/rust-nostr/nostr" }
|
nostr-memory = { git = "https://github.com/rust-nostr/nostr" }
|
||||||
nostr-blossom = { git = "https://github.com/rust-nostr/nostr" }
|
nostr-blossom = { git = "https://github.com/rust-nostr/nostr" }
|
||||||
|
nostr-gossip-memory = { git = "https://github.com/rust-nostr/nostr" }
|
||||||
|
nostr-connect = { git = "https://github.com/rust-nostr/nostr" }
|
||||||
nostr-sdk = { git = "https://github.com/rust-nostr/nostr" }
|
nostr-sdk = { git = "https://github.com/rust-nostr/nostr" }
|
||||||
nostr = { git = "https://github.com/rust-nostr/nostr", features = [ "nip96", "nip59", "nip49", "nip44" ] }
|
nostr = { git = "https://github.com/rust-nostr/nostr", features = [ "nip59", "nip49", "nip44" ] }
|
||||||
|
|
||||||
# Others
|
# Others
|
||||||
anyhow = "1.0.44"
|
anyhow = "1.0.44"
|
||||||
@@ -33,14 +35,20 @@ itertools = "0.13.0"
|
|||||||
log = "0.4"
|
log = "0.4"
|
||||||
oneshot = "0.1.10"
|
oneshot = "0.1.10"
|
||||||
flume = { version = "0.11.1", default-features = false, features = ["async", "select"] }
|
flume = { version = "0.11.1", default-features = false, features = ["async", "select"] }
|
||||||
rust-embed = "8.5.0"
|
rust-embed = { version = "8.5", features = ["include-exclude"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
schemars = "1"
|
schemars = "1"
|
||||||
smallvec = "1.14.0"
|
smallvec = "1.14.0"
|
||||||
smol = "2"
|
smol = "2"
|
||||||
tracing = "0.1.40"
|
|
||||||
webbrowser = "1.0.4"
|
webbrowser = "1.0.4"
|
||||||
|
tracing-subscriber = { version = "0.3.18", features = ["fmt", "env-filter"] }
|
||||||
|
errno = { version = "0.3.14", default-features = false }
|
||||||
|
instant = "0.1"
|
||||||
|
|
||||||
|
[patch.crates-io]
|
||||||
|
# Use stacker's psm version which may have better WASM support
|
||||||
|
psm = { git = "https://github.com/rust-lang/stacker", branch = "master" }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
strip = true
|
strip = true
|
||||||
|
|||||||
16
README.md
16
README.md
@@ -1,12 +1,12 @@
|
|||||||

|

|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a href="https://github.com/lumehq/coop/actions/workflows/rust.yml">
|
<a href="https://github.com/reyakov/coop/actions/workflows/rust.yml">
|
||||||
<img alt="Actions" src="https://github.com/lumehq/coop/actions/workflows/rust.yml/badge.svg">
|
<img alt="Actions" src="https://github.com/reyakov/coop/actions/workflows/rust.yml/badge.svg">
|
||||||
</a>
|
</a>
|
||||||
<img alt="GitHub repo size" src="https://img.shields.io/github/repo-size/lumehq/coop">
|
<img alt="GitHub repo size" src="https://img.shields.io/github/repo-size/reyakov/coop">
|
||||||
<img alt="GitHub issues" src="https://img.shields.io/github/issues-raw/lumehq/coop">
|
<img alt="GitHub issues" src="https://img.shields.io/github/issues-raw/reyakov/coop">
|
||||||
<img alt="GitHub pull requests" src="https://img.shields.io/github/issues-pr/lumehq/coop">
|
<img alt="GitHub pull requests" src="https://img.shields.io/github/issues-pr/reyakov/coop">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
Coop is a simple, fast, and reliable nostr client for secure messaging across all platforms.
|
Coop is a simple, fast, and reliable nostr client for secure messaging across all platforms.
|
||||||
@@ -36,7 +36,7 @@ To install Coop, follow these steps:
|
|||||||
|
|
||||||
1. **Download the Latest Release**:
|
1. **Download the Latest Release**:
|
||||||
|
|
||||||
- Visit the [Coop Releases page on GitHub](https://github.com/lumehq/coop/releases).
|
- Visit the [Coop Releases page on GitHub](https://github.com/reyakov/coop/releases).
|
||||||
- Download the package that matches your operating system (Windows, macOS, or Linux).
|
- Download the package that matches your operating system (Windows, macOS, or Linux).
|
||||||
|
|
||||||
2. **Install**:
|
2. **Install**:
|
||||||
@@ -65,7 +65,7 @@ Coop is built using Rust and GPUI. All Nostr related stuffs handled by [Rust Nos
|
|||||||
1. Clone the repository:
|
1. Clone the repository:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/lumehq/coop.git
|
git clone https://github.com/reyakov/coop.git
|
||||||
cd coop
|
cd coop
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -118,7 +118,7 @@ For more information, see the [Contributing](#contributing) section.
|
|||||||
- [Rust Nostr](https://github.com/rust-nostr/nostr/)
|
- [Rust Nostr](https://github.com/rust-nostr/nostr/)
|
||||||
- [GPUI](https://www.gpui.rs/)
|
- [GPUI](https://www.gpui.rs/)
|
||||||
- [GPUI Components](https://github.com/longbridge/gpui-component/)
|
- [GPUI Components](https://github.com/longbridge/gpui-component/)
|
||||||
- [Coop Issue Tracker](https://github.com/lumehq/coop/issues/)
|
- [Coop Issue Tracker](https://github.com/reyakov/coop/issues/)
|
||||||
|
|
||||||
### License
|
### License
|
||||||
|
|
||||||
|
|||||||
3
assets/icons/input.svg
Normal file
3
assets/icons/input.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M11.75 6.75H19.25C20.3546 6.75 21.25 7.64543 21.25 8.75V15.25C21.25 16.3546 20.3546 17.25 19.25 17.25H11.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M5.75 6.75H4.75C3.64543 6.75 2.75 7.64543 2.75 8.75V15.25C2.75 16.3546 3.64543 17.25 4.75 17.25H5.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M8.75 3.75V20.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 604 B |
144
assets/themes/aurora.json
Normal file
144
assets/themes/aurora.json
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
{
|
||||||
|
"id": "aurora",
|
||||||
|
"name": "Aurora",
|
||||||
|
"author": "Coop",
|
||||||
|
"url": "https://github.com/lumehq/coop",
|
||||||
|
"light": {
|
||||||
|
"background": "#fdfcfeff",
|
||||||
|
"surface_background": "#f8f8ffff",
|
||||||
|
"elevated_surface_background": "#f0f1feff",
|
||||||
|
"panel_background": "#fdfcfeff",
|
||||||
|
"overlay": "#211f4300",
|
||||||
|
"title_bar": "#f0f1feff",
|
||||||
|
"title_bar_inactive": "#fdfcfeff",
|
||||||
|
"window_border": "#dadcffff",
|
||||||
|
"border": "#dadcffff",
|
||||||
|
"border_variant": "#cbcdffff",
|
||||||
|
"border_focused": "#5b5bd6ff",
|
||||||
|
"border_selected": "#5b5bd6ff",
|
||||||
|
"border_transparent": "#00000000",
|
||||||
|
"border_disabled": "#e6e7ffff",
|
||||||
|
"ring": "#5151cdff",
|
||||||
|
"text": "#1f2d5cff",
|
||||||
|
"text_muted": "#5753c6ff",
|
||||||
|
"text_placeholder": "#9b9ef0ff",
|
||||||
|
"text_accent": "#5b5bd6ff",
|
||||||
|
"text_danger": "#e54d2eff",
|
||||||
|
"text_warning": "#f76b15ff",
|
||||||
|
"icon": "#5753c6ff",
|
||||||
|
"icon_muted": "#9b9ef0ff",
|
||||||
|
"icon_accent": "#5151cdff",
|
||||||
|
"element_foreground": "#ffffffff",
|
||||||
|
"element_background": "#5b5bd6ff",
|
||||||
|
"element_hover": "#5151cdff",
|
||||||
|
"element_active": "#6e56cfff",
|
||||||
|
"element_selected": "#654dc4ff",
|
||||||
|
"element_disabled": "#5b5bd64d",
|
||||||
|
"secondary_foreground": "#1f2d5cff",
|
||||||
|
"secondary_background": "#f0f1feff",
|
||||||
|
"secondary_hover": "#e6e7ffff",
|
||||||
|
"secondary_active": "#dadcffff",
|
||||||
|
"secondary_selected": "#dadcffff",
|
||||||
|
"secondary_disabled": "#5b5bd64d",
|
||||||
|
"danger_foreground": "#ffffffff",
|
||||||
|
"danger_background": "#feebe7ff",
|
||||||
|
"danger_hover": "#ffcdc2ff",
|
||||||
|
"danger_active": "#fdbdafff",
|
||||||
|
"danger_selected": "#fdbdafff",
|
||||||
|
"danger_disabled": "#e54d2e4d",
|
||||||
|
"warning_foreground": "#ffffffff",
|
||||||
|
"warning_background": "#fff7edff",
|
||||||
|
"warning_hover": "#ffd19aff",
|
||||||
|
"warning_active": "#ffc182ff",
|
||||||
|
"warning_selected": "#ffc182ff",
|
||||||
|
"warning_disabled": "#f76b154d",
|
||||||
|
"ghost_element_background": "#00000000",
|
||||||
|
"ghost_element_background_alt": "#f0f1feff",
|
||||||
|
"ghost_element_hover": "#211f430d",
|
||||||
|
"ghost_element_active": "#211f431a",
|
||||||
|
"ghost_element_selected": "#211f431a",
|
||||||
|
"ghost_element_disabled": "#211f4305",
|
||||||
|
"tab_background": "#f0f1feff",
|
||||||
|
"tab_foreground": "#5753c6ff",
|
||||||
|
"tab_hover_background": "#211f430d",
|
||||||
|
"tab_active_background": "#fdfcfeff",
|
||||||
|
"tab_active_foreground": "#1f2d5cff",
|
||||||
|
"scrollbar_thumb_background": "#211f431a",
|
||||||
|
"scrollbar_thumb_hover_background": "#211f4326",
|
||||||
|
"scrollbar_thumb_border": "#00000000",
|
||||||
|
"scrollbar_track_background": "#00000000",
|
||||||
|
"scrollbar_track_border": "#00000000",
|
||||||
|
"drop_target_background": "#5b5bd61a",
|
||||||
|
"cursor": "#5b5bd6ff",
|
||||||
|
"selection": "#5b5bd640"
|
||||||
|
},
|
||||||
|
"dark": {
|
||||||
|
"background": "#14121fff",
|
||||||
|
"surface_background": "#1b1525ff",
|
||||||
|
"elevated_surface_background": "#291f43ff",
|
||||||
|
"panel_background": "#14121fff",
|
||||||
|
"overlay": "#baa7ff1a",
|
||||||
|
"title_bar": "#291f43ff",
|
||||||
|
"title_bar_inactive": "#14121fff",
|
||||||
|
"window_border": "#473876ff",
|
||||||
|
"border": "#473876ff",
|
||||||
|
"border_variant": "#3c2e69ff",
|
||||||
|
"border_focused": "#7d66d9ff",
|
||||||
|
"border_selected": "#7d66d9ff",
|
||||||
|
"border_transparent": "#00000000",
|
||||||
|
"border_disabled": "#33255bff",
|
||||||
|
"ring": "#6e56cfff",
|
||||||
|
"text": "#e2ddfeff",
|
||||||
|
"text_muted": "#baa7ffff",
|
||||||
|
"text_placeholder": "#6958adff",
|
||||||
|
"text_accent": "#baa7ffff",
|
||||||
|
"text_danger": "#ff977dff",
|
||||||
|
"text_warning": "#ffa057ff",
|
||||||
|
"icon": "#baa7ffff",
|
||||||
|
"icon_muted": "#6958adff",
|
||||||
|
"icon_accent": "#6e56cfff",
|
||||||
|
"element_foreground": "#14121fff",
|
||||||
|
"element_background": "#7d66d9ff",
|
||||||
|
"element_hover": "#baa7ffff",
|
||||||
|
"element_active": "#6e56cfff",
|
||||||
|
"element_selected": "#654dc4ff",
|
||||||
|
"element_disabled": "#7d66d94d",
|
||||||
|
"secondary_foreground": "#e2ddfeff",
|
||||||
|
"secondary_background": "#291f43ff",
|
||||||
|
"secondary_hover": "#33255bff",
|
||||||
|
"secondary_active": "#3c2e69ff",
|
||||||
|
"secondary_selected": "#3c2e69ff",
|
||||||
|
"secondary_disabled": "#7d66d94d",
|
||||||
|
"danger_foreground": "#181111ff",
|
||||||
|
"danger_background": "#391714ff",
|
||||||
|
"danger_hover": "#5e1c16ff",
|
||||||
|
"danger_active": "#6e2920ff",
|
||||||
|
"danger_selected": "#6e2920ff",
|
||||||
|
"danger_disabled": "#ff977d4d",
|
||||||
|
"warning_foreground": "#17120eff",
|
||||||
|
"warning_background": "#331e0bff",
|
||||||
|
"warning_hover": "#562800ff",
|
||||||
|
"warning_active": "#66350cff",
|
||||||
|
"warning_selected": "#66350cff",
|
||||||
|
"warning_disabled": "#ffa0574d",
|
||||||
|
"ghost_element_background": "#00000000",
|
||||||
|
"ghost_element_background_alt": "#291f43ff",
|
||||||
|
"ghost_element_hover": "#baa7ff0d",
|
||||||
|
"ghost_element_active": "#baa7ff1a",
|
||||||
|
"ghost_element_selected": "#baa7ff1a",
|
||||||
|
"ghost_element_disabled": "#baa7ff05",
|
||||||
|
"tab_background": "#291f43ff",
|
||||||
|
"tab_foreground": "#baa7ffff",
|
||||||
|
"tab_hover_background": "#baa7ff0d",
|
||||||
|
"tab_active_background": "#14121fff",
|
||||||
|
"tab_active_foreground": "#e2ddfeff",
|
||||||
|
"scrollbar_thumb_background": "#baa7ff1a",
|
||||||
|
"scrollbar_thumb_hover_background": "#baa7ff26",
|
||||||
|
"scrollbar_thumb_border": "#00000000",
|
||||||
|
"scrollbar_track_background": "#00000000",
|
||||||
|
"scrollbar_track_border": "#00000000",
|
||||||
|
"drop_target_background": "#baa7ff1a",
|
||||||
|
"cursor": "#baa7ffff",
|
||||||
|
"selection": "#baa7ff40"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,74 +1,76 @@
|
|||||||
{
|
{
|
||||||
"id": "catppuccin-frappe",
|
"id": "catppuccin-frappe",
|
||||||
"name": "Catppuccin Frappé",
|
"name": "Catppuccin Frappé",
|
||||||
"author": "Catppuccin",
|
"author": "Catppuccin Org (ported by Coop)",
|
||||||
"url": "https://github.com/catppuccin/catppuccin",
|
"url": "https://catppuccin.com",
|
||||||
"light": {
|
"light": {
|
||||||
"background": "#303446",
|
"background": "#303446",
|
||||||
"surface_background": "#292c3c",
|
"surface_background": "#292c3c",
|
||||||
"elevated_surface_background": "#232634",
|
"elevated_surface_background": "#232634",
|
||||||
"panel_background": "#303446",
|
"panel_background": "#303446",
|
||||||
"overlay": "#c6d0f51a",
|
"overlay": "#c6d0f51a",
|
||||||
"title_bar": "#292c3c",
|
"title_bar": "#232634",
|
||||||
"title_bar_inactive": "#232634",
|
"title_bar_inactive": "#303446",
|
||||||
"window_border": "#737994",
|
"window_border": "#51576d",
|
||||||
"border": "#626880",
|
"border": "#51576d",
|
||||||
"border_variant": "#51576d",
|
"border_variant": "#414559",
|
||||||
"border_focused": "#8caaee",
|
"border_focused": "#8caaee",
|
||||||
"border_selected": "#8caaee",
|
"border_selected": "#8caaee",
|
||||||
"border_transparent": "#00000000",
|
"border_transparent": "#c6d0f500",
|
||||||
"border_disabled": "#414559",
|
"border_disabled": "#292c3c",
|
||||||
"ring": "#8caaee",
|
"ring": "#babbf1",
|
||||||
"text": "#c6d0f5",
|
"text": "#c6d0f5",
|
||||||
"text_muted": "#b5bfe2",
|
"text_muted": "#a5adce",
|
||||||
"text_placeholder": "#a5adce",
|
"text_placeholder": "#838ba7",
|
||||||
"text_accent": "#8caaee",
|
"text_accent": "#8caaee",
|
||||||
"icon": "#b5bfe2",
|
"text_danger": "#e78284",
|
||||||
"icon_muted": "#a5adce",
|
"text_warning": "#ef9f76",
|
||||||
"icon_accent": "#8caaee",
|
"icon": "#a5adce",
|
||||||
"element_foreground": "#232634",
|
"icon_muted": "#838ba7",
|
||||||
|
"icon_accent": "#babbf1",
|
||||||
|
"element_foreground": "#303446",
|
||||||
"element_background": "#8caaee",
|
"element_background": "#8caaee",
|
||||||
"element_hover": "#babbf1",
|
"element_hover": "#babbf1",
|
||||||
"element_active": "#7e99d6",
|
"element_active": "#99d1db",
|
||||||
"element_selected": "#7088bf",
|
"element_selected": "#85c1dc",
|
||||||
"element_disabled": "#8caaee4d",
|
"element_disabled": "#8caaee4d",
|
||||||
"secondary_foreground": "#7088bf",
|
"secondary_foreground": "#c6d0f5",
|
||||||
"secondary_background": "#292c3c",
|
"secondary_background": "#414559",
|
||||||
"secondary_hover": "#8caaee33",
|
"secondary_hover": "#51576d",
|
||||||
"secondary_active": "#232634",
|
"secondary_active": "#626880",
|
||||||
"secondary_selected": "#232634",
|
"secondary_selected": "#626880",
|
||||||
"secondary_disabled": "#8caaee4d",
|
"secondary_disabled": "#8caaee4d",
|
||||||
"danger_foreground": "#232634",
|
"danger_foreground": "#303446",
|
||||||
"danger_background": "#e78284",
|
"danger_background": "#e78284",
|
||||||
"danger_hover": "#ea999c",
|
"danger_hover": "#ea999c",
|
||||||
"danger_active": "#d07576",
|
"danger_active": "#ef9f76",
|
||||||
"danger_selected": "#b96869",
|
"danger_selected": "#e5c890",
|
||||||
"danger_disabled": "#e782844d",
|
"danger_disabled": "#e782844d",
|
||||||
"warning_foreground": "#232634",
|
"warning_foreground": "#303446",
|
||||||
"warning_background": "#e5c890",
|
"warning_background": "#ef9f76",
|
||||||
"warning_hover": "#ef9f76",
|
"warning_hover": "#e5c890",
|
||||||
"warning_active": "#ceb482",
|
"warning_active": "#a6d189",
|
||||||
"warning_selected": "#b7a074",
|
"warning_selected": "#81c8be",
|
||||||
"warning_disabled": "#e5c8904d",
|
"warning_disabled": "#ef9f764d",
|
||||||
"ghost_element_background": "#00000000",
|
"ghost_element_background": "#c6d0f500",
|
||||||
"ghost_element_background_alt": "#414559",
|
"ghost_element_background_alt": "#292c3c",
|
||||||
"ghost_element_hover": "#c6d0f533",
|
"ghost_element_hover": "#c6d0f50d",
|
||||||
"ghost_element_active": "#51576d",
|
"ghost_element_active": "#c6d0f51a",
|
||||||
"ghost_element_selected": "#51576d",
|
"ghost_element_selected": "#c6d0f51a",
|
||||||
"ghost_element_disabled": "#c6d0f50d",
|
"ghost_element_disabled": "#c6d0f505",
|
||||||
"tab_inactive_background": "#292c3c",
|
"tab_background": "#232634",
|
||||||
"tab_inactive_foreground": "#b5bfe2",
|
"tab_foreground": "#a5adce",
|
||||||
|
"tab_hover_background": "#c6d0f50d",
|
||||||
"tab_active_background": "#303446",
|
"tab_active_background": "#303446",
|
||||||
"tab_active_foreground": "#c6d0f5",
|
"tab_active_foreground": "#c6d0f5",
|
||||||
"tab_hover_foreground": "#babbf1",
|
"scrollbar_thumb_background": "#c6d0f51a",
|
||||||
"scrollbar_thumb_background": "#c6d0f533",
|
"scrollbar_thumb_hover_background": "#c6d0f526",
|
||||||
"scrollbar_thumb_hover_background": "#c6d0f580",
|
"scrollbar_thumb_border": "#c6d0f500",
|
||||||
"scrollbar_thumb_border": "#00000000",
|
"scrollbar_track_background": "#c6d0f500",
|
||||||
"scrollbar_track_background": "#00000000",
|
"scrollbar_track_border": "#c6d0f500",
|
||||||
"scrollbar_track_border": "#51576d",
|
|
||||||
"drop_target_background": "#8caaee1a",
|
"drop_target_background": "#8caaee1a",
|
||||||
"cursor": "#f2d5cf",
|
"cursor": "#8caaee",
|
||||||
"selection": "#949cbb40"
|
"selection": "#8caaee40"
|
||||||
},
|
},
|
||||||
"dark": {
|
"dark": {
|
||||||
"background": "#303446",
|
"background": "#303446",
|
||||||
@@ -76,65 +78,67 @@
|
|||||||
"elevated_surface_background": "#232634",
|
"elevated_surface_background": "#232634",
|
||||||
"panel_background": "#303446",
|
"panel_background": "#303446",
|
||||||
"overlay": "#c6d0f51a",
|
"overlay": "#c6d0f51a",
|
||||||
"title_bar": "#292c3c",
|
"title_bar": "#232634",
|
||||||
"title_bar_inactive": "#232634",
|
"title_bar_inactive": "#303446",
|
||||||
"window_border": "#737994",
|
"window_border": "#51576d",
|
||||||
"border": "#626880",
|
"border": "#51576d",
|
||||||
"border_variant": "#51576d",
|
"border_variant": "#414559",
|
||||||
"border_focused": "#8caaee",
|
"border_focused": "#8caaee",
|
||||||
"border_selected": "#8caaee",
|
"border_selected": "#8caaee",
|
||||||
"border_transparent": "#00000000",
|
"border_transparent": "#c6d0f500",
|
||||||
"border_disabled": "#414559",
|
"border_disabled": "#292c3c",
|
||||||
"ring": "#8caaee",
|
"ring": "#babbf1",
|
||||||
"text": "#c6d0f5",
|
"text": "#c6d0f5",
|
||||||
"text_muted": "#b5bfe2",
|
"text_muted": "#a5adce",
|
||||||
"text_placeholder": "#a5adce",
|
"text_placeholder": "#838ba7",
|
||||||
"text_accent": "#8caaee",
|
"text_accent": "#8caaee",
|
||||||
"icon": "#b5bfe2",
|
"text_danger": "#e78284",
|
||||||
"icon_muted": "#a5adce",
|
"text_warning": "#ef9f76",
|
||||||
"icon_accent": "#8caaee",
|
"icon": "#a5adce",
|
||||||
"element_foreground": "#232634",
|
"icon_muted": "#838ba7",
|
||||||
|
"icon_accent": "#babbf1",
|
||||||
|
"element_foreground": "#303446",
|
||||||
"element_background": "#8caaee",
|
"element_background": "#8caaee",
|
||||||
"element_hover": "#babbf1",
|
"element_hover": "#babbf1",
|
||||||
"element_active": "#7e99d6",
|
"element_active": "#99d1db",
|
||||||
"element_selected": "#7088bf",
|
"element_selected": "#85c1dc",
|
||||||
"element_disabled": "#8caaee4d",
|
"element_disabled": "#8caaee4d",
|
||||||
"secondary_foreground": "#7088bf",
|
"secondary_foreground": "#c6d0f5",
|
||||||
"secondary_background": "#292c3c",
|
"secondary_background": "#414559",
|
||||||
"secondary_hover": "#8caaee33",
|
"secondary_hover": "#51576d",
|
||||||
"secondary_active": "#232634",
|
"secondary_active": "#626880",
|
||||||
"secondary_selected": "#232634",
|
"secondary_selected": "#626880",
|
||||||
"secondary_disabled": "#8caaee4d",
|
"secondary_disabled": "#8caaee4d",
|
||||||
"danger_foreground": "#232634",
|
"danger_foreground": "#303446",
|
||||||
"danger_background": "#e78284",
|
"danger_background": "#e78284",
|
||||||
"danger_hover": "#ea999c",
|
"danger_hover": "#ea999c",
|
||||||
"danger_active": "#d07576",
|
"danger_active": "#ef9f76",
|
||||||
"danger_selected": "#b96869",
|
"danger_selected": "#e5c890",
|
||||||
"danger_disabled": "#e782844d",
|
"danger_disabled": "#e782844d",
|
||||||
"warning_foreground": "#232634",
|
"warning_foreground": "#303446",
|
||||||
"warning_background": "#e5c890",
|
"warning_background": "#ef9f76",
|
||||||
"warning_hover": "#ef9f76",
|
"warning_hover": "#e5c890",
|
||||||
"warning_active": "#ceb482",
|
"warning_active": "#a6d189",
|
||||||
"warning_selected": "#b7a074",
|
"warning_selected": "#81c8be",
|
||||||
"warning_disabled": "#e5c8904d",
|
"warning_disabled": "#ef9f764d",
|
||||||
"ghost_element_background": "#00000000",
|
"ghost_element_background": "#c6d0f500",
|
||||||
"ghost_element_background_alt": "#414559",
|
"ghost_element_background_alt": "#292c3c",
|
||||||
"ghost_element_hover": "#c6d0f533",
|
"ghost_element_hover": "#c6d0f50d",
|
||||||
"ghost_element_active": "#51576d",
|
"ghost_element_active": "#c6d0f51a",
|
||||||
"ghost_element_selected": "#51576d",
|
"ghost_element_selected": "#c6d0f51a",
|
||||||
"ghost_element_disabled": "#c6d0f50d",
|
"ghost_element_disabled": "#c6d0f505",
|
||||||
"tab_inactive_background": "#292c3c",
|
"tab_background": "#232634",
|
||||||
"tab_inactive_foreground": "#b5bfe2",
|
"tab_foreground": "#a5adce",
|
||||||
|
"tab_hover_background": "#c6d0f50d",
|
||||||
"tab_active_background": "#303446",
|
"tab_active_background": "#303446",
|
||||||
"tab_active_foreground": "#c6d0f5",
|
"tab_active_foreground": "#c6d0f5",
|
||||||
"tab_hover_foreground": "#babbf1",
|
"scrollbar_thumb_background": "#c6d0f51a",
|
||||||
"scrollbar_thumb_background": "#c6d0f533",
|
"scrollbar_thumb_hover_background": "#c6d0f526",
|
||||||
"scrollbar_thumb_hover_background": "#c6d0f580",
|
"scrollbar_thumb_border": "#c6d0f500",
|
||||||
"scrollbar_thumb_border": "#00000000",
|
"scrollbar_track_background": "#c6d0f500",
|
||||||
"scrollbar_track_background": "#00000000",
|
"scrollbar_track_border": "#c6d0f500",
|
||||||
"scrollbar_track_border": "#51576d",
|
|
||||||
"drop_target_background": "#8caaee1a",
|
"drop_target_background": "#8caaee1a",
|
||||||
"cursor": "#f2d5cf",
|
"cursor": "#8caaee",
|
||||||
"selection": "#949cbb40"
|
"selection": "#8caaee40"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,74 +1,76 @@
|
|||||||
{
|
{
|
||||||
"id": "catppuccin-latte",
|
"id": "catppuccin-latte",
|
||||||
"name": "Catppuccin Latte",
|
"name": "Catppuccin Latte",
|
||||||
"author": "Catppuccin",
|
"author": "Catppuccin Org (ported by Coop)",
|
||||||
"url": "https://github.com/catppuccin/catppuccin",
|
"url": "https://catppuccin.com",
|
||||||
"light": {
|
"light": {
|
||||||
"background": "#eff1f5",
|
"background": "#eff1f5",
|
||||||
"surface_background": "#e6e9ef",
|
"surface_background": "#e6e9ef",
|
||||||
"elevated_surface_background": "#dce0e8",
|
"elevated_surface_background": "#dce0e8",
|
||||||
"panel_background": "#eff1f5",
|
"panel_background": "#eff1f5",
|
||||||
"overlay": "#4c4f691a",
|
"overlay": "#4c4f691a",
|
||||||
"title_bar": "#e6e9ef",
|
"title_bar": "#dce0e8",
|
||||||
"title_bar_inactive": "#dce0e8",
|
"title_bar_inactive": "#eff1f5",
|
||||||
"window_border": "#9ca0b0",
|
"window_border": "#bcc0cc",
|
||||||
"border": "#acb0be",
|
"border": "#bcc0cc",
|
||||||
"border_variant": "#bcc0cc",
|
"border_variant": "#ccd0da",
|
||||||
"border_focused": "#1e66f5",
|
"border_focused": "#1e66f5",
|
||||||
"border_selected": "#1e66f5",
|
"border_selected": "#1e66f5",
|
||||||
"border_transparent": "#00000000",
|
"border_transparent": "#4c4f6900",
|
||||||
"border_disabled": "#ccd0da",
|
"border_disabled": "#e6e9ef",
|
||||||
"ring": "#1e66f5",
|
"ring": "#7287fd",
|
||||||
"text": "#4c4f69",
|
"text": "#4c4f69",
|
||||||
"text_muted": "#5c5f77",
|
"text_muted": "#6c6f85",
|
||||||
"text_placeholder": "#6c6f85",
|
"text_placeholder": "#8c8fa1",
|
||||||
"text_accent": "#1e66f5",
|
"text_accent": "#1e66f5",
|
||||||
"icon": "#5c5f77",
|
"text_danger": "#d20f39",
|
||||||
"icon_muted": "#6c6f85",
|
"text_warning": "#fe640b",
|
||||||
"icon_accent": "#1e66f5",
|
"icon": "#6c6f85",
|
||||||
|
"icon_muted": "#8c8fa1",
|
||||||
|
"icon_accent": "#7287fd",
|
||||||
"element_foreground": "#eff1f5",
|
"element_foreground": "#eff1f5",
|
||||||
"element_background": "#1e66f5",
|
"element_background": "#1e66f5",
|
||||||
"element_hover": "#8839ef",
|
"element_hover": "#7287fd",
|
||||||
"element_active": "#1c5ce0",
|
"element_active": "#04a5e5",
|
||||||
"element_selected": "#1a52cc",
|
"element_selected": "#209fb5",
|
||||||
"element_disabled": "#1e66f54d",
|
"element_disabled": "#1e66f54d",
|
||||||
"secondary_foreground": "#1a52cc",
|
"secondary_foreground": "#4c4f69",
|
||||||
"secondary_background": "#e6e9ef",
|
"secondary_background": "#ccd0da",
|
||||||
"secondary_hover": "#8839ef33",
|
"secondary_hover": "#bcc0cc",
|
||||||
"secondary_active": "#dce0e8",
|
"secondary_active": "#acb0be",
|
||||||
"secondary_selected": "#dce0e8",
|
"secondary_selected": "#acb0be",
|
||||||
"secondary_disabled": "#1e66f54d",
|
"secondary_disabled": "#1e66f54d",
|
||||||
"danger_foreground": "#eff1f5",
|
"danger_foreground": "#eff1f5",
|
||||||
"danger_background": "#d20f39",
|
"danger_background": "#d20f39",
|
||||||
"danger_hover": "#e64553",
|
"danger_hover": "#e64553",
|
||||||
"danger_active": "#bd0d33",
|
"danger_active": "#fe640b",
|
||||||
"danger_selected": "#a80b2d",
|
"danger_selected": "#df8e1d",
|
||||||
"danger_disabled": "#d20f394d",
|
"danger_disabled": "#d20f394d",
|
||||||
"warning_foreground": "#4c4f69",
|
"warning_foreground": "#eff1f5",
|
||||||
"warning_background": "#df8e1d",
|
"warning_background": "#fe640b",
|
||||||
"warning_hover": "#fe640b",
|
"warning_hover": "#df8e1d",
|
||||||
"warning_active": "#c9801a",
|
"warning_active": "#40a02b",
|
||||||
"warning_selected": "#b47217",
|
"warning_selected": "#179299",
|
||||||
"warning_disabled": "#df8e1d4d",
|
"warning_disabled": "#fe640b4d",
|
||||||
"ghost_element_background": "#00000000",
|
"ghost_element_background": "#4c4f6900",
|
||||||
"ghost_element_background_alt": "#ccd0da",
|
"ghost_element_background_alt": "#e6e9ef",
|
||||||
"ghost_element_hover": "#4c4f6933",
|
"ghost_element_hover": "#4c4f690d",
|
||||||
"ghost_element_active": "#bcc0cc",
|
"ghost_element_active": "#4c4f691a",
|
||||||
"ghost_element_selected": "#bcc0cc",
|
"ghost_element_selected": "#4c4f691a",
|
||||||
"ghost_element_disabled": "#4c4f690d",
|
"ghost_element_disabled": "#4c4f6905",
|
||||||
"tab_inactive_background": "#e6e9ef",
|
"tab_background": "#e6e9ef",
|
||||||
"tab_inactive_foreground": "#5c5f77",
|
"tab_foreground": "#6c6f85",
|
||||||
|
"tab_hover_background": "#4c4f690d",
|
||||||
"tab_active_background": "#eff1f5",
|
"tab_active_background": "#eff1f5",
|
||||||
"tab_active_foreground": "#4c4f69",
|
"tab_active_foreground": "#4c4f69",
|
||||||
"tab_hover_foreground": "#8839ef",
|
"scrollbar_thumb_background": "#4c4f691a",
|
||||||
"scrollbar_thumb_background": "#4c4f6933",
|
"scrollbar_thumb_hover_background": "#4c4f6926",
|
||||||
"scrollbar_thumb_hover_background": "#4c4f6980",
|
"scrollbar_thumb_border": "#4c4f6900",
|
||||||
"scrollbar_thumb_border": "#00000000",
|
"scrollbar_track_background": "#4c4f6900",
|
||||||
"scrollbar_track_background": "#00000000",
|
"scrollbar_track_border": "#4c4f6900",
|
||||||
"scrollbar_track_border": "#bcc0cc",
|
|
||||||
"drop_target_background": "#1e66f51a",
|
"drop_target_background": "#1e66f51a",
|
||||||
"cursor": "#dc8a78",
|
"cursor": "#1e66f5",
|
||||||
"selection": "#7c7f9340"
|
"selection": "#1e66f540"
|
||||||
},
|
},
|
||||||
"dark": {
|
"dark": {
|
||||||
"background": "#eff1f5",
|
"background": "#eff1f5",
|
||||||
@@ -76,65 +78,67 @@
|
|||||||
"elevated_surface_background": "#dce0e8",
|
"elevated_surface_background": "#dce0e8",
|
||||||
"panel_background": "#eff1f5",
|
"panel_background": "#eff1f5",
|
||||||
"overlay": "#4c4f691a",
|
"overlay": "#4c4f691a",
|
||||||
"title_bar": "#e6e9ef",
|
"title_bar": "#dce0e8",
|
||||||
"title_bar_inactive": "#dce0e8",
|
"title_bar_inactive": "#eff1f5",
|
||||||
"window_border": "#9ca0b0",
|
"window_border": "#bcc0cc",
|
||||||
"border": "#acb0be",
|
"border": "#bcc0cc",
|
||||||
"border_variant": "#bcc0cc",
|
"border_variant": "#ccd0da",
|
||||||
"border_focused": "#1e66f5",
|
"border_focused": "#1e66f5",
|
||||||
"border_selected": "#1e66f5",
|
"border_selected": "#1e66f5",
|
||||||
"border_transparent": "#00000000",
|
"border_transparent": "#4c4f6900",
|
||||||
"border_disabled": "#ccd0da",
|
"border_disabled": "#e6e9ef",
|
||||||
"ring": "#1e66f5",
|
"ring": "#7287fd",
|
||||||
"text": "#4c4f69",
|
"text": "#4c4f69",
|
||||||
"text_muted": "#5c5f77",
|
"text_muted": "#6c6f85",
|
||||||
"text_placeholder": "#6c6f85",
|
"text_placeholder": "#8c8fa1",
|
||||||
"text_accent": "#1e66f5",
|
"text_accent": "#1e66f5",
|
||||||
"icon": "#5c5f77",
|
"text_danger": "#d20f39",
|
||||||
"icon_muted": "#6c6f85",
|
"text_warning": "#fe640b",
|
||||||
"icon_accent": "#1e66f5",
|
"icon": "#6c6f85",
|
||||||
|
"icon_muted": "#8c8fa1",
|
||||||
|
"icon_accent": "#7287fd",
|
||||||
"element_foreground": "#eff1f5",
|
"element_foreground": "#eff1f5",
|
||||||
"element_background": "#1e66f5",
|
"element_background": "#1e66f5",
|
||||||
"element_hover": "#8839ef",
|
"element_hover": "#7287fd",
|
||||||
"element_active": "#1c5ce0",
|
"element_active": "#04a5e5",
|
||||||
"element_selected": "#1a52cc",
|
"element_selected": "#209fb5",
|
||||||
"element_disabled": "#1e66f54d",
|
"element_disabled": "#1e66f54d",
|
||||||
"secondary_foreground": "#1a52cc",
|
"secondary_foreground": "#4c4f69",
|
||||||
"secondary_background": "#e6e9ef",
|
"secondary_background": "#ccd0da",
|
||||||
"secondary_hover": "#8839ef33",
|
"secondary_hover": "#bcc0cc",
|
||||||
"secondary_active": "#dce0e8",
|
"secondary_active": "#acb0be",
|
||||||
"secondary_selected": "#dce0e8",
|
"secondary_selected": "#acb0be",
|
||||||
"secondary_disabled": "#1e66f54d",
|
"secondary_disabled": "#1e66f54d",
|
||||||
"danger_foreground": "#eff1f5",
|
"danger_foreground": "#eff1f5",
|
||||||
"danger_background": "#d20f39",
|
"danger_background": "#d20f39",
|
||||||
"danger_hover": "#e64553",
|
"danger_hover": "#e64553",
|
||||||
"danger_active": "#bd0d33",
|
"danger_active": "#fe640b",
|
||||||
"danger_selected": "#a80b2d",
|
"danger_selected": "#df8e1d",
|
||||||
"danger_disabled": "#d20f394d",
|
"danger_disabled": "#d20f394d",
|
||||||
"warning_foreground": "#4c4f69",
|
"warning_foreground": "#eff1f5",
|
||||||
"warning_background": "#df8e1d",
|
"warning_background": "#fe640b",
|
||||||
"warning_hover": "#fe640b",
|
"warning_hover": "#df8e1d",
|
||||||
"warning_active": "#c9801a",
|
"warning_active": "#40a02b",
|
||||||
"warning_selected": "#b47217",
|
"warning_selected": "#179299",
|
||||||
"warning_disabled": "#df8e1d4d",
|
"warning_disabled": "#fe640b4d",
|
||||||
"ghost_element_background": "#00000000",
|
"ghost_element_background": "#4c4f6900",
|
||||||
"ghost_element_background_alt": "#ccd0da",
|
"ghost_element_background_alt": "#e6e9ef",
|
||||||
"ghost_element_hover": "#4c4f6933",
|
"ghost_element_hover": "#4c4f690d",
|
||||||
"ghost_element_active": "#bcc0cc",
|
"ghost_element_active": "#4c4f691a",
|
||||||
"ghost_element_selected": "#bcc0cc",
|
"ghost_element_selected": "#4c4f691a",
|
||||||
"ghost_element_disabled": "#4c4f690d",
|
"ghost_element_disabled": "#4c4f6905",
|
||||||
"tab_inactive_background": "#e6e9ef",
|
"tab_background": "#e6e9ef",
|
||||||
"tab_inactive_foreground": "#5c5f77",
|
"tab_foreground": "#6c6f85",
|
||||||
|
"tab_hover_background": "#4c4f690d",
|
||||||
"tab_active_background": "#eff1f5",
|
"tab_active_background": "#eff1f5",
|
||||||
"tab_active_foreground": "#4c4f69",
|
"tab_active_foreground": "#4c4f69",
|
||||||
"tab_hover_foreground": "#8839ef",
|
"scrollbar_thumb_background": "#4c4f691a",
|
||||||
"scrollbar_thumb_background": "#4c4f6933",
|
"scrollbar_thumb_hover_background": "#4c4f6926",
|
||||||
"scrollbar_thumb_hover_background": "#4c4f6980",
|
"scrollbar_thumb_border": "#4c4f6900",
|
||||||
"scrollbar_thumb_border": "#00000000",
|
"scrollbar_track_background": "#4c4f6900",
|
||||||
"scrollbar_track_background": "#00000000",
|
"scrollbar_track_border": "#4c4f6900",
|
||||||
"scrollbar_track_border": "#bcc0cc",
|
|
||||||
"drop_target_background": "#1e66f51a",
|
"drop_target_background": "#1e66f51a",
|
||||||
"cursor": "#dc8a78",
|
"cursor": "#1e66f5",
|
||||||
"selection": "#7c7f9340"
|
"selection": "#1e66f540"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,74 +1,76 @@
|
|||||||
{
|
{
|
||||||
"id": "catppuccin-macchiato",
|
"id": "catppuccin-macchiato",
|
||||||
"name": "Catppuccin Macchiato",
|
"name": "Catppuccin Macchiato",
|
||||||
"author": "Catppuccin",
|
"author": "Catppuccin Org (ported by Coop)",
|
||||||
"url": "https://github.com/catppuccin/catppuccin",
|
"url": "https://catppuccin.com",
|
||||||
"light": {
|
"light": {
|
||||||
"background": "#24273a",
|
"background": "#24273a",
|
||||||
"surface_background": "#1e2030",
|
"surface_background": "#1e2030",
|
||||||
"elevated_surface_background": "#181926",
|
"elevated_surface_background": "#181926",
|
||||||
"panel_background": "#24273a",
|
"panel_background": "#24273a",
|
||||||
"overlay": "#cad3f51a",
|
"overlay": "#cad3f51a",
|
||||||
"title_bar": "#1e2030",
|
"title_bar": "#181926",
|
||||||
"title_bar_inactive": "#181926",
|
"title_bar_inactive": "#24273a",
|
||||||
"window_border": "#6e738d",
|
"window_border": "#494d64",
|
||||||
"border": "#5b6078",
|
"border": "#494d64",
|
||||||
"border_variant": "#494d64",
|
"border_variant": "#363a4f",
|
||||||
"border_focused": "#8aadf4",
|
"border_focused": "#8aadf4",
|
||||||
"border_selected": "#8aadf4",
|
"border_selected": "#8aadf4",
|
||||||
"border_transparent": "#00000000",
|
"border_transparent": "#cad3f500",
|
||||||
"border_disabled": "#363a4f",
|
"border_disabled": "#1e2030",
|
||||||
"ring": "#8aadf4",
|
"ring": "#b7bdf8",
|
||||||
"text": "#cad3f5",
|
"text": "#cad3f5",
|
||||||
"text_muted": "#b8c0e0",
|
"text_muted": "#a5adcb",
|
||||||
"text_placeholder": "#a5adcb",
|
"text_placeholder": "#8087a2",
|
||||||
"text_accent": "#8aadf4",
|
"text_accent": "#8aadf4",
|
||||||
"icon": "#b8c0e0",
|
"text_danger": "#ed8796",
|
||||||
"icon_muted": "#a5adcb",
|
"text_warning": "#f5a97f",
|
||||||
"icon_accent": "#8aadf4",
|
"icon": "#a5adcb",
|
||||||
"element_foreground": "#181926",
|
"icon_muted": "#8087a2",
|
||||||
|
"icon_accent": "#b7bdf8",
|
||||||
|
"element_foreground": "#24273a",
|
||||||
"element_background": "#8aadf4",
|
"element_background": "#8aadf4",
|
||||||
"element_hover": "#b7bdf8",
|
"element_hover": "#b7bdf8",
|
||||||
"element_active": "#7c9cdc",
|
"element_active": "#91d7e3",
|
||||||
"element_selected": "#6e8bc5",
|
"element_selected": "#7dc4e4",
|
||||||
"element_disabled": "#8aadf44d",
|
"element_disabled": "#8aadf44d",
|
||||||
"secondary_foreground": "#6e8bc5",
|
"secondary_foreground": "#cad3f5",
|
||||||
"secondary_background": "#1e2030",
|
"secondary_background": "#363a4f",
|
||||||
"secondary_hover": "#8aadf433",
|
"secondary_hover": "#494d64",
|
||||||
"secondary_active": "#181926",
|
"secondary_active": "#5b6078",
|
||||||
"secondary_selected": "#181926",
|
"secondary_selected": "#5b6078",
|
||||||
"secondary_disabled": "#8aadf44d",
|
"secondary_disabled": "#8aadf44d",
|
||||||
"danger_foreground": "#181926",
|
"danger_foreground": "#24273a",
|
||||||
"danger_background": "#ed8796",
|
"danger_background": "#ed8796",
|
||||||
"danger_hover": "#ee99a0",
|
"danger_hover": "#ee99a0",
|
||||||
"danger_active": "#d57a87",
|
"danger_active": "#f5a97f",
|
||||||
"danger_selected": "#be6d78",
|
"danger_selected": "#eed49f",
|
||||||
"danger_disabled": "#ed87964d",
|
"danger_disabled": "#ed87964d",
|
||||||
"warning_foreground": "#181926",
|
"warning_foreground": "#24273a",
|
||||||
"warning_background": "#eed49f",
|
"warning_background": "#f5a97f",
|
||||||
"warning_hover": "#f5a97f",
|
"warning_hover": "#eed49f",
|
||||||
"warning_active": "#d6bf8f",
|
"warning_active": "#a6da95",
|
||||||
"warning_selected": "#beaa7f",
|
"warning_selected": "#8bd5ca",
|
||||||
"warning_disabled": "#eed49f4d",
|
"warning_disabled": "#f5a97f4d",
|
||||||
"ghost_element_background": "#00000000",
|
"ghost_element_background": "#cad3f500",
|
||||||
"ghost_element_background_alt": "#363a4f",
|
"ghost_element_background_alt": "#1e2030",
|
||||||
"ghost_element_hover": "#cad3f533",
|
"ghost_element_hover": "#cad3f50d",
|
||||||
"ghost_element_active": "#494d64",
|
"ghost_element_active": "#cad3f51a",
|
||||||
"ghost_element_selected": "#494d64",
|
"ghost_element_selected": "#cad3f51a",
|
||||||
"ghost_element_disabled": "#cad3f50d",
|
"ghost_element_disabled": "#cad3f505",
|
||||||
"tab_inactive_background": "#1e2030",
|
"tab_background": "#181926",
|
||||||
"tab_inactive_foreground": "#b8c0e0",
|
"tab_foreground": "#a5adcb",
|
||||||
|
"tab_hover_background": "#cad3f50d",
|
||||||
"tab_active_background": "#24273a",
|
"tab_active_background": "#24273a",
|
||||||
"tab_active_foreground": "#cad3f5",
|
"tab_active_foreground": "#cad3f5",
|
||||||
"tab_hover_foreground": "#b7bdf8",
|
"scrollbar_thumb_background": "#cad3f51a",
|
||||||
"scrollbar_thumb_background": "#cad3f533",
|
"scrollbar_thumb_hover_background": "#cad3f526",
|
||||||
"scrollbar_thumb_hover_background": "#cad3f580",
|
"scrollbar_thumb_border": "#cad3f500",
|
||||||
"scrollbar_thumb_border": "#00000000",
|
"scrollbar_track_background": "#cad3f500",
|
||||||
"scrollbar_track_background": "#00000000",
|
"scrollbar_track_border": "#cad3f500",
|
||||||
"scrollbar_track_border": "#494d64",
|
|
||||||
"drop_target_background": "#8aadf41a",
|
"drop_target_background": "#8aadf41a",
|
||||||
"cursor": "#f4dbd6",
|
"cursor": "#8aadf4",
|
||||||
"selection": "#939ab740"
|
"selection": "#8aadf440"
|
||||||
},
|
},
|
||||||
"dark": {
|
"dark": {
|
||||||
"background": "#24273a",
|
"background": "#24273a",
|
||||||
@@ -76,65 +78,67 @@
|
|||||||
"elevated_surface_background": "#181926",
|
"elevated_surface_background": "#181926",
|
||||||
"panel_background": "#24273a",
|
"panel_background": "#24273a",
|
||||||
"overlay": "#cad3f51a",
|
"overlay": "#cad3f51a",
|
||||||
"title_bar": "#1e2030",
|
"title_bar": "#181926",
|
||||||
"title_bar_inactive": "#181926",
|
"title_bar_inactive": "#24273a",
|
||||||
"window_border": "#6e738d",
|
"window_border": "#494d64",
|
||||||
"border": "#5b6078",
|
"border": "#494d64",
|
||||||
"border_variant": "#494d64",
|
"border_variant": "#363a4f",
|
||||||
"border_focused": "#8aadf4",
|
"border_focused": "#8aadf4",
|
||||||
"border_selected": "#8aadf4",
|
"border_selected": "#8aadf4",
|
||||||
"border_transparent": "#00000000",
|
"border_transparent": "#cad3f500",
|
||||||
"border_disabled": "#363a4f",
|
"border_disabled": "#1e2030",
|
||||||
"ring": "#8aadf4",
|
"ring": "#b7bdf8",
|
||||||
"text": "#cad3f5",
|
"text": "#cad3f5",
|
||||||
"text_muted": "#b8c0e0",
|
"text_muted": "#a5adcb",
|
||||||
"text_placeholder": "#a5adcb",
|
"text_placeholder": "#8087a2",
|
||||||
"text_accent": "#8aadf4",
|
"text_accent": "#8aadf4",
|
||||||
"icon": "#b8c0e0",
|
"text_danger": "#ed8796",
|
||||||
"icon_muted": "#a5adcb",
|
"text_warning": "#f5a97f",
|
||||||
"icon_accent": "#8aadf4",
|
"icon": "#a5adcb",
|
||||||
"element_foreground": "#181926",
|
"icon_muted": "#8087a2",
|
||||||
|
"icon_accent": "#b7bdf8",
|
||||||
|
"element_foreground": "#24273a",
|
||||||
"element_background": "#8aadf4",
|
"element_background": "#8aadf4",
|
||||||
"element_hover": "#b7bdf8",
|
"element_hover": "#b7bdf8",
|
||||||
"element_active": "#7c9cdc",
|
"element_active": "#91d7e3",
|
||||||
"element_selected": "#6e8bc5",
|
"element_selected": "#7dc4e4",
|
||||||
"element_disabled": "#8aadf44d",
|
"element_disabled": "#8aadf44d",
|
||||||
"secondary_foreground": "#6e8bc5",
|
"secondary_foreground": "#cad3f5",
|
||||||
"secondary_background": "#1e2030",
|
"secondary_background": "#363a4f",
|
||||||
"secondary_hover": "#8aadf433",
|
"secondary_hover": "#494d64",
|
||||||
"secondary_active": "#181926",
|
"secondary_active": "#5b6078",
|
||||||
"secondary_selected": "#181926",
|
"secondary_selected": "#5b6078",
|
||||||
"secondary_disabled": "#8aadf44d",
|
"secondary_disabled": "#8aadf44d",
|
||||||
"danger_foreground": "#181926",
|
"danger_foreground": "#24273a",
|
||||||
"danger_background": "#ed8796",
|
"danger_background": "#ed8796",
|
||||||
"danger_hover": "#ee99a0",
|
"danger_hover": "#ee99a0",
|
||||||
"danger_active": "#d57a87",
|
"danger_active": "#f5a97f",
|
||||||
"danger_selected": "#be6d78",
|
"danger_selected": "#eed49f",
|
||||||
"danger_disabled": "#ed87964d",
|
"danger_disabled": "#ed87964d",
|
||||||
"warning_foreground": "#181926",
|
"warning_foreground": "#24273a",
|
||||||
"warning_background": "#eed49f",
|
"warning_background": "#f5a97f",
|
||||||
"warning_hover": "#f5a97f",
|
"warning_hover": "#eed49f",
|
||||||
"warning_active": "#d6bf8f",
|
"warning_active": "#a6da95",
|
||||||
"warning_selected": "#beaa7f",
|
"warning_selected": "#8bd5ca",
|
||||||
"warning_disabled": "#eed49f4d",
|
"warning_disabled": "#f5a97f4d",
|
||||||
"ghost_element_background": "#00000000",
|
"ghost_element_background": "#cad3f500",
|
||||||
"ghost_element_background_alt": "#363a4f",
|
"ghost_element_background_alt": "#1e2030",
|
||||||
"ghost_element_hover": "#cad3f533",
|
"ghost_element_hover": "#cad3f50d",
|
||||||
"ghost_element_active": "#494d64",
|
"ghost_element_active": "#cad3f51a",
|
||||||
"ghost_element_selected": "#494d64",
|
"ghost_element_selected": "#cad3f51a",
|
||||||
"ghost_element_disabled": "#cad3f50d",
|
"ghost_element_disabled": "#cad3f505",
|
||||||
"tab_inactive_background": "#1e2030",
|
"tab_background": "#181926",
|
||||||
"tab_inactive_foreground": "#b8c0e0",
|
"tab_foreground": "#a5adcb",
|
||||||
|
"tab_hover_background": "#cad3f50d",
|
||||||
"tab_active_background": "#24273a",
|
"tab_active_background": "#24273a",
|
||||||
"tab_active_foreground": "#cad3f5",
|
"tab_active_foreground": "#cad3f5",
|
||||||
"tab_hover_foreground": "#b7bdf8",
|
"scrollbar_thumb_background": "#cad3f51a",
|
||||||
"scrollbar_thumb_background": "#cad3f533",
|
"scrollbar_thumb_hover_background": "#cad3f526",
|
||||||
"scrollbar_thumb_hover_background": "#cad3f580",
|
"scrollbar_thumb_border": "#cad3f500",
|
||||||
"scrollbar_thumb_border": "#00000000",
|
"scrollbar_track_background": "#cad3f500",
|
||||||
"scrollbar_track_background": "#00000000",
|
"scrollbar_track_border": "#cad3f500",
|
||||||
"scrollbar_track_border": "#494d64",
|
|
||||||
"drop_target_background": "#8aadf41a",
|
"drop_target_background": "#8aadf41a",
|
||||||
"cursor": "#f4dbd6",
|
"cursor": "#8aadf4",
|
||||||
"selection": "#939ab740"
|
"selection": "#8aadf440"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,74 +1,76 @@
|
|||||||
{
|
{
|
||||||
"id": "catppuccin-mocha",
|
"id": "catppuccin-mocha",
|
||||||
"name": "Catppuccin Mocha",
|
"name": "Catppuccin Mocha",
|
||||||
"author": "Catppuccin",
|
"author": "Catppuccin Org (ported by Coop)",
|
||||||
"url": "https://github.com/catppuccin/catppuccin",
|
"url": "https://catppuccin.com",
|
||||||
"light": {
|
"light": {
|
||||||
"background": "#1e1e2e",
|
"background": "#1e1e2e",
|
||||||
"surface_background": "#181825",
|
"surface_background": "#181825",
|
||||||
"elevated_surface_background": "#11111b",
|
"elevated_surface_background": "#11111b",
|
||||||
"panel_background": "#1e1e2e",
|
"panel_background": "#1e1e2e",
|
||||||
"overlay": "#cdd6f41a",
|
"overlay": "#cdd6f41a",
|
||||||
"title_bar": "#181825",
|
"title_bar": "#11111b",
|
||||||
"title_bar_inactive": "#11111b",
|
"title_bar_inactive": "#1e1e2e",
|
||||||
"window_border": "#6c7086",
|
"window_border": "#45475a",
|
||||||
"border": "#585b70",
|
"border": "#45475a",
|
||||||
"border_variant": "#45475a",
|
"border_variant": "#313244",
|
||||||
"border_focused": "#89b4fa",
|
"border_focused": "#89b4fa",
|
||||||
"border_selected": "#89b4fa",
|
"border_selected": "#89b4fa",
|
||||||
"border_transparent": "#00000000",
|
"border_transparent": "#cdd6f400",
|
||||||
"border_disabled": "#313244",
|
"border_disabled": "#181825",
|
||||||
"ring": "#89b4fa",
|
"ring": "#b4befe",
|
||||||
"text": "#cdd6f4",
|
"text": "#cdd6f4",
|
||||||
"text_muted": "#bac2de",
|
"text_muted": "#a6adc8",
|
||||||
"text_placeholder": "#a6adc8",
|
"text_placeholder": "#7f849c",
|
||||||
"text_accent": "#89b4fa",
|
"text_accent": "#89b4fa",
|
||||||
"icon": "#bac2de",
|
"text_danger": "#f38ba8",
|
||||||
"icon_muted": "#a6adc8",
|
"text_warning": "#fab387",
|
||||||
"icon_accent": "#89b4fa",
|
"icon": "#a6adc8",
|
||||||
"element_foreground": "#11111b",
|
"icon_muted": "#7f849c",
|
||||||
|
"icon_accent": "#b4befe",
|
||||||
|
"element_foreground": "#1e1e2e",
|
||||||
"element_background": "#89b4fa",
|
"element_background": "#89b4fa",
|
||||||
"element_hover": "#b4befe",
|
"element_hover": "#b4befe",
|
||||||
"element_active": "#7ba2e1",
|
"element_active": "#89dceb",
|
||||||
"element_selected": "#6d90c9",
|
"element_selected": "#74c7ec",
|
||||||
"element_disabled": "#89b4fa4d",
|
"element_disabled": "#89b4fa4d",
|
||||||
"secondary_foreground": "#6d90c9",
|
"secondary_foreground": "#cdd6f4",
|
||||||
"secondary_background": "#181825",
|
"secondary_background": "#313244",
|
||||||
"secondary_hover": "#89b4fa33",
|
"secondary_hover": "#45475a",
|
||||||
"secondary_active": "#11111b",
|
"secondary_active": "#585b70",
|
||||||
"secondary_selected": "#11111b",
|
"secondary_selected": "#585b70",
|
||||||
"secondary_disabled": "#89b4fa4d",
|
"secondary_disabled": "#89b4fa4d",
|
||||||
"danger_foreground": "#11111b",
|
"danger_foreground": "#1e1e2e",
|
||||||
"danger_background": "#f38ba8",
|
"danger_background": "#f38ba8",
|
||||||
"danger_hover": "#eba0ac",
|
"danger_hover": "#eba0ac",
|
||||||
"danger_active": "#db7d98",
|
"danger_active": "#fab387",
|
||||||
"danger_selected": "#c46f88",
|
"danger_selected": "#f9e2af",
|
||||||
"danger_disabled": "#f38ba84d",
|
"danger_disabled": "#f38ba84d",
|
||||||
"warning_foreground": "#11111b",
|
"warning_foreground": "#1e1e2e",
|
||||||
"warning_background": "#f9e2af",
|
"warning_background": "#fab387",
|
||||||
"warning_hover": "#fab387",
|
"warning_hover": "#f9e2af",
|
||||||
"warning_active": "#e0cb9e",
|
"warning_active": "#a6e3a1",
|
||||||
"warning_selected": "#c8b48d",
|
"warning_selected": "#94e2d5",
|
||||||
"warning_disabled": "#f9e2af4d",
|
"warning_disabled": "#fab3874d",
|
||||||
"ghost_element_background": "#00000000",
|
"ghost_element_background": "#cdd6f400",
|
||||||
"ghost_element_background_alt": "#313244",
|
"ghost_element_background_alt": "#181825",
|
||||||
"ghost_element_hover": "#cdd6f433",
|
"ghost_element_hover": "#cdd6f40d",
|
||||||
"ghost_element_active": "#45475a",
|
"ghost_element_active": "#cdd6f41a",
|
||||||
"ghost_element_selected": "#45475a",
|
"ghost_element_selected": "#cdd6f41a",
|
||||||
"ghost_element_disabled": "#cdd6f40d",
|
"ghost_element_disabled": "#cdd6f405",
|
||||||
"tab_inactive_background": "#181825",
|
"tab_background": "#11111b",
|
||||||
"tab_inactive_foreground": "#bac2de",
|
"tab_foreground": "#a6adc8",
|
||||||
|
"tab_hover_background": "#cdd6f40d",
|
||||||
"tab_active_background": "#1e1e2e",
|
"tab_active_background": "#1e1e2e",
|
||||||
"tab_active_foreground": "#cdd6f4",
|
"tab_active_foreground": "#cdd6f4",
|
||||||
"tab_hover_foreground": "#b4befe",
|
"scrollbar_thumb_background": "#cdd6f41a",
|
||||||
"scrollbar_thumb_background": "#cdd6f433",
|
"scrollbar_thumb_hover_background": "#cdd6f426",
|
||||||
"scrollbar_thumb_hover_background": "#cdd6f580",
|
"scrollbar_thumb_border": "#cdd6f400",
|
||||||
"scrollbar_thumb_border": "#00000000",
|
"scrollbar_track_background": "#cdd6f400",
|
||||||
"scrollbar_track_background": "#00000000",
|
"scrollbar_track_border": "#cdd6f400",
|
||||||
"scrollbar_track_border": "#45475a",
|
|
||||||
"drop_target_background": "#89b4fa1a",
|
"drop_target_background": "#89b4fa1a",
|
||||||
"cursor": "#f5e0dc",
|
"cursor": "#89b4fa",
|
||||||
"selection": "#9399b240"
|
"selection": "#89b4fa40"
|
||||||
},
|
},
|
||||||
"dark": {
|
"dark": {
|
||||||
"background": "#1e1e2e",
|
"background": "#1e1e2e",
|
||||||
@@ -76,65 +78,67 @@
|
|||||||
"elevated_surface_background": "#11111b",
|
"elevated_surface_background": "#11111b",
|
||||||
"panel_background": "#1e1e2e",
|
"panel_background": "#1e1e2e",
|
||||||
"overlay": "#cdd6f41a",
|
"overlay": "#cdd6f41a",
|
||||||
"title_bar": "#181825",
|
"title_bar": "#11111b",
|
||||||
"title_bar_inactive": "#11111b",
|
"title_bar_inactive": "#1e1e2e",
|
||||||
"window_border": "#6c7086",
|
"window_border": "#45475a",
|
||||||
"border": "#585b70",
|
"border": "#45475a",
|
||||||
"border_variant": "#45475a",
|
"border_variant": "#313244",
|
||||||
"border_focused": "#89b4fa",
|
"border_focused": "#89b4fa",
|
||||||
"border_selected": "#89b4fa",
|
"border_selected": "#89b4fa",
|
||||||
"border_transparent": "#00000000",
|
"border_transparent": "#cdd6f400",
|
||||||
"border_disabled": "#313244",
|
"border_disabled": "#181825",
|
||||||
"ring": "#89b4fa",
|
"ring": "#b4befe",
|
||||||
"text": "#cdd6f4",
|
"text": "#cdd6f4",
|
||||||
"text_muted": "#bac2de",
|
"text_muted": "#a6adc8",
|
||||||
"text_placeholder": "#a6adc8",
|
"text_placeholder": "#7f849c",
|
||||||
"text_accent": "#89b4fa",
|
"text_accent": "#89b4fa",
|
||||||
"icon": "#bac2de",
|
"text_danger": "#f38ba8",
|
||||||
"icon_muted": "#a6adc8",
|
"text_warning": "#fab387",
|
||||||
"icon_accent": "#89b4fa",
|
"icon": "#a6adc8",
|
||||||
"element_foreground": "#11111b",
|
"icon_muted": "#7f849c",
|
||||||
|
"icon_accent": "#b4befe",
|
||||||
|
"element_foreground": "#1e1e2e",
|
||||||
"element_background": "#89b4fa",
|
"element_background": "#89b4fa",
|
||||||
"element_hover": "#b4befe",
|
"element_hover": "#b4befe",
|
||||||
"element_active": "#7ba2e1",
|
"element_active": "#89dceb",
|
||||||
"element_selected": "#6d90c9",
|
"element_selected": "#74c7ec",
|
||||||
"element_disabled": "#89b4fa4d",
|
"element_disabled": "#89b4fa4d",
|
||||||
"secondary_foreground": "#6d90c9",
|
"secondary_foreground": "#cdd6f4",
|
||||||
"secondary_background": "#181825",
|
"secondary_background": "#313244",
|
||||||
"secondary_hover": "#89b4fa33",
|
"secondary_hover": "#45475a",
|
||||||
"secondary_active": "#11111b",
|
"secondary_active": "#585b70",
|
||||||
"secondary_selected": "#11111b",
|
"secondary_selected": "#585b70",
|
||||||
"secondary_disabled": "#89b4fa4d",
|
"secondary_disabled": "#89b4fa4d",
|
||||||
"danger_foreground": "#11111b",
|
"danger_foreground": "#1e1e2e",
|
||||||
"danger_background": "#f38ba8",
|
"danger_background": "#f38ba8",
|
||||||
"danger_hover": "#eba0ac",
|
"danger_hover": "#eba0ac",
|
||||||
"danger_active": "#db7d98",
|
"danger_active": "#fab387",
|
||||||
"danger_selected": "#c46f88",
|
"danger_selected": "#f9e2af",
|
||||||
"danger_disabled": "#f38ba84d",
|
"danger_disabled": "#f38ba84d",
|
||||||
"warning_foreground": "#11111b",
|
"warning_foreground": "#1e1e2e",
|
||||||
"warning_background": "#f9e2af",
|
"warning_background": "#fab387",
|
||||||
"warning_hover": "#fab387",
|
"warning_hover": "#f9e2af",
|
||||||
"warning_active": "#e0cb9e",
|
"warning_active": "#a6e3a1",
|
||||||
"warning_selected": "#c8b48d",
|
"warning_selected": "#94e2d5",
|
||||||
"warning_disabled": "#f9e2af4d",
|
"warning_disabled": "#fab3874d",
|
||||||
"ghost_element_background": "#00000000",
|
"ghost_element_background": "#cdd6f400",
|
||||||
"ghost_element_background_alt": "#313244",
|
"ghost_element_background_alt": "#181825",
|
||||||
"ghost_element_hover": "#cdd6f433",
|
"ghost_element_hover": "#cdd6f40d",
|
||||||
"ghost_element_active": "#45475a",
|
"ghost_element_active": "#cdd6f41a",
|
||||||
"ghost_element_selected": "#45475a",
|
"ghost_element_selected": "#cdd6f41a",
|
||||||
"ghost_element_disabled": "#cdd6f40d",
|
"ghost_element_disabled": "#cdd6f405",
|
||||||
"tab_inactive_background": "#181825",
|
"tab_background": "#11111b",
|
||||||
"tab_inactive_foreground": "#bac2de",
|
"tab_foreground": "#a6adc8",
|
||||||
|
"tab_hover_background": "#cdd6f40d",
|
||||||
"tab_active_background": "#1e1e2e",
|
"tab_active_background": "#1e1e2e",
|
||||||
"tab_active_foreground": "#cdd6f4",
|
"tab_active_foreground": "#cdd6f4",
|
||||||
"tab_hover_foreground": "#b4befe",
|
"scrollbar_thumb_background": "#cdd6f41a",
|
||||||
"scrollbar_thumb_background": "#cdd6f433",
|
"scrollbar_thumb_hover_background": "#cdd6f426",
|
||||||
"scrollbar_thumb_hover_background": "#cdd6f580",
|
"scrollbar_thumb_border": "#cdd6f400",
|
||||||
"scrollbar_thumb_border": "#00000000",
|
"scrollbar_track_background": "#cdd6f400",
|
||||||
"scrollbar_track_background": "#00000000",
|
"scrollbar_track_border": "#cdd6f400",
|
||||||
"scrollbar_track_border": "#45475a",
|
|
||||||
"drop_target_background": "#89b4fa1a",
|
"drop_target_background": "#89b4fa1a",
|
||||||
"cursor": "#f5e0dc",
|
"cursor": "#89b4fa",
|
||||||
"selection": "#9399b240"
|
"selection": "#89b4fa40"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,140 +1,144 @@
|
|||||||
{
|
{
|
||||||
"id": "flexoki",
|
"id": "flexoki",
|
||||||
"name": "Flexoki",
|
"name": "Flexoki",
|
||||||
"author": "Stephan Ango",
|
"author": "Steph Ango (ported by Coop)",
|
||||||
"url": "https://stephango.com/flexoki",
|
"url": "https://stephango.com/flexoki",
|
||||||
"light": {
|
"light": {
|
||||||
"background": "#FFFCF0",
|
"background": "#FFFCF0",
|
||||||
"surface_background": "#F2F0E5",
|
"surface_background": "#F2F0E5",
|
||||||
"elevated_surface_background": "#E6E4D9",
|
"elevated_surface_background": "#E6E4D9",
|
||||||
"panel_background": "#FFFCF0",
|
"panel_background": "#FFFCF0",
|
||||||
"overlay": "#100F0F1a",
|
"overlay": "#100F0F1A",
|
||||||
"title_bar": "#F2F0E5",
|
"title_bar": "#E6E4D9",
|
||||||
"title_bar_inactive": "#E6E4D9",
|
"title_bar_inactive": "#FFFCF0",
|
||||||
"window_border": "#B7B5AC",
|
"window_border": "#CECDC3",
|
||||||
"border": "#CECDC3",
|
"border": "#CECDC3",
|
||||||
"border_variant": "#DAD8CE",
|
"border_variant": "#DAD8CE",
|
||||||
"border_focused": "#205EA6",
|
"border_focused": "#24837B",
|
||||||
"border_selected": "#205EA6",
|
"border_selected": "#24837B",
|
||||||
"border_transparent": "#00000000",
|
"border_transparent": "#100F0F00",
|
||||||
"border_disabled": "#E6E4D9",
|
"border_disabled": "#E6E4D9",
|
||||||
"ring": "#205EA6",
|
"ring": "#3AA99F",
|
||||||
"text": "#100F0F",
|
"text": "#100F0F",
|
||||||
"text_muted": "#6F6E69",
|
"text_muted": "#6F6E69",
|
||||||
"text_placeholder": "#9F9D96",
|
"text_placeholder": "#B7B5AC",
|
||||||
"text_accent": "#205EA6",
|
"text_accent": "#24837B",
|
||||||
|
"text_danger": "#AF3029",
|
||||||
|
"text_warning": "#BC5215",
|
||||||
"icon": "#6F6E69",
|
"icon": "#6F6E69",
|
||||||
"icon_muted": "#9F9D96",
|
"icon_muted": "#B7B5AC",
|
||||||
"icon_accent": "#205EA6",
|
"icon_accent": "#3AA99F",
|
||||||
"element_foreground": "#FFFCF0",
|
"element_foreground": "#FFFCF0",
|
||||||
"element_background": "#205EA6",
|
"element_background": "#24837B",
|
||||||
"element_hover": "#1A4F8C",
|
"element_hover": "#3AA99F",
|
||||||
"element_active": "#163B66",
|
"element_active": "#1C1B1A",
|
||||||
"element_selected": "#133051",
|
"element_selected": "#100F0F",
|
||||||
"element_disabled": "#205EA64d",
|
"element_disabled": "#24837B4D",
|
||||||
"secondary_foreground": "#163B66",
|
"secondary_foreground": "#100F0F",
|
||||||
"secondary_background": "#F2F0E5",
|
"secondary_background": "#E6E4D9",
|
||||||
"secondary_hover": "#205EA61a",
|
"secondary_hover": "#DAD8CE",
|
||||||
"secondary_active": "#E6E4D9",
|
"secondary_active": "#CECDC3",
|
||||||
"secondary_selected": "#E6E4D9",
|
"secondary_selected": "#CECDC3",
|
||||||
"secondary_disabled": "#205EA64d",
|
"secondary_disabled": "#24837B4D",
|
||||||
"danger_foreground": "#FFFCF0",
|
"danger_foreground": "#FFFCF0",
|
||||||
"danger_background": "#D14D41",
|
"danger_background": "#AF3029",
|
||||||
"danger_hover": "#C03E35",
|
"danger_hover": "#D14D41",
|
||||||
"danger_active": "#AF3029",
|
"danger_active": "#1C1B1A",
|
||||||
"danger_selected": "#942822",
|
"danger_selected": "#100F0F",
|
||||||
"danger_disabled": "#D14D414d",
|
"danger_disabled": "#AF30294D",
|
||||||
"warning_foreground": "#100F0F",
|
"warning_foreground": "#FFFCF0",
|
||||||
"warning_background": "#D0A215",
|
"warning_background": "#BC5215",
|
||||||
"warning_hover": "#BE9207",
|
"warning_hover": "#DA702C",
|
||||||
"warning_active": "#AD8301",
|
"warning_active": "#1C1B1A",
|
||||||
"warning_selected": "#8E6B01",
|
"warning_selected": "#100F0F",
|
||||||
"warning_disabled": "#D0A2154d",
|
"warning_disabled": "#BC52154D",
|
||||||
"ghost_element_background": "#00000000",
|
"ghost_element_background": "#100F0F00",
|
||||||
"ghost_element_background_alt": "#E6E4D9",
|
"ghost_element_background_alt": "#F2F0E5",
|
||||||
"ghost_element_hover": "#100F0F1a",
|
"ghost_element_hover": "#100F0F0D",
|
||||||
"ghost_element_active": "#DAD8CE",
|
"ghost_element_active": "#100F0F1A",
|
||||||
"ghost_element_selected": "#DAD8CE",
|
"ghost_element_selected": "#100F0F1A",
|
||||||
"ghost_element_disabled": "#100F0F0d",
|
"ghost_element_disabled": "#100F0F05",
|
||||||
"tab_inactive_background": "#F2F0E5",
|
"tab_background": "#E6E4D9",
|
||||||
"tab_inactive_foreground": "#6F6E69",
|
"tab_foreground": "#6F6E69",
|
||||||
|
"tab_hover_background": "#100F0F0D",
|
||||||
"tab_active_background": "#FFFCF0",
|
"tab_active_background": "#FFFCF0",
|
||||||
"tab_active_foreground": "#100F0F",
|
"tab_active_foreground": "#100F0F",
|
||||||
"tab_hover_foreground": "#205EA6",
|
"scrollbar_thumb_background": "#100F0F1A",
|
||||||
"scrollbar_thumb_background": "#100F0F33",
|
"scrollbar_thumb_hover_background": "#100F0F26",
|
||||||
"scrollbar_thumb_hover_background": "#100F0F4d",
|
"scrollbar_thumb_border": "#100F0F00",
|
||||||
"scrollbar_thumb_border": "#00000000",
|
"scrollbar_track_background": "#100F0F00",
|
||||||
"scrollbar_track_background": "#00000000",
|
"scrollbar_track_border": "#100F0F00",
|
||||||
"scrollbar_track_border": "#DAD8CE",
|
"drop_target_background": "#24837B1A",
|
||||||
"drop_target_background": "#205EA61a",
|
"cursor": "#24837B",
|
||||||
"cursor": "#205EA6",
|
"selection": "#24837B40"
|
||||||
"selection": "#205EA640"
|
|
||||||
},
|
},
|
||||||
"dark": {
|
"dark": {
|
||||||
"background": "#100F0F",
|
"background": "#100F0F",
|
||||||
"surface_background": "#1C1B1A",
|
"surface_background": "#1C1B1A",
|
||||||
"elevated_surface_background": "#282726",
|
"elevated_surface_background": "#282726",
|
||||||
"panel_background": "#100F0F",
|
"panel_background": "#100F0F",
|
||||||
"overlay": "#FFFCF01a",
|
"overlay": "#FFFCF01A",
|
||||||
"title_bar": "#1C1B1A",
|
"title_bar": "#282726",
|
||||||
"title_bar_inactive": "#282726",
|
"title_bar_inactive": "#100F0F",
|
||||||
"window_border": "#575653",
|
"window_border": "#403E3C",
|
||||||
"border": "#403E3C",
|
"border": "#403E3C",
|
||||||
"border_variant": "#343331",
|
"border_variant": "#343331",
|
||||||
"border_focused": "#4385BE",
|
"border_focused": "#3AA99F",
|
||||||
"border_selected": "#4385BE",
|
"border_selected": "#3AA99F",
|
||||||
"border_transparent": "#00000000",
|
"border_transparent": "#100F0F00",
|
||||||
"border_disabled": "#282726",
|
"border_disabled": "#282726",
|
||||||
"ring": "#4385BE",
|
"ring": "#24837B",
|
||||||
"text": "#FFFCF0",
|
"text": "#CECDC3",
|
||||||
"text_muted": "#878580",
|
"text_muted": "#878580",
|
||||||
"text_placeholder": "#6F6E69",
|
"text_placeholder": "#575653",
|
||||||
"text_accent": "#4385BE",
|
"text_accent": "#3AA99F",
|
||||||
|
"text_danger": "#D14D41",
|
||||||
|
"text_warning": "#DA702C",
|
||||||
"icon": "#878580",
|
"icon": "#878580",
|
||||||
"icon_muted": "#6F6E69",
|
"icon_muted": "#575653",
|
||||||
"icon_accent": "#4385BE",
|
"icon_accent": "#24837B",
|
||||||
"element_foreground": "#100F0F",
|
"element_foreground": "#100F0F",
|
||||||
"element_background": "#4385BE",
|
"element_background": "#3AA99F",
|
||||||
"element_hover": "#3171B2",
|
"element_hover": "#24837B",
|
||||||
"element_active": "#205EA6",
|
"element_active": "#CECDC3",
|
||||||
"element_selected": "#1A4F8C",
|
"element_selected": "#F2F0E5",
|
||||||
"element_disabled": "#4385BE4d",
|
"element_disabled": "#3AA99F4D",
|
||||||
"secondary_foreground": "#205EA6",
|
"secondary_foreground": "#CECDC3",
|
||||||
"secondary_background": "#1C1B1A",
|
"secondary_background": "#1C1B1A",
|
||||||
"secondary_hover": "#4385BE1a",
|
"secondary_hover": "#282726",
|
||||||
"secondary_active": "#282726",
|
"secondary_active": "#343331",
|
||||||
"secondary_selected": "#282726",
|
"secondary_selected": "#343331",
|
||||||
"secondary_disabled": "#4385BE4d",
|
"secondary_disabled": "#3AA99F4D",
|
||||||
"danger_foreground": "#100F0F",
|
"danger_foreground": "#100F0F",
|
||||||
"danger_background": "#E8705F",
|
"danger_background": "#D14D41",
|
||||||
"danger_hover": "#D14D41",
|
"danger_hover": "#AF3029",
|
||||||
"danger_active": "#C03E35",
|
"danger_active": "#CECDC3",
|
||||||
"danger_selected": "#AF3029",
|
"danger_selected": "#F2F0E5",
|
||||||
"danger_disabled": "#E8705F4d",
|
"danger_disabled": "#D14D414D",
|
||||||
"warning_foreground": "#100F0F",
|
"warning_foreground": "#100F0F",
|
||||||
"warning_background": "#DFB431",
|
"warning_background": "#DA702C",
|
||||||
"warning_hover": "#D0A215",
|
"warning_hover": "#BC5215",
|
||||||
"warning_active": "#BE9207",
|
"warning_active": "#CECDC3",
|
||||||
"warning_selected": "#AD8301",
|
"warning_selected": "#F2F0E5",
|
||||||
"warning_disabled": "#DFB4314d",
|
"warning_disabled": "#DA702C4D",
|
||||||
"ghost_element_background": "#00000000",
|
"ghost_element_background": "#100F0F00",
|
||||||
"ghost_element_background_alt": "#282726",
|
"ghost_element_background_alt": "#1C1B1A",
|
||||||
"ghost_element_hover": "#FFFCF01a",
|
"ghost_element_hover": "#FFFCF00D",
|
||||||
"ghost_element_active": "#343331",
|
"ghost_element_active": "#FFFCF01A",
|
||||||
"ghost_element_selected": "#343331",
|
"ghost_element_selected": "#FFFCF01A",
|
||||||
"ghost_element_disabled": "#FFFCF00d",
|
"ghost_element_disabled": "#FFFCF005",
|
||||||
"tab_inactive_background": "#1C1B1A",
|
"tab_background": "#282726",
|
||||||
"tab_inactive_foreground": "#878580",
|
"tab_foreground": "#878580",
|
||||||
|
"tab_hover_background": "#FFFCF00D",
|
||||||
"tab_active_background": "#100F0F",
|
"tab_active_background": "#100F0F",
|
||||||
"tab_active_foreground": "#FFFCF0",
|
"tab_active_foreground": "#CECDC3",
|
||||||
"tab_hover_foreground": "#4385BE",
|
"scrollbar_thumb_background": "#FFFCF01A",
|
||||||
"scrollbar_thumb_background": "#FFFCF033",
|
"scrollbar_thumb_hover_background": "#FFFCF026",
|
||||||
"scrollbar_thumb_hover_background": "#FFFCF04d",
|
"scrollbar_thumb_border": "#100F0F00",
|
||||||
"scrollbar_thumb_border": "#00000000",
|
"scrollbar_track_background": "#100F0F00",
|
||||||
"scrollbar_track_background": "#00000000",
|
"scrollbar_track_border": "#100F0F00",
|
||||||
"scrollbar_track_border": "#343331",
|
"drop_target_background": "#3AA99F1A",
|
||||||
"drop_target_background": "#4385BE1a",
|
"cursor": "#3AA99F",
|
||||||
"cursor": "#4385BE",
|
"selection": "#3AA99F40"
|
||||||
"selection": "#4385BE40"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
144
assets/themes/forest.json
Normal file
144
assets/themes/forest.json
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
{
|
||||||
|
"id": "forest",
|
||||||
|
"name": "Forest",
|
||||||
|
"author": "Coop",
|
||||||
|
"url": "https://github.com/lumehq/coop",
|
||||||
|
"light": {
|
||||||
|
"background": "#fbfefcff",
|
||||||
|
"surface_background": "#f4fbf6ff",
|
||||||
|
"elevated_surface_background": "#e9f6e9ff",
|
||||||
|
"panel_background": "#fbfefcff",
|
||||||
|
"overlay": "#193b2d1a",
|
||||||
|
"title_bar": "#e9f6e9ff",
|
||||||
|
"title_bar_inactive": "#fbfefcff",
|
||||||
|
"window_border": "#c4e8d1ff",
|
||||||
|
"border": "#c4e8d1ff",
|
||||||
|
"border_variant": "#b2ddb5ff",
|
||||||
|
"border_focused": "#30a46cff",
|
||||||
|
"border_selected": "#30a46cff",
|
||||||
|
"border_transparent": "#00000000",
|
||||||
|
"border_disabled": "#e0f3e6ff",
|
||||||
|
"ring": "#2b9a66ff",
|
||||||
|
"text": "#193b2dff",
|
||||||
|
"text_muted": "#2f7c57ff",
|
||||||
|
"text_placeholder": "#8eceaaff",
|
||||||
|
"text_accent": "#30a46cff",
|
||||||
|
"text_danger": "#e54d2eff",
|
||||||
|
"text_warning": "#f76b15ff",
|
||||||
|
"icon": "#2f7c57ff",
|
||||||
|
"icon_muted": "#8eceaaff",
|
||||||
|
"icon_accent": "#2b9a66ff",
|
||||||
|
"element_foreground": "#ffffffff",
|
||||||
|
"element_background": "#30a46cff",
|
||||||
|
"element_hover": "#2b9a66ff",
|
||||||
|
"element_active": "#2a7e3bff",
|
||||||
|
"element_selected": "#218358ff",
|
||||||
|
"element_disabled": "#30a46c4d",
|
||||||
|
"secondary_foreground": "#193b2dff",
|
||||||
|
"secondary_background": "#e9f6e9ff",
|
||||||
|
"secondary_hover": "#daf1dbff",
|
||||||
|
"secondary_active": "#c4e8d1ff",
|
||||||
|
"secondary_selected": "#c4e8d1ff",
|
||||||
|
"secondary_disabled": "#30a46c4d",
|
||||||
|
"danger_foreground": "#ffffffff",
|
||||||
|
"danger_background": "#feebe7ff",
|
||||||
|
"danger_hover": "#ffcdc2ff",
|
||||||
|
"danger_active": "#fdbdafff",
|
||||||
|
"danger_selected": "#fdbdafff",
|
||||||
|
"danger_disabled": "#e54d2e4d",
|
||||||
|
"warning_foreground": "#ffffffff",
|
||||||
|
"warning_background": "#fff7edff",
|
||||||
|
"warning_hover": "#ffd19aff",
|
||||||
|
"warning_active": "#ffc182ff",
|
||||||
|
"warning_selected": "#ffc182ff",
|
||||||
|
"warning_disabled": "#f76b154d",
|
||||||
|
"ghost_element_background": "#00000000",
|
||||||
|
"ghost_element_background_alt": "#e9f6e9ff",
|
||||||
|
"ghost_element_hover": "#193b2d0d",
|
||||||
|
"ghost_element_active": "#193b2d1a",
|
||||||
|
"ghost_element_selected": "#193b2d1a",
|
||||||
|
"ghost_element_disabled": "#193b2d05",
|
||||||
|
"tab_background": "#e9f6e9ff",
|
||||||
|
"tab_foreground": "#2f7c57ff",
|
||||||
|
"tab_hover_background": "#193b2d0d",
|
||||||
|
"tab_active_background": "#fbfefcff",
|
||||||
|
"tab_active_foreground": "#193b2dff",
|
||||||
|
"scrollbar_thumb_background": "#193b2d1a",
|
||||||
|
"scrollbar_thumb_hover_background": "#193b2d26",
|
||||||
|
"scrollbar_thumb_border": "#00000000",
|
||||||
|
"scrollbar_track_background": "#00000000",
|
||||||
|
"scrollbar_track_border": "#00000000",
|
||||||
|
"drop_target_background": "#30a46c1a",
|
||||||
|
"cursor": "#30a46cff",
|
||||||
|
"selection": "#30a46c40"
|
||||||
|
},
|
||||||
|
"dark": {
|
||||||
|
"background": "#0e1512ff",
|
||||||
|
"surface_background": "#121b17ff",
|
||||||
|
"elevated_surface_background": "#132d21ff",
|
||||||
|
"panel_background": "#0e1512ff",
|
||||||
|
"overlay": "#b1f1cb1a",
|
||||||
|
"title_bar": "#132d21ff",
|
||||||
|
"title_bar_inactive": "#0e1512ff",
|
||||||
|
"window_border": "#20573eff",
|
||||||
|
"border": "#20573eff",
|
||||||
|
"border_variant": "#174933ff",
|
||||||
|
"border_focused": "#33b074ff",
|
||||||
|
"border_selected": "#33b074ff",
|
||||||
|
"border_transparent": "#00000000",
|
||||||
|
"border_disabled": "#113b29ff",
|
||||||
|
"ring": "#33b074ff",
|
||||||
|
"text": "#b1f1cbff",
|
||||||
|
"text_muted": "#71d083ff",
|
||||||
|
"text_placeholder": "#2f7c57ff",
|
||||||
|
"text_accent": "#3dd68cff",
|
||||||
|
"text_danger": "#ff977dff",
|
||||||
|
"text_warning": "#ffa057ff",
|
||||||
|
"icon": "#71d083ff",
|
||||||
|
"icon_muted": "#2f7c57ff",
|
||||||
|
"icon_accent": "#33b074ff",
|
||||||
|
"element_foreground": "#0e1512ff",
|
||||||
|
"element_background": "#3dd68cff",
|
||||||
|
"element_hover": "#71d083ff",
|
||||||
|
"element_active": "#33b074ff",
|
||||||
|
"element_selected": "#30a46cff",
|
||||||
|
"element_disabled": "#3dd68c4d",
|
||||||
|
"secondary_foreground": "#b1f1cbff",
|
||||||
|
"secondary_background": "#132d21ff",
|
||||||
|
"secondary_hover": "#113b29ff",
|
||||||
|
"secondary_active": "#174933ff",
|
||||||
|
"secondary_selected": "#174933ff",
|
||||||
|
"secondary_disabled": "#3dd68c4d",
|
||||||
|
"danger_foreground": "#181111ff",
|
||||||
|
"danger_background": "#391714ff",
|
||||||
|
"danger_hover": "#5e1c16ff",
|
||||||
|
"danger_active": "#6e2920ff",
|
||||||
|
"danger_selected": "#6e2920ff",
|
||||||
|
"danger_disabled": "#ff977d4d",
|
||||||
|
"warning_foreground": "#17120eff",
|
||||||
|
"warning_background": "#331e0bff",
|
||||||
|
"warning_hover": "#562800ff",
|
||||||
|
"warning_active": "#66350cff",
|
||||||
|
"warning_selected": "#66350cff",
|
||||||
|
"warning_disabled": "#ffa0574d",
|
||||||
|
"ghost_element_background": "#00000000",
|
||||||
|
"ghost_element_background_alt": "#132d21ff",
|
||||||
|
"ghost_element_hover": "#b1f1cb0d",
|
||||||
|
"ghost_element_active": "#b1f1cb1a",
|
||||||
|
"ghost_element_selected": "#b1f1cb1a",
|
||||||
|
"ghost_element_disabled": "#b1f1cb05",
|
||||||
|
"tab_background": "#132d21ff",
|
||||||
|
"tab_foreground": "#71d083ff",
|
||||||
|
"tab_hover_background": "#b1f1cb0d",
|
||||||
|
"tab_active_background": "#0e1512ff",
|
||||||
|
"tab_active_foreground": "#b1f1cbff",
|
||||||
|
"scrollbar_thumb_background": "#b1f1cb1a",
|
||||||
|
"scrollbar_thumb_hover_background": "#b1f1cb26",
|
||||||
|
"scrollbar_thumb_border": "#00000000",
|
||||||
|
"scrollbar_track_background": "#00000000",
|
||||||
|
"scrollbar_track_border": "#00000000",
|
||||||
|
"drop_target_background": "#3dd68c1a",
|
||||||
|
"cursor": "#3dd68cff",
|
||||||
|
"selection": "#3dd68c40"
|
||||||
|
}
|
||||||
|
}
|
||||||
144
assets/themes/ocean.json
Normal file
144
assets/themes/ocean.json
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
{
|
||||||
|
"id": "ocean",
|
||||||
|
"name": "Ocean",
|
||||||
|
"author": "Coop",
|
||||||
|
"url": "https://github.com/lumehq/coop",
|
||||||
|
"light": {
|
||||||
|
"background": "#fafefeff",
|
||||||
|
"surface_background": "#f2fbfaff",
|
||||||
|
"elevated_surface_background": "#e6f7f7ff",
|
||||||
|
"panel_background": "#fafefeff",
|
||||||
|
"overlay": "#00333f1a",
|
||||||
|
"title_bar": "#e6f7f7ff",
|
||||||
|
"title_bar_inactive": "#fafefeff",
|
||||||
|
"window_border": "#cce5e9ff",
|
||||||
|
"border": "#cce5e9ff",
|
||||||
|
"border_variant": "#b8dde3ff",
|
||||||
|
"border_focused": "#00a2c7ff",
|
||||||
|
"border_selected": "#00a2c7ff",
|
||||||
|
"border_transparent": "#00000000",
|
||||||
|
"border_disabled": "#e0f0f2ff",
|
||||||
|
"ring": "#0797b9ff",
|
||||||
|
"text": "#0d3c48ff",
|
||||||
|
"text_muted": "#107d98ff",
|
||||||
|
"text_placeholder": "#60b3d7ff",
|
||||||
|
"text_accent": "#00a2c7ff",
|
||||||
|
"text_danger": "#e54d2eff",
|
||||||
|
"text_warning": "#f76b15ff",
|
||||||
|
"icon": "#107d98ff",
|
||||||
|
"icon_muted": "#60b3d7ff",
|
||||||
|
"icon_accent": "#0797b9ff",
|
||||||
|
"element_foreground": "#ffffffff",
|
||||||
|
"element_background": "#00a2c7ff",
|
||||||
|
"element_hover": "#0797b9ff",
|
||||||
|
"element_active": "#12667eff",
|
||||||
|
"element_selected": "#0d4a5cff",
|
||||||
|
"element_disabled": "#00a2c74d",
|
||||||
|
"secondary_foreground": "#0d4a5cff",
|
||||||
|
"secondary_background": "#ddf9f2ff",
|
||||||
|
"secondary_hover": "#c8f4e9ff",
|
||||||
|
"secondary_active": "#b3ecdeff",
|
||||||
|
"secondary_selected": "#b3ecdeff",
|
||||||
|
"secondary_disabled": "#00a2c74d",
|
||||||
|
"danger_foreground": "#ffffffff",
|
||||||
|
"danger_background": "#feebe7ff",
|
||||||
|
"danger_hover": "#ffcdc2ff",
|
||||||
|
"danger_active": "#fdbdafff",
|
||||||
|
"danger_selected": "#fdbdafff",
|
||||||
|
"danger_disabled": "#e54d2e4d",
|
||||||
|
"warning_foreground": "#ffffffff",
|
||||||
|
"warning_background": "#fff7edff",
|
||||||
|
"warning_hover": "#ffd19aff",
|
||||||
|
"warning_active": "#ffc182ff",
|
||||||
|
"warning_selected": "#ffc182ff",
|
||||||
|
"warning_disabled": "#f76b154d",
|
||||||
|
"ghost_element_background": "#00000000",
|
||||||
|
"ghost_element_background_alt": "#e6f7f7ff",
|
||||||
|
"ghost_element_hover": "#00333f0d",
|
||||||
|
"ghost_element_active": "#00333f1a",
|
||||||
|
"ghost_element_selected": "#00333f1a",
|
||||||
|
"ghost_element_disabled": "#00333f05",
|
||||||
|
"tab_background": "#e6f7f7ff",
|
||||||
|
"tab_foreground": "#107d98ff",
|
||||||
|
"tab_hover_background": "#00333f0d",
|
||||||
|
"tab_active_background": "#fafefeff",
|
||||||
|
"tab_active_foreground": "#0d3c48ff",
|
||||||
|
"scrollbar_thumb_background": "#00333f1a",
|
||||||
|
"scrollbar_thumb_hover_background": "#00333f26",
|
||||||
|
"scrollbar_thumb_border": "#00000000",
|
||||||
|
"scrollbar_track_background": "#00000000",
|
||||||
|
"scrollbar_track_border": "#00000000",
|
||||||
|
"drop_target_background": "#00a2c71a",
|
||||||
|
"cursor": "#00a2c7ff",
|
||||||
|
"selection": "#00a2c740"
|
||||||
|
},
|
||||||
|
"dark": {
|
||||||
|
"background": "#0b161aff",
|
||||||
|
"surface_background": "#101b20ff",
|
||||||
|
"elevated_surface_background": "#082c36ff",
|
||||||
|
"panel_background": "#0b161aff",
|
||||||
|
"overlay": "#c2f3ff1a",
|
||||||
|
"title_bar": "#082c36ff",
|
||||||
|
"title_bar_inactive": "#0b161aff",
|
||||||
|
"window_border": "#1b537bff",
|
||||||
|
"border": "#1b537bff",
|
||||||
|
"border_variant": "#154467ff",
|
||||||
|
"border_focused": "#00a2c7ff",
|
||||||
|
"border_selected": "#00a2c7ff",
|
||||||
|
"border_transparent": "#00000000",
|
||||||
|
"border_disabled": "#112840ff",
|
||||||
|
"ring": "#23afd0ff",
|
||||||
|
"text": "#b6ecf7ff",
|
||||||
|
"text_muted": "#4ccce6ff",
|
||||||
|
"text_placeholder": "#197caeff",
|
||||||
|
"text_accent": "#7ce2feff",
|
||||||
|
"text_danger": "#ff977dff",
|
||||||
|
"text_warning": "#ffa057ff",
|
||||||
|
"icon": "#4ccce6ff",
|
||||||
|
"icon_muted": "#197caeff",
|
||||||
|
"icon_accent": "#23afd0ff",
|
||||||
|
"element_foreground": "#0b161aff",
|
||||||
|
"element_background": "#7ce2feff",
|
||||||
|
"element_hover": "#a8eeffff",
|
||||||
|
"element_active": "#23afd0ff",
|
||||||
|
"element_selected": "#00a2c7ff",
|
||||||
|
"element_disabled": "#7ce2fe4d",
|
||||||
|
"secondary_foreground": "#adf0ddff",
|
||||||
|
"secondary_background": "#0d2d2aff",
|
||||||
|
"secondary_hover": "#023b37ff",
|
||||||
|
"secondary_active": "#084843ff",
|
||||||
|
"secondary_selected": "#084843ff",
|
||||||
|
"secondary_disabled": "#7ce2fe4d",
|
||||||
|
"danger_foreground": "#181111ff",
|
||||||
|
"danger_background": "#391714ff",
|
||||||
|
"danger_hover": "#5e1c16ff",
|
||||||
|
"danger_active": "#6e2920ff",
|
||||||
|
"danger_selected": "#6e2920ff",
|
||||||
|
"danger_disabled": "#ff977d4d",
|
||||||
|
"warning_foreground": "#17120eff",
|
||||||
|
"warning_background": "#331e0bff",
|
||||||
|
"warning_hover": "#562800ff",
|
||||||
|
"warning_active": "#66350cff",
|
||||||
|
"warning_selected": "#66350cff",
|
||||||
|
"warning_disabled": "#ffa0574d",
|
||||||
|
"ghost_element_background": "#00000000",
|
||||||
|
"ghost_element_background_alt": "#082c36ff",
|
||||||
|
"ghost_element_hover": "#c2f3ff0d",
|
||||||
|
"ghost_element_active": "#c2f3ff1a",
|
||||||
|
"ghost_element_selected": "#c2f3ff1a",
|
||||||
|
"ghost_element_disabled": "#c2f3ff05",
|
||||||
|
"tab_background": "#082c36ff",
|
||||||
|
"tab_foreground": "#4ccce6ff",
|
||||||
|
"tab_hover_background": "#c2f3ff0d",
|
||||||
|
"tab_active_background": "#0b161aff",
|
||||||
|
"tab_active_foreground": "#b6ecf7ff",
|
||||||
|
"scrollbar_thumb_background": "#c2f3ff1a",
|
||||||
|
"scrollbar_thumb_hover_background": "#c2f3ff26",
|
||||||
|
"scrollbar_thumb_border": "#00000000",
|
||||||
|
"scrollbar_track_background": "#00000000",
|
||||||
|
"scrollbar_track_border": "#00000000",
|
||||||
|
"drop_target_background": "#7ce2fe1a",
|
||||||
|
"cursor": "#7ce2feff",
|
||||||
|
"selection": "#7ce2fe40"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "rose-pine-dawn",
|
|
||||||
"name": "Rosé Pine Dawn",
|
|
||||||
"author": "Rosé Pine",
|
|
||||||
"url": "https://rosepinetheme.com/",
|
|
||||||
"light": {
|
|
||||||
"background": "#faf4ed",
|
|
||||||
"surface_background": "#fffaf3",
|
|
||||||
"elevated_surface_background": "#f2e9e1",
|
|
||||||
"panel_background": "#faf4ed",
|
|
||||||
"overlay": "#5752791a",
|
|
||||||
"title_bar": "#fffaf3",
|
|
||||||
"title_bar_inactive": "#f2e9e1",
|
|
||||||
"window_border": "#cecacd",
|
|
||||||
"border": "#dfdad9",
|
|
||||||
"border_variant": "#f4ede8",
|
|
||||||
"border_focused": "#907aa9",
|
|
||||||
"border_selected": "#907aa9",
|
|
||||||
"border_transparent": "#00000000",
|
|
||||||
"border_disabled": "#f2e9e1",
|
|
||||||
"ring": "#907aa9",
|
|
||||||
"text": "#575279",
|
|
||||||
"text_muted": "#797593",
|
|
||||||
"text_placeholder": "#9893a5",
|
|
||||||
"text_accent": "#907aa9",
|
|
||||||
"icon": "#797593",
|
|
||||||
"icon_muted": "#9893a5",
|
|
||||||
"icon_accent": "#907aa9",
|
|
||||||
"element_foreground": "#faf4ed",
|
|
||||||
"element_background": "#907aa9",
|
|
||||||
"element_hover": "#907aa9e6",
|
|
||||||
"element_active": "#826b95",
|
|
||||||
"element_selected": "#745c81",
|
|
||||||
"element_disabled": "#907aa94d",
|
|
||||||
"secondary_foreground": "#745c81",
|
|
||||||
"secondary_background": "#fffaf3",
|
|
||||||
"secondary_hover": "#907aa91a",
|
|
||||||
"secondary_active": "#f2e9e1",
|
|
||||||
"secondary_selected": "#f2e9e1",
|
|
||||||
"secondary_disabled": "#907aa94d",
|
|
||||||
"danger_foreground": "#faf4ed",
|
|
||||||
"danger_background": "#b4637a",
|
|
||||||
"danger_hover": "#a7586e",
|
|
||||||
"danger_active": "#9a4d62",
|
|
||||||
"danger_selected": "#8d4256",
|
|
||||||
"danger_disabled": "#b4637a4d",
|
|
||||||
"warning_foreground": "#575279",
|
|
||||||
"warning_background": "#ea9d34",
|
|
||||||
"warning_hover": "#d98e2f",
|
|
||||||
"warning_active": "#c87f2a",
|
|
||||||
"warning_selected": "#b77025",
|
|
||||||
"warning_disabled": "#ea9d344d",
|
|
||||||
"ghost_element_background": "#00000000",
|
|
||||||
"ghost_element_background_alt": "#f2e9e1",
|
|
||||||
"ghost_element_hover": "#5752791a",
|
|
||||||
"ghost_element_active": "#dfdad9",
|
|
||||||
"ghost_element_selected": "#dfdad9",
|
|
||||||
"ghost_element_disabled": "#5752790d",
|
|
||||||
"tab_inactive_background": "#fffaf3",
|
|
||||||
"tab_inactive_foreground": "#797593",
|
|
||||||
"tab_active_background": "#faf4ed",
|
|
||||||
"tab_active_foreground": "#575279",
|
|
||||||
"tab_hover_foreground": "#907aa9",
|
|
||||||
"scrollbar_thumb_background": "#57527933",
|
|
||||||
"scrollbar_thumb_hover_background": "#5752794d",
|
|
||||||
"scrollbar_thumb_border": "#00000000",
|
|
||||||
"scrollbar_track_background": "#00000000",
|
|
||||||
"scrollbar_track_border": "#dfdad9",
|
|
||||||
"drop_target_background": "#907aa91a",
|
|
||||||
"cursor": "#907aa9",
|
|
||||||
"selection": "#907aa940"
|
|
||||||
},
|
|
||||||
"dark": {
|
|
||||||
"background": "#faf4ed",
|
|
||||||
"surface_background": "#fffaf3",
|
|
||||||
"elevated_surface_background": "#f2e9e1",
|
|
||||||
"panel_background": "#faf4ed",
|
|
||||||
"overlay": "#5752791a",
|
|
||||||
"title_bar": "#fffaf3",
|
|
||||||
"title_bar_inactive": "#f2e9e1",
|
|
||||||
"window_border": "#cecacd",
|
|
||||||
"border": "#dfdad9",
|
|
||||||
"border_variant": "#f4ede8",
|
|
||||||
"border_focused": "#907aa9",
|
|
||||||
"border_selected": "#907aa9",
|
|
||||||
"border_transparent": "#00000000",
|
|
||||||
"border_disabled": "#f2e9e1",
|
|
||||||
"ring": "#907aa9",
|
|
||||||
"text": "#575279",
|
|
||||||
"text_muted": "#797593",
|
|
||||||
"text_placeholder": "#9893a5",
|
|
||||||
"text_accent": "#907aa9",
|
|
||||||
"icon": "#797593",
|
|
||||||
"icon_muted": "#9893a5",
|
|
||||||
"icon_accent": "#907aa9",
|
|
||||||
"element_foreground": "#faf4ed",
|
|
||||||
"element_background": "#907aa9",
|
|
||||||
"element_hover": "#907aa9e6",
|
|
||||||
"element_active": "#826b95",
|
|
||||||
"element_selected": "#745c81",
|
|
||||||
"element_disabled": "#907aa94d",
|
|
||||||
"secondary_foreground": "#745c81",
|
|
||||||
"secondary_background": "#fffaf3",
|
|
||||||
"secondary_hover": "#907aa91a",
|
|
||||||
"secondary_active": "#f2e9e1",
|
|
||||||
"secondary_selected": "#f2e9e1",
|
|
||||||
"secondary_disabled": "#907aa94d",
|
|
||||||
"danger_foreground": "#faf4ed",
|
|
||||||
"danger_background": "#b4637a",
|
|
||||||
"danger_hover": "#a7586e",
|
|
||||||
"danger_active": "#9a4d62",
|
|
||||||
"danger_selected": "#8d4256",
|
|
||||||
"danger_disabled": "#b4637a4d",
|
|
||||||
"warning_foreground": "#575279",
|
|
||||||
"warning_background": "#ea9d34",
|
|
||||||
"warning_hover": "#d98e2f",
|
|
||||||
"warning_active": "#c87f2a",
|
|
||||||
"warning_selected": "#b77025",
|
|
||||||
"warning_disabled": "#ea9d344d",
|
|
||||||
"ghost_element_background": "#00000000",
|
|
||||||
"ghost_element_background_alt": "#f2e9e1",
|
|
||||||
"ghost_element_hover": "#5752791a",
|
|
||||||
"ghost_element_active": "#dfdad9",
|
|
||||||
"ghost_element_selected": "#dfdad9",
|
|
||||||
"ghost_element_disabled": "#5752790d",
|
|
||||||
"tab_inactive_background": "#fffaf3",
|
|
||||||
"tab_inactive_foreground": "#797593",
|
|
||||||
"tab_active_background": "#faf4ed",
|
|
||||||
"tab_active_foreground": "#575279",
|
|
||||||
"tab_hover_foreground": "#907aa9",
|
|
||||||
"scrollbar_thumb_background": "#57527933",
|
|
||||||
"scrollbar_thumb_hover_background": "#5752794d",
|
|
||||||
"scrollbar_thumb_border": "#00000000",
|
|
||||||
"scrollbar_track_background": "#00000000",
|
|
||||||
"scrollbar_track_border": "#dfdad9",
|
|
||||||
"drop_target_background": "#907aa91a",
|
|
||||||
"cursor": "#907aa9",
|
|
||||||
"selection": "#907aa940"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "rose-pine-moon",
|
|
||||||
"name": "Rosé Pine Moon",
|
|
||||||
"author": "Rosé Pine",
|
|
||||||
"url": "https://rosepinetheme.com/",
|
|
||||||
"light": {
|
|
||||||
"background": "#232136",
|
|
||||||
"surface_background": "#2a273f",
|
|
||||||
"elevated_surface_background": "#393552",
|
|
||||||
"panel_background": "#232136",
|
|
||||||
"overlay": "#e0def41a",
|
|
||||||
"title_bar": "#2a273f",
|
|
||||||
"title_bar_inactive": "#393552",
|
|
||||||
"window_border": "#56526e",
|
|
||||||
"border": "#44415a",
|
|
||||||
"border_variant": "#393552",
|
|
||||||
"border_focused": "#c4a7e7",
|
|
||||||
"border_selected": "#c4a7e7",
|
|
||||||
"border_transparent": "#00000000",
|
|
||||||
"border_disabled": "#393552",
|
|
||||||
"ring": "#c4a7e7",
|
|
||||||
"text": "#e0def4",
|
|
||||||
"text_muted": "#908caa",
|
|
||||||
"text_placeholder": "#6e6a86",
|
|
||||||
"text_accent": "#c4a7e7",
|
|
||||||
"icon": "#908caa",
|
|
||||||
"icon_muted": "#6e6a86",
|
|
||||||
"icon_accent": "#c4a7e7",
|
|
||||||
"element_foreground": "#232136",
|
|
||||||
"element_background": "#c4a7e7",
|
|
||||||
"element_hover": "#c4a7e7e6",
|
|
||||||
"element_active": "#b296d6",
|
|
||||||
"element_selected": "#a085c5",
|
|
||||||
"element_disabled": "#c4a7e74d",
|
|
||||||
"secondary_foreground": "#a085c5",
|
|
||||||
"secondary_background": "#393552",
|
|
||||||
"secondary_hover": "#c4a7e71a",
|
|
||||||
"secondary_active": "#44415a",
|
|
||||||
"secondary_selected": "#44415a",
|
|
||||||
"secondary_disabled": "#c4a7e74d",
|
|
||||||
"danger_foreground": "#232136",
|
|
||||||
"danger_background": "#eb6f92",
|
|
||||||
"danger_hover": "#e55a82",
|
|
||||||
"danger_active": "#df4572",
|
|
||||||
"danger_selected": "#d93062",
|
|
||||||
"danger_disabled": "#eb6f924d",
|
|
||||||
"warning_foreground": "#232136",
|
|
||||||
"warning_background": "#f6c177",
|
|
||||||
"warning_hover": "#f4b35e",
|
|
||||||
"warning_active": "#f2a545",
|
|
||||||
"warning_selected": "#f0972c",
|
|
||||||
"warning_disabled": "#f6c1774d",
|
|
||||||
"ghost_element_background": "#00000000",
|
|
||||||
"ghost_element_background_alt": "#393552",
|
|
||||||
"ghost_element_hover": "#e0def41a",
|
|
||||||
"ghost_element_active": "#44415a",
|
|
||||||
"ghost_element_selected": "#44415a",
|
|
||||||
"ghost_element_disabled": "#e0def40d",
|
|
||||||
"tab_inactive_background": "#2a273f",
|
|
||||||
"tab_inactive_foreground": "#908caa",
|
|
||||||
"tab_active_background": "#232136",
|
|
||||||
"tab_active_foreground": "#e0def4",
|
|
||||||
"tab_hover_foreground": "#c4a7e7",
|
|
||||||
"scrollbar_thumb_background": "#e0def433",
|
|
||||||
"scrollbar_thumb_hover_background": "#e0def44d",
|
|
||||||
"scrollbar_thumb_border": "#00000000",
|
|
||||||
"scrollbar_track_background": "#00000000",
|
|
||||||
"scrollbar_track_border": "#44415a",
|
|
||||||
"drop_target_background": "#c4a7e71a",
|
|
||||||
"cursor": "#c4a7e7",
|
|
||||||
"selection": "#c4a7e740"
|
|
||||||
},
|
|
||||||
"dark": {
|
|
||||||
"background": "#232136",
|
|
||||||
"surface_background": "#2a273f",
|
|
||||||
"elevated_surface_background": "#393552",
|
|
||||||
"panel_background": "#232136",
|
|
||||||
"overlay": "#e0def41a",
|
|
||||||
"title_bar": "#2a273f",
|
|
||||||
"title_bar_inactive": "#393552",
|
|
||||||
"window_border": "#56526e",
|
|
||||||
"border": "#44415a",
|
|
||||||
"border_variant": "#393552",
|
|
||||||
"border_focused": "#c4a7e7",
|
|
||||||
"border_selected": "#c4a7e7",
|
|
||||||
"border_transparent": "#00000000",
|
|
||||||
"border_disabled": "#393552",
|
|
||||||
"ring": "#c4a7e7",
|
|
||||||
"text": "#e0def4",
|
|
||||||
"text_muted": "#908caa",
|
|
||||||
"text_placeholder": "#6e6a86",
|
|
||||||
"text_accent": "#c4a7e7",
|
|
||||||
"icon": "#908caa",
|
|
||||||
"icon_muted": "#6e6a86",
|
|
||||||
"icon_accent": "#c4a7e7",
|
|
||||||
"element_foreground": "#232136",
|
|
||||||
"element_background": "#c4a7e7",
|
|
||||||
"element_hover": "#c4a7e7e6",
|
|
||||||
"element_active": "#b296d6",
|
|
||||||
"element_selected": "#a085c5",
|
|
||||||
"element_disabled": "#c4a7e74d",
|
|
||||||
"secondary_foreground": "#a085c5",
|
|
||||||
"secondary_background": "#393552",
|
|
||||||
"secondary_hover": "#c4a7e71a",
|
|
||||||
"secondary_active": "#44415a",
|
|
||||||
"secondary_selected": "#44415a",
|
|
||||||
"secondary_disabled": "#c4a7e74d",
|
|
||||||
"danger_foreground": "#232136",
|
|
||||||
"danger_background": "#eb6f92",
|
|
||||||
"danger_hover": "#e55a82",
|
|
||||||
"danger_active": "#df4572",
|
|
||||||
"danger_selected": "#d93062",
|
|
||||||
"danger_disabled": "#eb6f924d",
|
|
||||||
"warning_foreground": "#232136",
|
|
||||||
"warning_background": "#f6c177",
|
|
||||||
"warning_hover": "#f4b35e",
|
|
||||||
"warning_active": "#f2a545",
|
|
||||||
"warning_selected": "#f0972c",
|
|
||||||
"warning_disabled": "#f6c1774d",
|
|
||||||
"ghost_element_background": "#00000000",
|
|
||||||
"ghost_element_background_alt": "#393552",
|
|
||||||
"ghost_element_hover": "#e0def41a",
|
|
||||||
"ghost_element_active": "#44415a",
|
|
||||||
"ghost_element_selected": "#44415a",
|
|
||||||
"ghost_element_disabled": "#e0def40d",
|
|
||||||
"tab_inactive_background": "#2a273f",
|
|
||||||
"tab_inactive_foreground": "#908caa",
|
|
||||||
"tab_active_background": "#232136",
|
|
||||||
"tab_active_foreground": "#e0def4",
|
|
||||||
"tab_hover_foreground": "#c4a7e7",
|
|
||||||
"scrollbar_thumb_background": "#e0def433",
|
|
||||||
"scrollbar_thumb_hover_background": "#e0def44d",
|
|
||||||
"scrollbar_thumb_border": "#00000000",
|
|
||||||
"scrollbar_track_background": "#00000000",
|
|
||||||
"scrollbar_track_border": "#44415a",
|
|
||||||
"drop_target_background": "#c4a7e71a",
|
|
||||||
"cursor": "#c4a7e7",
|
|
||||||
"selection": "#c4a7e740"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "rose-pine",
|
|
||||||
"name": "Rosé Pine",
|
|
||||||
"author": "Rosé Pine",
|
|
||||||
"url": "https://rosepinetheme.com/",
|
|
||||||
"light": {
|
|
||||||
"background": "#191724",
|
|
||||||
"surface_background": "#1f1d2e",
|
|
||||||
"elevated_surface_background": "#26233a",
|
|
||||||
"panel_background": "#191724",
|
|
||||||
"overlay": "#e0def41a",
|
|
||||||
"title_bar": "#1f1d2e",
|
|
||||||
"title_bar_inactive": "#26233a",
|
|
||||||
"window_border": "#524f67",
|
|
||||||
"border": "#403d52",
|
|
||||||
"border_variant": "#26233a",
|
|
||||||
"border_focused": "#c4a7e7",
|
|
||||||
"border_selected": "#c4a7e7",
|
|
||||||
"border_transparent": "#00000000",
|
|
||||||
"border_disabled": "#26233a",
|
|
||||||
"ring": "#c4a7e7",
|
|
||||||
"text": "#e0def4",
|
|
||||||
"text_muted": "#908caa",
|
|
||||||
"text_placeholder": "#6e6a86",
|
|
||||||
"text_accent": "#c4a7e7",
|
|
||||||
"icon": "#908caa",
|
|
||||||
"icon_muted": "#6e6a86",
|
|
||||||
"icon_accent": "#c4a7e7",
|
|
||||||
"element_foreground": "#191724",
|
|
||||||
"element_background": "#c4a7e7",
|
|
||||||
"element_hover": "#c4a7e7e6",
|
|
||||||
"element_active": "#b296d6",
|
|
||||||
"element_selected": "#a085c5",
|
|
||||||
"element_disabled": "#c4a7e74d",
|
|
||||||
"secondary_foreground": "#a085c5",
|
|
||||||
"secondary_background": "#26233a",
|
|
||||||
"secondary_hover": "#c4a7e71a",
|
|
||||||
"secondary_active": "#403d52",
|
|
||||||
"secondary_selected": "#403d52",
|
|
||||||
"secondary_disabled": "#c4a7e74d",
|
|
||||||
"danger_foreground": "#191724",
|
|
||||||
"danger_background": "#eb6f92",
|
|
||||||
"danger_hover": "#e55a82",
|
|
||||||
"danger_active": "#df4572",
|
|
||||||
"danger_selected": "#d93062",
|
|
||||||
"danger_disabled": "#eb6f924d",
|
|
||||||
"warning_foreground": "#191724",
|
|
||||||
"warning_background": "#f6c177",
|
|
||||||
"warning_hover": "#f4b35e",
|
|
||||||
"warning_active": "#f2a545",
|
|
||||||
"warning_selected": "#f0972c",
|
|
||||||
"warning_disabled": "#f6c1774d",
|
|
||||||
"ghost_element_background": "#00000000",
|
|
||||||
"ghost_element_background_alt": "#26233a",
|
|
||||||
"ghost_element_hover": "#e0def41a",
|
|
||||||
"ghost_element_active": "#403d52",
|
|
||||||
"ghost_element_selected": "#403d52",
|
|
||||||
"ghost_element_disabled": "#e0def40d",
|
|
||||||
"tab_inactive_background": "#1f1d2e",
|
|
||||||
"tab_inactive_foreground": "#908caa",
|
|
||||||
"tab_active_background": "#191724",
|
|
||||||
"tab_active_foreground": "#e0def4",
|
|
||||||
"tab_hover_foreground": "#c4a7e7",
|
|
||||||
"scrollbar_thumb_background": "#e0def433",
|
|
||||||
"scrollbar_thumb_hover_background": "#e0def44d",
|
|
||||||
"scrollbar_thumb_border": "#00000000",
|
|
||||||
"scrollbar_track_background": "#00000000",
|
|
||||||
"scrollbar_track_border": "#403d52",
|
|
||||||
"drop_target_background": "#c4a7e71a",
|
|
||||||
"cursor": "#c4a7e7",
|
|
||||||
"selection": "#c4a7e740"
|
|
||||||
},
|
|
||||||
"dark": {
|
|
||||||
"background": "#191724",
|
|
||||||
"surface_background": "#1f1d2e",
|
|
||||||
"elevated_surface_background": "#26233a",
|
|
||||||
"panel_background": "#191724",
|
|
||||||
"overlay": "#e0def41a",
|
|
||||||
"title_bar": "#1f1d2e",
|
|
||||||
"title_bar_inactive": "#26233a",
|
|
||||||
"window_border": "#524f67",
|
|
||||||
"border": "#403d52",
|
|
||||||
"border_variant": "#26233a",
|
|
||||||
"border_focused": "#c4a7e7",
|
|
||||||
"border_selected": "#c4a7e7",
|
|
||||||
"border_transparent": "#00000000",
|
|
||||||
"border_disabled": "#26233a",
|
|
||||||
"ring": "#c4a7e7",
|
|
||||||
"text": "#e0def4",
|
|
||||||
"text_muted": "#908caa",
|
|
||||||
"text_placeholder": "#6e6a86",
|
|
||||||
"text_accent": "#c4a7e7",
|
|
||||||
"icon": "#908caa",
|
|
||||||
"icon_muted": "#6e6a86",
|
|
||||||
"icon_accent": "#c4a7e7",
|
|
||||||
"element_foreground": "#191724",
|
|
||||||
"element_background": "#c4a7e7",
|
|
||||||
"element_hover": "#c4a7e7e6",
|
|
||||||
"element_active": "#b296d6",
|
|
||||||
"element_selected": "#a085c5",
|
|
||||||
"element_disabled": "#c4a7e74d",
|
|
||||||
"secondary_foreground": "#a085c5",
|
|
||||||
"secondary_background": "#26233a",
|
|
||||||
"secondary_hover": "#c4a7e71a",
|
|
||||||
"secondary_active": "#403d52",
|
|
||||||
"secondary_selected": "#403d52",
|
|
||||||
"secondary_disabled": "#c4a7e74d",
|
|
||||||
"danger_foreground": "#191724",
|
|
||||||
"danger_background": "#eb6f92",
|
|
||||||
"danger_hover": "#e55a82",
|
|
||||||
"danger_active": "#df4572",
|
|
||||||
"danger_selected": "#d93062",
|
|
||||||
"danger_disabled": "#eb6f924d",
|
|
||||||
"warning_foreground": "#191724",
|
|
||||||
"warning_background": "#f6c177",
|
|
||||||
"warning_hover": "#f4b35e",
|
|
||||||
"warning_active": "#f2a545",
|
|
||||||
"warning_selected": "#f0972c",
|
|
||||||
"warning_disabled": "#f6c1774d",
|
|
||||||
"ghost_element_background": "#00000000",
|
|
||||||
"ghost_element_background_alt": "#26233a",
|
|
||||||
"ghost_element_hover": "#e0def41a",
|
|
||||||
"ghost_element_active": "#403d52",
|
|
||||||
"ghost_element_selected": "#403d52",
|
|
||||||
"ghost_element_disabled": "#e0def40d",
|
|
||||||
"tab_inactive_background": "#1f1d2e",
|
|
||||||
"tab_inactive_foreground": "#908caa",
|
|
||||||
"tab_active_background": "#191724",
|
|
||||||
"tab_active_foreground": "#e0def4",
|
|
||||||
"tab_hover_foreground": "#c4a7e7",
|
|
||||||
"scrollbar_thumb_background": "#e0def433",
|
|
||||||
"scrollbar_thumb_hover_background": "#e0def44d",
|
|
||||||
"scrollbar_thumb_border": "#00000000",
|
|
||||||
"scrollbar_track_background": "#00000000",
|
|
||||||
"scrollbar_track_border": "#403d52",
|
|
||||||
"drop_target_background": "#c4a7e71a",
|
|
||||||
"cursor": "#c4a7e7",
|
|
||||||
"selection": "#c4a7e740"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,9 +8,8 @@ publish.workspace = true
|
|||||||
common = { path = "../common" }
|
common = { path = "../common" }
|
||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
gpui_tokio.workspace = true
|
instant.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
smol.workspace = true
|
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
@@ -18,4 +17,6 @@ serde_json.workspace = true
|
|||||||
|
|
||||||
semver = "1.0.27"
|
semver = "1.0.27"
|
||||||
tempfile = "3.23.0"
|
tempfile = "3.23.0"
|
||||||
futures.workspace = true
|
|
||||||
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
|
smol.workspace = true
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
#![cfg(not(target_arch = "wasm32"))]
|
||||||
|
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use anyhow::{Context as AnyhowContext, Error, anyhow};
|
use anyhow::{Context as AnyhowContext, Error, anyhow};
|
||||||
use gpui::http_client::{AsyncBody, HttpClient};
|
use gpui::http_client::{AsyncBody, HttpClient};
|
||||||
@@ -9,6 +10,7 @@ use gpui::{
|
|||||||
App, AppContext, AsyncApp, BackgroundExecutor, Context, Entity, Global, Subscription, Task,
|
App, AppContext, AsyncApp, BackgroundExecutor, Context, Entity, Global, Subscription, Task,
|
||||||
Window,
|
Window,
|
||||||
};
|
};
|
||||||
|
use instant::Duration;
|
||||||
use semver::Version;
|
use semver::Version;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use smallvec::{SmallVec, smallvec};
|
use smallvec::{SmallVec, smallvec};
|
||||||
|
|||||||
@@ -13,15 +13,16 @@ settings = { path = "../settings" }
|
|||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
|
instant.workspace = true
|
||||||
|
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
itertools.workspace = true
|
itertools.workspace = true
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
smol.workspace = true
|
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
futures.workspace = true
|
|
||||||
flume.workspace = true
|
flume.workspace = true
|
||||||
serde.workspace = true
|
|
||||||
serde_json.workspace = true
|
|
||||||
|
|
||||||
|
futures.workspace = true
|
||||||
fuzzy-matcher = "0.3.7"
|
fuzzy-matcher = "0.3.7"
|
||||||
|
|
||||||
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
|
smol.workspace = true
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,110 @@
|
|||||||
use std::hash::Hash;
|
use std::hash::Hash;
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
|
|
||||||
use common::{EventUtils, NostrParser};
|
use common::{EventExt, NostrParser, extract_and_remove_media_urls};
|
||||||
|
use gpui::{SharedString, SharedUri};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
|
||||||
|
/// Rendered message.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Message {
|
||||||
|
pub id: EventId,
|
||||||
|
/// Author's public key
|
||||||
|
pub author: PublicKey,
|
||||||
|
/// The content/text of the message
|
||||||
|
pub content: String,
|
||||||
|
/// List of media URLs in the message
|
||||||
|
pub media: Vec<SharedUri>,
|
||||||
|
/// Message created time as unix timestamp
|
||||||
|
pub created_at: Timestamp,
|
||||||
|
/// List of mentioned public keys in the message
|
||||||
|
pub mentions: Vec<Mention>,
|
||||||
|
/// List of event of the message this message is a reply to
|
||||||
|
pub replies_to: Vec<EventId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Event> for Message {
|
||||||
|
fn from(val: &Event) -> Self {
|
||||||
|
let mentions = extract_mentions(&val.content);
|
||||||
|
let replies_to = extract_reply_ids(&val.tags);
|
||||||
|
let (media, string) = extract_and_remove_media_urls(&val.content);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
id: val.id,
|
||||||
|
author: val.pubkey,
|
||||||
|
content: string,
|
||||||
|
media,
|
||||||
|
created_at: val.created_at,
|
||||||
|
mentions,
|
||||||
|
replies_to,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&UnsignedEvent> for Message {
|
||||||
|
fn from(val: &UnsignedEvent) -> Self {
|
||||||
|
let mentions = extract_mentions(&val.content);
|
||||||
|
let replies_to = extract_reply_ids(&val.tags);
|
||||||
|
let (media, string) = extract_and_remove_media_urls(&val.content);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
// Event ID must be known
|
||||||
|
id: val.id.unwrap(),
|
||||||
|
author: val.pubkey,
|
||||||
|
content: string,
|
||||||
|
media,
|
||||||
|
created_at: val.created_at,
|
||||||
|
mentions,
|
||||||
|
replies_to,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&NewMessage> for Message {
|
||||||
|
fn from(val: &NewMessage) -> Self {
|
||||||
|
let mentions = extract_mentions(&val.rumor.content);
|
||||||
|
let replies_to = extract_reply_ids(&val.rumor.tags);
|
||||||
|
let (media, string) = extract_and_remove_media_urls(&val.rumor.content);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
// Event ID must be known
|
||||||
|
id: val.rumor.id.unwrap(),
|
||||||
|
author: val.rumor.pubkey,
|
||||||
|
content: string,
|
||||||
|
media,
|
||||||
|
created_at: val.rumor.created_at,
|
||||||
|
mentions,
|
||||||
|
replies_to,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for Message {}
|
||||||
|
|
||||||
|
impl PartialEq for Message {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.id == other.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for Message {
|
||||||
|
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||||
|
self.created_at.cmp(&other.created_at)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for Message {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hash for Message {
|
||||||
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||||
|
self.id.hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// New message.
|
/// New message.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub struct NewMessage {
|
pub struct NewMessage {
|
||||||
@@ -24,71 +125,22 @@ impl NewMessage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Message.
|
/// Trash message.
|
||||||
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub enum Message {
|
pub struct FailedMessage {
|
||||||
User(RenderedMessage),
|
pub raw_event: SharedString,
|
||||||
Warning(String, Timestamp),
|
pub reason: SharedString,
|
||||||
System(Timestamp),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Message {
|
impl FailedMessage {
|
||||||
pub fn user<I>(user: I) -> Self
|
pub fn new<T>(event: &Event, reason: T) -> Self
|
||||||
where
|
where
|
||||||
I: Into<RenderedMessage>,
|
T: Into<SharedString>,
|
||||||
{
|
{
|
||||||
Self::User(user.into())
|
Self {
|
||||||
|
raw_event: SharedString::from(event.as_json()),
|
||||||
|
reason: reason.into(),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn warning<I>(content: I) -> Self
|
|
||||||
where
|
|
||||||
I: Into<String>,
|
|
||||||
{
|
|
||||||
Self::Warning(content.into(), Timestamp::now())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn system() -> Self {
|
|
||||||
Self::System(Timestamp::default())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn timestamp(&self) -> &Timestamp {
|
|
||||||
match self {
|
|
||||||
Message::User(msg) => &msg.created_at,
|
|
||||||
Message::Warning(_, ts) => ts,
|
|
||||||
Message::System(ts) => ts,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&NewMessage> for Message {
|
|
||||||
fn from(val: &NewMessage) -> Self {
|
|
||||||
Self::User(val.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&UnsignedEvent> for Message {
|
|
||||||
fn from(val: &UnsignedEvent) -> Self {
|
|
||||||
Self::User(val.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Ord for Message {
|
|
||||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
|
||||||
match (self, other) {
|
|
||||||
// System always comes first
|
|
||||||
(Message::System(_), Message::System(_)) => self.timestamp().cmp(other.timestamp()),
|
|
||||||
(Message::System(_), _) => std::cmp::Ordering::Less,
|
|
||||||
(_, Message::System(_)) => std::cmp::Ordering::Greater,
|
|
||||||
|
|
||||||
// For non-system messages, compare by timestamp
|
|
||||||
_ => self.timestamp().cmp(other.timestamp()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialOrd for Message {
|
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
|
||||||
Some(self.cmp(other))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,98 +156,6 @@ impl Mention {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Rendered message.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct RenderedMessage {
|
|
||||||
pub id: EventId,
|
|
||||||
/// Author's public key
|
|
||||||
pub author: PublicKey,
|
|
||||||
/// The content/text of the message
|
|
||||||
pub content: String,
|
|
||||||
/// Message created time as unix timestamp
|
|
||||||
pub created_at: Timestamp,
|
|
||||||
/// List of mentioned public keys in the message
|
|
||||||
pub mentions: Vec<Mention>,
|
|
||||||
/// List of event of the message this message is a reply to
|
|
||||||
pub replies_to: Vec<EventId>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&Event> for RenderedMessage {
|
|
||||||
fn from(val: &Event) -> Self {
|
|
||||||
let mentions = extract_mentions(&val.content);
|
|
||||||
let replies_to = extract_reply_ids(&val.tags);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
id: val.id,
|
|
||||||
author: val.pubkey,
|
|
||||||
content: val.content.clone(),
|
|
||||||
created_at: val.created_at,
|
|
||||||
mentions,
|
|
||||||
replies_to,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&UnsignedEvent> for RenderedMessage {
|
|
||||||
fn from(val: &UnsignedEvent) -> Self {
|
|
||||||
let mentions = extract_mentions(&val.content);
|
|
||||||
let replies_to = extract_reply_ids(&val.tags);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
// Event ID must be known
|
|
||||||
id: val.id.unwrap(),
|
|
||||||
author: val.pubkey,
|
|
||||||
content: val.content.clone(),
|
|
||||||
created_at: val.created_at,
|
|
||||||
mentions,
|
|
||||||
replies_to,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&NewMessage> for RenderedMessage {
|
|
||||||
fn from(val: &NewMessage) -> Self {
|
|
||||||
let mentions = extract_mentions(&val.rumor.content);
|
|
||||||
let replies_to = extract_reply_ids(&val.rumor.tags);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
// Event ID must be known
|
|
||||||
id: val.rumor.id.unwrap(),
|
|
||||||
author: val.rumor.pubkey,
|
|
||||||
content: val.rumor.content.clone(),
|
|
||||||
created_at: val.rumor.created_at,
|
|
||||||
mentions,
|
|
||||||
replies_to,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Eq for RenderedMessage {}
|
|
||||||
|
|
||||||
impl PartialEq for RenderedMessage {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
self.id == other.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Ord for RenderedMessage {
|
|
||||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
|
||||||
self.created_at.cmp(&other.created_at)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialOrd for RenderedMessage {
|
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
|
||||||
Some(self.cmp(other))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Hash for RenderedMessage {
|
|
||||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
|
||||||
self.id.hash(state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extracts all mentions (public keys) from a content string.
|
/// Extracts all mentions (public keys) from a content string.
|
||||||
fn extract_mentions(content: &str) -> Vec<Mention> {
|
fn extract_mentions(content: &str) -> Vec<Mention> {
|
||||||
let parser = NostrParser::new();
|
let parser = NostrParser::new();
|
||||||
@@ -214,13 +174,13 @@ fn extract_mentions(content: &str) -> Vec<Mention> {
|
|||||||
fn extract_reply_ids(inner: &Tags) -> Vec<EventId> {
|
fn extract_reply_ids(inner: &Tags) -> Vec<EventId> {
|
||||||
let mut replies_to = vec![];
|
let mut replies_to = vec![];
|
||||||
|
|
||||||
for tag in inner.filter(TagKind::e()) {
|
for tag in inner.iter().filter(|tag| tag.kind() == "e") {
|
||||||
if let Some(id) = tag.content().and_then(|id| EventId::parse(id).ok()) {
|
if let Some(id) = tag.content().and_then(|id| EventId::parse(id).ok()) {
|
||||||
replies_to.push(id);
|
replies_to.push(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for tag in inner.filter(TagKind::q()) {
|
for tag in inner.iter().filter(|tag| tag.kind() == "q") {
|
||||||
if let Some(id) = tag.content().and_then(|id| EventId::parse(id).ok()) {
|
if let Some(id) = tag.content().and_then(|id| EventId::parse(id).ok()) {
|
||||||
replies_to.push(id);
|
replies_to.push(id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
use std::time::Duration;
|
use instant::Duration;
|
||||||
|
|
||||||
use anyhow::Error;
|
use anyhow::{Error, anyhow};
|
||||||
use common::EventUtils;
|
use common::EventExt;
|
||||||
|
use device::DeviceRegistry;
|
||||||
use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task};
|
use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use person::{Person, PersonRegistry};
|
use person::{Person, PersonRegistry};
|
||||||
use settings::{RoomConfig, SignerKind};
|
use settings::{RoomConfig, SignerKind};
|
||||||
use state::{NostrRegistry, BOOTSTRAP_RELAYS, TIMEOUT};
|
use state::{NostrRegistry, TIMEOUT, UniversalSigner};
|
||||||
|
|
||||||
use crate::NewMessage;
|
use crate::NewMessage;
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ pub struct SendReport {
|
|||||||
pub receiver: PublicKey,
|
pub receiver: PublicKey,
|
||||||
pub gift_wrap_id: Option<EventId>,
|
pub gift_wrap_id: Option<EventId>,
|
||||||
pub error: Option<SharedString>,
|
pub error: Option<SharedString>,
|
||||||
pub output: Option<Output<EventId>>,
|
pub output: Option<Output<EventId, EventSendStatus>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SendReport {
|
impl SendReport {
|
||||||
@@ -42,7 +42,7 @@ impl SendReport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Set the output.
|
/// Set the output.
|
||||||
pub fn output(mut self, output: Output<EventId>) -> Self {
|
pub fn output(mut self, output: Output<EventId, EventSendStatus>) -> Self {
|
||||||
self.output = Some(output);
|
self.output = Some(output);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@@ -58,16 +58,44 @@ impl SendReport {
|
|||||||
|
|
||||||
/// Returns true if the send is pending.
|
/// Returns true if the send is pending.
|
||||||
pub fn pending(&self) -> bool {
|
pub fn pending(&self) -> bool {
|
||||||
self.output.is_none() && self.error.is_none()
|
self.error.is_none()
|
||||||
|
&& self
|
||||||
|
.output
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|o| o.success.is_empty() && o.failed.is_empty())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if the send was successful.
|
/// Returns true if the send was successful.
|
||||||
pub fn success(&self) -> bool {
|
pub fn success(&self) -> bool {
|
||||||
if let Some(output) = self.output.as_ref() {
|
self.error.is_none() && self.output.as_ref().is_some_and(|o| !o.success.is_empty())
|
||||||
!output.failed.is_empty()
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns true if the send failed.
|
||||||
|
pub fn failed(&self) -> bool {
|
||||||
|
self.error.is_some() && self.output.as_ref().is_some_and(|o| !o.failed.is_empty())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum SendStatus {
|
||||||
|
Ok {
|
||||||
|
id: EventId,
|
||||||
|
relay: RelayUrl,
|
||||||
|
},
|
||||||
|
Failed {
|
||||||
|
id: EventId,
|
||||||
|
relay: RelayUrl,
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SendStatus {
|
||||||
|
pub fn ok(id: EventId, relay: RelayUrl) -> Self {
|
||||||
|
Self::Ok { id, relay }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn failed(id: EventId, relay: RelayUrl, message: String) -> Self {
|
||||||
|
Self::Failed { id, relay, message }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,7 +172,8 @@ impl From<&UnsignedEvent> for Room {
|
|||||||
let members = val.extract_public_keys();
|
let members = val.extract_public_keys();
|
||||||
let subject = val
|
let subject = val
|
||||||
.tags
|
.tags
|
||||||
.find(TagKind::Subject)
|
.iter()
|
||||||
|
.find(|tag| tag.kind() == "subject")
|
||||||
.and_then(|tag| tag.content().map(|s| s.to_owned().into()));
|
.and_then(|tag| tag.content().map(|s| s.to_owned().into()));
|
||||||
|
|
||||||
Room {
|
Room {
|
||||||
@@ -178,7 +207,7 @@ impl Room {
|
|||||||
// WARNING: never sign this event
|
// WARNING: never sign this event
|
||||||
let mut event = EventBuilder::new(Kind::PrivateDirectMessage, "")
|
let mut event = EventBuilder::new(Kind::PrivateDirectMessage, "")
|
||||||
.tags(tags)
|
.tags(tags)
|
||||||
.build(author);
|
.finalize_unsigned(author);
|
||||||
|
|
||||||
// Ensure that the ID is set
|
// Ensure that the ID is set
|
||||||
event.ensure_id();
|
event.ensure_id();
|
||||||
@@ -205,11 +234,9 @@ impl Room {
|
|||||||
|
|
||||||
/// Sets this room is ongoing conversation
|
/// Sets this room is ongoing conversation
|
||||||
pub fn set_ongoing(&mut self, cx: &mut Context<Self>) {
|
pub fn set_ongoing(&mut self, cx: &mut Context<Self>) {
|
||||||
if self.kind != RoomKind::Ongoing {
|
|
||||||
self.kind = RoomKind::Ongoing;
|
self.kind = RoomKind::Ongoing;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// Updates the creation timestamp of the room
|
/// Updates the creation timestamp of the room
|
||||||
pub fn set_created_at(&mut self, created_at: impl Into<Timestamp>, cx: &mut Context<Self>) {
|
pub fn set_created_at(&mut self, created_at: impl Into<Timestamp>, cx: &mut Context<Self>) {
|
||||||
@@ -329,46 +356,39 @@ impl Room {
|
|||||||
pub fn connect(&self, cx: &App) -> Task<Result<(), Error>> {
|
pub fn connect(&self, cx: &App) -> Task<Result<(), Error>> {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
|
let members = self.members();
|
||||||
let signer = nostr.read(cx).signer();
|
|
||||||
let sender = signer.public_key();
|
|
||||||
|
|
||||||
// Get room's id
|
|
||||||
let id = self.id;
|
|
||||||
|
|
||||||
// Get all members, excluding the sender
|
|
||||||
let members: Vec<PublicKey> = self
|
|
||||||
.members
|
|
||||||
.iter()
|
|
||||||
.filter(|public_key| Some(**public_key) != sender)
|
|
||||||
.copied()
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
let id = SubscriptionId::new(format!("room-{id}"));
|
|
||||||
let opts = SubscribeAutoCloseOptions::default()
|
let opts = SubscribeAutoCloseOptions::default()
|
||||||
.exit_policy(ReqExitPolicy::ExitOnEOSE)
|
.exit_policy(ReqExitPolicy::ExitOnEOSE)
|
||||||
.timeout(Some(Duration::from_secs(TIMEOUT)));
|
.timeout(Some(Duration::from_secs(TIMEOUT)));
|
||||||
|
|
||||||
// Construct filters for each member
|
let tasks: Vec<_> = members
|
||||||
let filters: Vec<Filter> = members
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|public_key| {
|
.map(|public_key| {
|
||||||
Filter::new()
|
let client = client.clone();
|
||||||
|
async move {
|
||||||
|
let inbox = Filter::new()
|
||||||
.author(public_key)
|
.author(public_key)
|
||||||
.kind(Kind::RelayList)
|
.kind(Kind::InboxRelays)
|
||||||
.limit(1)
|
.limit(1);
|
||||||
|
|
||||||
|
let announcement = Filter::new()
|
||||||
|
.author(public_key)
|
||||||
|
.kind(Kind::Custom(10044))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
client
|
||||||
|
.subscribe(vec![inbox, announcement])
|
||||||
|
.close_on(opts)
|
||||||
|
.await
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Construct target for subscription
|
for result in futures::future::join_all(tasks).await {
|
||||||
let target: HashMap<&str, Vec<Filter>> = BOOTSTRAP_RELAYS
|
result?;
|
||||||
.into_iter()
|
}
|
||||||
.map(|relay| (relay, filters.clone()))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Subscribe to the target
|
|
||||||
client.subscribe(target).close_on(opts).with_id(id).await?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
@@ -378,12 +398,12 @@ impl Room {
|
|||||||
pub fn get_messages(&self, cx: &App) -> Task<Result<Vec<UnsignedEvent>, Error>> {
|
pub fn get_messages(&self, cx: &App) -> Task<Result<Vec<UnsignedEvent>, Error>> {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
let conversation_id = self.id.to_string();
|
let room_id = self.id.to_string();
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
let filter = Filter::new()
|
let filter = Filter::new()
|
||||||
.kind(Kind::ApplicationSpecificData)
|
.kind(Kind::ApplicationSpecificData)
|
||||||
.custom_tag(SingleLetterTag::lowercase(Alphabet::C), conversation_id);
|
.custom_tag(SingleLetterTag::lowercase(Alphabet::R), room_id);
|
||||||
|
|
||||||
let messages = client
|
let messages = client
|
||||||
.database()
|
.database()
|
||||||
@@ -412,24 +432,14 @@ impl Room {
|
|||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
|
||||||
// Get current user's public key
|
// Get current user's public key
|
||||||
let sender = nostr.read(cx).signer().public_key()?;
|
let sender = nostr.read(cx).current_user()?;
|
||||||
|
|
||||||
// Get all members, excluding the sender
|
|
||||||
let members: Vec<Person> = self
|
|
||||||
.members
|
|
||||||
.iter()
|
|
||||||
.filter(|public_key| public_key != &&sender)
|
|
||||||
.map(|member| persons.read(cx).get(member, cx))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Construct event's tags
|
// Construct event's tags
|
||||||
let mut tags = vec![];
|
let mut tags = vec![];
|
||||||
|
|
||||||
// Add subject tag if present
|
// Add subject tag if present
|
||||||
if let Some(value) = self.subject.as_ref() {
|
if let Some(value) = self.subject.as_ref() {
|
||||||
tags.push(Tag::from_standardized_without_cell(TagStandard::Subject(
|
tags.push(Tag::custom("subject", vec![value.to_string()]));
|
||||||
value.to_string(),
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add all reply tags
|
// Add all reply tags
|
||||||
@@ -437,21 +447,23 @@ impl Room {
|
|||||||
tags.push(Tag::event(id))
|
tags.push(Tag::event(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add all receiver tags
|
// Add all receiver tags (no intermediate allocation)
|
||||||
for member in members.into_iter() {
|
for public_key in self.members.iter().filter(|pk| *pk != &sender) {
|
||||||
tags.push(Tag::from_standardized_without_cell(
|
let member = persons.read(cx).get(public_key, cx);
|
||||||
TagStandard::PublicKey {
|
tags.push(
|
||||||
|
Nip01Tag::PublicKey {
|
||||||
public_key: member.public_key(),
|
public_key: member.public_key(),
|
||||||
relay_url: member.messaging_relay_hint(),
|
relay_hint: member.messaging_relay_hint(),
|
||||||
alias: None,
|
}
|
||||||
uppercase: false,
|
.to_tag(),
|
||||||
},
|
);
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct a direct message rumor event
|
// Construct a direct message rumor event
|
||||||
// WARNING: never sign and send this event to relays
|
// WARNING: never sign and send this event to relays
|
||||||
let mut event = EventBuilder::new(kind, content).tags(tags).build(sender);
|
let mut event = EventBuilder::new(kind, content)
|
||||||
|
.tags(tags)
|
||||||
|
.finalize_unsigned(sender);
|
||||||
|
|
||||||
// Ensure that the ID is set
|
// Ensure that the ID is set
|
||||||
event.ensure_id();
|
event.ensure_id();
|
||||||
@@ -459,17 +471,45 @@ impl Room {
|
|||||||
Some(event)
|
Some(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Select the appropriate signer based on signer kind and available keys.
|
||||||
|
fn select_signer(
|
||||||
|
signer_kind: &SignerKind,
|
||||||
|
has_announcement: bool,
|
||||||
|
encryption_signer: &Option<UniversalSigner>,
|
||||||
|
user_signer: &UniversalSigner,
|
||||||
|
) -> UniversalSigner {
|
||||||
|
match signer_kind {
|
||||||
|
SignerKind::Auto => {
|
||||||
|
if has_announcement {
|
||||||
|
encryption_signer
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| user_signer.clone())
|
||||||
|
} else {
|
||||||
|
user_signer.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SignerKind::Encryption => encryption_signer
|
||||||
|
.clone()
|
||||||
|
.expect("encryption signer must be set"),
|
||||||
|
SignerKind::User => user_signer.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Send rumor event to all members's messaging relays
|
/// Send rumor event to all members's messaging relays
|
||||||
pub fn send(&self, rumor: UnsignedEvent, cx: &App) -> Option<Task<Vec<SendReport>>> {
|
pub fn send(&self, rumor: UnsignedEvent, cx: &App) -> Option<Task<Vec<SendReport>>> {
|
||||||
let config = self.config.clone();
|
let config = self.config.clone();
|
||||||
let persons = PersonRegistry::global(cx);
|
|
||||||
|
let device = DeviceRegistry::global(cx);
|
||||||
|
let encryption_signer = device.read(cx).signer(cx);
|
||||||
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
let signer = nostr.read(cx).signer();
|
let user_signer = nostr.read(cx).signer();
|
||||||
|
let current_user = nostr.read(cx).current_user()?;
|
||||||
|
|
||||||
// Get current user's public key
|
// Get sender's profile
|
||||||
let public_key = nostr.read(cx).signer().public_key()?;
|
let persons = PersonRegistry::global(cx);
|
||||||
let sender = persons.read(cx).get(&public_key, cx);
|
let sender = persons.read(cx).get(¤t_user, cx);
|
||||||
|
|
||||||
// Get all members (excluding sender)
|
// Get all members (excluding sender)
|
||||||
let members: Vec<Person> = self
|
let members: Vec<Person> = self
|
||||||
@@ -483,65 +523,45 @@ impl Room {
|
|||||||
let signer_kind = config.signer_kind();
|
let signer_kind = config.signer_kind();
|
||||||
let backup = config.backup();
|
let backup = config.backup();
|
||||||
|
|
||||||
let user_signer = signer.get().await;
|
|
||||||
let encryption_signer = signer.get_encryption_signer().await;
|
|
||||||
|
|
||||||
let mut sents = 0;
|
let mut sents = 0;
|
||||||
let mut reports = Vec::new();
|
let mut reports = Vec::new();
|
||||||
|
|
||||||
// Process each member
|
// Process each member
|
||||||
for member in members {
|
for member in members {
|
||||||
let relays = member.messaging_relays();
|
|
||||||
let announcement = member.announcement();
|
let announcement = member.announcement();
|
||||||
let public_key = member.public_key();
|
let public_key = member.public_key();
|
||||||
|
|
||||||
if relays.is_empty() {
|
|
||||||
reports.push(SendReport::new(public_key).error("No messaging relays"));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle encryption signer requirements
|
// Handle encryption signer requirements
|
||||||
if signer_kind.encryption() {
|
if signer_kind.encryption() {
|
||||||
|
// Receiver didn't set up a decoupled encryption key
|
||||||
if announcement.is_none() {
|
if announcement.is_none() {
|
||||||
reports.push(SendReport::new(public_key).error(NO_DEKEY));
|
reports.push(SendReport::new(public_key).error(NO_DEKEY));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sender didn't set up a decoupled encryption key
|
||||||
if encryption_signer.is_none() {
|
if encryption_signer.is_none() {
|
||||||
reports.push(SendReport::new(sender.public_key()).error(USER_NO_DEKEY));
|
reports.push(SendReport::new(sender.public_key()).error(USER_NO_DEKEY));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine receiver and signer
|
// Determine the signer to use
|
||||||
let (receiver, signer) = match signer_kind {
|
let signer = Self::select_signer(
|
||||||
SignerKind::Auto => {
|
signer_kind,
|
||||||
if let Some(announcement) = announcement {
|
announcement.is_some(),
|
||||||
if let Some(enc_signer) = encryption_signer.as_ref() {
|
&encryption_signer,
|
||||||
(announcement.public_key(), enc_signer.clone())
|
&user_signer,
|
||||||
} else {
|
);
|
||||||
(member.public_key(), user_signer.clone())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
(member.public_key(), user_signer.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SignerKind::Encryption => {
|
|
||||||
// Safe to unwrap due to earlier checks
|
|
||||||
(
|
|
||||||
announcement.unwrap().public_key(),
|
|
||||||
encryption_signer.as_ref().unwrap().clone(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
SignerKind::User => (member.public_key(), user_signer.clone()),
|
|
||||||
};
|
|
||||||
|
|
||||||
match send_gift_wrap(&client, &signer, &receiver, &rumor, relays, public_key).await
|
// Send the gift wrap event and collect the report
|
||||||
{
|
match send_gift_wrap(&client, &signer, &member, &rumor, signer_kind).await {
|
||||||
Ok((report, _)) => {
|
Ok(report) => {
|
||||||
reports.push(report);
|
reports.push(report);
|
||||||
sents += 1;
|
sents += 1;
|
||||||
}
|
}
|
||||||
Err(report) => {
|
Err(error) => {
|
||||||
|
let report = SendReport::new(public_key).error(error.to_string());
|
||||||
reports.push(report);
|
reports.push(report);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -549,14 +569,22 @@ impl Room {
|
|||||||
|
|
||||||
// Send backup to current user if needed
|
// Send backup to current user if needed
|
||||||
if backup && sents >= 1 {
|
if backup && sents >= 1 {
|
||||||
let relays = sender.messaging_relays();
|
|
||||||
let public_key = sender.public_key();
|
let public_key = sender.public_key();
|
||||||
let signer = encryption_signer.as_ref().unwrap_or(&user_signer);
|
|
||||||
|
|
||||||
match send_gift_wrap(&client, signer, &public_key, &rumor, relays, public_key).await
|
// Determine the signer to use
|
||||||
{
|
let signer = Self::select_signer(
|
||||||
Ok((report, _)) => reports.push(report),
|
signer_kind,
|
||||||
Err(report) => reports.push(report),
|
sender.announcement().is_some(),
|
||||||
|
&encryption_signer,
|
||||||
|
&user_signer,
|
||||||
|
);
|
||||||
|
|
||||||
|
match send_gift_wrap(&client, &signer, &sender, &rumor, signer_kind).await {
|
||||||
|
Ok(report) => reports.push(report),
|
||||||
|
Err(error) => {
|
||||||
|
let report = SendReport::new(public_key).error(error.to_string());
|
||||||
|
reports.push(report);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -566,39 +594,54 @@ impl Room {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to send a gift-wrapped event
|
// Helper function to send a gift-wrapped event
|
||||||
async fn send_gift_wrap<T>(
|
async fn send_gift_wrap(
|
||||||
client: &Client,
|
client: &Client,
|
||||||
signer: &T,
|
signer: &UniversalSigner,
|
||||||
receiver: &PublicKey,
|
receiver: &Person,
|
||||||
rumor: &UnsignedEvent,
|
rumor: &UnsignedEvent,
|
||||||
relays: &[RelayUrl],
|
config: &SignerKind,
|
||||||
public_key: PublicKey,
|
) -> Result<SendReport, Error> {
|
||||||
) -> Result<(SendReport, bool), SendReport>
|
let k_tag = Tag::custom("k", vec!["14"]);
|
||||||
where
|
let mut extra_tags = vec![k_tag];
|
||||||
T: NostrSigner + 'static,
|
|
||||||
{
|
|
||||||
// Ensure relay connections
|
|
||||||
for url in relays {
|
|
||||||
client.add_relay(url).and_connect().await.ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
match EventBuilder::gift_wrap(signer, receiver, rumor.clone(), []).await {
|
// Determine the receiver public key based on the config
|
||||||
Ok(event) => {
|
let receiver = match config {
|
||||||
match client
|
SignerKind::Auto => {
|
||||||
|
if let Some(announcement) = receiver.announcement().as_ref() {
|
||||||
|
extra_tags.push(Tag::public_key(receiver.public_key()));
|
||||||
|
announcement.public_key()
|
||||||
|
} else {
|
||||||
|
receiver.public_key()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SignerKind::Encryption => {
|
||||||
|
if let Some(announcement) = receiver.announcement().as_ref() {
|
||||||
|
extra_tags.push(Tag::public_key(receiver.public_key()));
|
||||||
|
announcement.public_key()
|
||||||
|
} else {
|
||||||
|
return Err(anyhow!("User has no encryption announcement"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SignerKind::User => receiver.public_key(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Construct the gift wrap event
|
||||||
|
let event = nip59::GiftWrapBuilder::new(receiver, rumor.clone())
|
||||||
|
.extra_tags(extra_tags)
|
||||||
|
.finalize_async(signer)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Send the gift wrap event and collect the report
|
||||||
|
let report = client
|
||||||
.send_event(&event)
|
.send_event(&event)
|
||||||
.to(relays)
|
.to_nip17()
|
||||||
.ack_policy(AckPolicy::none())
|
.ack_policy(AckPolicy::none())
|
||||||
.await
|
.await
|
||||||
{
|
.map(|output| {
|
||||||
Ok(output) => Ok((
|
SendReport::new(receiver)
|
||||||
SendReport::new(public_key)
|
|
||||||
.gift_wrap_id(event.id)
|
.gift_wrap_id(event.id)
|
||||||
.output(output),
|
.output(output)
|
||||||
true,
|
})?;
|
||||||
)),
|
|
||||||
Err(e) => Err(SendReport::new(public_key).error(e.to_string())),
|
Ok(report)
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => Err(SendReport::new(public_key).error(e.to_string())),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,19 +14,15 @@ chat = { path = "../chat" }
|
|||||||
settings = { path = "../settings" }
|
settings = { path = "../settings" }
|
||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
gpui_tokio.workspace = true
|
|
||||||
|
|
||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
itertools.workspace = true
|
itertools.workspace = true
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
smol.workspace = true
|
|
||||||
flume.workspace = true
|
flume.workspace = true
|
||||||
|
futures.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
|
||||||
|
|
||||||
once_cell = "1.19.0"
|
|
||||||
regex = "1"
|
|
||||||
linkify = "0.10.0"
|
linkify = "0.10.0"
|
||||||
pulldown-cmark = "0.13.1"
|
pulldown-cmark = "0.13.1"
|
||||||
|
|
||||||
|
|||||||
@@ -7,21 +7,11 @@ use settings::SignerKind;
|
|||||||
#[action(namespace = chat, no_json)]
|
#[action(namespace = chat, no_json)]
|
||||||
pub enum Command {
|
pub enum Command {
|
||||||
Insert(&'static str),
|
Insert(&'static str),
|
||||||
ChangeSubject(&'static str),
|
ChangeSubject(String),
|
||||||
ChangeSigner(SignerKind),
|
ChangeSigner(SignerKind),
|
||||||
ToggleBackup,
|
ToggleBackup,
|
||||||
|
Copy(PublicKey),
|
||||||
|
Relays(PublicKey),
|
||||||
|
Njump(PublicKey),
|
||||||
|
Trace(EventId),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
|
|
||||||
#[action(namespace = chat, no_json)]
|
|
||||||
pub struct SeenOn(pub EventId);
|
|
||||||
|
|
||||||
/// Define a open public key action
|
|
||||||
#[derive(Action, Clone, PartialEq, Eq, Deserialize, Debug)]
|
|
||||||
#[action(namespace = pubkey, no_json)]
|
|
||||||
pub struct OpenPublicKey(pub PublicKey);
|
|
||||||
|
|
||||||
/// Define a copy inline public key action
|
|
||||||
#[derive(Action, Clone, PartialEq, Eq, Deserialize, Debug)]
|
|
||||||
#[action(namespace = pubkey, no_json)]
|
|
||||||
pub struct CopyPublicKey(pub PublicKey);
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,60 +0,0 @@
|
|||||||
use gpui::{
|
|
||||||
div, App, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString,
|
|
||||||
Styled, Window,
|
|
||||||
};
|
|
||||||
use theme::ActiveTheme;
|
|
||||||
use ui::input::{InputState, TextInput};
|
|
||||||
use ui::{v_flex, Sizable};
|
|
||||||
|
|
||||||
pub fn init(subject: Option<String>, window: &mut Window, cx: &mut App) -> Entity<Subject> {
|
|
||||||
cx.new(|cx| Subject::new(subject, window, cx))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Subject {
|
|
||||||
input: Entity<InputState>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Subject {
|
|
||||||
pub fn new(subject: Option<String>, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
|
||||||
let input = cx.new(|cx| InputState::new(window, cx).placeholder("Plan for holiday"));
|
|
||||||
|
|
||||||
if let Some(value) = subject {
|
|
||||||
input.update(cx, |this, cx| {
|
|
||||||
this.set_value(value, window, cx);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Self { input }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_subject(&self, cx: &App) -> SharedString {
|
|
||||||
self.input.read(cx).value()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for Subject {
|
|
||||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
v_flex()
|
|
||||||
.gap_2()
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.gap_1()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_sm()
|
|
||||||
.text_color(cx.theme().text_muted)
|
|
||||||
.child(SharedString::from("Subject:")),
|
|
||||||
)
|
|
||||||
.child(TextInput::new(&self.input).small()),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_xs()
|
|
||||||
.italic()
|
|
||||||
.text_color(cx.theme().text_placeholder)
|
|
||||||
.child(SharedString::from(
|
|
||||||
"Subject will be updated when you send a new message.",
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -69,6 +69,7 @@ impl RenderedText {
|
|||||||
|
|
||||||
pub fn element(&self, id: ElementId, window: &Window, cx: &App) -> AnyElement {
|
pub fn element(&self, id: ElementId, window: &Window, cx: &App) -> AnyElement {
|
||||||
let code_background = cx.theme().elevated_surface_background;
|
let code_background = cx.theme().elevated_surface_background;
|
||||||
|
let color = cx.theme().text_accent;
|
||||||
|
|
||||||
InteractiveText::new(
|
InteractiveText::new(
|
||||||
id,
|
id,
|
||||||
@@ -100,6 +101,7 @@ impl RenderedText {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Highlight::Mention => HighlightStyle {
|
Highlight::Mention => HighlightStyle {
|
||||||
|
color: Some(color),
|
||||||
underline: Some(UnderlineStyle {
|
underline: Some(UnderlineStyle {
|
||||||
thickness: 1.0.into(),
|
thickness: 1.0.into(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|||||||
@@ -7,16 +7,14 @@ publish.workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
nostr.workspace = true
|
nostr.workspace = true
|
||||||
|
instant.workspace = true
|
||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
|
|
||||||
anyhow.workspace = true
|
|
||||||
itertools.workspace = true
|
itertools.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
smallvec.workspace = true
|
|
||||||
smol.workspace = true
|
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
|
|
||||||
dirs = "5.0"
|
dirs = "5.0"
|
||||||
qrcode = "0.14.1"
|
qrcode = "0.14.1"
|
||||||
bech32 = "0.11.1"
|
regex = "1.10"
|
||||||
|
|||||||
135
crates/common/src/caching.rs
Normal file
135
crates/common/src/caching.rs
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
use std::collections::{HashMap, VecDeque};
|
||||||
|
use std::mem::take;
|
||||||
|
|
||||||
|
use futures::FutureExt;
|
||||||
|
use gpui::{
|
||||||
|
App, AppContext, Asset, AssetLogger, ElementId, Entity, ImageAssetLoader, ImageCache,
|
||||||
|
ImageCacheItem, ImageCacheProvider, ImageSource, Resource, hash,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn coop_cache(id: impl Into<ElementId>, max_items: usize) -> CoopImageCacheProvider {
|
||||||
|
CoopImageCacheProvider {
|
||||||
|
id: id.into(),
|
||||||
|
max_items,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CoopImageCacheProvider {
|
||||||
|
id: ElementId,
|
||||||
|
max_items: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImageCacheProvider for CoopImageCacheProvider {
|
||||||
|
fn provide(&mut self, window: &mut gpui::Window, cx: &mut App) -> gpui::AnyImageCache {
|
||||||
|
window
|
||||||
|
.with_global_id(self.id.clone(), |id, window| {
|
||||||
|
window.with_element_state(id, |cache, _| {
|
||||||
|
let cache = cache.unwrap_or_else(|| CoopImageCache::new(self.max_items, cx));
|
||||||
|
(cache.clone(), cache)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CoopImageCache {
|
||||||
|
max_items: usize,
|
||||||
|
usage_list: VecDeque<u64>,
|
||||||
|
cache: HashMap<u64, (ImageCacheItem, Resource)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CoopImageCache {
|
||||||
|
pub fn new(max_items: usize, cx: &mut App) -> Entity<Self> {
|
||||||
|
cx.new(|cx| {
|
||||||
|
log::info!("Creating CoopImageCache");
|
||||||
|
cx.on_release(|this: &mut Self, cx| {
|
||||||
|
for (ix, (mut image, resource)) in take(&mut this.cache) {
|
||||||
|
if let Some(Ok(image)) = image.get() {
|
||||||
|
log::info!("Dropping image {ix}");
|
||||||
|
cx.drop_image(image, None);
|
||||||
|
}
|
||||||
|
ImageSource::Resource(resource).remove_asset(cx);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
CoopImageCache {
|
||||||
|
max_items,
|
||||||
|
usage_list: VecDeque::with_capacity(max_items),
|
||||||
|
cache: HashMap::with_capacity(max_items),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImageCache for CoopImageCache {
|
||||||
|
fn load(
|
||||||
|
&mut self,
|
||||||
|
resource: &Resource,
|
||||||
|
window: &mut gpui::Window,
|
||||||
|
cx: &mut gpui::App,
|
||||||
|
) -> Option<Result<std::sync::Arc<gpui::RenderImage>, gpui::ImageCacheError>> {
|
||||||
|
let hash = hash(resource);
|
||||||
|
|
||||||
|
if let Some(item) = self.cache.get_mut(&hash) {
|
||||||
|
let current_idx = self
|
||||||
|
.usage_list
|
||||||
|
.iter()
|
||||||
|
.position(|item| *item == hash)
|
||||||
|
.expect("cache has an item usage_list doesn't");
|
||||||
|
|
||||||
|
self.usage_list.remove(current_idx);
|
||||||
|
self.usage_list.push_front(hash);
|
||||||
|
|
||||||
|
return item.0.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
let load_future = AssetLogger::<ImageAssetLoader>::load(resource.clone(), cx);
|
||||||
|
let task = cx.background_executor().spawn(load_future).shared();
|
||||||
|
|
||||||
|
if self.usage_list.len() >= self.max_items {
|
||||||
|
log::info!("Image cache is full, evicting oldest item");
|
||||||
|
|
||||||
|
if let Some(oldest) = self.usage_list.pop_back() {
|
||||||
|
let mut image = self
|
||||||
|
.cache
|
||||||
|
.remove(&oldest)
|
||||||
|
.expect("usage_list has an item cache doesn't");
|
||||||
|
|
||||||
|
if let Some(Ok(image)) = image.0.get() {
|
||||||
|
log::info!("requesting image to be dropped");
|
||||||
|
cx.drop_image(image, Some(window));
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageSource::Resource(image.1).remove_asset(cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.cache.insert(
|
||||||
|
hash,
|
||||||
|
(
|
||||||
|
gpui::ImageCacheItem::Loading(task.clone()),
|
||||||
|
resource.clone(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
self.usage_list.push_front(hash);
|
||||||
|
|
||||||
|
let entity = window.current_view();
|
||||||
|
|
||||||
|
window
|
||||||
|
.spawn(cx, async move |cx| {
|
||||||
|
let result = task.await;
|
||||||
|
|
||||||
|
if let Err(err) = result {
|
||||||
|
log::error!("error loading image into cache: {:?}", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.on_next_frame(move |_, cx| {
|
||||||
|
cx.notify(entity);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
use std::time::Duration;
|
use instant::Duration;
|
||||||
|
|
||||||
use futures::channel::oneshot;
|
use futures::channel::oneshot;
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::{anyhow, Error};
|
|
||||||
use chrono::{Local, TimeZone};
|
use chrono::{Local, TimeZone};
|
||||||
use gpui::{Image, ImageFormat, SharedString};
|
use gpui::{Image, ImageFormat, SharedString};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use qrcode::render::svg;
|
|
||||||
use qrcode::QrCode;
|
use qrcode::QrCode;
|
||||||
|
use qrcode::render::svg;
|
||||||
|
|
||||||
const NOW: &str = "now";
|
const NOW: &str = "now";
|
||||||
const SECONDS_IN_MINUTE: i64 = 60;
|
const SECONDS_IN_MINUTE: i64 = 60;
|
||||||
@@ -13,12 +12,12 @@ const MINUTES_IN_HOUR: i64 = 60;
|
|||||||
const HOURS_IN_DAY: i64 = 24;
|
const HOURS_IN_DAY: i64 = 24;
|
||||||
const DAYS_IN_MONTH: i64 = 30;
|
const DAYS_IN_MONTH: i64 = 30;
|
||||||
|
|
||||||
pub trait RenderedTimestamp {
|
pub trait TimestampExt {
|
||||||
fn to_human_time(&self) -> SharedString;
|
fn to_human_time(&self) -> SharedString;
|
||||||
fn to_ago(&self) -> SharedString;
|
fn to_ago(&self) -> SharedString;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderedTimestamp for Timestamp {
|
impl TimestampExt for Timestamp {
|
||||||
fn to_human_time(&self) -> SharedString {
|
fn to_human_time(&self) -> SharedString {
|
||||||
let input_time = match Local.timestamp_opt(self.as_secs() as i64, 0) {
|
let input_time = match Local.timestamp_opt(self.as_secs() as i64, 0) {
|
||||||
chrono::LocalResult::Single(time) => time,
|
chrono::LocalResult::Single(time) => time,
|
||||||
@@ -61,23 +60,11 @@ impl RenderedTimestamp for Timestamp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait TextUtils {
|
pub trait StringExt {
|
||||||
fn to_public_key(&self) -> Result<PublicKey, Error>;
|
|
||||||
fn to_qr(&self) -> Option<Arc<Image>>;
|
fn to_qr(&self) -> Option<Arc<Image>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: AsRef<str>> TextUtils for T {
|
impl<T: AsRef<str>> StringExt for T {
|
||||||
fn to_public_key(&self) -> Result<PublicKey, Error> {
|
|
||||||
let s = self.as_ref();
|
|
||||||
if s.starts_with("nprofile1") {
|
|
||||||
Ok(Nip19Profile::from_bech32(s)?.public_key)
|
|
||||||
} else if s.starts_with("npub1") {
|
|
||||||
Ok(PublicKey::parse(s)?)
|
|
||||||
} else {
|
|
||||||
Err(anyhow!("Invalid public key"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn to_qr(&self) -> Option<Arc<Image>> {
|
fn to_qr(&self) -> Option<Arc<Image>> {
|
||||||
let s = self.as_ref();
|
let s = self.as_ref();
|
||||||
let code = QrCode::new(s).unwrap();
|
let code = QrCode::new(s).unwrap();
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ use std::hash::{DefaultHasher, Hash, Hasher};
|
|||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
|
||||||
pub trait EventUtils {
|
pub trait EventExt {
|
||||||
fn uniq_id(&self) -> u64;
|
fn uniq_id(&self) -> u64;
|
||||||
fn extract_public_keys(&self) -> Vec<PublicKey>;
|
fn extract_public_keys(&self) -> Vec<PublicKey>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventUtils for Event {
|
impl EventExt for Event {
|
||||||
fn uniq_id(&self) -> u64 {
|
fn uniq_id(&self) -> u64 {
|
||||||
let mut hasher = DefaultHasher::new();
|
let mut hasher = DefaultHasher::new();
|
||||||
let mut pubkeys: Vec<PublicKey> = self.extract_public_keys();
|
let mut pubkeys: Vec<PublicKey> = self.extract_public_keys();
|
||||||
@@ -18,14 +18,14 @@ impl EventUtils for Event {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn extract_public_keys(&self) -> Vec<PublicKey> {
|
fn extract_public_keys(&self) -> Vec<PublicKey> {
|
||||||
let mut public_keys: Vec<PublicKey> = self.tags.public_keys().copied().collect();
|
let mut public_keys: Vec<PublicKey> = self.tags.public_keys().collect();
|
||||||
public_keys.push(self.pubkey);
|
public_keys.push(self.pubkey);
|
||||||
|
|
||||||
public_keys.into_iter().unique().collect()
|
public_keys.into_iter().unique().collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventUtils for UnsignedEvent {
|
impl EventExt for UnsignedEvent {
|
||||||
fn uniq_id(&self) -> u64 {
|
fn uniq_id(&self) -> u64 {
|
||||||
let mut hasher = DefaultHasher::new();
|
let mut hasher = DefaultHasher::new();
|
||||||
let mut pubkeys: Vec<PublicKey> = vec![];
|
let mut pubkeys: Vec<PublicKey> = vec![];
|
||||||
@@ -46,7 +46,7 @@ impl EventUtils for UnsignedEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn extract_public_keys(&self) -> Vec<PublicKey> {
|
fn extract_public_keys(&self) -> Vec<PublicKey> {
|
||||||
let mut public_keys: Vec<PublicKey> = self.tags.public_keys().copied().collect();
|
let mut public_keys: Vec<PublicKey> = self.tags.public_keys().collect();
|
||||||
public_keys.push(self.pubkey);
|
public_keys.push(self.pubkey);
|
||||||
public_keys.into_iter().unique().sorted().collect()
|
public_keys.into_iter().unique().sorted().collect()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
|
pub use caching::*;
|
||||||
pub use debounced_delay::*;
|
pub use debounced_delay::*;
|
||||||
pub use display::*;
|
pub use display::*;
|
||||||
pub use event::*;
|
pub use event::*;
|
||||||
|
pub use media_extractor::*;
|
||||||
pub use parser::*;
|
pub use parser::*;
|
||||||
pub use paths::*;
|
pub use paths::*;
|
||||||
pub use range::*;
|
pub use range::*;
|
||||||
|
|
||||||
|
mod caching;
|
||||||
mod debounced_delay;
|
mod debounced_delay;
|
||||||
mod display;
|
mod display;
|
||||||
mod event;
|
mod event;
|
||||||
|
mod media_extractor;
|
||||||
mod parser;
|
mod parser;
|
||||||
mod paths;
|
mod paths;
|
||||||
mod range;
|
mod range;
|
||||||
|
|||||||
117
crates/common/src/media_extractor.rs
Normal file
117
crates/common/src/media_extractor.rs
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
use gpui::SharedUri;
|
||||||
|
use regex::Regex;
|
||||||
|
|
||||||
|
/// Extracts media URLs from a string and returns both the extracted URLs
|
||||||
|
/// and the string with media URLs removed
|
||||||
|
pub struct MediaExtractor {
|
||||||
|
image_regex: Regex,
|
||||||
|
video_regex: Regex,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MediaExtractor {
|
||||||
|
/// Creates a new MediaExtractor with compiled regex patterns
|
||||||
|
pub fn new() -> Self {
|
||||||
|
MediaExtractor {
|
||||||
|
// Match common image extensions
|
||||||
|
image_regex: Regex::new(
|
||||||
|
r#"(?i)\bhttps?://[^\s<>"']+\.(?:jpg|jpeg|png|gif|bmp|webp|svg|ico)(?:\?[^\s<>"']*)?\b"#,
|
||||||
|
).unwrap(),
|
||||||
|
// Match common video extensions
|
||||||
|
video_regex: Regex::new(
|
||||||
|
r#"(?i)\bhttps?://[^\s<>"']+\.(?:mp4|mov|avi|mkv|webm|flv|wmv|m4v|3gp)(?:\?[^\s<>"']*)?\b"#,
|
||||||
|
).unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extracts all media URLs from a string
|
||||||
|
pub fn extract_media_urls(&self, text: &str) -> Vec<SharedUri> {
|
||||||
|
let mut urls = Vec::new();
|
||||||
|
|
||||||
|
// Extract image URLs
|
||||||
|
for capture in self.image_regex.find_iter(text) {
|
||||||
|
urls.push(capture.as_str().to_string().into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract video URLs
|
||||||
|
// for capture in self.video_regex.find_iter(text) {
|
||||||
|
// urls.push(capture.as_str().to_string().into());
|
||||||
|
// }
|
||||||
|
|
||||||
|
urls
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes all media URLs from a string and returns the cleaned text
|
||||||
|
pub fn remove_media_urls(&self, text: &str) -> String {
|
||||||
|
let mut result = text.to_string();
|
||||||
|
|
||||||
|
// Remove image URLs
|
||||||
|
result = self.image_regex.replace_all(&result, "").to_string();
|
||||||
|
|
||||||
|
// Remove video URLs
|
||||||
|
// result = self.video_regex.replace_all(&result, "").to_string();
|
||||||
|
|
||||||
|
// Clean up extra whitespace that might result from removal
|
||||||
|
self.cleanup_text(&result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extracts media URLs and removes them from the string, returning both
|
||||||
|
pub fn extract_and_remove(&self, text: &str) -> (Vec<SharedUri>, String) {
|
||||||
|
let urls = self.extract_media_urls(text);
|
||||||
|
let cleaned_text = self.remove_media_urls(text);
|
||||||
|
(urls, cleaned_text)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper function to clean up text after URL removal
|
||||||
|
fn cleanup_text(&self, text: &str) -> String {
|
||||||
|
let text = text.trim();
|
||||||
|
|
||||||
|
// Remove multiple consecutive spaces
|
||||||
|
let re = Regex::new(r"\s+").unwrap();
|
||||||
|
re.replace_all(text, " ").trim().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates if a URL is a valid media URL
|
||||||
|
pub fn is_media_url(&self, url: &str) -> bool {
|
||||||
|
self.image_regex.is_match(url) || self.video_regex.is_match(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Categorizes extracted URLs into images and videos
|
||||||
|
pub fn categorize_urls(&self, urls: &[SharedUri]) -> (Vec<SharedUri>, Vec<SharedUri>) {
|
||||||
|
let mut images = Vec::new();
|
||||||
|
let mut videos = Vec::new();
|
||||||
|
|
||||||
|
for url in urls {
|
||||||
|
if self.image_regex.is_match(url) {
|
||||||
|
images.push(url.clone());
|
||||||
|
} else if self.video_regex.is_match(url) {
|
||||||
|
videos.push(url.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(images, videos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MediaExtractor {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience function for one-time extraction and removal
|
||||||
|
pub fn extract_and_remove_media_urls(text: &str) -> (Vec<SharedUri>, String) {
|
||||||
|
let extractor = MediaExtractor::new();
|
||||||
|
extractor.extract_and_remove(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience function for just extracting media URLs
|
||||||
|
pub fn extract_media_urls(text: &str) -> Vec<SharedUri> {
|
||||||
|
let extractor = MediaExtractor::new();
|
||||||
|
extractor.extract_media_urls(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience function for just removing media URLs
|
||||||
|
pub fn remove_media_urls(text: &str) -> String {
|
||||||
|
let extractor = MediaExtractor::new();
|
||||||
|
extractor.remove_media_urls(text)
|
||||||
|
}
|
||||||
@@ -7,6 +7,13 @@ pub fn home_dir() -> &'static PathBuf {
|
|||||||
HOME_DIR.get_or_init(|| dirs::home_dir().expect("failed to determine home directory"))
|
HOME_DIR.get_or_init(|| dirs::home_dir().expect("failed to determine home directory"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the path to the user's download directory.
|
||||||
|
pub fn download_dir() -> &'static PathBuf {
|
||||||
|
static DOWNLOAD_DIR: OnceLock<PathBuf> = OnceLock::new();
|
||||||
|
DOWNLOAD_DIR
|
||||||
|
.get_or_init(|| dirs::download_dir().expect("failed to determine download directory"))
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the path to the configuration directory used by Coop.
|
/// Returns the path to the configuration directory used by Coop.
|
||||||
pub fn config_dir() -> &'static PathBuf {
|
pub fn config_dir() -> &'static PathBuf {
|
||||||
static CONFIG_DIR: OnceLock<PathBuf> = OnceLock::new();
|
static CONFIG_DIR: OnceLock<PathBuf> = OnceLock::new();
|
||||||
@@ -56,9 +63,3 @@ pub fn support_dir() -> &'static PathBuf {
|
|||||||
config_dir().clone()
|
config_dir().clone()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the path to the `nostr` file.
|
|
||||||
pub fn nostr_file() -> &'static PathBuf {
|
|
||||||
static NOSTR_FILE: OnceLock<PathBuf> = OnceLock::new();
|
|
||||||
NOSTR_FILE.get_or_init(|| support_dir().join("nostr-db"))
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "coop"
|
|
||||||
version.workspace = true
|
|
||||||
edition.workspace = true
|
|
||||||
publish.workspace = true
|
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "coop"
|
|
||||||
path = "src/main.rs"
|
|
||||||
|
|
||||||
[package.metadata.packager]
|
|
||||||
name = "Coop"
|
|
||||||
product-name = "Coop"
|
|
||||||
description = "Chat Freely, Stay Private on Nostr"
|
|
||||||
identifier = "su.reya.coop"
|
|
||||||
category = "SocialNetworking"
|
|
||||||
version = "1.0.0-beta"
|
|
||||||
out-dir = "../../dist"
|
|
||||||
before-packaging-command = "cargo build --release"
|
|
||||||
resources = ["Cargo.toml", "src"]
|
|
||||||
icons = [
|
|
||||||
"resources/32x32.png",
|
|
||||||
"resources/128x128.png",
|
|
||||||
"resources/128x128@2x.png",
|
|
||||||
"resources/icon.icns",
|
|
||||||
"resources/icon.ico",
|
|
||||||
]
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
assets = { path = "../assets" }
|
|
||||||
ui = { path = "../ui" }
|
|
||||||
title_bar = { path = "../title_bar" }
|
|
||||||
theme = { path = "../theme" }
|
|
||||||
common = { path = "../common" }
|
|
||||||
state = { path = "../state" }
|
|
||||||
device = { path = "../device" }
|
|
||||||
chat = { path = "../chat" }
|
|
||||||
chat_ui = { path = "../chat_ui" }
|
|
||||||
settings = { path = "../settings" }
|
|
||||||
auto_update = { path = "../auto_update" }
|
|
||||||
person = { path = "../person" }
|
|
||||||
relay_auth = { path = "../relay_auth" }
|
|
||||||
|
|
||||||
gpui.workspace = true
|
|
||||||
gpui_platform.workspace = true
|
|
||||||
gpui_linux.workspace = true
|
|
||||||
gpui_windows.workspace = true
|
|
||||||
gpui_macos.workspace = true
|
|
||||||
gpui_tokio.workspace = true
|
|
||||||
reqwest_client.workspace = true
|
|
||||||
|
|
||||||
nostr-connect.workspace = true
|
|
||||||
nostr-sdk.workspace = true
|
|
||||||
|
|
||||||
anyhow.workspace = true
|
|
||||||
serde.workspace = true
|
|
||||||
serde_json.workspace = true
|
|
||||||
itertools.workspace = true
|
|
||||||
log.workspace = true
|
|
||||||
smallvec.workspace = true
|
|
||||||
smol.workspace = true
|
|
||||||
futures.workspace = true
|
|
||||||
oneshot.workspace = true
|
|
||||||
webbrowser.workspace = true
|
|
||||||
|
|
||||||
indexset = "0.12.3"
|
|
||||||
tracing-subscriber = { version = "0.3.18", features = ["fmt", "env-filter"] }
|
|
||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
|
||||||
# Temporary workaround https://github.com/zed-industries/zed/issues/47168
|
|
||||||
core-text = "=21.0.0"
|
|
||||||
@@ -1,256 +0,0 @@
|
|||||||
use anyhow::Error;
|
|
||||||
use gpui::prelude::FluentBuilder;
|
|
||||||
use gpui::{
|
|
||||||
div, px, App, AppContext, Context, Entity, InteractiveElement, IntoElement, ParentElement,
|
|
||||||
Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Task, Window,
|
|
||||||
};
|
|
||||||
use nostr_sdk::prelude::*;
|
|
||||||
use person::PersonRegistry;
|
|
||||||
use state::{NostrRegistry, SignerEvent};
|
|
||||||
use theme::ActiveTheme;
|
|
||||||
use ui::avatar::Avatar;
|
|
||||||
use ui::button::{Button, ButtonVariants};
|
|
||||||
use ui::indicator::Indicator;
|
|
||||||
use ui::{h_flex, v_flex, Disableable, Icon, IconName, Sizable, WindowExtension};
|
|
||||||
|
|
||||||
use crate::dialogs::connect::ConnectSigner;
|
|
||||||
use crate::dialogs::import::ImportKey;
|
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<AccountSelector> {
|
|
||||||
cx.new(|cx| AccountSelector::new(window, cx))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Account selector
|
|
||||||
pub struct AccountSelector {
|
|
||||||
/// Public key currently being chosen for login
|
|
||||||
logging_in: Entity<Option<PublicKey>>,
|
|
||||||
|
|
||||||
/// The error message displayed when an error occurs.
|
|
||||||
error: Entity<Option<SharedString>>,
|
|
||||||
|
|
||||||
/// Async tasks
|
|
||||||
tasks: Vec<Task<Result<(), Error>>>,
|
|
||||||
|
|
||||||
/// Subscription to the signer events
|
|
||||||
_subscription: Option<Subscription>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AccountSelector {
|
|
||||||
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
|
||||||
let logging_in = cx.new(|_| None);
|
|
||||||
let error = cx.new(|_| None);
|
|
||||||
|
|
||||||
// Subscribe to the signer events
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
|
||||||
let subscription = cx.subscribe_in(&nostr, window, |this, _state, event, window, cx| {
|
|
||||||
match event {
|
|
||||||
SignerEvent::Set => {
|
|
||||||
window.close_all_modals(cx);
|
|
||||||
window.refresh();
|
|
||||||
}
|
|
||||||
SignerEvent::Error(e) => {
|
|
||||||
this.set_error(e.to_string(), cx);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
Self {
|
|
||||||
logging_in,
|
|
||||||
error,
|
|
||||||
tasks: vec![],
|
|
||||||
_subscription: Some(subscription),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn logging_in(&self, public_key: &PublicKey, cx: &App) -> bool {
|
|
||||||
self.logging_in.read(cx) == &Some(*public_key)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_logging_in(&mut self, public_key: PublicKey, cx: &mut Context<Self>) {
|
|
||||||
self.logging_in.update(cx, |this, cx| {
|
|
||||||
*this = Some(public_key);
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_error<T>(&mut self, error: T, cx: &mut Context<Self>)
|
|
||||||
where
|
|
||||||
T: Into<SharedString>,
|
|
||||||
{
|
|
||||||
self.error.update(cx, |this, cx| {
|
|
||||||
*this = Some(error.into());
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
|
|
||||||
self.logging_in.update(cx, |this, cx| {
|
|
||||||
*this = None;
|
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn login(&mut self, public_key: PublicKey, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
|
||||||
let task = nostr.read(cx).get_signer(&public_key, cx);
|
|
||||||
|
|
||||||
// Mark the public key as being logged in
|
|
||||||
self.set_logging_in(public_key, cx);
|
|
||||||
|
|
||||||
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
|
||||||
match task.await {
|
|
||||||
Ok(signer) => {
|
|
||||||
nostr.update(cx, |this, cx| {
|
|
||||||
this.set_signer(signer, cx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.set_error(e.to_string(), cx);
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Ok(())
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove(&mut self, public_key: PublicKey, cx: &mut Context<Self>) {
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
|
||||||
|
|
||||||
nostr.update(cx, |this, cx| {
|
|
||||||
this.remove_signer(&public_key, cx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn open_import(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let import = cx.new(|cx| ImportKey::new(window, cx));
|
|
||||||
|
|
||||||
window.open_modal(cx, move |this, _window, _cx| {
|
|
||||||
this.width(px(460.))
|
|
||||||
.title("Import a Secret Key or Bunker Connection")
|
|
||||||
.show_close(true)
|
|
||||||
.pb_2()
|
|
||||||
.child(import.clone())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn open_connect(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let connect = cx.new(|cx| ConnectSigner::new(window, cx));
|
|
||||||
|
|
||||||
window.open_modal(cx, move |this, _window, _cx| {
|
|
||||||
this.width(px(460.))
|
|
||||||
.title("Scan QR Code to Connect")
|
|
||||||
.show_close(true)
|
|
||||||
.pb_2()
|
|
||||||
.child(connect.clone())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for AccountSelector {
|
|
||||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
let persons = PersonRegistry::global(cx);
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
|
||||||
let npubs = nostr.read(cx).npubs();
|
|
||||||
let loading = self.logging_in.read(cx).is_some();
|
|
||||||
|
|
||||||
v_flex()
|
|
||||||
.size_full()
|
|
||||||
.gap_2()
|
|
||||||
.when_some(self.error.read(cx).as_ref(), |this, error| {
|
|
||||||
this.child(
|
|
||||||
div()
|
|
||||||
.italic()
|
|
||||||
.text_xs()
|
|
||||||
.text_center()
|
|
||||||
.text_color(cx.theme().danger_active)
|
|
||||||
.child(error.clone()),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.children({
|
|
||||||
let mut items = vec![];
|
|
||||||
|
|
||||||
for (ix, public_key) in npubs.read(cx).iter().enumerate() {
|
|
||||||
let profile = persons.read(cx).get(public_key, cx);
|
|
||||||
let logging_in = self.logging_in(public_key, cx);
|
|
||||||
|
|
||||||
items.push(
|
|
||||||
h_flex()
|
|
||||||
.id(ix)
|
|
||||||
.group("")
|
|
||||||
.px_2()
|
|
||||||
.h_10()
|
|
||||||
.justify_between()
|
|
||||||
.w_full()
|
|
||||||
.rounded(cx.theme().radius)
|
|
||||||
.bg(cx.theme().ghost_element_background)
|
|
||||||
.hover(|this| this.bg(cx.theme().ghost_element_hover))
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.gap_2()
|
|
||||||
.child(Avatar::new(profile.avatar()).small())
|
|
||||||
.child(div().text_sm().child(profile.name())),
|
|
||||||
)
|
|
||||||
.when(logging_in, |this| this.child(Indicator::new().small()))
|
|
||||||
.when(!logging_in, |this| {
|
|
||||||
this.child(
|
|
||||||
h_flex()
|
|
||||||
.gap_1()
|
|
||||||
.invisible()
|
|
||||||
.group_hover("", |this| this.visible())
|
|
||||||
.child(
|
|
||||||
Button::new(format!("del-{ix}"))
|
|
||||||
.icon(IconName::Close)
|
|
||||||
.ghost()
|
|
||||||
.small()
|
|
||||||
.disabled(logging_in)
|
|
||||||
.on_click(cx.listener({
|
|
||||||
let public_key = *public_key;
|
|
||||||
move |this, _ev, _window, cx| {
|
|
||||||
cx.stop_propagation();
|
|
||||||
this.remove(public_key, cx);
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.when(!logging_in, |this| {
|
|
||||||
let public_key = *public_key;
|
|
||||||
this.on_click(cx.listener(move |this, _ev, window, cx| {
|
|
||||||
this.login(public_key, window, cx);
|
|
||||||
}))
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
items
|
|
||||||
})
|
|
||||||
.child(div().w_full().h_px().bg(cx.theme().border_variant))
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.gap_1()
|
|
||||||
.justify_end()
|
|
||||||
.w_full()
|
|
||||||
.child(
|
|
||||||
Button::new("input")
|
|
||||||
.icon(Icon::new(IconName::Usb))
|
|
||||||
.label("Import")
|
|
||||||
.ghost()
|
|
||||||
.small()
|
|
||||||
.disabled(loading)
|
|
||||||
.on_click(cx.listener(move |this, _ev, window, cx| {
|
|
||||||
this.open_import(window, cx);
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
Button::new("qr")
|
|
||||||
.icon(Icon::new(IconName::Scan))
|
|
||||||
.label("Scan QR to connect")
|
|
||||||
.ghost()
|
|
||||||
.small()
|
|
||||||
.disabled(loading)
|
|
||||||
.on_click(cx.listener(move |this, _ev, window, cx| {
|
|
||||||
this.open_connect(window, cx);
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use common::TextUtils;
|
|
||||||
use gpui::prelude::FluentBuilder;
|
|
||||||
use gpui::{
|
|
||||||
div, img, px, AppContext, Context, Entity, Image, IntoElement, ParentElement, Render,
|
|
||||||
SharedString, Styled, Subscription, Window,
|
|
||||||
};
|
|
||||||
use nostr_connect::prelude::*;
|
|
||||||
use state::{
|
|
||||||
CoopAuthUrlHandler, NostrRegistry, SignerEvent, CLIENT_NAME, NOSTR_CONNECT_RELAY,
|
|
||||||
NOSTR_CONNECT_TIMEOUT,
|
|
||||||
};
|
|
||||||
use theme::ActiveTheme;
|
|
||||||
use ui::v_flex;
|
|
||||||
|
|
||||||
pub struct ConnectSigner {
|
|
||||||
/// QR Code
|
|
||||||
qr_code: Option<Arc<Image>>,
|
|
||||||
|
|
||||||
/// Error message
|
|
||||||
error: Entity<Option<SharedString>>,
|
|
||||||
|
|
||||||
/// Subscription to the signer event
|
|
||||||
_subscription: Option<Subscription>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ConnectSigner {
|
|
||||||
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
|
||||||
let error = cx.new(|_| None);
|
|
||||||
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
|
||||||
let app_keys = nostr.read(cx).app_keys.clone();
|
|
||||||
|
|
||||||
let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT);
|
|
||||||
let relay = RelayUrl::parse(NOSTR_CONNECT_RELAY).unwrap();
|
|
||||||
|
|
||||||
// Generate the nostr connect uri
|
|
||||||
let uri = NostrConnectUri::client(app_keys.public_key(), vec![relay], CLIENT_NAME);
|
|
||||||
|
|
||||||
// Generate the nostr connect
|
|
||||||
let mut signer = NostrConnect::new(uri.clone(), app_keys.clone(), timeout, None).unwrap();
|
|
||||||
|
|
||||||
// Handle the auth request
|
|
||||||
signer.auth_url_handler(CoopAuthUrlHandler);
|
|
||||||
|
|
||||||
// Generate a QR code for quick connection
|
|
||||||
let qr_code = uri.to_string().to_qr();
|
|
||||||
|
|
||||||
// Set signer in the background
|
|
||||||
nostr.update(cx, |this, cx| {
|
|
||||||
this.add_nip46_signer(&signer, cx);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Subscribe to the signer event
|
|
||||||
let subscription = cx.subscribe_in(&nostr, window, |this, _state, event, _window, cx| {
|
|
||||||
if let SignerEvent::Error(e) = event {
|
|
||||||
this.set_error(e, cx);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Self {
|
|
||||||
qr_code,
|
|
||||||
error,
|
|
||||||
_subscription: Some(subscription),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_error<S>(&mut self, message: S, cx: &mut Context<Self>)
|
|
||||||
where
|
|
||||||
S: Into<SharedString>,
|
|
||||||
{
|
|
||||||
self.error.update(cx, |this, cx| {
|
|
||||||
*this = Some(message.into());
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for ConnectSigner {
|
|
||||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
const MSG: &str = "Scan with any Nostr Connect-compatible app to connect";
|
|
||||||
|
|
||||||
v_flex()
|
|
||||||
.size_full()
|
|
||||||
.items_center()
|
|
||||||
.justify_center()
|
|
||||||
.p_4()
|
|
||||||
.when_some(self.qr_code.as_ref(), |this, qr| {
|
|
||||||
this.child(
|
|
||||||
img(qr.clone())
|
|
||||||
.size(px(256.))
|
|
||||||
.rounded(cx.theme().radius_lg)
|
|
||||||
.border_1()
|
|
||||||
.border_color(cx.theme().border),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.when_some(self.error.read(cx).as_ref(), |this, error| {
|
|
||||||
this.child(
|
|
||||||
div()
|
|
||||||
.text_xs()
|
|
||||||
.text_center()
|
|
||||||
.text_color(cx.theme().danger_active)
|
|
||||||
.child(error.clone()),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_xs()
|
|
||||||
.text_color(cx.theme().text_muted)
|
|
||||||
.child(SharedString::from(MSG)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
pub mod accounts;
|
|
||||||
pub mod screening;
|
|
||||||
pub mod settings;
|
|
||||||
|
|
||||||
mod connect;
|
|
||||||
mod import;
|
|
||||||
@@ -10,15 +10,15 @@ state = { path = "../state" }
|
|||||||
person = { path = "../person" }
|
person = { path = "../person" }
|
||||||
ui = { path = "../ui" }
|
ui = { path = "../ui" }
|
||||||
theme = { path = "../theme" }
|
theme = { path = "../theme" }
|
||||||
|
settings = { path = "../settings" }
|
||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
|
instant.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
itertools.workspace = true
|
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
smol.workspace = true
|
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
flume.workspace = true
|
flume.workspace = true
|
||||||
serde.workspace = true
|
|
||||||
serde_json.workspace = true
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
|
smol.workspace = true
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -10,8 +10,9 @@ state = { path = "../state" }
|
|||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
|
instant.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
smol.workspace = true
|
|
||||||
flume.workspace = true
|
flume.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
|
urlencoding = "2.1.3"
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
use std::cell::RefCell;
|
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::rc::Rc;
|
use std::sync::RwLock;
|
||||||
use std::time::Duration;
|
use instant::Duration;
|
||||||
|
|
||||||
use anyhow::{anyhow, Error};
|
use anyhow::{Error, anyhow};
|
||||||
use common::EventUtils;
|
use common::EventExt;
|
||||||
use gpui::{App, AppContext, Context, Entity, Global, Task};
|
use gpui::{App, AppContext, Context, Entity, Global, Task, Window};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{SmallVec, smallvec};
|
||||||
use state::{Announcement, NostrRegistry, BOOTSTRAP_RELAYS, TIMEOUT};
|
use state::{Announcement, BOOTSTRAP_RELAYS, NostrRegistry, TIMEOUT};
|
||||||
|
|
||||||
mod person;
|
mod person;
|
||||||
|
|
||||||
pub use person::*;
|
pub use person::*;
|
||||||
|
|
||||||
pub fn init(cx: &mut App) {
|
pub fn init(window: &mut Window, cx: &mut App) {
|
||||||
PersonRegistry::set_global(cx.new(PersonRegistry::new), cx);
|
PersonRegistry::set_global(cx.new(|cx| PersonRegistry::new(window, cx)), cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
struct GlobalPersonRegistry(Entity<PersonRegistry>);
|
struct GlobalPersonRegistry(Entity<PersonRegistry>);
|
||||||
@@ -24,9 +23,9 @@ impl Global for GlobalPersonRegistry {}
|
|||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
enum Dispatch {
|
enum Dispatch {
|
||||||
Person(Box<Person>),
|
Person(Person),
|
||||||
Announcement(Box<Event>),
|
Announcement(Event),
|
||||||
Relays(Box<Event>),
|
Relays(Event),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Person Registry
|
/// Person Registry
|
||||||
@@ -36,13 +35,13 @@ pub struct PersonRegistry {
|
|||||||
persons: HashMap<PublicKey, Entity<Person>>,
|
persons: HashMap<PublicKey, Entity<Person>>,
|
||||||
|
|
||||||
/// Set of public keys that have been seen
|
/// Set of public keys that have been seen
|
||||||
seen: Rc<RefCell<HashSet<PublicKey>>>,
|
seen: RwLock<HashSet<PublicKey>>,
|
||||||
|
|
||||||
/// Sender for requesting metadata
|
/// Sender for requesting metadata
|
||||||
sender: flume::Sender<PublicKey>,
|
sender: flume::Sender<PublicKey>,
|
||||||
|
|
||||||
/// Tasks for asynchronous operations
|
/// Tasks for asynchronous operations
|
||||||
_tasks: SmallVec<[Task<()>; 4]>,
|
tasks: SmallVec<[Task<()>; 4]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PersonRegistry {
|
impl PersonRegistry {
|
||||||
@@ -57,46 +56,32 @@ impl PersonRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new person registry instance
|
/// Create a new person registry instance
|
||||||
fn new(cx: &mut Context<Self>) -> Self {
|
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
|
|
||||||
// Channel for communication between nostr and gpui
|
// Channel for communication between nostr and gpui
|
||||||
let (tx, rx) = flume::bounded::<Dispatch>(100);
|
let (tx, rx) = flume::bounded::<Dispatch>(100);
|
||||||
let (mta_tx, mta_rx) = flume::bounded::<PublicKey>(100);
|
let (metadata_tx, metadata_rx) = flume::unbounded::<PublicKey>();
|
||||||
|
|
||||||
let mut tasks = smallvec![];
|
let mut tasks = smallvec![];
|
||||||
|
|
||||||
tasks.push(
|
let client2 = client.clone();
|
||||||
// Handle nostr notifications
|
tasks.push(cx.background_spawn(async move {
|
||||||
cx.background_spawn({
|
Self::handle_notifications(&client2, &tx).await;
|
||||||
let client = client.clone();
|
}));
|
||||||
|
|
||||||
async move {
|
let client3 = client.clone();
|
||||||
Self::handle_notifications(&client, &tx).await;
|
tasks.push(cx.background_spawn(async move {
|
||||||
}
|
Self::handle_requests(&client3, &metadata_rx).await;
|
||||||
}),
|
}));
|
||||||
);
|
|
||||||
|
|
||||||
tasks.push(
|
tasks.push(cx.spawn(async move |this, cx| {
|
||||||
// Handle metadata requests
|
|
||||||
cx.background_spawn({
|
|
||||||
let client = client.clone();
|
|
||||||
|
|
||||||
async move {
|
|
||||||
Self::handle_requests(&client, &mta_rx).await;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
tasks.push(
|
|
||||||
// Update GPUI state
|
|
||||||
cx.spawn(async move |this, cx| {
|
|
||||||
while let Ok(event) = rx.recv_async().await {
|
while let Ok(event) = rx.recv_async().await {
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
match event {
|
match event {
|
||||||
Dispatch::Person(person) => {
|
Dispatch::Person(person) => {
|
||||||
this.insert(*person, cx);
|
this.insert(person, cx);
|
||||||
}
|
}
|
||||||
Dispatch::Announcement(event) => {
|
Dispatch::Announcement(event) => {
|
||||||
this.set_announcement(&event, cx);
|
this.set_announcement(&event, cx);
|
||||||
@@ -108,36 +93,18 @@ impl PersonRegistry {
|
|||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
}),
|
}));
|
||||||
);
|
|
||||||
|
|
||||||
tasks.push(
|
|
||||||
// Load all user profiles from the database
|
// Load all user profiles from the database
|
||||||
cx.spawn(async move |this, cx| {
|
cx.defer_in(window, |this, _window, cx| {
|
||||||
let result = cx
|
this.load(cx);
|
||||||
.background_executor()
|
});
|
||||||
.await_on_background(async move { load_persons(&client).await })
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(persons) => {
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.bulk_inserts(persons, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("Failed to load all persons from the database: {e}");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
persons: HashMap::new(),
|
persons: HashMap::new(),
|
||||||
seen: Rc::new(RefCell::new(HashSet::new())),
|
seen: RwLock::new(HashSet::new()),
|
||||||
sender: mta_tx,
|
sender: metadata_tx,
|
||||||
_tasks: tasks,
|
tasks,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +119,7 @@ impl PersonRegistry {
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
if let RelayMessage::Event { event, .. } = message {
|
if let RelayMessage::Event { event, .. } = *message {
|
||||||
// Skip if the event has already been processed
|
// Skip if the event has already been processed
|
||||||
if !processed.insert(event.id) {
|
if !processed.insert(event.id) {
|
||||||
continue;
|
continue;
|
||||||
@@ -162,28 +129,25 @@ impl PersonRegistry {
|
|||||||
Kind::Metadata => {
|
Kind::Metadata => {
|
||||||
let metadata = Metadata::from_json(&event.content).unwrap_or_default();
|
let metadata = Metadata::from_json(&event.content).unwrap_or_default();
|
||||||
let person = Person::new(event.pubkey, metadata);
|
let person = Person::new(event.pubkey, metadata);
|
||||||
let val = Box::new(person);
|
if tx.send_async(Dispatch::Person(person)).await.is_err() {
|
||||||
|
log::warn!("PersonRegistry channel closed, dropping metadata event");
|
||||||
// Send
|
}
|
||||||
tx.send_async(Dispatch::Person(val)).await.ok();
|
|
||||||
}
|
}
|
||||||
Kind::ContactList => {
|
Kind::ContactList => {
|
||||||
let public_keys = event.extract_public_keys();
|
let public_keys = event.extract_public_keys();
|
||||||
|
if let Err(e) = get_metadata(client, public_keys).await {
|
||||||
// Get metadata for all public keys
|
log::warn!("Failed to get metadata for contact list: {e}");
|
||||||
get_metadata(client, public_keys).await.ok();
|
}
|
||||||
}
|
}
|
||||||
Kind::InboxRelays => {
|
Kind::InboxRelays => {
|
||||||
let val = Box::new(event.into_owned());
|
tx.send_async(Dispatch::Relays(event.into_owned()))
|
||||||
|
.await
|
||||||
// Send
|
.ok();
|
||||||
tx.send_async(Dispatch::Relays(val)).await.ok();
|
|
||||||
}
|
}
|
||||||
Kind::Custom(10044) => {
|
Kind::Custom(10044) => {
|
||||||
let val = Box::new(event.into_owned());
|
tx.send_async(Dispatch::Announcement(event.into_owned()))
|
||||||
|
.await
|
||||||
// Send
|
.ok();
|
||||||
tx.send_async(Dispatch::Announcement(val)).await.ok();
|
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -198,50 +162,95 @@ impl PersonRegistry {
|
|||||||
loop {
|
loop {
|
||||||
match flume::Selector::new()
|
match flume::Selector::new()
|
||||||
.recv(rx, |result| result.ok())
|
.recv(rx, |result| result.ok())
|
||||||
.wait_timeout(Duration::from_secs(2))
|
.wait_timeout(Duration::from_secs(TIMEOUT))
|
||||||
{
|
{
|
||||||
Ok(Some(public_key)) => {
|
Ok(Some(public_key)) => {
|
||||||
batch.insert(public_key);
|
batch.insert(public_key);
|
||||||
// Process the batch if it's full
|
// Process the batch if it's full
|
||||||
if batch.len() >= 20 {
|
if batch.len() >= 20
|
||||||
get_metadata(client, std::mem::take(&mut batch)).await.ok();
|
&& let Err(e) = get_metadata(client, std::mem::take(&mut batch)).await
|
||||||
|
{
|
||||||
|
log::warn!("Failed to get metadata batch: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
get_metadata(client, std::mem::take(&mut batch)).await.ok();
|
if !batch.is_empty()
|
||||||
|
&& let Err(e) = get_metadata(client, std::mem::take(&mut batch)).await
|
||||||
|
{
|
||||||
|
log::warn!("Failed to get metadata batch: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load all user profiles from the database
|
||||||
|
fn load(&mut self, cx: &mut Context<Self>) {
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let client = nostr.read(cx).client();
|
||||||
|
|
||||||
|
let task: Task<Result<Vec<Person>, Error>> = cx.background_spawn(async move {
|
||||||
|
let filter = Filter::new().kind(Kind::Metadata).limit(200);
|
||||||
|
let events = client.database().query(filter).await?;
|
||||||
|
let persons = events
|
||||||
|
.into_iter()
|
||||||
|
.map(|event| {
|
||||||
|
let metadata = Metadata::from_json(event.content).unwrap_or_default();
|
||||||
|
Person::new(event.pubkey, metadata)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(persons)
|
||||||
|
});
|
||||||
|
|
||||||
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
|
if let Ok(persons) = task.await {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.bulk_insert(persons, cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
/// Set profile encryption keys announcement
|
/// Set profile encryption keys announcement
|
||||||
fn set_announcement(&mut self, event: &Event, cx: &mut App) {
|
fn set_announcement(&mut self, event: &Event, cx: &mut App) {
|
||||||
if let Some(person) = self.persons.get(&event.pubkey) {
|
|
||||||
let announcement = Announcement::from(event);
|
let announcement = Announcement::from(event);
|
||||||
|
|
||||||
|
if let Some(person) = self.persons.get(&event.pubkey) {
|
||||||
person.update(cx, |person, cx| {
|
person.update(cx, |person, cx| {
|
||||||
person.set_announcement(announcement);
|
person.set_announcement(announcement);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
let person =
|
||||||
|
Person::new(event.pubkey, Metadata::default()).with_announcement(announcement);
|
||||||
|
self.insert(person, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set messaging relays for a person
|
/// Set messaging relays for a person
|
||||||
fn set_messaging_relays(&mut self, event: &Event, cx: &mut App) {
|
fn set_messaging_relays(&mut self, event: &Event, cx: &mut App) {
|
||||||
if let Some(person) = self.persons.get(&event.pubkey) {
|
let urls: Vec<RelayUrl> = nip17::extract_relay_list(event).collect();
|
||||||
let urls: Vec<RelayUrl> = nip17::extract_relay_list(event).cloned().collect();
|
|
||||||
|
|
||||||
|
if let Some(person) = self.persons.get(&event.pubkey) {
|
||||||
person.update(cx, |person, cx| {
|
person.update(cx, |person, cx| {
|
||||||
person.set_messaging_relays(urls);
|
person.set_messaging_relays(urls);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
let person = Person::new(event.pubkey, Metadata::default()).with_messaging_relays(urls);
|
||||||
|
self.insert(person, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Insert batch of persons
|
/// Insert batch of persons
|
||||||
fn bulk_inserts(&mut self, persons: Vec<Person>, cx: &mut Context<Self>) {
|
fn bulk_insert(&mut self, persons: Vec<Person>, cx: &mut Context<Self>) {
|
||||||
for person in persons.into_iter() {
|
for person in persons.into_iter() {
|
||||||
self.persons.insert(person.public_key(), cx.new(|_| person));
|
let public_key = person.public_key();
|
||||||
|
self.persons
|
||||||
|
.entry(public_key)
|
||||||
|
.or_insert_with(|| cx.new(|_| person));
|
||||||
}
|
}
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
@@ -270,15 +279,14 @@ impl PersonRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let public_key = *public_key;
|
let public_key = *public_key;
|
||||||
let mut seen = self.seen.borrow_mut();
|
|
||||||
|
|
||||||
if seen.insert(public_key) {
|
if self.seen.write().unwrap().insert(public_key) {
|
||||||
let sender = self.sender.clone();
|
let sender = self.sender.clone();
|
||||||
|
|
||||||
// Spawn background task to request metadata
|
// Spawn background task to request metadata
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
if let Err(e) = sender.send_async(public_key).await {
|
if let Err(e) = sender.send_async(public_key).await {
|
||||||
log::warn!("Failed to send public key for metadata request: {}", e);
|
log::warn!("Failed to send public key for metadata request: {e}");
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
@@ -322,19 +330,3 @@ where
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load all user profiles from the database
|
|
||||||
async fn load_persons(client: &Client) -> Result<Vec<Person>, Error> {
|
|
||||||
let filter = Filter::new().kind(Kind::Metadata).limit(200);
|
|
||||||
let events = client.database().query(filter).await?;
|
|
||||||
|
|
||||||
let mut persons = vec![];
|
|
||||||
|
|
||||||
for event in events.into_iter() {
|
|
||||||
let metadata = Metadata::from_json(event.content).unwrap_or_default();
|
|
||||||
let person = Person::new(event.pubkey, metadata);
|
|
||||||
persons.push(person);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(persons)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -65,6 +65,21 @@ impl Person {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build profile encryption keys announcement
|
||||||
|
pub fn with_announcement(mut self, announcement: Announcement) -> Self {
|
||||||
|
self.announcement = Some(announcement);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build profile messaging relays
|
||||||
|
pub fn with_messaging_relays<I>(mut self, relays: I) -> Self
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = RelayUrl>,
|
||||||
|
{
|
||||||
|
self.messaging_relays = relays.into_iter().collect();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Get profile public key
|
/// Get profile public key
|
||||||
pub fn public_key(&self) -> PublicKey {
|
pub fn public_key(&self) -> PublicKey {
|
||||||
self.public_key
|
self.public_key
|
||||||
@@ -75,21 +90,11 @@ impl Person {
|
|||||||
self.metadata.clone()
|
self.metadata.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set profile metadata
|
|
||||||
pub fn set_metadata(&mut self, metadata: Metadata) {
|
|
||||||
self.metadata = metadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get profile encryption keys announcement
|
/// Get profile encryption keys announcement
|
||||||
pub fn announcement(&self) -> Option<Announcement> {
|
pub fn announcement(&self) -> Option<Announcement> {
|
||||||
self.announcement.clone()
|
self.announcement.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set profile encryption keys announcement
|
|
||||||
pub fn set_announcement(&mut self, announcement: Announcement) {
|
|
||||||
self.announcement = Some(announcement);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get profile messaging relays
|
/// Get profile messaging relays
|
||||||
pub fn messaging_relays(&self) -> &Vec<RelayUrl> {
|
pub fn messaging_relays(&self) -> &Vec<RelayUrl> {
|
||||||
&self.messaging_relays
|
&self.messaging_relays
|
||||||
@@ -100,14 +105,6 @@ impl Person {
|
|||||||
self.messaging_relays.first().cloned()
|
self.messaging_relays.first().cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set profile messaging relays
|
|
||||||
pub fn set_messaging_relays<I>(&mut self, relays: I)
|
|
||||||
where
|
|
||||||
I: IntoIterator<Item = RelayUrl>,
|
|
||||||
{
|
|
||||||
self.messaging_relays = relays.into_iter().collect();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get profile avatar
|
/// Get profile avatar
|
||||||
pub fn avatar(&self) -> SharedString {
|
pub fn avatar(&self) -> SharedString {
|
||||||
self.metadata()
|
self.metadata()
|
||||||
@@ -115,8 +112,9 @@ impl Person {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.filter(|picture| !picture.is_empty())
|
.filter(|picture| !picture.is_empty())
|
||||||
.map(|picture| {
|
.map(|picture| {
|
||||||
|
let encoded_picture = urlencoding::encode(picture);
|
||||||
let url = format!(
|
let url = format!(
|
||||||
"{IMAGE_RESIZER}/?url={picture}&w=100&h=100&fit=cover&mask=circle&n=-1"
|
"{IMAGE_RESIZER}/?url={encoded_picture}&w=100&h=100&fit=cover&mask=circle&n=-1"
|
||||||
);
|
);
|
||||||
url.into()
|
url.into()
|
||||||
})
|
})
|
||||||
@@ -125,20 +123,38 @@ impl Person {
|
|||||||
|
|
||||||
/// Get profile name
|
/// Get profile name
|
||||||
pub fn name(&self) -> SharedString {
|
pub fn name(&self) -> SharedString {
|
||||||
if let Some(display_name) = self.metadata().display_name.as_ref() {
|
if let Some(display_name) = self.metadata().display_name.as_ref()
|
||||||
if !display_name.is_empty() {
|
&& !display_name.is_empty()
|
||||||
return SharedString::from(display_name);
|
{
|
||||||
}
|
return SharedString::from(display_name.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(name) = self.metadata().name.as_ref() {
|
if let Some(name) = self.metadata().name.as_ref()
|
||||||
if !name.is_empty() {
|
&& !name.is_empty()
|
||||||
return SharedString::from(name);
|
{
|
||||||
}
|
return SharedString::from(name.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
SharedString::from(shorten_pubkey(self.public_key(), 4))
|
SharedString::from(shorten_pubkey(self.public_key(), 4))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set profile metadata
|
||||||
|
pub fn set_metadata(&mut self, metadata: Metadata) {
|
||||||
|
self.metadata = metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set profile encryption keys announcement
|
||||||
|
pub fn set_announcement(&mut self, announcement: Announcement) {
|
||||||
|
self.announcement = Some(announcement);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set profile messaging relays
|
||||||
|
pub fn set_messaging_relays<I>(&mut self, relays: I)
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = RelayUrl>,
|
||||||
|
{
|
||||||
|
self.messaging_relays = relays.into_iter().collect();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shorten a [`PublicKey`] to a string with the first and last `len` characters
|
/// Shorten a [`PublicKey`] to a string with the first and last `len` characters
|
||||||
@@ -148,7 +164,7 @@ pub fn shorten_pubkey(public_key: PublicKey, len: usize) -> String {
|
|||||||
let Ok(pubkey) = public_key.to_bech32();
|
let Ok(pubkey) = public_key.to_bech32();
|
||||||
|
|
||||||
format!(
|
format!(
|
||||||
"{}:{}",
|
"{}...{}",
|
||||||
&pubkey[0..(len + 1)],
|
&pubkey[0..(len + 1)],
|
||||||
&pubkey[pubkey.len() - len..]
|
&pubkey[pubkey.len() - len..]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,376 +0,0 @@
|
|||||||
use std::borrow::Cow;
|
|
||||||
use std::cell::Cell;
|
|
||||||
use std::collections::HashSet;
|
|
||||||
use std::hash::Hash;
|
|
||||||
use std::rc::Rc;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use anyhow::{anyhow, Context as AnyhowContext, Error};
|
|
||||||
use gpui::{
|
|
||||||
App, AppContext, Context, Entity, Global, IntoElement, ParentElement, SharedString, Styled,
|
|
||||||
Task, Window,
|
|
||||||
};
|
|
||||||
use nostr_sdk::prelude::*;
|
|
||||||
use settings::{AppSettings, AuthMode};
|
|
||||||
use smallvec::{smallvec, SmallVec};
|
|
||||||
use state::NostrRegistry;
|
|
||||||
use theme::ActiveTheme;
|
|
||||||
use ui::button::{Button, ButtonVariants};
|
|
||||||
use ui::notification::Notification;
|
|
||||||
use ui::{v_flex, Disableable, IconName, Sizable, WindowExtension};
|
|
||||||
|
|
||||||
const AUTH_MESSAGE: &str =
|
|
||||||
"Approve the authentication request to allow Coop to continue sending or receiving events.";
|
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) {
|
|
||||||
RelayAuth::set_global(cx.new(|cx| RelayAuth::new(window, cx)), cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Authentication request
|
|
||||||
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
|
||||||
struct AuthRequest {
|
|
||||||
url: RelayUrl,
|
|
||||||
challenge: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AuthRequest {
|
|
||||||
pub fn new(challenge: impl Into<String>, url: RelayUrl) -> Self {
|
|
||||||
Self {
|
|
||||||
challenge: challenge.into(),
|
|
||||||
url,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn url(&self) -> &RelayUrl {
|
|
||||||
&self.url
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn challenge(&self) -> &str {
|
|
||||||
&self.challenge
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
|
||||||
enum Signal {
|
|
||||||
Auth(Arc<AuthRequest>),
|
|
||||||
Pending((EventId, RelayUrl)),
|
|
||||||
}
|
|
||||||
|
|
||||||
struct GlobalRelayAuth(Entity<RelayAuth>);
|
|
||||||
|
|
||||||
impl Global for GlobalRelayAuth {}
|
|
||||||
|
|
||||||
// Relay authentication
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct RelayAuth {
|
|
||||||
/// Pending events waiting for resend after authentication
|
|
||||||
pending_events: HashSet<(EventId, RelayUrl)>,
|
|
||||||
|
|
||||||
/// Tasks for asynchronous operations
|
|
||||||
_tasks: SmallVec<[Task<()>; 2]>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RelayAuth {
|
|
||||||
/// Retrieve the global relay auth state
|
|
||||||
pub fn global(cx: &App) -> Entity<Self> {
|
|
||||||
cx.global::<GlobalRelayAuth>().0.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the global relay auth instance
|
|
||||||
fn set_global(state: Entity<Self>, cx: &mut App) {
|
|
||||||
cx.set_global(GlobalRelayAuth(state));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new relay auth instance
|
|
||||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
|
||||||
let client = nostr.read(cx).client();
|
|
||||||
|
|
||||||
let mut tasks = smallvec![];
|
|
||||||
|
|
||||||
// Channel for communication between nostr and gpui
|
|
||||||
let (tx, rx) = flume::bounded::<Signal>(256);
|
|
||||||
|
|
||||||
tasks.push(cx.background_spawn(async move {
|
|
||||||
let mut notifications = client.notifications();
|
|
||||||
let mut challenges: HashSet<Cow<'_, str>> = HashSet::default();
|
|
||||||
|
|
||||||
while let Some(notification) = notifications.next().await {
|
|
||||||
if let ClientNotification::Message { relay_url, message } = notification {
|
|
||||||
match message {
|
|
||||||
RelayMessage::Auth { challenge } => {
|
|
||||||
if challenges.insert(challenge.clone()) {
|
|
||||||
let request = Arc::new(AuthRequest::new(challenge, relay_url));
|
|
||||||
let signal = Signal::Auth(request);
|
|
||||||
|
|
||||||
tx.send_async(signal).await.ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
RelayMessage::Closed {
|
|
||||||
subscription_id,
|
|
||||||
message,
|
|
||||||
} => {
|
|
||||||
let msg = MachineReadablePrefix::parse(&message);
|
|
||||||
|
|
||||||
if let Some(MachineReadablePrefix::AuthRequired) = msg {
|
|
||||||
if let Ok(Some(relay)) = client.relay(&relay_url).await {
|
|
||||||
// Send close message to relay
|
|
||||||
relay
|
|
||||||
.send_msg(ClientMessage::Close(subscription_id))
|
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
RelayMessage::Ok {
|
|
||||||
event_id, message, ..
|
|
||||||
} => {
|
|
||||||
let msg = MachineReadablePrefix::parse(&message);
|
|
||||||
|
|
||||||
// Handle authentication messages
|
|
||||||
if let Some(MachineReadablePrefix::AuthRequired) = msg {
|
|
||||||
let signal = Signal::Pending((event_id, relay_url));
|
|
||||||
tx.send_async(signal).await.ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
tasks.push(cx.spawn_in(window, async move |this, cx| {
|
|
||||||
while let Ok(signal) = rx.recv_async().await {
|
|
||||||
match signal {
|
|
||||||
Signal::Auth(req) => {
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
|
||||||
this.handle_auth(&req, window, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
Signal::Pending((event_id, relay_url)) => {
|
|
||||||
this.update_in(cx, |this, _window, cx| {
|
|
||||||
this.insert_pending_event(event_id, relay_url, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
Self {
|
|
||||||
pending_events: HashSet::default(),
|
|
||||||
_tasks: tasks,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Insert a pending event waiting for resend after authentication
|
|
||||||
fn insert_pending_event(&mut self, id: EventId, relay: RelayUrl, cx: &mut Context<Self>) {
|
|
||||||
self.pending_events.insert((id, relay));
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get all pending events for a specific relay,
|
|
||||||
fn get_pending_events(&self, relay: &RelayUrl, _cx: &App) -> Vec<EventId> {
|
|
||||||
self.pending_events
|
|
||||||
.iter()
|
|
||||||
.filter(|(_, pending_relay)| pending_relay == relay)
|
|
||||||
.map(|(id, _relay)| id)
|
|
||||||
.cloned()
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clear all pending events for a specific relay,
|
|
||||||
fn clear_pending_events(&mut self, relay: &RelayUrl, cx: &mut Context<Self>) {
|
|
||||||
self.pending_events
|
|
||||||
.retain(|(_, pending_relay)| pending_relay != relay);
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle authentication request
|
|
||||||
fn handle_auth(&mut self, req: &Arc<AuthRequest>, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let settings = AppSettings::global(cx);
|
|
||||||
let trusted_relay = settings.read(cx).trusted_relay(req.url(), cx);
|
|
||||||
let mode = AppSettings::get_auth_mode(cx);
|
|
||||||
|
|
||||||
if trusted_relay && mode == AuthMode::Auto {
|
|
||||||
// Automatically authenticate if the relay is authenticated before
|
|
||||||
self.response(req, window, cx);
|
|
||||||
} else {
|
|
||||||
// Otherwise open the auth request popup
|
|
||||||
self.ask_for_approval(req, window, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Send auth response and wait for confirmation
|
|
||||||
fn auth(&self, req: &Arc<AuthRequest>, cx: &App) -> Task<Result<(), Error>> {
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
|
||||||
let client = nostr.read(cx).client();
|
|
||||||
let req = req.clone();
|
|
||||||
|
|
||||||
// Get all pending events for the relay
|
|
||||||
let pending_events = self.get_pending_events(req.url(), cx);
|
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
|
||||||
// Construct event
|
|
||||||
let builder = EventBuilder::auth(req.challenge(), req.url().clone());
|
|
||||||
let event = client.sign_event_builder(builder).await?;
|
|
||||||
|
|
||||||
// Get the event ID
|
|
||||||
let id = event.id;
|
|
||||||
|
|
||||||
// Get the relay
|
|
||||||
let relay = client.relay(req.url()).await?.context("Relay not found")?;
|
|
||||||
|
|
||||||
// Subscribe to notifications
|
|
||||||
let mut notifications = relay.notifications();
|
|
||||||
|
|
||||||
// Send the AUTH message
|
|
||||||
relay
|
|
||||||
.send_msg(ClientMessage::Auth(Cow::Borrowed(&event)))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
log::info!("Sending AUTH event");
|
|
||||||
|
|
||||||
while let Some(notification) = notifications.next().await {
|
|
||||||
match notification {
|
|
||||||
RelayNotification::Message {
|
|
||||||
message: RelayMessage::Ok { event_id, .. },
|
|
||||||
} => {
|
|
||||||
if id != event_id {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all subscriptions
|
|
||||||
let subscriptions = relay.subscriptions().await;
|
|
||||||
|
|
||||||
// Re-subscribe to previous subscriptions
|
|
||||||
for (id, filters) in subscriptions.into_iter() {
|
|
||||||
if !filters.is_empty() {
|
|
||||||
relay.send_msg(ClientMessage::req(id, filters)).await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-send pending events
|
|
||||||
for id in pending_events {
|
|
||||||
if let Some(event) = client.database().event_by_id(&id).await? {
|
|
||||||
relay.send_event(&event).await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
RelayNotification::AuthenticationFailed => break,
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(anyhow!("Authentication failed"))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Respond to an authentication request.
|
|
||||||
fn response(&self, req: &Arc<AuthRequest>, window: &Window, cx: &Context<Self>) {
|
|
||||||
let settings = AppSettings::global(cx);
|
|
||||||
let req = req.clone();
|
|
||||||
let challenge = req.challenge().to_string();
|
|
||||||
|
|
||||||
// Create a task for authentication
|
|
||||||
let task = self.auth(&req, cx);
|
|
||||||
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
|
||||||
let result = task.await;
|
|
||||||
let url = req.url();
|
|
||||||
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
|
||||||
window.clear_notification(challenge, cx);
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(_) => {
|
|
||||||
// Clear pending events for the authenticated relay
|
|
||||||
this.clear_pending_events(url, cx);
|
|
||||||
|
|
||||||
// Save the authenticated relay to automatically authenticate future requests
|
|
||||||
settings.update(cx, |this, cx| {
|
|
||||||
this.add_trusted_relay(url, cx);
|
|
||||||
});
|
|
||||||
|
|
||||||
window.push_notification(format!("{} has been authenticated", url), cx);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
window.push_notification(Notification::error(e.to_string()), cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Push a popup to approve the authentication request.
|
|
||||||
fn ask_for_approval(&self, req: &Arc<AuthRequest>, window: &Window, cx: &Context<Self>) {
|
|
||||||
let notification = self.notification(req, cx);
|
|
||||||
|
|
||||||
cx.spawn_in(window, async move |_this, cx| {
|
|
||||||
cx.update(|window, cx| {
|
|
||||||
window.push_notification(notification, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build a notification for the authentication request.
|
|
||||||
fn notification(&self, req: &Arc<AuthRequest>, cx: &Context<Self>) -> Notification {
|
|
||||||
let req = req.clone();
|
|
||||||
let url = SharedString::from(req.url().to_string());
|
|
||||||
let entity = cx.entity().downgrade();
|
|
||||||
let loading = Rc::new(Cell::new(false));
|
|
||||||
|
|
||||||
Notification::new()
|
|
||||||
.custom_id(SharedString::from(&req.challenge))
|
|
||||||
.autohide(false)
|
|
||||||
.icon(IconName::Info)
|
|
||||||
.title(SharedString::from("Authentication Required"))
|
|
||||||
.content(move |_window, cx| {
|
|
||||||
v_flex()
|
|
||||||
.gap_2()
|
|
||||||
.text_sm()
|
|
||||||
.child(SharedString::from(AUTH_MESSAGE))
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.py_1()
|
|
||||||
.px_1p5()
|
|
||||||
.rounded_sm()
|
|
||||||
.text_xs()
|
|
||||||
.bg(cx.theme().elevated_surface_background)
|
|
||||||
.text_color(cx.theme().text_accent)
|
|
||||||
.child(url.clone()),
|
|
||||||
)
|
|
||||||
.into_any_element()
|
|
||||||
})
|
|
||||||
.action(move |_window, _cx| {
|
|
||||||
let view = entity.clone();
|
|
||||||
let req = req.clone();
|
|
||||||
|
|
||||||
Button::new("approve")
|
|
||||||
.label("Approve")
|
|
||||||
.small()
|
|
||||||
.primary()
|
|
||||||
.loading(loading.get())
|
|
||||||
.disabled(loading.get())
|
|
||||||
.on_click({
|
|
||||||
let loading = Rc::clone(&loading);
|
|
||||||
move |_ev, window, cx| {
|
|
||||||
// Set loading state to true
|
|
||||||
loading.set(true);
|
|
||||||
// Process to approve the request
|
|
||||||
view.update(cx, |this, cx| {
|
|
||||||
this.response(&req, window, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,6 @@ common = { path = "../common" }
|
|||||||
|
|
||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
smol.workspace = true
|
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
@@ -18,3 +17,6 @@ serde.workspace = true
|
|||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
|
||||||
paste = "1.0.15"
|
paste = "1.0.15"
|
||||||
|
|
||||||
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
|
smol.workspace = true
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
use std::collections::{HashMap, HashSet};
|
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
@@ -20,14 +19,16 @@ macro_rules! setting_accessors {
|
|||||||
$(
|
$(
|
||||||
paste::paste! {
|
paste::paste! {
|
||||||
pub fn [<get_ $field>](cx: &App) -> $type {
|
pub fn [<get_ $field>](cx: &App) -> $type {
|
||||||
Self::global(cx).read(cx).values.$field.clone()
|
Self::global(cx).read(cx).inner.read(cx).$field.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn [<update_ $field>](value: $type, cx: &mut App) {
|
pub fn [<update_ $field>](value: $type, cx: &mut App) {
|
||||||
Self::global(cx).update(cx, |this, cx| {
|
Self::global(cx).update(cx, |this, cx| {
|
||||||
this.values.$field = value;
|
this.inner.update(cx, |inner, cx| {
|
||||||
|
inner.$field = value;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)*
|
)*
|
||||||
@@ -40,9 +41,9 @@ setting_accessors! {
|
|||||||
pub theme_mode: ThemeMode,
|
pub theme_mode: ThemeMode,
|
||||||
pub hide_avatar: bool,
|
pub hide_avatar: bool,
|
||||||
pub screening: bool,
|
pub screening: bool,
|
||||||
|
pub nip4e: bool,
|
||||||
pub auth_mode: AuthMode,
|
pub auth_mode: AuthMode,
|
||||||
pub trusted_relays: HashSet<RelayUrl>,
|
pub trusted_relays: Vec<String>,
|
||||||
pub room_configs: HashMap<u64, RoomConfig>,
|
|
||||||
pub file_server: Url,
|
pub file_server: Url,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,10 +67,10 @@ impl Display for AuthMode {
|
|||||||
/// Signer kind
|
/// Signer kind
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub enum SignerKind {
|
pub enum SignerKind {
|
||||||
#[default]
|
|
||||||
Auto,
|
Auto,
|
||||||
User,
|
|
||||||
Encryption,
|
Encryption,
|
||||||
|
#[default]
|
||||||
|
User,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SignerKind {
|
impl SignerKind {
|
||||||
@@ -97,7 +98,7 @@ impl RoomConfig {
|
|||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
backup: true,
|
backup: true,
|
||||||
signer_kind: SignerKind::Auto,
|
signer_kind: SignerKind::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,14 +138,14 @@ pub struct Settings {
|
|||||||
/// Enable screening for unknown chat requests
|
/// Enable screening for unknown chat requests
|
||||||
pub screening: bool,
|
pub screening: bool,
|
||||||
|
|
||||||
|
/// Enable decoupling encryption key
|
||||||
|
pub nip4e: bool,
|
||||||
|
|
||||||
/// Authentication mode
|
/// Authentication mode
|
||||||
pub auth_mode: AuthMode,
|
pub auth_mode: AuthMode,
|
||||||
|
|
||||||
/// Trusted relays; Coop will automatically authenticate with these relays
|
/// Trusted relays; Coop will automatically authenticate with these relays
|
||||||
pub trusted_relays: HashSet<RelayUrl>,
|
pub trusted_relays: Vec<String>,
|
||||||
|
|
||||||
/// Configuration for each chat room
|
|
||||||
pub room_configs: HashMap<u64, RoomConfig>,
|
|
||||||
|
|
||||||
/// Server for blossom media attachments
|
/// Server for blossom media attachments
|
||||||
pub file_server: Url,
|
pub file_server: Url,
|
||||||
@@ -157,9 +158,9 @@ impl Default for Settings {
|
|||||||
theme_mode: ThemeMode::default(),
|
theme_mode: ThemeMode::default(),
|
||||||
hide_avatar: false,
|
hide_avatar: false,
|
||||||
screening: true,
|
screening: true,
|
||||||
|
nip4e: false,
|
||||||
auth_mode: AuthMode::default(),
|
auth_mode: AuthMode::default(),
|
||||||
trusted_relays: HashSet::default(),
|
trusted_relays: vec![],
|
||||||
room_configs: HashMap::default(),
|
|
||||||
file_server: Url::parse("https://blossom.band/").unwrap(),
|
file_server: Url::parse("https://blossom.band/").unwrap(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -178,7 +179,7 @@ impl Global for GlobalAppSettings {}
|
|||||||
/// Application settings
|
/// Application settings
|
||||||
pub struct AppSettings {
|
pub struct AppSettings {
|
||||||
/// Settings
|
/// Settings
|
||||||
values: Settings,
|
inner: Entity<Settings>,
|
||||||
|
|
||||||
/// Event subscriptions
|
/// Event subscriptions
|
||||||
_subscriptions: SmallVec<[Subscription; 2]>,
|
_subscriptions: SmallVec<[Subscription; 2]>,
|
||||||
@@ -196,11 +197,12 @@ impl AppSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
|
let inner = cx.new(|_| Settings::default());
|
||||||
let mut subscriptions = smallvec![];
|
let mut subscriptions = smallvec![];
|
||||||
|
|
||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
// Observe and automatically save settings on changes
|
// Observe and automatically save settings on changes
|
||||||
cx.observe_self(|this, cx| {
|
cx.observe(&inner, |this, _inner, cx| {
|
||||||
this.save(cx);
|
this.save(cx);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -211,27 +213,30 @@ impl AppSettings {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
values: Settings::default(),
|
inner,
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update settings
|
/// Update settings
|
||||||
fn set_settings(&mut self, settings: Settings, cx: &mut Context<Self>) {
|
fn set_settings(&mut self, settings: Settings, cx: &mut Context<Self>) {
|
||||||
self.values = settings;
|
self.inner.update(cx, |this, cx| {
|
||||||
|
*this = settings;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load settings
|
/// Load settings
|
||||||
fn load(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn load(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let task: Task<Result<Settings, Error>> = cx.background_spawn(async move {
|
let task: Task<Result<Settings, Error>> = cx.background_spawn(async move {
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
{
|
||||||
let path = config_dir().join(".settings");
|
let path = config_dir().join(".settings");
|
||||||
|
|
||||||
if let Ok(content) = smol::fs::read_to_string(&path).await {
|
if let Ok(content) = smol::fs::read_to_string(&path).await {
|
||||||
Ok(serde_json::from_str(&content)?)
|
return Ok(serde_json::from_str(&content)?);
|
||||||
} else {
|
|
||||||
Err(anyhow!("Not found"))
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
Err(anyhow!("Not found"))
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
@@ -249,19 +254,15 @@ impl AppSettings {
|
|||||||
|
|
||||||
/// Save settings
|
/// Save settings
|
||||||
pub fn save(&mut self, cx: &mut Context<Self>) {
|
pub fn save(&mut self, cx: &mut Context<Self>) {
|
||||||
let settings = self.values.clone();
|
let settings = self.inner.read(cx);
|
||||||
|
if let Ok(content) = serde_json::to_string(&settings) {
|
||||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
cx.background_spawn(async move {
|
||||||
let path = config_dir().join(".settings");
|
let path = config_dir().join(".settings");
|
||||||
let content = serde_json::to_string(&settings)?;
|
smol::fs::write(&path, content).await.ok();
|
||||||
|
})
|
||||||
// Write settings to file
|
.detach();
|
||||||
smol::fs::write(&path, content).await?;
|
}
|
||||||
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
|
|
||||||
task.detach();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set theme
|
/// Set theme
|
||||||
@@ -270,52 +271,66 @@ impl AppSettings {
|
|||||||
T: Into<String>,
|
T: Into<String>,
|
||||||
{
|
{
|
||||||
// Update settings
|
// Update settings
|
||||||
self.values.theme = Some(theme.into());
|
self.inner.update(cx, |this, cx| {
|
||||||
|
this.theme = Some(theme.into());
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
});
|
||||||
|
|
||||||
// Apply the new theme
|
// Apply the new theme
|
||||||
self.apply_theme(window, cx);
|
self.apply_theme(window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reset theme
|
||||||
|
pub fn reset_theme(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.inner.update(cx, |this, cx| {
|
||||||
|
this.theme = None;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
self.apply_theme(window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
/// Apply theme
|
/// Apply theme
|
||||||
pub fn apply_theme(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
pub fn apply_theme(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
if let Some(name) = self.values.theme.as_ref() {
|
if let Some(name) = self.inner.read(cx).theme.as_ref() {
|
||||||
|
let mode = self.inner.read(cx).theme_mode;
|
||||||
|
|
||||||
if let Ok(new_theme) = ThemeFamily::from_assets(name) {
|
if let Ok(new_theme) = ThemeFamily::from_assets(name) {
|
||||||
Theme::apply_theme(Rc::new(new_theme), Some(window), cx);
|
Theme::apply_theme(Rc::new(new_theme), Some(window), cx);
|
||||||
|
Theme::change(mode, Some(window), cx);
|
||||||
|
} else {
|
||||||
|
log::info!("Failed to load theme: {name}");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Theme::apply_theme(Rc::new(ThemeFamily::default()), Some(window), cx);
|
Theme::apply_theme(Rc::new(ThemeFamily::default()), Some(window), cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reset theme
|
/// Check if decoupling encryption key is enabled
|
||||||
pub fn reset_theme(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
pub fn is_nip4e_enabled(&self, cx: &App) -> bool {
|
||||||
self.values.theme = None;
|
self.inner.read(cx).nip4e
|
||||||
cx.notify();
|
|
||||||
|
|
||||||
self.apply_theme(window, cx);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the given relay is already authenticated
|
/// Check if the given relay is already authenticated
|
||||||
pub fn trusted_relay(&self, url: &RelayUrl, _cx: &App) -> bool {
|
pub fn trusted_relay(&self, url: &RelayUrl, cx: &App) -> bool {
|
||||||
self.values.trusted_relays.iter().any(|relay| {
|
self.inner
|
||||||
relay.as_str_without_trailing_slash() == url.as_str_without_trailing_slash()
|
.read(cx)
|
||||||
})
|
.trusted_relays
|
||||||
|
.iter()
|
||||||
|
.any(|relay| relay == url.as_str_without_trailing_slash())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a relay to the trusted list
|
/// Add a relay to the trusted list
|
||||||
pub fn add_trusted_relay(&mut self, url: &RelayUrl, cx: &mut Context<Self>) {
|
pub fn add_trusted_relay(&mut self, url: &RelayUrl, cx: &mut Context<Self>) {
|
||||||
self.values.trusted_relays.insert(url.clone());
|
self.inner.update(cx, |this, cx| {
|
||||||
|
if !this
|
||||||
|
.trusted_relays
|
||||||
|
.iter()
|
||||||
|
.any(|relay| relay == url.as_str_without_trailing_slash())
|
||||||
|
{
|
||||||
|
this.trusted_relays
|
||||||
|
.push(url.as_str_without_trailing_slash().to_string());
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
/// Add a room configuration
|
|
||||||
pub fn add_room_config(&mut self, id: u64, config: RoomConfig, cx: &mut Context<Self>) {
|
|
||||||
self.values
|
|
||||||
.room_configs
|
|
||||||
.entry(id)
|
|
||||||
.and_modify(|this| *this = config)
|
|
||||||
.or_default();
|
|
||||||
cx.notify();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,21 +9,26 @@ common = { path = "../common" }
|
|||||||
|
|
||||||
nostr.workspace = true
|
nostr.workspace = true
|
||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
nostr-lmdb.workspace = true
|
nostr-gossip-memory.workspace = true
|
||||||
nostr-connect.workspace = true
|
|
||||||
nostr-blossom.workspace = true
|
nostr-blossom.workspace = true
|
||||||
|
nostr-connect.workspace = true
|
||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
gpui_tokio.workspace = true
|
instant.workspace = true
|
||||||
smol.workspace = true
|
|
||||||
flume.workspace = true
|
flume.workspace = true
|
||||||
|
futures.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
webbrowser.workspace = true
|
webbrowser.workspace = true
|
||||||
serde.workspace = true
|
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
|
||||||
rustls = "0.23"
|
|
||||||
petname = "2.0.2"
|
|
||||||
whoami = "1.6.1"
|
|
||||||
mime_guess = "2.0.4"
|
mime_guess = "2.0.4"
|
||||||
|
|
||||||
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
|
nostr-memory.workspace = true
|
||||||
|
|
||||||
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
|
nostr-lmdb.workspace = true
|
||||||
|
smol.workspace = true
|
||||||
|
gpui_tokio.workspace = true
|
||||||
|
rustls = "0.23"
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use anyhow::{anyhow, Error};
|
use anyhow::{Error, anyhow};
|
||||||
use gpui::AsyncApp;
|
use gpui::AsyncApp;
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
use gpui_tokio::Tokio;
|
use gpui_tokio::Tokio;
|
||||||
use mime_guess::from_path;
|
use mime_guess::from_path;
|
||||||
use nostr_blossom::prelude::*;
|
use nostr_blossom::prelude::*;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub async fn upload(server: Url, path: PathBuf, cx: &AsyncApp) -> Result<Url, Error> {
|
pub async fn upload(server: Url, path: PathBuf, cx: &AsyncApp) -> Result<Url, Error> {
|
||||||
let content_type = from_path(&path).first_or_octet_stream().to_string();
|
let content_type = from_path(&path).first_or_octet_stream().to_string();
|
||||||
let data = smol::fs::read(path).await?;
|
let data = smol::fs::read(path).await?;
|
||||||
@@ -25,3 +27,8 @@ pub async fn upload(server: Url, path: PathBuf, cx: &AsyncApp) -> Result<Url, Er
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| anyhow!("Upload error: {e}"))?
|
.map_err(|e| anyhow!("Upload error: {e}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
pub async fn upload(_server: Url, _path: PathBuf, _cx: &AsyncApp) -> Result<Url, Error> {
|
||||||
|
Err(anyhow!("File upload not supported on web"))
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
use std::sync::OnceLock;
|
|
||||||
|
|
||||||
/// Client name (Application name)
|
/// Client name (Application name)
|
||||||
pub const CLIENT_NAME: &str = "Coop";
|
pub const CLIENT_NAME: &str = "Coop";
|
||||||
|
|
||||||
@@ -10,11 +8,15 @@ pub const COOP_PUBKEY: &str = "npub1j3rz3ndl902lya6ywxvy5c983lxs8mpukqnx4pa4lt5w
|
|||||||
pub const APP_ID: &str = "su.reya.coop";
|
pub const APP_ID: &str = "su.reya.coop";
|
||||||
|
|
||||||
/// Keyring name
|
/// Keyring name
|
||||||
pub const KEYRING: &str = "Coop Safe Storage";
|
pub const MASTER_KEYRING: &str = "Coop Master Key";
|
||||||
|
pub const USER_KEYRING: &str = "Coop User Credential";
|
||||||
|
|
||||||
/// Default timeout for subscription
|
/// Default timeout for subscription
|
||||||
pub const TIMEOUT: u64 = 2;
|
pub const TIMEOUT: u64 = 2;
|
||||||
|
|
||||||
|
/// Default image cache size
|
||||||
|
pub const IMAGE_CACHE_SIZE: usize = 20;
|
||||||
|
|
||||||
/// Default delay for searching
|
/// Default delay for searching
|
||||||
pub const FIND_DELAY: u64 = 600;
|
pub const FIND_DELAY: u64 = 600;
|
||||||
|
|
||||||
@@ -36,24 +38,15 @@ pub const NOSTR_CONNECT_RELAY: &str = "wss://relay.nip46.com";
|
|||||||
/// Default vertex relays
|
/// Default vertex relays
|
||||||
pub const WOT_RELAYS: [&str; 1] = ["wss://relay.vertexlab.io"];
|
pub const WOT_RELAYS: [&str; 1] = ["wss://relay.vertexlab.io"];
|
||||||
|
|
||||||
|
/// Default search relays
|
||||||
|
pub const INDEXER_RELAYS: [&str; 1] = ["wss://indexer.coracle.social"];
|
||||||
|
|
||||||
/// Default search relays
|
/// Default search relays
|
||||||
pub const SEARCH_RELAYS: [&str; 2] = ["wss://antiprimal.net", "wss://search.nos.today"];
|
pub const SEARCH_RELAYS: [&str; 2] = ["wss://antiprimal.net", "wss://search.nos.today"];
|
||||||
|
|
||||||
/// Default bootstrap relays
|
/// Default bootstrap relays
|
||||||
pub const BOOTSTRAP_RELAYS: [&str; 3] = [
|
pub const BOOTSTRAP_RELAYS: [&str; 3] = [
|
||||||
|
"wss://relay.ditto.pub",
|
||||||
"wss://relay.primal.net",
|
"wss://relay.primal.net",
|
||||||
"wss://indexer.coracle.social",
|
|
||||||
"wss://user.kindpag.es",
|
"wss://user.kindpag.es",
|
||||||
];
|
];
|
||||||
|
|
||||||
static APP_NAME: OnceLock<String> = OnceLock::new();
|
|
||||||
|
|
||||||
/// Get the app name
|
|
||||||
pub fn app_name() -> &'static String {
|
|
||||||
APP_NAME.get_or_init(|| {
|
|
||||||
let devicename = whoami::devicename();
|
|
||||||
let platform = whoami::platform();
|
|
||||||
|
|
||||||
format!("{CLIENT_NAME} on {platform} ({devicename})")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
use std::collections::{HashMap, HashSet};
|
|
||||||
|
|
||||||
use gpui::SharedString;
|
|
||||||
use nostr_sdk::prelude::*;
|
|
||||||
|
|
||||||
/// Gossip
|
|
||||||
#[derive(Debug, Clone, Default)]
|
|
||||||
pub struct Gossip {
|
|
||||||
relays: HashMap<PublicKey, HashSet<(RelayUrl, Option<RelayMetadata>)>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Gossip {
|
|
||||||
pub fn read_only_relays(&self, public_key: &PublicKey) -> Vec<SharedString> {
|
|
||||||
self.relays
|
|
||||||
.get(public_key)
|
|
||||||
.map(|relays| {
|
|
||||||
relays
|
|
||||||
.iter()
|
|
||||||
.map(|(url, _)| url.to_string().into())
|
|
||||||
.collect()
|
|
||||||
})
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get read relays for a given public key
|
|
||||||
pub fn read_relays(&self, public_key: &PublicKey) -> Vec<RelayUrl> {
|
|
||||||
self.relays
|
|
||||||
.get(public_key)
|
|
||||||
.map(|relays| {
|
|
||||||
relays
|
|
||||||
.iter()
|
|
||||||
.filter_map(|(url, metadata)| {
|
|
||||||
if metadata.is_none() || metadata == &Some(RelayMetadata::Read) {
|
|
||||||
Some(url.to_owned())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
})
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get write relays for a given public key
|
|
||||||
pub fn write_relays(&self, public_key: &PublicKey) -> Vec<RelayUrl> {
|
|
||||||
self.relays
|
|
||||||
.get(public_key)
|
|
||||||
.map(|relays| {
|
|
||||||
relays
|
|
||||||
.iter()
|
|
||||||
.filter_map(|(url, metadata)| {
|
|
||||||
if metadata.is_none() || metadata == &Some(RelayMetadata::Write) {
|
|
||||||
Some(url.to_owned())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
})
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Insert gossip relays for a public key
|
|
||||||
pub fn insert_relays(&mut self, event: &Event) {
|
|
||||||
self.relays.entry(event.pubkey).or_default().extend(
|
|
||||||
event
|
|
||||||
.tags
|
|
||||||
.iter()
|
|
||||||
.filter_map(|tag| {
|
|
||||||
if let Some(TagStandard::RelayMetadata {
|
|
||||||
relay_url,
|
|
||||||
metadata,
|
|
||||||
}) = tag.clone().to_standardized()
|
|
||||||
{
|
|
||||||
Some((relay_url, metadata))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.take(3),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,9 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
|
use futures::io::AsyncReadExt;
|
||||||
use gpui::http_client::{AsyncBody, HttpClient};
|
use gpui::http_client::{AsyncBody, HttpClient};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use smol::io::AsyncReadExt;
|
|
||||||
|
|
||||||
#[allow(async_fn_in_trait)]
|
#[allow(async_fn_in_trait)]
|
||||||
pub trait NostrAddress {
|
pub trait NostrAddress {
|
||||||
|
|||||||
@@ -1,40 +1,6 @@
|
|||||||
use std::fmt::Display;
|
|
||||||
|
|
||||||
use gpui::SharedString;
|
use gpui::SharedString;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
|
|
||||||
pub enum DeviceState {
|
|
||||||
#[default]
|
|
||||||
Idle,
|
|
||||||
Requesting,
|
|
||||||
Set,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for DeviceState {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
DeviceState::Idle => write!(f, "Idle"),
|
|
||||||
DeviceState::Requesting => write!(f, "Wait for approval"),
|
|
||||||
DeviceState::Set => write!(f, "Encryption Key is ready"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DeviceState {
|
|
||||||
pub fn idle(&self) -> bool {
|
|
||||||
matches!(self, DeviceState::Idle)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn requesting(&self) -> bool {
|
|
||||||
matches!(self, DeviceState::Requesting)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set(&self) -> bool {
|
|
||||||
matches!(self, DeviceState::Set)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Announcement
|
/// Announcement
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
pub struct Announcement {
|
pub struct Announcement {
|
||||||
@@ -50,14 +16,15 @@ impl From<&Event> for Announcement {
|
|||||||
let public_key = val
|
let public_key = val
|
||||||
.tags
|
.tags
|
||||||
.iter()
|
.iter()
|
||||||
.find(|tag| tag.kind().as_str() == "n")
|
.find(|tag| tag.kind() == "n")
|
||||||
.and_then(|tag| tag.content())
|
.and_then(|tag| tag.content())
|
||||||
.and_then(|c| PublicKey::parse(c).ok())
|
.and_then(|c| PublicKey::parse(c).ok())
|
||||||
.unwrap_or(val.pubkey);
|
.unwrap_or(val.pubkey);
|
||||||
|
|
||||||
let client_name = val
|
let client_name = val
|
||||||
.tags
|
.tags
|
||||||
.find(TagKind::Client)
|
.iter()
|
||||||
|
.find(|tag| tag.kind() == "client")
|
||||||
.and_then(|tag| tag.content())
|
.and_then(|tag| tag.content())
|
||||||
.map(|c| c.to_string());
|
.map(|c| c.to_string());
|
||||||
|
|
||||||
@@ -1,134 +1,206 @@
|
|||||||
use std::borrow::Cow;
|
use std::error::Error;
|
||||||
use std::result::Result;
|
use std::fmt;
|
||||||
use std::sync::Arc;
|
use std::future::Future;
|
||||||
|
use std::pin::Pin;
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
|
use nostr_connect::client::AuthUrlHandler;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use smol::lock::RwLock;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct CoopSigner {
|
pub struct UniversalSignerError(Box<dyn Error + Send + Sync + 'static>);
|
||||||
/// User's signer
|
|
||||||
signer: RwLock<Arc<dyn NostrSigner>>,
|
|
||||||
|
|
||||||
/// User's signer public key
|
impl fmt::Display for UniversalSignerError {
|
||||||
signer_pkey: RwLock<Option<PublicKey>>,
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
/// Specific signer for encryption purposes
|
}
|
||||||
encryption_signer: RwLock<Option<Arc<dyn NostrSigner>>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CoopSigner {
|
impl Error for UniversalSignerError {
|
||||||
|
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||||
|
Some(&*self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UniversalSignerError {
|
||||||
|
pub fn new<E>(err: E) -> Self
|
||||||
|
where
|
||||||
|
E: Error + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
UniversalSignerError(Box::new(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct UniversalSigner {
|
||||||
|
inner: Arc<RwLock<Arc<dyn InnerSigner>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UniversalSigner {
|
||||||
pub fn new<T>(signer: T) -> Self
|
pub fn new<T>(signer: T) -> Self
|
||||||
where
|
where
|
||||||
T: IntoNostrSigner,
|
T: AsyncGetPublicKey + AsyncSignEvent + AsyncNip44 + 'static,
|
||||||
|
<T as AsyncGetPublicKey>::Error: Error + Send + Sync + 'static,
|
||||||
|
<T as AsyncSignEvent>::Error: Error + Send + Sync + 'static,
|
||||||
|
<T as AsyncNip44>::Error: Error + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
Self {
|
Self {
|
||||||
signer: RwLock::new(signer.into_nostr_signer()),
|
inner: Arc::new(RwLock::new(Arc::new(InnerSignerImpl(signer)))),
|
||||||
signer_pkey: RwLock::new(None),
|
|
||||||
encryption_signer: RwLock::new(None),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the current signer.
|
/// Swap the inner signer in-place. All clones see the new signer.
|
||||||
pub async fn get(&self) -> Arc<dyn NostrSigner> {
|
pub fn swap_inner<T>(&self, new_signer: T)
|
||||||
self.signer.read().await.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the encryption signer.
|
|
||||||
pub async fn get_encryption_signer(&self) -> Option<Arc<dyn NostrSigner>> {
|
|
||||||
self.encryption_signer.read().await.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get public key
|
|
||||||
///
|
|
||||||
/// Ensure to call this method after the signer has been initialized.
|
|
||||||
/// Otherwise, this method will panic.
|
|
||||||
pub fn public_key(&self) -> Option<PublicKey> {
|
|
||||||
*self.signer_pkey.read_blocking()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Switch the current signer to a new signer.
|
|
||||||
pub async fn switch<T>(&self, new: T)
|
|
||||||
where
|
where
|
||||||
T: IntoNostrSigner,
|
T: AsyncGetPublicKey + AsyncSignEvent + AsyncNip44 + 'static,
|
||||||
|
<T as AsyncGetPublicKey>::Error: Error + Send + Sync + 'static,
|
||||||
|
<T as AsyncSignEvent>::Error: Error + Send + Sync + 'static,
|
||||||
|
<T as AsyncNip44>::Error: Error + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
let new_signer = new.into_nostr_signer();
|
*self.inner.write().expect("RwLock poisoned") = Arc::new(InnerSignerImpl(new_signer));
|
||||||
let public_key = new_signer.get_public_key().await.ok();
|
|
||||||
let mut signer = self.signer.write().await;
|
|
||||||
let mut signer_pkey = self.signer_pkey.write().await;
|
|
||||||
let mut encryption_signer = self.encryption_signer.write().await;
|
|
||||||
|
|
||||||
// Switch to the new signer
|
|
||||||
*signer = new_signer;
|
|
||||||
|
|
||||||
// Update the public key
|
|
||||||
*signer_pkey = public_key;
|
|
||||||
|
|
||||||
// Reset the encryption signer
|
|
||||||
*encryption_signer = None;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the encryption signer.
|
|
||||||
pub async fn set_encryption_signer<T>(&self, new: T)
|
|
||||||
where
|
|
||||||
T: IntoNostrSigner,
|
|
||||||
{
|
|
||||||
let mut encryption_signer = self.encryption_signer.write().await;
|
|
||||||
*encryption_signer = Some(new.into_nostr_signer());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NostrSigner for CoopSigner {
|
trait InnerSigner: fmt::Debug + Send + Sync + 'static {
|
||||||
#[allow(mismatched_lifetime_syntaxes)]
|
fn get_public_key_async(
|
||||||
fn backend(&self) -> SignerBackend {
|
&self,
|
||||||
SignerBackend::Custom(Cow::Borrowed("custom"))
|
) -> Pin<Box<dyn Future<Output = Result<PublicKey, UniversalSignerError>> + Send + '_>>;
|
||||||
}
|
fn sign_event_async(
|
||||||
|
&self,
|
||||||
fn get_public_key<'a>(&'a self) -> BoxedFuture<'a, Result<PublicKey, SignerError>> {
|
|
||||||
Box::pin(async move { self.get().await.get_public_key().await })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sign_event<'a>(
|
|
||||||
&'a self,
|
|
||||||
unsigned: UnsignedEvent,
|
unsigned: UnsignedEvent,
|
||||||
) -> BoxedFuture<'a, Result<Event, SignerError>> {
|
) -> Pin<Box<dyn Future<Output = Result<Event, UniversalSignerError>> + Send + '_>>;
|
||||||
Box::pin(async move { self.get().await.sign_event(unsigned).await })
|
fn nip44_encrypt_async<'a>(
|
||||||
}
|
|
||||||
|
|
||||||
fn nip04_encrypt<'a>(
|
|
||||||
&'a self,
|
&'a self,
|
||||||
public_key: &'a PublicKey,
|
public_key: &'a PublicKey,
|
||||||
content: &'a str,
|
content: &'a str,
|
||||||
) -> BoxedFuture<'a, Result<String, SignerError>> {
|
) -> Pin<Box<dyn Future<Output = Result<String, UniversalSignerError>> + Send + 'a>>;
|
||||||
Box::pin(async move { self.get().await.nip04_encrypt(public_key, content).await })
|
fn nip44_decrypt_async<'a>(
|
||||||
}
|
|
||||||
|
|
||||||
fn nip04_decrypt<'a>(
|
|
||||||
&'a self,
|
|
||||||
public_key: &'a PublicKey,
|
|
||||||
encrypted_content: &'a str,
|
|
||||||
) -> BoxedFuture<'a, Result<String, SignerError>> {
|
|
||||||
Box::pin(async move {
|
|
||||||
self.get()
|
|
||||||
.await
|
|
||||||
.nip04_decrypt(public_key, encrypted_content)
|
|
||||||
.await
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn nip44_encrypt<'a>(
|
|
||||||
&'a self,
|
|
||||||
public_key: &'a PublicKey,
|
|
||||||
content: &'a str,
|
|
||||||
) -> BoxedFuture<'a, Result<String, SignerError>> {
|
|
||||||
Box::pin(async move { self.get().await.nip44_encrypt(public_key, content).await })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn nip44_decrypt<'a>(
|
|
||||||
&'a self,
|
&'a self,
|
||||||
public_key: &'a PublicKey,
|
public_key: &'a PublicKey,
|
||||||
payload: &'a str,
|
payload: &'a str,
|
||||||
) -> BoxedFuture<'a, Result<String, SignerError>> {
|
) -> Pin<Box<dyn Future<Output = Result<String, UniversalSignerError>> + Send + 'a>>;
|
||||||
Box::pin(async move { self.get().await.nip44_decrypt(public_key, payload).await })
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct InnerSignerImpl<T>(T);
|
||||||
|
|
||||||
|
impl<T> InnerSigner for InnerSignerImpl<T>
|
||||||
|
where
|
||||||
|
T: AsyncGetPublicKey + AsyncSignEvent + AsyncNip44 + Send + Sync + 'static,
|
||||||
|
<T as AsyncGetPublicKey>::Error: Error + Send + Sync + 'static,
|
||||||
|
<T as AsyncSignEvent>::Error: Error + Send + Sync + 'static,
|
||||||
|
<T as AsyncNip44>::Error: Error + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
fn get_public_key_async(
|
||||||
|
&self,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Result<PublicKey, UniversalSignerError>> + Send + '_>> {
|
||||||
|
Box::pin(async move {
|
||||||
|
AsyncGetPublicKey::get_public_key_async(&self.0)
|
||||||
|
.await
|
||||||
|
.map_err(UniversalSignerError::new)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sign_event_async(
|
||||||
|
&self,
|
||||||
|
unsigned: UnsignedEvent,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Result<Event, UniversalSignerError>> + Send + '_>> {
|
||||||
|
Box::pin(async move {
|
||||||
|
AsyncSignEvent::sign_event_async(&self.0, unsigned)
|
||||||
|
.await
|
||||||
|
.map_err(UniversalSignerError::new)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nip44_encrypt_async<'a>(
|
||||||
|
&'a self,
|
||||||
|
public_key: &'a PublicKey,
|
||||||
|
content: &'a str,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Result<String, UniversalSignerError>> + Send + 'a>> {
|
||||||
|
Box::pin(async move {
|
||||||
|
AsyncNip44::nip44_encrypt_async(&self.0, public_key, content)
|
||||||
|
.await
|
||||||
|
.map_err(UniversalSignerError::new)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nip44_decrypt_async<'a>(
|
||||||
|
&'a self,
|
||||||
|
public_key: &'a PublicKey,
|
||||||
|
payload: &'a str,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Result<String, UniversalSignerError>> + Send + 'a>> {
|
||||||
|
Box::pin(async move {
|
||||||
|
AsyncNip44::nip44_decrypt_async(&self.0, public_key, payload)
|
||||||
|
.await
|
||||||
|
.map_err(UniversalSignerError::new)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UniversalSigner {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn with_inner<R>(&self, f: impl FnOnce(&dyn InnerSigner) -> R) -> R {
|
||||||
|
let guard = self.inner.read().expect("RwLock poisoned");
|
||||||
|
f(&**guard)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsyncGetPublicKey for UniversalSigner {
|
||||||
|
type Error = UniversalSignerError;
|
||||||
|
|
||||||
|
fn get_public_key_async(
|
||||||
|
&self,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Result<PublicKey, Self::Error>> + Send + '_>> {
|
||||||
|
let inner = self.inner.read().expect("RwLock poisoned").clone();
|
||||||
|
Box::pin(async move { inner.get_public_key_async().await })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsyncSignEvent for UniversalSigner {
|
||||||
|
type Error = UniversalSignerError;
|
||||||
|
|
||||||
|
fn sign_event_async(
|
||||||
|
&self,
|
||||||
|
unsigned: UnsignedEvent,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Result<Event, Self::Error>> + Send + '_>> {
|
||||||
|
let inner = self.inner.read().expect("RwLock poisoned").clone();
|
||||||
|
Box::pin(async move { inner.sign_event_async(unsigned).await })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsyncNip44 for UniversalSigner {
|
||||||
|
type Error = UniversalSignerError;
|
||||||
|
|
||||||
|
fn nip44_encrypt_async<'a>(
|
||||||
|
&'a self,
|
||||||
|
public_key: &'a PublicKey,
|
||||||
|
content: &'a str,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Result<String, Self::Error>> + Send + 'a>> {
|
||||||
|
let inner = self.inner.read().expect("RwLock poisoned").clone();
|
||||||
|
Box::pin(async move { inner.nip44_encrypt_async(public_key, content).await })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nip44_decrypt_async<'a>(
|
||||||
|
&'a self,
|
||||||
|
public_key: &'a PublicKey,
|
||||||
|
payload: &'a str,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Result<String, Self::Error>> + Send + 'a>> {
|
||||||
|
let inner = self.inner.read().expect("RwLock poisoned").clone();
|
||||||
|
Box::pin(async move { inner.nip44_decrypt_async(public_key, payload).await })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CoopAuthUrlHandler;
|
||||||
|
|
||||||
|
impl AuthUrlHandler for CoopAuthUrlHandler {
|
||||||
|
fn on_auth_url(
|
||||||
|
&self,
|
||||||
|
auth_url: Url,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Result<(), nostr_connect::error::Error>> + Send + '_>> {
|
||||||
|
Box::pin(async move {
|
||||||
|
webbrowser::open(auth_url.as_str()).unwrap();
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use gpui::{hsla, Hsla, Rgba};
|
use gpui::{Hsla, Rgba, hsla};
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@@ -30,6 +30,8 @@ pub struct ThemeColors {
|
|||||||
pub text_muted: Hsla,
|
pub text_muted: Hsla,
|
||||||
pub text_placeholder: Hsla,
|
pub text_placeholder: Hsla,
|
||||||
pub text_accent: Hsla,
|
pub text_accent: Hsla,
|
||||||
|
pub text_danger: Hsla,
|
||||||
|
pub text_warning: Hsla,
|
||||||
|
|
||||||
// Icon colors
|
// Icon colors
|
||||||
pub icon: Hsla,
|
pub icon: Hsla,
|
||||||
@@ -77,11 +79,11 @@ pub struct ThemeColors {
|
|||||||
pub ghost_element_disabled: Hsla,
|
pub ghost_element_disabled: Hsla,
|
||||||
|
|
||||||
// Tab colors
|
// Tab colors
|
||||||
pub tab_inactive_background: Hsla,
|
pub tab_background: Hsla,
|
||||||
pub tab_inactive_foreground: Hsla,
|
pub tab_foreground: Hsla,
|
||||||
|
pub tab_hover_background: Hsla,
|
||||||
pub tab_active_background: Hsla,
|
pub tab_active_background: Hsla,
|
||||||
pub tab_active_foreground: Hsla,
|
pub tab_active_foreground: Hsla,
|
||||||
pub tab_hover_foreground: Hsla,
|
|
||||||
|
|
||||||
// Scrollbar colors
|
// Scrollbar colors
|
||||||
pub scrollbar_thumb_background: Hsla,
|
pub scrollbar_thumb_background: Hsla,
|
||||||
@@ -110,8 +112,8 @@ impl ThemeColors {
|
|||||||
elevated_surface_background: neutral().light().step_3(),
|
elevated_surface_background: neutral().light().step_3(),
|
||||||
panel_background: neutral().light().step_1(),
|
panel_background: neutral().light().step_1(),
|
||||||
overlay: neutral().light_alpha().step_3(),
|
overlay: neutral().light_alpha().step_3(),
|
||||||
title_bar: neutral().light().step_2(),
|
title_bar: neutral().light().step_3(),
|
||||||
title_bar_inactive: neutral().light().step_3(),
|
title_bar_inactive: neutral().light().step_1(),
|
||||||
window_border: hsl(240.0, 5.9, 78.0),
|
window_border: hsl(240.0, 5.9, 78.0),
|
||||||
|
|
||||||
border: neutral().light().step_6(),
|
border: neutral().light().step_6(),
|
||||||
@@ -125,7 +127,9 @@ impl ThemeColors {
|
|||||||
text: neutral().light().step_12(),
|
text: neutral().light().step_12(),
|
||||||
text_muted: neutral().light().step_11(),
|
text_muted: neutral().light().step_11(),
|
||||||
text_placeholder: neutral().light().step_10(),
|
text_placeholder: neutral().light().step_10(),
|
||||||
text_accent: brand().light().step_11(),
|
text_accent: brand().light().step_9(),
|
||||||
|
text_danger: danger().light().step_9(),
|
||||||
|
text_warning: warning().light().step_9(),
|
||||||
|
|
||||||
icon: neutral().light().step_11(),
|
icon: neutral().light().step_11(),
|
||||||
icon_muted: neutral().light().step_10(),
|
icon_muted: neutral().light().step_10(),
|
||||||
@@ -166,17 +170,17 @@ impl ThemeColors {
|
|||||||
ghost_element_selected: neutral().light().step_5(),
|
ghost_element_selected: neutral().light().step_5(),
|
||||||
ghost_element_disabled: neutral().light_alpha().step_2(),
|
ghost_element_disabled: neutral().light_alpha().step_2(),
|
||||||
|
|
||||||
tab_inactive_background: neutral().light().step_2(),
|
tab_background: neutral().light().step_3(),
|
||||||
tab_inactive_foreground: neutral().light().step_11(),
|
tab_foreground: neutral().light().step_11(),
|
||||||
|
tab_hover_background: neutral().light_alpha().step_4(),
|
||||||
tab_active_background: neutral().light().step_1(),
|
tab_active_background: neutral().light().step_1(),
|
||||||
tab_active_foreground: neutral().light().step_12(),
|
tab_active_foreground: neutral().light().step_12(),
|
||||||
tab_hover_foreground: brand().light().step_9(),
|
|
||||||
|
|
||||||
scrollbar_thumb_background: neutral().light_alpha().step_3(),
|
scrollbar_thumb_background: neutral().light_alpha().step_3(),
|
||||||
scrollbar_thumb_hover_background: neutral().light_alpha().step_4(),
|
scrollbar_thumb_hover_background: neutral().light_alpha().step_4(),
|
||||||
scrollbar_thumb_border: gpui::transparent_black(),
|
scrollbar_thumb_border: gpui::transparent_black(),
|
||||||
scrollbar_track_background: gpui::transparent_black(),
|
scrollbar_track_background: gpui::transparent_black(),
|
||||||
scrollbar_track_border: neutral().light().step_5(),
|
scrollbar_track_border: gpui::transparent_black(),
|
||||||
|
|
||||||
drop_target_background: brand().light_alpha().step_2(),
|
drop_target_background: brand().light_alpha().step_2(),
|
||||||
cursor: hsl(200., 100., 50.),
|
cursor: hsl(200., 100., 50.),
|
||||||
@@ -192,9 +196,9 @@ impl ThemeColors {
|
|||||||
background: neutral().dark().step_1(),
|
background: neutral().dark().step_1(),
|
||||||
surface_background: neutral().dark().step_2(),
|
surface_background: neutral().dark().step_2(),
|
||||||
elevated_surface_background: neutral().dark().step_3(),
|
elevated_surface_background: neutral().dark().step_3(),
|
||||||
panel_background: gpui::black(),
|
panel_background: neutral().dark().step_1(),
|
||||||
overlay: neutral().dark_alpha().step_3(),
|
overlay: neutral().dark_alpha().step_3(),
|
||||||
title_bar: gpui::transparent_black(),
|
title_bar: neutral().dark().step_3(),
|
||||||
title_bar_inactive: neutral().dark().step_1(),
|
title_bar_inactive: neutral().dark().step_1(),
|
||||||
window_border: hsl(240.0, 3.7, 28.0),
|
window_border: hsl(240.0, 3.7, 28.0),
|
||||||
|
|
||||||
@@ -209,7 +213,9 @@ impl ThemeColors {
|
|||||||
text: neutral().dark().step_12(),
|
text: neutral().dark().step_12(),
|
||||||
text_muted: neutral().dark().step_11(),
|
text_muted: neutral().dark().step_11(),
|
||||||
text_placeholder: neutral().dark().step_10(),
|
text_placeholder: neutral().dark().step_10(),
|
||||||
text_accent: brand().dark().step_11(),
|
text_accent: brand().dark().step_9(),
|
||||||
|
text_danger: danger().dark().step_9(),
|
||||||
|
text_warning: warning().dark().step_9(),
|
||||||
|
|
||||||
icon: neutral().dark().step_11(),
|
icon: neutral().dark().step_11(),
|
||||||
icon_muted: neutral().dark().step_10(),
|
icon_muted: neutral().dark().step_10(),
|
||||||
@@ -250,17 +256,17 @@ impl ThemeColors {
|
|||||||
ghost_element_selected: neutral().dark().step_5(),
|
ghost_element_selected: neutral().dark().step_5(),
|
||||||
ghost_element_disabled: neutral().dark_alpha().step_2(),
|
ghost_element_disabled: neutral().dark_alpha().step_2(),
|
||||||
|
|
||||||
tab_inactive_background: neutral().dark().step_2(),
|
tab_background: neutral().dark().step_3(),
|
||||||
tab_inactive_foreground: neutral().dark().step_11(),
|
tab_foreground: neutral().dark().step_11(),
|
||||||
tab_active_background: neutral().dark().step_3(),
|
tab_hover_background: neutral().dark_alpha().step_4(),
|
||||||
|
tab_active_background: neutral().dark().step_1(),
|
||||||
tab_active_foreground: neutral().dark().step_12(),
|
tab_active_foreground: neutral().dark().step_12(),
|
||||||
tab_hover_foreground: brand().dark().step_9(),
|
|
||||||
|
|
||||||
scrollbar_thumb_background: neutral().dark_alpha().step_3(),
|
scrollbar_thumb_background: neutral().dark_alpha().step_3(),
|
||||||
scrollbar_thumb_hover_background: neutral().dark_alpha().step_4(),
|
scrollbar_thumb_hover_background: neutral().dark_alpha().step_4(),
|
||||||
scrollbar_thumb_border: gpui::transparent_black(),
|
scrollbar_thumb_border: gpui::transparent_black(),
|
||||||
scrollbar_track_background: gpui::transparent_black(),
|
scrollbar_track_background: gpui::transparent_black(),
|
||||||
scrollbar_track_border: neutral().dark().step_5(),
|
scrollbar_track_border: gpui::transparent_black(),
|
||||||
|
|
||||||
drop_target_background: brand().dark_alpha().step_2(),
|
drop_target_background: brand().dark_alpha().step_2(),
|
||||||
cursor: hsl(200., 100., 50.),
|
cursor: hsl(200., 100., 50.),
|
||||||
|
|||||||
159
crates/theme/src/geometry.rs
Normal file
159
crates/theme/src/geometry.rs
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
use std::fmt::{self, Debug, Display, Formatter};
|
||||||
|
|
||||||
|
use gpui::{AbsoluteLength, Axis, Length, Pixels};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// A enum for defining the placement of the element.
|
||||||
|
///
|
||||||
|
/// See also: [`Side`] if you need to define the left, right side.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum Placement {
|
||||||
|
#[serde(rename = "top")]
|
||||||
|
Top,
|
||||||
|
#[serde(rename = "bottom")]
|
||||||
|
Bottom,
|
||||||
|
#[serde(rename = "left")]
|
||||||
|
Left,
|
||||||
|
#[serde(rename = "right")]
|
||||||
|
Right,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Placement {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Placement::Top => write!(f, "Top"),
|
||||||
|
Placement::Bottom => write!(f, "Bottom"),
|
||||||
|
Placement::Left => write!(f, "Left"),
|
||||||
|
Placement::Right => write!(f, "Right"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Placement {
|
||||||
|
#[inline]
|
||||||
|
pub fn is_horizontal(&self) -> bool {
|
||||||
|
matches!(self, Placement::Left | Placement::Right)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn is_vertical(&self) -> bool {
|
||||||
|
matches!(self, Placement::Top | Placement::Bottom)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn axis(&self) -> Axis {
|
||||||
|
match self {
|
||||||
|
Placement::Top | Placement::Bottom => Axis::Vertical,
|
||||||
|
Placement::Left | Placement::Right => Axis::Horizontal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A enum for defining the side of the element.
|
||||||
|
///
|
||||||
|
/// See also: [`Placement`] if you need to define the 4 edges.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum Side {
|
||||||
|
#[serde(rename = "left")]
|
||||||
|
Left,
|
||||||
|
#[serde(rename = "right")]
|
||||||
|
Right,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Side {
|
||||||
|
/// Returns true if the side is left.
|
||||||
|
#[inline]
|
||||||
|
pub fn is_left(&self) -> bool {
|
||||||
|
matches!(self, Self::Left)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the side is right.
|
||||||
|
#[inline]
|
||||||
|
pub fn is_right(&self) -> bool {
|
||||||
|
matches!(self, Self::Right)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A trait to extend the [`Axis`] enum with utility methods.
|
||||||
|
pub trait AxisExt {
|
||||||
|
#[allow(clippy::wrong_self_convention)]
|
||||||
|
fn is_horizontal(self) -> bool;
|
||||||
|
#[allow(clippy::wrong_self_convention)]
|
||||||
|
fn is_vertical(self) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AxisExt for Axis {
|
||||||
|
#[inline]
|
||||||
|
fn is_horizontal(self) -> bool {
|
||||||
|
self == Axis::Horizontal
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn is_vertical(self) -> bool {
|
||||||
|
self == Axis::Vertical
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A trait for converting [`Pixels`] to `f32` and `f64`.
|
||||||
|
pub trait PixelsExt {
|
||||||
|
fn as_f32(&self) -> f32;
|
||||||
|
#[allow(clippy::wrong_self_convention)]
|
||||||
|
fn as_f64(self) -> f64;
|
||||||
|
}
|
||||||
|
impl PixelsExt for Pixels {
|
||||||
|
fn as_f32(&self) -> f32 {
|
||||||
|
f32::from(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_f64(self) -> f64 {
|
||||||
|
f64::from(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A trait to extend the [`Length`] enum with utility methods.
|
||||||
|
pub trait LengthExt {
|
||||||
|
/// Converts the [`Length`] to [`Pixels`] based on a given `base_size` and `rem_size`.
|
||||||
|
///
|
||||||
|
/// If the [`Length`] is [`Length::Auto`], it returns `None`.
|
||||||
|
fn to_pixels(&self, base_size: AbsoluteLength, rem_size: Pixels) -> Option<Pixels>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LengthExt for Length {
|
||||||
|
fn to_pixels(&self, base_size: AbsoluteLength, rem_size: Pixels) -> Option<Pixels> {
|
||||||
|
match self {
|
||||||
|
Length::Auto => None,
|
||||||
|
Length::Definite(len) => Some(len.to_pixels(base_size, rem_size)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A struct for defining the edges of an element.
|
||||||
|
///
|
||||||
|
/// A extend version of [`gpui::Edges`] to serialize/deserialize.
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, Eq, PartialEq)]
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct Edges<T: Clone + Debug + Default + PartialEq> {
|
||||||
|
/// The size of the top edge.
|
||||||
|
pub top: T,
|
||||||
|
/// The size of the right edge.
|
||||||
|
pub right: T,
|
||||||
|
/// The size of the bottom edge.
|
||||||
|
pub bottom: T,
|
||||||
|
/// The size of the left edge.
|
||||||
|
pub left: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Edges<T>
|
||||||
|
where
|
||||||
|
T: Clone + Debug + Default + PartialEq,
|
||||||
|
{
|
||||||
|
/// Creates a new `Edges` instance with all edges set to the same value.
|
||||||
|
pub fn all(value: T) -> Self {
|
||||||
|
Self {
|
||||||
|
top: value.clone(),
|
||||||
|
right: value.clone(),
|
||||||
|
bottom: value.clone(),
|
||||||
|
left: value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ use std::rc::Rc;
|
|||||||
use gpui::{App, Global, Pixels, SharedString, Window, px};
|
use gpui::{App, Global, Pixels, SharedString, Window, px};
|
||||||
|
|
||||||
mod colors;
|
mod colors;
|
||||||
|
mod geometry;
|
||||||
|
mod notification;
|
||||||
mod platform_kind;
|
mod platform_kind;
|
||||||
mod registry;
|
mod registry;
|
||||||
mod scale;
|
mod scale;
|
||||||
@@ -11,6 +13,8 @@ mod scrollbar_mode;
|
|||||||
mod theme;
|
mod theme;
|
||||||
|
|
||||||
pub use colors::*;
|
pub use colors::*;
|
||||||
|
pub use geometry::*;
|
||||||
|
pub use notification::*;
|
||||||
pub use platform_kind::PlatformKind;
|
pub use platform_kind::PlatformKind;
|
||||||
pub use registry::*;
|
pub use registry::*;
|
||||||
pub use scale::*;
|
pub use scale::*;
|
||||||
@@ -82,6 +86,9 @@ pub struct Theme {
|
|||||||
/// Show the scrollbar mode, default: scrolling
|
/// Show the scrollbar mode, default: scrolling
|
||||||
pub scrollbar_mode: ScrollbarMode,
|
pub scrollbar_mode: ScrollbarMode,
|
||||||
|
|
||||||
|
/// Notification settings
|
||||||
|
pub notification: NotificationSettings,
|
||||||
|
|
||||||
/// Platform kind
|
/// Platform kind
|
||||||
pub platform: PlatformKind,
|
pub platform: PlatformKind,
|
||||||
}
|
}
|
||||||
@@ -185,7 +192,6 @@ impl From<ThemeFamily> for Theme {
|
|||||||
let mode = ThemeMode::default();
|
let mode = ThemeMode::default();
|
||||||
|
|
||||||
// Define the font family based on the platform.
|
// Define the font family based on the platform.
|
||||||
// TODO: Use native fonts on Linux too.
|
|
||||||
let font_family = match platform {
|
let font_family = match platform {
|
||||||
PlatformKind::Linux => "Inter",
|
PlatformKind::Linux => "Inter",
|
||||||
_ => ".SystemUIFont",
|
_ => ".SystemUIFont",
|
||||||
@@ -200,10 +206,11 @@ impl From<ThemeFamily> for Theme {
|
|||||||
Theme {
|
Theme {
|
||||||
font_size: px(15.),
|
font_size: px(15.),
|
||||||
font_family: font_family.into(),
|
font_family: font_family.into(),
|
||||||
radius: px(5.),
|
radius: px(6.),
|
||||||
radius_lg: px(10.),
|
radius_lg: px(10.),
|
||||||
shadow: true,
|
shadow: true,
|
||||||
scrollbar_mode: ScrollbarMode::default(),
|
scrollbar_mode: ScrollbarMode::default(),
|
||||||
|
notification: NotificationSettings::default(),
|
||||||
mode,
|
mode,
|
||||||
colors: *colors,
|
colors: *colors,
|
||||||
theme: Rc::new(family),
|
theme: Rc::new(family),
|
||||||
|
|||||||
30
crates/theme/src/notification.rs
Normal file
30
crates/theme/src/notification.rs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
use gpui::{Anchor, Pixels, px};
|
||||||
|
|
||||||
|
use crate::{Edges, TITLEBAR_HEIGHT};
|
||||||
|
|
||||||
|
/// The settings for notifications.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct NotificationSettings {
|
||||||
|
/// The placement of the notification, default: [`Anchor::TopRight`]
|
||||||
|
pub placement: Anchor,
|
||||||
|
/// The margins of the notification with respect to the window edges.
|
||||||
|
pub margins: Edges<Pixels>,
|
||||||
|
/// The maximum number of notifications to show at once, default: 10
|
||||||
|
pub max_items: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for NotificationSettings {
|
||||||
|
fn default() -> Self {
|
||||||
|
let offset = px(16.);
|
||||||
|
Self {
|
||||||
|
placement: Anchor::TopRight,
|
||||||
|
margins: Edges {
|
||||||
|
top: TITLEBAR_HEIGHT + offset, // avoid overlap with title bar
|
||||||
|
right: offset,
|
||||||
|
bottom: offset,
|
||||||
|
left: offset,
|
||||||
|
},
|
||||||
|
max_items: 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ pub enum PlatformKind {
|
|||||||
Mac,
|
Mac,
|
||||||
Linux,
|
Linux,
|
||||||
Windows,
|
Windows,
|
||||||
|
Web,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PlatformKind {
|
impl PlatformKind {
|
||||||
@@ -11,22 +12,21 @@ impl PlatformKind {
|
|||||||
Self::Linux
|
Self::Linux
|
||||||
} else if cfg!(target_os = "windows") {
|
} else if cfg!(target_os = "windows") {
|
||||||
Self::Windows
|
Self::Windows
|
||||||
} else {
|
} else if cfg!(target_os = "macos") {
|
||||||
Self::Mac
|
Self::Mac
|
||||||
|
} else {
|
||||||
|
Self::Web
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn is_linux(&self) -> bool {
|
pub fn is_linux(&self) -> bool {
|
||||||
matches!(self, Self::Linux)
|
matches!(self, Self::Linux)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn is_windows(&self) -> bool {
|
pub fn is_windows(&self) -> bool {
|
||||||
matches!(self, Self::Windows)
|
matches!(self, Self::Windows)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn is_mac(&self) -> bool {
|
pub fn is_mac(&self) -> bool {
|
||||||
matches!(self, Self::Mac)
|
matches!(self, Self::Mac)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "title_bar"
|
|
||||||
version.workspace = true
|
|
||||||
edition.workspace = true
|
|
||||||
publish.workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
common = { path = "../common" }
|
|
||||||
theme = { path = "../theme" }
|
|
||||||
ui = { path = "../ui" }
|
|
||||||
|
|
||||||
nostr-sdk.workspace = true
|
|
||||||
gpui.workspace = true
|
|
||||||
smol.workspace = true
|
|
||||||
smallvec.workspace = true
|
|
||||||
anyhow.workspace = true
|
|
||||||
log.workspace = true
|
|
||||||
|
|
||||||
[target.'cfg(target_os = "windows")'.dependencies]
|
|
||||||
windows = { version = "0.61", features = ["Wdk_System_SystemServices"] }
|
|
||||||
|
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
|
||||||
linicon = "2.3.0"
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
#[cfg(target_os = "linux")]
|
|
||||||
use gpui::MouseButton;
|
|
||||||
use gpui::prelude::FluentBuilder;
|
|
||||||
use gpui::{
|
|
||||||
AnyElement, Context, Decorations, Hsla, InteractiveElement as _, IntoElement, ParentElement,
|
|
||||||
Pixels, Render, StatefulInteractiveElement as _, Styled, Window, WindowControlArea, px,
|
|
||||||
};
|
|
||||||
use smallvec::{SmallVec, smallvec};
|
|
||||||
use theme::{ActiveTheme, CLIENT_SIDE_DECORATION_ROUNDING, PlatformKind};
|
|
||||||
use ui::h_flex;
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
use crate::platforms::linux::LinuxWindowControls;
|
|
||||||
use crate::platforms::mac::TRAFFIC_LIGHT_PADDING;
|
|
||||||
use crate::platforms::windows::WindowsWindowControls;
|
|
||||||
|
|
||||||
mod platforms;
|
|
||||||
|
|
||||||
/// Titlebar
|
|
||||||
pub struct TitleBar {
|
|
||||||
/// Children elements of the title bar.
|
|
||||||
children: SmallVec<[AnyElement; 2]>,
|
|
||||||
|
|
||||||
/// Whether the title bar is currently being moved.
|
|
||||||
should_move: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TitleBar {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
children: smallvec![],
|
|
||||||
should_move: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
pub fn height(&self, window: &mut Window) -> Pixels {
|
|
||||||
(1.75 * window.rem_size()).max(px(34.))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
pub fn height(&self, _window: &mut Window) -> Pixels {
|
|
||||||
px(32.)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn titlebar_color(&self, window: &mut Window, cx: &mut Context<Self>) -> Hsla {
|
|
||||||
if cfg!(any(target_os = "linux", target_os = "freebsd")) {
|
|
||||||
if window.is_window_active() && !self.should_move {
|
|
||||||
cx.theme().title_bar
|
|
||||||
} else {
|
|
||||||
cx.theme().title_bar_inactive
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cx.theme().title_bar
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_children<T>(&mut self, children: T)
|
|
||||||
where
|
|
||||||
T: IntoIterator<Item = AnyElement>,
|
|
||||||
{
|
|
||||||
self.children = children.into_iter().collect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for TitleBar {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for TitleBar {
|
|
||||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
let height = self.height(window);
|
|
||||||
let color = self.titlebar_color(window, cx);
|
|
||||||
let children = std::mem::take(&mut self.children);
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
let supported_controls = window.window_controls();
|
|
||||||
let decorations = window.window_decorations();
|
|
||||||
|
|
||||||
h_flex()
|
|
||||||
.window_control_area(WindowControlArea::Drag)
|
|
||||||
.h(height)
|
|
||||||
.w_full()
|
|
||||||
.map(|this| {
|
|
||||||
if window.is_fullscreen() {
|
|
||||||
this.px_2()
|
|
||||||
} else if cx.theme().platform.is_mac() {
|
|
||||||
this.pr_2().pl(px(TRAFFIC_LIGHT_PADDING))
|
|
||||||
} else {
|
|
||||||
this.px_2()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.map(|this| match decorations {
|
|
||||||
Decorations::Server => this,
|
|
||||||
Decorations::Client { tiling } => this
|
|
||||||
.when(!(tiling.top || tiling.right), |div| {
|
|
||||||
div.rounded_tr(CLIENT_SIDE_DECORATION_ROUNDING)
|
|
||||||
})
|
|
||||||
.when(!(tiling.top || tiling.left), |div| {
|
|
||||||
div.rounded_tl(CLIENT_SIDE_DECORATION_ROUNDING)
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.bg(color)
|
|
||||||
.border_b_1()
|
|
||||||
.border_color(cx.theme().border)
|
|
||||||
.content_stretch()
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.id("title-bar")
|
|
||||||
.justify_between()
|
|
||||||
.w_full()
|
|
||||||
.when(cx.theme().platform.is_mac(), |this| {
|
|
||||||
this.on_click(|event, window, _| {
|
|
||||||
if event.click_count() == 2 {
|
|
||||||
window.titlebar_double_click();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.when(cx.theme().platform.is_linux(), |this| {
|
|
||||||
this.on_click(|event, window, _| {
|
|
||||||
if event.click_count() == 2 {
|
|
||||||
window.zoom_window();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.when(!cx.theme().platform.is_mac(), |this| this.pr_2())
|
|
||||||
.children(children),
|
|
||||||
)
|
|
||||||
.when(!window.is_fullscreen(), |this| match cx.theme().platform {
|
|
||||||
PlatformKind::Linux => {
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
if matches!(decorations, Decorations::Client { .. }) {
|
|
||||||
this.child(LinuxWindowControls::new(None))
|
|
||||||
.when(supported_controls.window_menu, |this| {
|
|
||||||
this.on_mouse_down(MouseButton::Right, move |ev, window, _| {
|
|
||||||
window.show_window_menu(ev.position)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.on_mouse_move(cx.listener(move |this, _ev, window, _| {
|
|
||||||
if this.should_move {
|
|
||||||
this.should_move = false;
|
|
||||||
window.start_window_move();
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
.on_mouse_down_out(cx.listener(move |this, _ev, _window, _cx| {
|
|
||||||
this.should_move = false;
|
|
||||||
}))
|
|
||||||
.on_mouse_up(
|
|
||||||
MouseButton::Left,
|
|
||||||
cx.listener(move |this, _ev, _window, _cx| {
|
|
||||||
this.should_move = false;
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.on_mouse_down(
|
|
||||||
MouseButton::Left,
|
|
||||||
cx.listener(move |this, _ev, _window, _cx| {
|
|
||||||
this.should_move = true;
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
this
|
|
||||||
}
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
|
||||||
this
|
|
||||||
}
|
|
||||||
PlatformKind::Windows => this.child(WindowsWindowControls::new(height)),
|
|
||||||
PlatformKind::Mac => this,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
use std::sync::OnceLock;
|
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder;
|
|
||||||
use gpui::{
|
|
||||||
svg, Action, App, InteractiveElement, IntoElement, MouseButton, ParentElement, RenderOnce,
|
|
||||||
SharedString, StatefulInteractiveElement, Styled, Window,
|
|
||||||
};
|
|
||||||
use linicon::{lookup_icon, IconType};
|
|
||||||
use theme::ActiveTheme;
|
|
||||||
use ui::{h_flex, Icon, IconName, Sizable};
|
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
|
||||||
pub struct LinuxWindowControls {
|
|
||||||
close_window_action: Option<Box<dyn Action>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LinuxWindowControls {
|
|
||||||
pub fn new(close_window_action: Option<Box<dyn Action>>) -> Self {
|
|
||||||
Self {
|
|
||||||
close_window_action,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderOnce for LinuxWindowControls {
|
|
||||||
fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
|
||||||
let supported_controls = window.window_controls();
|
|
||||||
|
|
||||||
h_flex()
|
|
||||||
.id("linux-window-controls")
|
|
||||||
.gap_2()
|
|
||||||
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
|
||||||
.when(supported_controls.minimize, |this| {
|
|
||||||
this.child(WindowControl::new(
|
|
||||||
LinuxControl::Minimize,
|
|
||||||
IconName::WindowMinimize,
|
|
||||||
))
|
|
||||||
})
|
|
||||||
.when(supported_controls.maximize, |this| {
|
|
||||||
this.child({
|
|
||||||
if window.is_maximized() {
|
|
||||||
WindowControl::new(LinuxControl::Restore, IconName::WindowRestore)
|
|
||||||
} else {
|
|
||||||
WindowControl::new(LinuxControl::Maximize, IconName::WindowMaximize)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
WindowControl::new(LinuxControl::Close, IconName::WindowClose)
|
|
||||||
.when_some(self.close_window_action, |this, close_action| {
|
|
||||||
this.close_action(close_action)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
|
||||||
pub struct WindowControl {
|
|
||||||
kind: LinuxControl,
|
|
||||||
fallback: IconName,
|
|
||||||
close_action: Option<Box<dyn Action>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WindowControl {
|
|
||||||
pub fn new(kind: LinuxControl, fallback: IconName) -> Self {
|
|
||||||
Self {
|
|
||||||
kind,
|
|
||||||
fallback,
|
|
||||||
close_action: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn close_action(mut self, action: Box<dyn Action>) -> Self {
|
|
||||||
self.close_action = Some(action);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_gnome(&self) -> bool {
|
|
||||||
matches!(detect_desktop_environment(), DesktopEnvironment::Gnome)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderOnce for WindowControl {
|
|
||||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
|
||||||
let is_gnome = self.is_gnome();
|
|
||||||
|
|
||||||
h_flex()
|
|
||||||
.id(self.kind.as_icon_name())
|
|
||||||
.group("")
|
|
||||||
.justify_center()
|
|
||||||
.items_center()
|
|
||||||
.rounded_full()
|
|
||||||
.size_6()
|
|
||||||
.when(is_gnome, |this| {
|
|
||||||
this.bg(cx.theme().ghost_element_background_alt)
|
|
||||||
.hover(|this| this.bg(cx.theme().ghost_element_hover))
|
|
||||||
.active(|this| this.bg(cx.theme().ghost_element_active))
|
|
||||||
})
|
|
||||||
.map(|this| {
|
|
||||||
if let Some(Some(path)) = linux_controls().get(&self.kind).cloned() {
|
|
||||||
this.child(
|
|
||||||
svg()
|
|
||||||
.external_path(SharedString::from(path))
|
|
||||||
.size_4()
|
|
||||||
.text_color(cx.theme().text),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
this.child(Icon::new(self.fallback).small().text_color(cx.theme().text))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.on_mouse_move(|_, _window, cx| cx.stop_propagation())
|
|
||||||
.on_click(move |_, window, cx| {
|
|
||||||
cx.stop_propagation();
|
|
||||||
match self.kind {
|
|
||||||
LinuxControl::Minimize => window.minimize_window(),
|
|
||||||
LinuxControl::Restore => window.zoom_window(),
|
|
||||||
LinuxControl::Maximize => window.zoom_window(),
|
|
||||||
LinuxControl::Close => cx.quit(),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static DE: OnceLock<DesktopEnvironment> = OnceLock::new();
|
|
||||||
static LINUX_CONTROLS: OnceLock<HashMap<LinuxControl, Option<String>>> = OnceLock::new();
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
||||||
pub enum DesktopEnvironment {
|
|
||||||
Gnome,
|
|
||||||
Kde,
|
|
||||||
Unknown,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Detect the current desktop environment
|
|
||||||
pub fn detect_desktop_environment() -> &'static DesktopEnvironment {
|
|
||||||
DE.get_or_init(|| {
|
|
||||||
// Try to use environment variables first
|
|
||||||
if let Ok(output) = std::env::var("XDG_CURRENT_DESKTOP") {
|
|
||||||
let desktop = output.to_lowercase();
|
|
||||||
if desktop.contains("gnome") {
|
|
||||||
return DesktopEnvironment::Gnome;
|
|
||||||
} else if desktop.contains("kde") {
|
|
||||||
return DesktopEnvironment::Kde;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback detection methods
|
|
||||||
if let Ok(output) = std::env::var("DESKTOP_SESSION") {
|
|
||||||
let session = output.to_lowercase();
|
|
||||||
if session.contains("gnome") {
|
|
||||||
return DesktopEnvironment::Gnome;
|
|
||||||
} else if session.contains("kde") || session.contains("plasma") {
|
|
||||||
return DesktopEnvironment::Kde;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DesktopEnvironment::Unknown
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
|
||||||
pub enum LinuxControl {
|
|
||||||
Minimize,
|
|
||||||
Restore,
|
|
||||||
Maximize,
|
|
||||||
Close,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LinuxControl {
|
|
||||||
pub fn as_icon_name(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
LinuxControl::Close => "window-close",
|
|
||||||
LinuxControl::Minimize => "window-minimize",
|
|
||||||
LinuxControl::Maximize => "window-maximize",
|
|
||||||
LinuxControl::Restore => "window-restore",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn linux_controls() -> &'static HashMap<LinuxControl, Option<String>> {
|
|
||||||
LINUX_CONTROLS.get_or_init(|| {
|
|
||||||
let mut icons = HashMap::new();
|
|
||||||
icons.insert(LinuxControl::Close, None);
|
|
||||||
icons.insert(LinuxControl::Minimize, None);
|
|
||||||
icons.insert(LinuxControl::Maximize, None);
|
|
||||||
icons.insert(LinuxControl::Restore, None);
|
|
||||||
|
|
||||||
let icon_names = [
|
|
||||||
(LinuxControl::Close, vec!["window-close", "dialog-close"]),
|
|
||||||
(
|
|
||||||
LinuxControl::Minimize,
|
|
||||||
vec!["window-minimize", "window-lower"],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
LinuxControl::Maximize,
|
|
||||||
vec!["window-maximize", "window-expand"],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
LinuxControl::Restore,
|
|
||||||
vec!["window-restore", "window-return"],
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (control, icon_names) in icon_names {
|
|
||||||
for icon_name in icon_names {
|
|
||||||
// Try GNOME-style naming first
|
|
||||||
let mut control_icon = lookup_icon(format!("{icon_name}-symbolic"))
|
|
||||||
.find(|icon| matches!(icon, Ok(icon) if icon.icon_type == IconType::SVG));
|
|
||||||
|
|
||||||
// If not found, try KDE-style naming
|
|
||||||
if control_icon.is_none() {
|
|
||||||
control_icon = lookup_icon(icon_name)
|
|
||||||
.find(|icon| matches!(icon, Ok(icon) if icon.icon_type == IconType::SVG));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(Ok(icon)) = control_icon {
|
|
||||||
icons
|
|
||||||
.entry(control)
|
|
||||||
.and_modify(|v| *v = Some(icon.path.to_string_lossy().to_string()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
icons
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
/// Use pixels here instead of a rem-based size because the macOS traffic
|
|
||||||
/// lights are a static size, and don't scale with the rest of the UI.
|
|
||||||
///
|
|
||||||
/// Magic number: There is one extra pixel of padding on the left side due to
|
|
||||||
/// the 1px border around the window on macOS apps.
|
|
||||||
pub const TRAFFIC_LIGHT_PADDING: f32 = 80.;
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
#[cfg(target_os = "linux")]
|
|
||||||
pub mod linux;
|
|
||||||
pub mod mac;
|
|
||||||
pub mod windows;
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
use gpui::prelude::FluentBuilder;
|
|
||||||
use gpui::{
|
|
||||||
div, px, App, ElementId, Hsla, InteractiveElement, IntoElement, ParentElement, Pixels,
|
|
||||||
RenderOnce, Rgba, StatefulInteractiveElement, Styled, Window, WindowControlArea,
|
|
||||||
};
|
|
||||||
use theme::ActiveTheme;
|
|
||||||
use ui::h_flex;
|
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
|
||||||
pub struct WindowsWindowControls {
|
|
||||||
button_height: Pixels,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WindowsWindowControls {
|
|
||||||
pub fn new(button_height: Pixels) -> Self {
|
|
||||||
Self { button_height }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
fn get_font() -> &'static str {
|
|
||||||
"Segoe Fluent Icons"
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
fn get_font() -> &'static str {
|
|
||||||
use windows::Wdk::System::SystemServices::RtlGetVersion;
|
|
||||||
|
|
||||||
let mut version = unsafe { std::mem::zeroed() };
|
|
||||||
let status = unsafe { RtlGetVersion(&mut version) };
|
|
||||||
|
|
||||||
if status.is_ok() && version.dwBuildNumber >= 22000 {
|
|
||||||
"Segoe Fluent Icons"
|
|
||||||
} else {
|
|
||||||
"Segoe MDL2 Assets"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderOnce for WindowsWindowControls {
|
|
||||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
|
||||||
let close_button_hover_color = Rgba {
|
|
||||||
r: 232.0 / 255.0,
|
|
||||||
g: 17.0 / 255.0,
|
|
||||||
b: 32.0 / 255.0,
|
|
||||||
a: 1.0,
|
|
||||||
};
|
|
||||||
|
|
||||||
let button_hover_color = cx.theme().ghost_element_hover;
|
|
||||||
let button_active_color = cx.theme().ghost_element_active;
|
|
||||||
|
|
||||||
div()
|
|
||||||
.id("windows-window-controls")
|
|
||||||
.font_family(Self::get_font())
|
|
||||||
.flex()
|
|
||||||
.flex_row()
|
|
||||||
.justify_center()
|
|
||||||
.content_stretch()
|
|
||||||
.max_h(self.button_height)
|
|
||||||
.min_h(self.button_height)
|
|
||||||
.child(WindowsCaptionButton::new(
|
|
||||||
"minimize",
|
|
||||||
WindowsCaptionButtonIcon::Minimize,
|
|
||||||
button_hover_color,
|
|
||||||
button_active_color,
|
|
||||||
))
|
|
||||||
.child(WindowsCaptionButton::new(
|
|
||||||
"maximize-or-restore",
|
|
||||||
if window.is_maximized() {
|
|
||||||
WindowsCaptionButtonIcon::Restore
|
|
||||||
} else {
|
|
||||||
WindowsCaptionButtonIcon::Maximize
|
|
||||||
},
|
|
||||||
button_hover_color,
|
|
||||||
button_active_color,
|
|
||||||
))
|
|
||||||
.child(WindowsCaptionButton::new(
|
|
||||||
"close",
|
|
||||||
WindowsCaptionButtonIcon::Close,
|
|
||||||
close_button_hover_color,
|
|
||||||
button_active_color,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
|
||||||
enum WindowsCaptionButtonIcon {
|
|
||||||
Minimize,
|
|
||||||
Restore,
|
|
||||||
Maximize,
|
|
||||||
Close,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
|
||||||
struct WindowsCaptionButton {
|
|
||||||
id: ElementId,
|
|
||||||
icon: WindowsCaptionButtonIcon,
|
|
||||||
hover_background_color: Hsla,
|
|
||||||
active_background_color: Hsla,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WindowsCaptionButton {
|
|
||||||
pub fn new(
|
|
||||||
id: impl Into<ElementId>,
|
|
||||||
icon: WindowsCaptionButtonIcon,
|
|
||||||
hover_background_color: impl Into<Hsla>,
|
|
||||||
active_background_color: impl Into<Hsla>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
id: id.into(),
|
|
||||||
icon,
|
|
||||||
hover_background_color: hover_background_color.into(),
|
|
||||||
active_background_color: active_background_color.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderOnce for WindowsCaptionButton {
|
|
||||||
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
|
||||||
h_flex()
|
|
||||||
.id(self.id)
|
|
||||||
.justify_center()
|
|
||||||
.content_center()
|
|
||||||
.occlude()
|
|
||||||
.w(px(36.))
|
|
||||||
.h_full()
|
|
||||||
.text_size(px(10.0))
|
|
||||||
.hover(|style| style.bg(self.hover_background_color))
|
|
||||||
.active(|style| style.bg(self.active_background_color))
|
|
||||||
.map(|this| match self.icon {
|
|
||||||
WindowsCaptionButtonIcon::Close => {
|
|
||||||
this.window_control_area(WindowControlArea::Close)
|
|
||||||
}
|
|
||||||
WindowsCaptionButtonIcon::Maximize | WindowsCaptionButtonIcon::Restore => {
|
|
||||||
this.window_control_area(WindowControlArea::Max)
|
|
||||||
}
|
|
||||||
WindowsCaptionButtonIcon::Minimize => {
|
|
||||||
this.window_control_area(WindowControlArea::Min)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.child(match self.icon {
|
|
||||||
WindowsCaptionButtonIcon::Minimize => "\u{e921}",
|
|
||||||
WindowsCaptionButtonIcon::Restore => "\u{e923}",
|
|
||||||
WindowsCaptionButtonIcon::Maximize => "\u{e922}",
|
|
||||||
WindowsCaptionButtonIcon::Close => "\u{e8bb}",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,9 +9,8 @@ common = { path = "../common" }
|
|||||||
theme = { path = "../theme" }
|
theme = { path = "../theme" }
|
||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
smol.workspace = true
|
instant.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
itertools.workspace = true
|
itertools.workspace = true
|
||||||
@@ -20,7 +19,10 @@ log.workspace = true
|
|||||||
unicode-segmentation = "1.12.0"
|
unicode-segmentation = "1.12.0"
|
||||||
uuid = "1.10"
|
uuid = "1.10"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
image = "0.25.1"
|
|
||||||
lsp-types = "0.97.0"
|
lsp-types = "0.97.0"
|
||||||
rope = { git = "https://github.com/zed-industries/zed" }
|
ropey = { version = "=2.0.0-beta.1", features = ["metric_lines_lf", "metric_utf16"] }
|
||||||
sum_tree = { git = "https://github.com/zed-industries/zed" }
|
sum_tree = { git = "https://github.com/zed-industries/zed" }
|
||||||
|
|
||||||
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
|
smol.workspace = true
|
||||||
|
tree-sitter = "0.26"
|
||||||
|
|||||||
@@ -1,191 +0,0 @@
|
|||||||
Copyright 2024 Longbridge <https://longbridge.com>
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
|
|
||||||
|
|
||||||
Apache License
|
|
||||||
Version 2.0, January 2004
|
|
||||||
http://www.apache.org/licenses/
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
||||||
|
|
||||||
1. Definitions.
|
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction,
|
|
||||||
and distribution as defined by Sections 1 through 9 of this document.
|
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by
|
|
||||||
the copyright owner that is granting the License.
|
|
||||||
|
|
||||||
"Legal Entity" shall mean the union of the acting entity and all
|
|
||||||
other entities that control, are controlled by, or are under common
|
|
||||||
control with that entity. For the purposes of this definition,
|
|
||||||
"control" means (i) the power, direct or indirect, to cause the
|
|
||||||
direction or management of such entity, whether by contract or
|
|
||||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
||||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity
|
|
||||||
exercising permissions granted by this License.
|
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications,
|
|
||||||
including but not limited to software source code, documentation
|
|
||||||
source, and configuration files.
|
|
||||||
|
|
||||||
"Object" form shall mean any form resulting from mechanical
|
|
||||||
transformation or translation of a Source form, including but
|
|
||||||
not limited to compiled object code, generated documentation,
|
|
||||||
and conversions to other media types.
|
|
||||||
|
|
||||||
"Work" shall mean the work of authorship, whether in Source or
|
|
||||||
Object form, made available under the License, as indicated by a
|
|
||||||
copyright notice that is included in or attached to the work
|
|
||||||
(an example is provided in the Appendix below).
|
|
||||||
|
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object
|
|
||||||
form, that is based on (or derived from) the Work and for which the
|
|
||||||
editorial revisions, annotations, elaborations, or other modifications
|
|
||||||
represent, as a whole, an original work of authorship. For the purposes
|
|
||||||
of this License, Derivative Works shall not include works that remain
|
|
||||||
separable from, or merely link (or bind by name) to the interfaces of,
|
|
||||||
the Work and Derivative Works thereof.
|
|
||||||
|
|
||||||
"Contribution" shall mean any work of authorship, including
|
|
||||||
the original version of the Work and any modifications or additions
|
|
||||||
to that Work or Derivative Works thereof, that is intentionally
|
|
||||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
||||||
or by an individual or Legal Entity authorized to submit on behalf of
|
|
||||||
the copyright owner. For the purposes of this definition, "submitted"
|
|
||||||
means any form of electronic, verbal, or written communication sent
|
|
||||||
to the Licensor or its representatives, including but not limited to
|
|
||||||
communication on electronic mailing lists, source code control systems,
|
|
||||||
and issue tracking systems that are managed by, or on behalf of, the
|
|
||||||
Licensor for the purpose of discussing and improving the Work, but
|
|
||||||
excluding communication that is conspicuously marked or otherwise
|
|
||||||
designated in writing by the copyright owner as "Not a Contribution."
|
|
||||||
|
|
||||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
||||||
on behalf of whom a Contribution has been received by Licensor and
|
|
||||||
subsequently incorporated within the Work.
|
|
||||||
|
|
||||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
copyright license to reproduce, prepare Derivative Works of,
|
|
||||||
publicly display, publicly perform, sublicense, and distribute the
|
|
||||||
Work and such Derivative Works in Source or Object form.
|
|
||||||
|
|
||||||
3. Grant of Patent License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
(except as stated in this section) patent license to make, have made,
|
|
||||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
||||||
where such license applies only to those patent claims licensable
|
|
||||||
by such Contributor that are necessarily infringed by their
|
|
||||||
Contribution(s) alone or by combination of their Contribution(s)
|
|
||||||
with the Work to which such Contribution(s) was submitted. If You
|
|
||||||
institute patent litigation against any entity (including a
|
|
||||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
||||||
or a Contribution incorporated within the Work constitutes direct
|
|
||||||
or contributory patent infringement, then any patent licenses
|
|
||||||
granted to You under this License for that Work shall terminate
|
|
||||||
as of the date such litigation is filed.
|
|
||||||
|
|
||||||
4. Redistribution. You may reproduce and distribute copies of the
|
|
||||||
Work or Derivative Works thereof in any medium, with or without
|
|
||||||
modifications, and in Source or Object form, provided that You
|
|
||||||
meet the following conditions:
|
|
||||||
|
|
||||||
(a) You must give any other recipients of the Work or
|
|
||||||
Derivative Works a copy of this License; and
|
|
||||||
|
|
||||||
(b) You must cause any modified files to carry prominent notices
|
|
||||||
stating that You changed the files; and
|
|
||||||
|
|
||||||
(c) You must retain, in the Source form of any Derivative Works
|
|
||||||
that You distribute, all copyright, patent, trademark, and
|
|
||||||
attribution notices from the Source form of the Work,
|
|
||||||
excluding those notices that do not pertain to any part of
|
|
||||||
the Derivative Works; and
|
|
||||||
|
|
||||||
(d) If the Work includes a "NOTICE" text file as part of its
|
|
||||||
distribution, then any Derivative Works that You distribute must
|
|
||||||
include a readable copy of the attribution notices contained
|
|
||||||
within such NOTICE file, excluding those notices that do not
|
|
||||||
pertain to any part of the Derivative Works, in at least one
|
|
||||||
of the following places: within a NOTICE text file distributed
|
|
||||||
as part of the Derivative Works; within the Source form or
|
|
||||||
documentation, if provided along with the Derivative Works; or,
|
|
||||||
within a display generated by the Derivative Works, if and
|
|
||||||
wherever such third-party notices normally appear. The contents
|
|
||||||
of the NOTICE file are for informational purposes only and
|
|
||||||
do not modify the License. You may add Your own attribution
|
|
||||||
notices within Derivative Works that You distribute, alongside
|
|
||||||
or as an addendum to the NOTICE text from the Work, provided
|
|
||||||
that such additional attribution notices cannot be construed
|
|
||||||
as modifying the License.
|
|
||||||
|
|
||||||
You may add Your own copyright statement to Your modifications and
|
|
||||||
may provide additional or different license terms and conditions
|
|
||||||
for use, reproduction, or distribution of Your modifications, or
|
|
||||||
for any such Derivative Works as a whole, provided Your use,
|
|
||||||
reproduction, and distribution of the Work otherwise complies with
|
|
||||||
the conditions stated in this License.
|
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
||||||
any Contribution intentionally submitted for inclusion in the Work
|
|
||||||
by You to the Licensor shall be under the terms and conditions of
|
|
||||||
this License, without any additional terms or conditions.
|
|
||||||
Notwithstanding the above, nothing herein shall supersede or modify
|
|
||||||
the terms of any separate license agreement you may have executed
|
|
||||||
with Licensor regarding such Contributions.
|
|
||||||
|
|
||||||
6. Trademarks. This License does not grant permission to use the trade
|
|
||||||
names, trademarks, service marks, or product names of the Licensor,
|
|
||||||
except as required for reasonable and customary use in describing the
|
|
||||||
origin of the Work and reproducing the content of the NOTICE file.
|
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
||||||
agreed to in writing, Licensor provides the Work (and each
|
|
||||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
implied, including, without limitation, any warranties or conditions
|
|
||||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
||||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
||||||
appropriateness of using or redistributing the Work and assume any
|
|
||||||
risks associated with Your exercise of permissions under this License.
|
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory,
|
|
||||||
whether in tort (including negligence), contract, or otherwise,
|
|
||||||
unless required by applicable law (such as deliberate and grossly
|
|
||||||
negligent acts) or agreed to in writing, shall any Contributor be
|
|
||||||
liable to You for damages, including any direct, indirect, special,
|
|
||||||
incidental, or consequential damages of any character arising as a
|
|
||||||
result of this License or out of the use or inability to use the
|
|
||||||
Work (including but not limited to damages for loss of goodwill,
|
|
||||||
work stoppage, computer failure or malfunction, or any and all
|
|
||||||
other commercial damages or losses), even if such Contributor
|
|
||||||
has been advised of the possibility of such damages.
|
|
||||||
|
|
||||||
9. Accepting Warranty or Additional Liability. While redistributing
|
|
||||||
the Work or Derivative Works thereof, You may choose to offer,
|
|
||||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
||||||
or other liability obligations and/or rights consistent with this
|
|
||||||
License. However, in accepting such obligations, You may act only
|
|
||||||
on Your own behalf and on Your sole responsibility, not on behalf
|
|
||||||
of any other Contributor, and only if You agree to indemnify,
|
|
||||||
defend, and hold each Contributor harmless for any liability
|
|
||||||
incurred by, or claims asserted against, such Contributor by reason
|
|
||||||
of your accepting any such warranty or additional liability.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
@@ -1,333 +0,0 @@
|
|||||||
//! This is a fork of gpui's anchored element that adds support for offsetting
|
|
||||||
//! https://github.com/zed-industries/zed/blob/b06f4088a3565c5e30663106ff79c1ced645d87a/crates/gpui/src/elements/anchored.rs
|
|
||||||
use gpui::{
|
|
||||||
point, px, AnyElement, App, Axis, Bounds, Display, Edges, Element, GlobalElementId, Half,
|
|
||||||
InspectorElementId, IntoElement, LayoutId, ParentElement, Pixels, Point, Position, Size, Style,
|
|
||||||
Window,
|
|
||||||
};
|
|
||||||
use smallvec::SmallVec;
|
|
||||||
|
|
||||||
use crate::Anchor;
|
|
||||||
|
|
||||||
/// The state that the anchored element element uses to track its children.
|
|
||||||
pub struct AnchoredState {
|
|
||||||
child_layout_ids: SmallVec<[LayoutId; 4]>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An anchored element that can be used to display UI that
|
|
||||||
/// will avoid overflowing the window bounds.
|
|
||||||
pub(crate) struct Anchored {
|
|
||||||
children: SmallVec<[AnyElement; 2]>,
|
|
||||||
anchor_corner: Anchor,
|
|
||||||
fit_mode: AnchoredFitMode,
|
|
||||||
anchor_position: Option<Point<Pixels>>,
|
|
||||||
position_mode: AnchoredPositionMode,
|
|
||||||
offset: Option<Point<Pixels>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// anchored gives you an element that will avoid overflowing the window bounds.
|
|
||||||
/// Its children should have no margin to avoid measurement issues.
|
|
||||||
pub(crate) fn anchored() -> Anchored {
|
|
||||||
Anchored {
|
|
||||||
children: SmallVec::new(),
|
|
||||||
anchor_corner: Anchor::TopLeft,
|
|
||||||
fit_mode: AnchoredFitMode::SwitchAnchor,
|
|
||||||
anchor_position: None,
|
|
||||||
position_mode: AnchoredPositionMode::Window,
|
|
||||||
offset: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
impl Anchored {
|
|
||||||
/// Sets which corner of the anchored element should be anchored to the current position.
|
|
||||||
pub fn anchor(mut self, anchor: Anchor) -> Self {
|
|
||||||
self.anchor_corner = anchor;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the position in window coordinates
|
|
||||||
/// (otherwise the location the anchored element is rendered is used)
|
|
||||||
pub fn position(mut self, anchor: Point<Pixels>) -> Self {
|
|
||||||
self.anchor_position = Some(anchor);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Offset the final position by this amount.
|
|
||||||
/// Useful when you want to anchor to an element but offset from it, such as in PopoverMenu.
|
|
||||||
pub fn offset(mut self, offset: Point<Pixels>) -> Self {
|
|
||||||
self.offset = Some(offset);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the position mode for this anchored element. Local will have this
|
|
||||||
/// interpret its [`Anchored::position`] as relative to the parent element.
|
|
||||||
/// While Window will have it interpret the position as relative to the window.
|
|
||||||
pub fn position_mode(mut self, mode: AnchoredPositionMode) -> Self {
|
|
||||||
self.position_mode = mode;
|
|
||||||
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 = AnchoredFitMode::SnapToWindow;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Snap to window edge and leave some margins.
|
|
||||||
pub fn snap_to_window_with_margin(mut self, edges: impl Into<Edges<Pixels>>) -> Self {
|
|
||||||
self.fit_mode = AnchoredFitMode::SnapToWindowWithMargin(edges.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ParentElement for Anchored {
|
|
||||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
|
||||||
self.children.extend(elements)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Element for Anchored {
|
|
||||||
type PrepaintState = ();
|
|
||||||
type RequestLayoutState = AnchoredState;
|
|
||||||
|
|
||||||
fn id(&self) -> Option<gpui::ElementId> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn request_layout(
|
|
||||||
&mut self,
|
|
||||||
_id: Option<&GlobalElementId>,
|
|
||||||
_inspector_id: Option<&InspectorElementId>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> (gpui::LayoutId, Self::RequestLayoutState) {
|
|
||||||
let child_layout_ids = self
|
|
||||||
.children
|
|
||||||
.iter_mut()
|
|
||||||
.map(|child| child.request_layout(window, cx))
|
|
||||||
.collect::<SmallVec<_>>();
|
|
||||||
|
|
||||||
let anchored_style = Style {
|
|
||||||
position: Position::Absolute,
|
|
||||||
display: Display::Flex,
|
|
||||||
..Style::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let layout_id = window.request_layout(anchored_style, child_layout_ids.iter().copied(), cx);
|
|
||||||
|
|
||||||
(layout_id, AnchoredState { child_layout_ids })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepaint(
|
|
||||||
&mut self,
|
|
||||||
_id: Option<&GlobalElementId>,
|
|
||||||
_inspector_id: Option<&InspectorElementId>,
|
|
||||||
bounds: Bounds<Pixels>,
|
|
||||||
request_layout: &mut Self::RequestLayoutState,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) {
|
|
||||||
if request_layout.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 &request_layout.child_layout_ids {
|
|
||||||
let child_bounds = window.layout_bounds(*child_layout_id);
|
|
||||||
child_min = child_min.min(&child_bounds.origin);
|
|
||||||
child_max = child_max.max(&child_bounds.bottom_right());
|
|
||||||
}
|
|
||||||
let size: Size<Pixels> = (child_max - child_min).into();
|
|
||||||
|
|
||||||
let (origin, mut desired) = self.position_mode.get_position_and_bounds(
|
|
||||||
self.anchor_position,
|
|
||||||
self.anchor_corner,
|
|
||||||
size,
|
|
||||||
bounds,
|
|
||||||
self.offset,
|
|
||||||
);
|
|
||||||
|
|
||||||
let limits = Bounds {
|
|
||||||
origin: Point::default(),
|
|
||||||
size: window.viewport_size(),
|
|
||||||
};
|
|
||||||
|
|
||||||
if self.fit_mode == AnchoredFitMode::SwitchAnchor {
|
|
||||||
let mut anchor_corner = self.anchor_corner;
|
|
||||||
|
|
||||||
if desired.left() < limits.left() || desired.right() > limits.right() {
|
|
||||||
let switched = Bounds::from_corner_and_size(
|
|
||||||
anchor_corner
|
|
||||||
.other_side_corner_along(Axis::Horizontal)
|
|
||||||
.into(),
|
|
||||||
origin,
|
|
||||||
size,
|
|
||||||
);
|
|
||||||
if !(switched.left() < limits.left() || switched.right() > limits.right()) {
|
|
||||||
anchor_corner = anchor_corner.other_side_corner_along(Axis::Horizontal);
|
|
||||||
desired = switched
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if desired.top() < limits.top() || desired.bottom() > limits.bottom() {
|
|
||||||
let switched = Bounds::from_corner_and_size(
|
|
||||||
anchor_corner.other_side_corner_along(Axis::Vertical).into(),
|
|
||||||
origin,
|
|
||||||
size,
|
|
||||||
);
|
|
||||||
if !(switched.top() < limits.top() || switched.bottom() > limits.bottom()) {
|
|
||||||
desired = switched;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let client_inset = window.client_inset().unwrap_or(px(0.));
|
|
||||||
let edges = match self.fit_mode {
|
|
||||||
AnchoredFitMode::SnapToWindowWithMargin(edges) => edges,
|
|
||||||
_ => Edges::default(),
|
|
||||||
}
|
|
||||||
.map(|edge| *edge + client_inset);
|
|
||||||
|
|
||||||
// Snap the horizontal edges of the anchored element to the horizontal edges of the window if
|
|
||||||
// its horizontal bounds overflow, aligning to the left if it is wider than the limits.
|
|
||||||
if desired.right() > limits.right() {
|
|
||||||
desired.origin.x -= desired.right() - limits.right() + edges.right;
|
|
||||||
}
|
|
||||||
if desired.left() < limits.left() {
|
|
||||||
desired.origin.x = limits.origin.x + edges.left;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Snap the vertical edges of the anchored element to the vertical edges of the window if
|
|
||||||
// its vertical bounds overflow, aligning to the top if it is taller than the limits.
|
|
||||||
if desired.bottom() > limits.bottom() {
|
|
||||||
desired.origin.y -= desired.bottom() - limits.bottom() + edges.bottom;
|
|
||||||
}
|
|
||||||
if desired.top() < limits.top() {
|
|
||||||
desired.origin.y = limits.origin.y + edges.top;
|
|
||||||
}
|
|
||||||
|
|
||||||
let offset = desired.origin - bounds.origin;
|
|
||||||
let offset = point(offset.x.round(), offset.y.round());
|
|
||||||
|
|
||||||
window.with_element_offset(offset, |window| {
|
|
||||||
for child in &mut self.children {
|
|
||||||
child.prepaint(window, cx);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn paint(
|
|
||||||
&mut self,
|
|
||||||
_id: Option<&GlobalElementId>,
|
|
||||||
_inspector_id: Option<&InspectorElementId>,
|
|
||||||
_bounds: Bounds<Pixels>,
|
|
||||||
_request_layout: &mut Self::RequestLayoutState,
|
|
||||||
_prepaint: &mut Self::PrepaintState,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) {
|
|
||||||
for child in &mut self.children {
|
|
||||||
child.paint(window, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoElement for Anchored {
|
|
||||||
type Element = Self;
|
|
||||||
|
|
||||||
fn into_element(self) -> Self::Element {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Which algorithm to use when fitting the anchored element to be inside the window.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Copy, Clone, PartialEq)]
|
|
||||||
pub enum AnchoredFitMode {
|
|
||||||
/// Snap the anchored element to the window edge.
|
|
||||||
SnapToWindow,
|
|
||||||
/// Snap to window edge and leave some margins.
|
|
||||||
SnapToWindowWithMargin(Edges<Pixels>),
|
|
||||||
/// Switch which corner anchor this anchored element is attached to.
|
|
||||||
SwitchAnchor,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Which algorithm to use when positioning the anchored element.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Copy, Clone, PartialEq)]
|
|
||||||
pub enum AnchoredPositionMode {
|
|
||||||
/// Position the anchored element relative to the window.
|
|
||||||
Window,
|
|
||||||
/// Position the anchored element relative to its parent.
|
|
||||||
Local,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AnchoredPositionMode {
|
|
||||||
fn get_position_and_bounds(
|
|
||||||
&self,
|
|
||||||
anchor_position: Option<Point<Pixels>>,
|
|
||||||
anchor_corner: Anchor,
|
|
||||||
size: Size<Pixels>,
|
|
||||||
bounds: Bounds<Pixels>,
|
|
||||||
offset: Option<Point<Pixels>>,
|
|
||||||
) -> (Point<Pixels>, Bounds<Pixels>) {
|
|
||||||
let offset = offset.unwrap_or_default();
|
|
||||||
|
|
||||||
match self {
|
|
||||||
AnchoredPositionMode::Window => {
|
|
||||||
let anchor_position = anchor_position.unwrap_or(bounds.origin);
|
|
||||||
let bounds =
|
|
||||||
Self::from_corner_and_size(anchor_corner, anchor_position + offset, size);
|
|
||||||
(anchor_position, bounds)
|
|
||||||
}
|
|
||||||
AnchoredPositionMode::Local => {
|
|
||||||
let anchor_position = anchor_position.unwrap_or_default();
|
|
||||||
let bounds = Self::from_corner_and_size(
|
|
||||||
anchor_corner,
|
|
||||||
bounds.origin + anchor_position + offset,
|
|
||||||
size,
|
|
||||||
);
|
|
||||||
(anchor_position, bounds)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ref https://github.com/zed-industries/zed/blob/b06f4088a3565c5e30663106ff79c1ced645d87a/crates/gpui/src/geometry.rs#L863
|
|
||||||
fn from_corner_and_size(
|
|
||||||
anchor: Anchor,
|
|
||||||
origin: Point<Pixels>,
|
|
||||||
size: Size<Pixels>,
|
|
||||||
) -> Bounds<Pixels> {
|
|
||||||
let origin = match anchor {
|
|
||||||
Anchor::TopLeft => origin,
|
|
||||||
Anchor::TopCenter => Point {
|
|
||||||
x: origin.x - size.width.half(),
|
|
||||||
y: origin.y,
|
|
||||||
},
|
|
||||||
Anchor::TopRight => Point {
|
|
||||||
x: origin.x - size.width,
|
|
||||||
y: origin.y,
|
|
||||||
},
|
|
||||||
Anchor::BottomLeft => Point {
|
|
||||||
x: origin.x,
|
|
||||||
y: origin.y - size.height,
|
|
||||||
},
|
|
||||||
Anchor::BottomCenter => Point {
|
|
||||||
x: origin.x - size.width.half(),
|
|
||||||
y: origin.y - size.height,
|
|
||||||
},
|
|
||||||
Anchor::BottomRight => Point {
|
|
||||||
x: origin.x - size.width,
|
|
||||||
y: origin.y - size.height,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
Bounds { origin, size }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, img, px, AbsoluteLength, App, Div, Hsla, ImageSource, Img, InteractiveElement,
|
AbsoluteLength, App, Div, Hsla, ImageSource, Img, InteractiveElement, Interactivity,
|
||||||
Interactivity, IntoElement, ParentElement, RenderOnce, StyleRefinement, Styled, StyledImage,
|
IntoElement, ParentElement, RenderOnce, StyleRefinement, Styled, StyledImage, Window, div, img,
|
||||||
Window,
|
px,
|
||||||
};
|
};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
use crate::{Sizable, Size};
|
use crate::{Selectable, Sizable, Size};
|
||||||
|
|
||||||
/// Returns the size of the avatar based on the given [`Size`].
|
/// Returns the size of the avatar based on the given [`Size`].
|
||||||
pub(super) fn avatar_size(size: Size) -> AbsoluteLength {
|
pub(super) fn avatar_size(size: Size) -> AbsoluteLength {
|
||||||
@@ -37,6 +37,7 @@ pub struct Avatar {
|
|||||||
style: StyleRefinement,
|
style: StyleRefinement,
|
||||||
size: Size,
|
size: Size,
|
||||||
border_color: Option<Hsla>,
|
border_color: Option<Hsla>,
|
||||||
|
selected: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Avatar {
|
impl Avatar {
|
||||||
@@ -48,6 +49,7 @@ impl Avatar {
|
|||||||
style: StyleRefinement::default(),
|
style: StyleRefinement::default(),
|
||||||
size: Size::Medium,
|
size: Size::Medium,
|
||||||
border_color: None,
|
border_color: None,
|
||||||
|
selected: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,6 +91,17 @@ impl Styled for Avatar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Selectable for Avatar {
|
||||||
|
fn is_selected(&self) -> bool {
|
||||||
|
self.selected
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selected(mut self, selected: bool) -> Self {
|
||||||
|
self.selected = selected;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl InteractiveElement for Avatar {
|
impl InteractiveElement for Avatar {
|
||||||
fn interactivity(&mut self) -> &mut Interactivity {
|
fn interactivity(&mut self) -> &mut Interactivity {
|
||||||
self.base.interactivity()
|
self.base.interactivity()
|
||||||
|
|||||||
@@ -2,15 +2,15 @@ use std::rc::Rc;
|
|||||||
|
|
||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, relative, AnyElement, App, ClickEvent, Div, ElementId, Hsla, InteractiveElement,
|
AnyElement, App, ClickEvent, Div, ElementId, Hsla, InteractiveElement, IntoElement,
|
||||||
IntoElement, ParentElement, RenderOnce, SharedString, Stateful,
|
ParentElement, RenderOnce, SharedString, Stateful, StatefulInteractiveElement as _,
|
||||||
StatefulInteractiveElement as _, StyleRefinement, Styled, Window,
|
StyleRefinement, Styled, Window, div, relative,
|
||||||
};
|
};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
use crate::indicator::Indicator;
|
use crate::indicator::Indicator;
|
||||||
use crate::tooltip::Tooltip;
|
use crate::tooltip::Tooltip;
|
||||||
use crate::{h_flex, Disableable, Icon, IconName, Selectable, Sizable, Size, StyledExt};
|
use crate::{Disableable, Icon, IconName, Selectable, Sizable, Size, StyledExt, h_flex};
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
pub struct ButtonCustomVariant {
|
pub struct ButtonCustomVariant {
|
||||||
@@ -617,7 +617,7 @@ impl ButtonVariant {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let fg = match self {
|
let fg = match self {
|
||||||
ButtonVariant::Primary => cx.theme().text_muted, // TODO: use a different color?
|
ButtonVariant::Primary => cx.theme().text_muted,
|
||||||
_ => cx.theme().text_muted,
|
_ => cx.theme().text_muted,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::time::Duration;
|
use instant::Duration;
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
|
|||||||
@@ -3,20 +3,26 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, App, AppContext, Axis, Context, Element, Entity, IntoElement, MouseMoveEvent,
|
App, AppContext, Axis, Context, Element, Empty, Entity, IntoElement, MouseMoveEvent,
|
||||||
MouseUpEvent, ParentElement as _, Pixels, Point, Render, Style, Styled as _, WeakEntity,
|
MouseUpEvent, ParentElement as _, Pixels, Point, Render, Style, StyleRefinement, Styled as _,
|
||||||
Window,
|
WeakEntity, Window, div, px,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{DockArea, DockItem};
|
use super::{DockArea, DockItem};
|
||||||
use crate::dock_area::panel::PanelView;
|
|
||||||
use crate::dock_area::tab_panel::TabPanel;
|
|
||||||
use crate::resizable::{resize_handle, PANEL_MIN_SIZE};
|
|
||||||
use crate::StyledExt;
|
use crate::StyledExt;
|
||||||
|
use crate::dock::panel::PanelView;
|
||||||
|
use crate::dock::tab_panel::TabPanel;
|
||||||
|
use crate::resizable::{PANEL_MIN_SIZE, resize_handle};
|
||||||
|
|
||||||
#[derive(Clone, Render)]
|
#[derive(Clone)]
|
||||||
struct ResizePanel;
|
struct ResizePanel;
|
||||||
|
|
||||||
|
impl Render for ResizePanel {
|
||||||
|
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
Empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum DockPlacement {
|
pub enum DockPlacement {
|
||||||
Center,
|
Center,
|
||||||
@@ -208,6 +214,8 @@ impl Dock {
|
|||||||
pub fn set_open(&mut self, open: bool, window: &mut Window, cx: &mut Context<Self>) {
|
pub fn set_open(&mut self, open: bool, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.open = open;
|
self.open = open;
|
||||||
let item = self.panel.clone();
|
let item = self.panel.clone();
|
||||||
|
// Use defer_in (not window.defer) so the callback is cancelled
|
||||||
|
// if this Dock entity is dropped before the deferred frame runs.
|
||||||
cx.defer_in(window, move |_, window, cx| {
|
cx.defer_in(window, move |_, window, cx| {
|
||||||
item.set_collapsed(!open, window, cx);
|
item.set_collapsed(!open, window, cx);
|
||||||
});
|
});
|
||||||
@@ -265,24 +273,24 @@ impl Dock {
|
|||||||
let mut right_dock_size = px(0.0);
|
let mut right_dock_size = px(0.0);
|
||||||
|
|
||||||
// Get the size of the left dock if it's open and not the current dock
|
// Get the size of the left dock if it's open and not the current dock
|
||||||
if let Some(left_dock) = &dock_area.left_dock {
|
if let Some(left_dock) = &dock_area.left_dock
|
||||||
if left_dock.entity_id() != cx.entity().entity_id() {
|
&& left_dock.entity_id() != cx.entity().entity_id()
|
||||||
|
{
|
||||||
let left_dock_read = left_dock.read(cx);
|
let left_dock_read = left_dock.read(cx);
|
||||||
if left_dock_read.is_open() {
|
if left_dock_read.is_open() {
|
||||||
left_dock_size = left_dock_read.size;
|
left_dock_size = left_dock_read.size;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Get the size of the right dock if it's open and not the current dock
|
// Get the size of the right dock if it's open and not the current dock
|
||||||
if let Some(right_dock) = &dock_area.right_dock {
|
if let Some(right_dock) = &dock_area.right_dock
|
||||||
if right_dock.entity_id() != cx.entity().entity_id() {
|
&& right_dock.entity_id() != cx.entity().entity_id()
|
||||||
|
{
|
||||||
let right_dock_read = right_dock.read(cx);
|
let right_dock_read = right_dock.read(cx);
|
||||||
if right_dock_read.is_open() {
|
if right_dock_read.is_open() {
|
||||||
right_dock_size = right_dock_read.size;
|
right_dock_size = right_dock_read.size;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let size = match self.placement {
|
let size = match self.placement {
|
||||||
DockPlacement::Left => mouse_position.x - area_bounds.left(),
|
DockPlacement::Left => mouse_position.x - area_bounds.left(),
|
||||||
@@ -321,6 +329,8 @@ impl Render for Dock {
|
|||||||
return div();
|
return div();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let cache_style = StyleRefinement::default().absolute().size_full();
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.relative()
|
.relative()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
@@ -336,7 +346,7 @@ impl Render for Dock {
|
|||||||
.map(|this| match &self.panel {
|
.map(|this| match &self.panel {
|
||||||
DockItem::Split { view, .. } => this.child(view.clone()),
|
DockItem::Split { view, .. } => this.child(view.clone()),
|
||||||
DockItem::Tabs { view, .. } => this.child(view.clone()),
|
DockItem::Tabs { view, .. } => this.child(view.clone()),
|
||||||
DockItem::Panel { view, .. } => this.child(view.clone().view()),
|
DockItem::Panel { view, .. } => this.child(view.clone().view().cached(cache_style)),
|
||||||
})
|
})
|
||||||
.child(self.render_resize_handle(window, cx))
|
.child(self.render_resize_handle(window, cx))
|
||||||
.child(DockElement {
|
.child(DockElement {
|
||||||
@@ -2,22 +2,24 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, div, px, AnyElement, AnyView, App, AppContext, Axis, Bounds, Context, Decorations,
|
AnyElement, AnyView, App, AppContext, Axis, Bounds, Context, Decorations, Edges, Entity,
|
||||||
Edges, Entity, EntityId, EventEmitter, Focusable, InteractiveElement as _, IntoElement,
|
EntityId, EventEmitter, Focusable, InteractiveElement as _, IntoElement, ParentElement as _,
|
||||||
ParentElement as _, Pixels, Render, SharedString, Styled, Subscription, WeakEntity, Window,
|
Pixels, Render, SharedString, Styled, Subscription, WeakEntity, Window, actions, div, px,
|
||||||
};
|
};
|
||||||
use theme::CLIENT_SIDE_DECORATION_ROUNDING;
|
use theme::CLIENT_SIDE_DECORATION_ROUNDING;
|
||||||
|
|
||||||
use crate::dock_area::dock::{Dock, DockPlacement};
|
|
||||||
use crate::dock_area::panel::{Panel, PanelEvent, PanelStyle, PanelView};
|
|
||||||
use crate::dock_area::stack_panel::StackPanel;
|
|
||||||
use crate::dock_area::tab_panel::TabPanel;
|
|
||||||
use crate::ElementExt;
|
use crate::ElementExt;
|
||||||
|
|
||||||
pub mod dock;
|
#[allow(clippy::module_inception)]
|
||||||
pub mod panel;
|
mod dock;
|
||||||
pub mod stack_panel;
|
mod panel;
|
||||||
pub mod tab_panel;
|
mod stack_panel;
|
||||||
|
mod tab_panel;
|
||||||
|
|
||||||
|
pub use dock::*;
|
||||||
|
pub use panel::*;
|
||||||
|
pub use stack_panel::*;
|
||||||
|
pub use tab_panel::*;
|
||||||
|
|
||||||
actions!(dock, [ToggleZoom, ClosePanel]);
|
actions!(dock, [ToggleZoom, ClosePanel]);
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, AnyView, App, Element, Entity, EventEmitter, FocusHandle, Focusable, Hsla, Render,
|
AnyElement, AnyView, App, Element, Entity, EventEmitter, FocusHandle, Focusable, Render,
|
||||||
SharedString, Window,
|
SharedString, Window,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -21,12 +21,6 @@ pub enum PanelStyle {
|
|||||||
TabBar,
|
TabBar,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub struct TitleStyle {
|
|
||||||
pub background: Hsla,
|
|
||||||
pub foreground: Hsla,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait Panel: EventEmitter<PanelEvent> + Render + Focusable {
|
pub trait Panel: EventEmitter<PanelEvent> + Render + Focusable {
|
||||||
/// The name of the panel used to serialize, deserialize and identify the panel.
|
/// The name of the panel used to serialize, deserialize and identify the panel.
|
||||||
///
|
///
|
||||||
@@ -7,16 +7,16 @@ use gpui::{
|
|||||||
Window,
|
Window,
|
||||||
};
|
};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use theme::{ActiveTheme, CLIENT_SIDE_DECORATION_ROUNDING};
|
use theme::{ActiveTheme, AxisExt as _, CLIENT_SIDE_DECORATION_ROUNDING, Placement};
|
||||||
|
|
||||||
use super::{DockArea, PanelEvent};
|
use super::{DockArea, PanelEvent};
|
||||||
use crate::dock_area::panel::{Panel, PanelView};
|
use crate::dock::panel::{Panel, PanelView};
|
||||||
use crate::dock_area::tab_panel::TabPanel;
|
use crate::dock::tab_panel::TabPanel;
|
||||||
|
use crate::h_flex;
|
||||||
use crate::resizable::{
|
use crate::resizable::{
|
||||||
resizable_panel, ResizablePanelEvent, ResizablePanelGroup, ResizablePanelState, ResizableState,
|
PANEL_MIN_SIZE, ResizablePanelEvent, ResizablePanelGroup, ResizablePanelState, ResizableState,
|
||||||
PANEL_MIN_SIZE,
|
resizable_panel,
|
||||||
};
|
};
|
||||||
use crate::{h_flex, AxisExt as _, Placement};
|
|
||||||
|
|
||||||
pub struct StackPanel {
|
pub struct StackPanel {
|
||||||
pub(super) parent: Option<WeakEntity<StackPanel>>,
|
pub(super) parent: Option<WeakEntity<StackPanel>>,
|
||||||
@@ -70,11 +70,11 @@ impl StackPanel {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(parent) = &self.parent {
|
if let Some(parent) = &self.parent
|
||||||
if let Some(parent) = parent.upgrade() {
|
&& let Some(parent) = parent.upgrade()
|
||||||
|
{
|
||||||
return parent.read(cx).is_last_panel(cx);
|
return parent.read(cx).is_last_panel(cx);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -297,13 +297,12 @@ impl StackPanel {
|
|||||||
|
|
||||||
/// Find the first top left in the stack.
|
/// Find the first top left in the stack.
|
||||||
pub fn left_top_tab_panel(&self, check_parent: bool, cx: &App) -> Option<Entity<TabPanel>> {
|
pub fn left_top_tab_panel(&self, check_parent: bool, cx: &App) -> Option<Entity<TabPanel>> {
|
||||||
if check_parent {
|
if check_parent
|
||||||
if let Some(parent) = self.parent.as_ref().and_then(|parent| parent.upgrade()) {
|
&& let Some(parent) = self.parent.as_ref().and_then(|parent| parent.upgrade())
|
||||||
if let Some(panel) = parent.read(cx).left_top_tab_panel(true, cx) {
|
&& let Some(panel) = parent.read(cx).left_top_tab_panel(true, cx)
|
||||||
|
{
|
||||||
return Some(panel);
|
return Some(panel);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let first_panel = self.panels.first();
|
let first_panel = self.panels.first();
|
||||||
if let Some(view) = first_panel {
|
if let Some(view) = first_panel {
|
||||||
@@ -321,13 +320,12 @@ impl StackPanel {
|
|||||||
|
|
||||||
/// Find the first top right in the stack.
|
/// Find the first top right in the stack.
|
||||||
pub fn right_top_tab_panel(&self, check_parent: bool, cx: &App) -> Option<Entity<TabPanel>> {
|
pub fn right_top_tab_panel(&self, check_parent: bool, cx: &App) -> Option<Entity<TabPanel>> {
|
||||||
if check_parent {
|
if check_parent
|
||||||
if let Some(parent) = self.parent.as_ref().and_then(|parent| parent.upgrade()) {
|
&& let Some(parent) = self.parent.as_ref().and_then(|parent| parent.upgrade())
|
||||||
if let Some(panel) = parent.read(cx).right_top_tab_panel(true, cx) {
|
&& let Some(panel) = parent.read(cx).right_top_tab_panel(true, cx)
|
||||||
|
{
|
||||||
return Some(panel);
|
return Some(panel);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let panel = if self.axis.is_vertical() {
|
let panel = if self.axis.is_vertical() {
|
||||||
self.panels.first()
|
self.panels.first()
|
||||||
@@ -2,22 +2,22 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, rems, App, AppContext, Context, Corner, DefiniteLength, DismissEvent, DragMoveEvent,
|
Anchor, App, AppContext, Context, DefiniteLength, DismissEvent, DragMoveEvent, Empty, Entity,
|
||||||
Empty, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement as _, IntoElement,
|
EventEmitter, FocusHandle, Focusable, InteractiveElement as _, IntoElement, MouseButton,
|
||||||
MouseButton, ParentElement, Pixels, Render, ScrollHandle, SharedString,
|
ParentElement, Pixels, Render, ScrollHandle, SharedString, StatefulInteractiveElement, Styled,
|
||||||
StatefulInteractiveElement, Styled, WeakEntity, Window,
|
WeakEntity, Window, div, px, rems,
|
||||||
};
|
};
|
||||||
use theme::{ActiveTheme, CLIENT_SIDE_DECORATION_ROUNDING, TABBAR_HEIGHT};
|
use theme::{ActiveTheme, AxisExt, CLIENT_SIDE_DECORATION_ROUNDING, Placement, TABBAR_HEIGHT};
|
||||||
|
|
||||||
use crate::button::{Button, ButtonVariants as _};
|
use crate::button::{Button, ButtonVariants as _};
|
||||||
use crate::dock_area::dock::DockPlacement;
|
use crate::dock::dock::DockPlacement;
|
||||||
use crate::dock_area::panel::{Panel, PanelView};
|
use crate::dock::panel::{Panel, PanelView};
|
||||||
use crate::dock_area::stack_panel::StackPanel;
|
use crate::dock::stack_panel::StackPanel;
|
||||||
use crate::dock_area::{ClosePanel, DockArea, PanelEvent, PanelStyle, ToggleZoom};
|
use crate::dock::{ClosePanel, DockArea, PanelEvent, PanelStyle, ToggleZoom};
|
||||||
use crate::menu::{DropdownMenu, PopupMenu};
|
use crate::menu::{DropdownMenu, PopupMenu};
|
||||||
use crate::tab::tab_bar::TabBar;
|
|
||||||
use crate::tab::Tab;
|
use crate::tab::Tab;
|
||||||
use crate::{h_flex, v_flex, AxisExt, IconName, Placement, Selectable, Sizable, StyledExt};
|
use crate::tab::tab_bar::TabBar;
|
||||||
|
use crate::{IconName, Selectable, Sizable, StyledExt, h_flex, v_flex};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct TabState {
|
struct TabState {
|
||||||
@@ -42,22 +42,20 @@ impl DragPanel {
|
|||||||
|
|
||||||
impl Render for DragPanel {
|
impl Render for DragPanel {
|
||||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
div()
|
h_flex()
|
||||||
.id("drag-panel")
|
.id("drag-panel")
|
||||||
.cursor_grab()
|
.cursor_grab()
|
||||||
.py_1()
|
.p_2()
|
||||||
.px_2()
|
.min_w_24()
|
||||||
.w_24()
|
|
||||||
.flex()
|
|
||||||
.items_center()
|
|
||||||
.justify_center()
|
.justify_center()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.whitespace_nowrap()
|
.whitespace_nowrap()
|
||||||
.rounded(cx.theme().radius_lg)
|
.rounded(cx.theme().radius)
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.when(cx.theme().shadow, |this| this.shadow_lg())
|
.text_color(cx.theme().text)
|
||||||
|
.text_ellipsis()
|
||||||
|
.when(cx.theme().shadow, |this| this.shadow_xs())
|
||||||
.bg(cx.theme().background)
|
.bg(cx.theme().background)
|
||||||
.text_color(cx.theme().text_accent)
|
|
||||||
.child(self.panel.title(cx))
|
.child(self.panel.title(cx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -234,15 +232,14 @@ impl TabPanel {
|
|||||||
.any(|p| p.panel_id(cx) == panel.panel_id(cx))
|
.any(|p| p.panel_id(cx) == panel.panel_id(cx))
|
||||||
{
|
{
|
||||||
// Set the active panel to the matched panel
|
// Set the active panel to the matched panel
|
||||||
if active {
|
if active
|
||||||
if let Some(ix) = self
|
&& let Some(ix) = self
|
||||||
.panels
|
.panels
|
||||||
.iter()
|
.iter()
|
||||||
.position(|p| p.panel_id(cx) == panel.panel_id(cx))
|
.position(|p| p.panel_id(cx) == panel.panel_id(cx))
|
||||||
{
|
{
|
||||||
self.set_active_ix(ix, window, cx);
|
self.set_active_ix(ix, window, cx);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -374,13 +371,12 @@ impl TabPanel {
|
|||||||
|
|
||||||
/// Return true if self or parent only have last panel.
|
/// Return true if self or parent only have last panel.
|
||||||
fn is_last_panel(&self, cx: &App) -> bool {
|
fn is_last_panel(&self, cx: &App) -> bool {
|
||||||
if let Some(parent) = &self.stack_panel {
|
if let Some(parent) = &self.stack_panel
|
||||||
if let Some(stack_panel) = parent.upgrade() {
|
&& let Some(stack_panel) = parent.upgrade()
|
||||||
if !stack_panel.read(cx).is_last_panel(cx) {
|
&& !stack_panel.read(cx).is_last_panel(cx)
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.panels.len() <= 1
|
self.panels.len() <= 1
|
||||||
}
|
}
|
||||||
@@ -425,14 +421,13 @@ impl TabPanel {
|
|||||||
let view = cx.entity().clone();
|
let view = cx.entity().clone();
|
||||||
let build_popup_menu = move |this, cx: &App| view.read(cx).popup_menu(this, cx);
|
let build_popup_menu = move |this, cx: &App| view.read(cx).popup_menu(this, cx);
|
||||||
let toolbar = self.toolbar_buttons(window, cx);
|
let toolbar = self.toolbar_buttons(window, cx);
|
||||||
let has_toolbar = !toolbar.is_empty();
|
|
||||||
|
|
||||||
h_flex()
|
h_flex()
|
||||||
.p_0p5()
|
.p_0p5()
|
||||||
.gap_1()
|
.gap_1p5()
|
||||||
.occlude()
|
.occlude()
|
||||||
.rounded_full()
|
.rounded_full()
|
||||||
.children(toolbar.into_iter().map(|btn| btn.small().ghost().rounded()))
|
.children(toolbar.into_iter().map(|btn| btn.small().ghost()))
|
||||||
.when(self.zoomed, |this| {
|
.when(self.zoomed, |this| {
|
||||||
this.child(
|
this.child(
|
||||||
Button::new("zoom")
|
Button::new("zoom")
|
||||||
@@ -445,15 +440,11 @@ impl TabPanel {
|
|||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.when(has_toolbar, |this| {
|
|
||||||
this.child(div().flex_shrink_0().h_4().w_px().bg(cx.theme().border))
|
|
||||||
})
|
|
||||||
.child(
|
.child(
|
||||||
Button::new("menu")
|
Button::new("menu")
|
||||||
.icon(IconName::Ellipsis)
|
.icon(IconName::Ellipsis)
|
||||||
.small()
|
.small()
|
||||||
.ghost()
|
.ghost()
|
||||||
.rounded()
|
|
||||||
.dropdown_menu({
|
.dropdown_menu({
|
||||||
let zoomable = state.zoomable;
|
let zoomable = state.zoomable;
|
||||||
let closable = state.closable;
|
let closable = state.closable;
|
||||||
@@ -469,7 +460,7 @@ impl TabPanel {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.anchor(Corner::TopRight),
|
.anchor(Anchor::TopRight),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -578,6 +569,7 @@ impl TabPanel {
|
|||||||
let right_dock_button = self.render_dock_toggle_button(DockPlacement::Right, window, cx);
|
let right_dock_button = self.render_dock_toggle_button(DockPlacement::Right, window, cx);
|
||||||
let has_extend_dock_button = left_dock_button.is_some() || bottom_dock_button.is_some();
|
let has_extend_dock_button = left_dock_button.is_some() || bottom_dock_button.is_some();
|
||||||
let tabs_count = self.panels.len();
|
let tabs_count = self.panels.len();
|
||||||
|
let is_bottom_dock = bottom_dock_button.is_some();
|
||||||
|
|
||||||
if tabs_count == 1 && dock_area.read(cx).panel_style == PanelStyle::Default {
|
if tabs_count == 1 && dock_area.read(cx).panel_style == PanelStyle::Default {
|
||||||
let panel = self.panels.first().unwrap();
|
let panel = self.panels.first().unwrap();
|
||||||
@@ -646,7 +638,7 @@ impl TabPanel {
|
|||||||
.into_any_element();
|
.into_any_element();
|
||||||
}
|
}
|
||||||
|
|
||||||
TabBar::new()
|
TabBar::new("tab-bar")
|
||||||
.track_scroll(&self.tab_bar_scroll_handle)
|
.track_scroll(&self.tab_bar_scroll_handle)
|
||||||
.h(TABBAR_HEIGHT)
|
.h(TABBAR_HEIGHT)
|
||||||
.when(has_extend_dock_button, |this| {
|
.when(has_extend_dock_button, |this| {
|
||||||
@@ -659,8 +651,9 @@ impl TabPanel {
|
|||||||
.border_b_1()
|
.border_b_1()
|
||||||
.h_full()
|
.h_full()
|
||||||
.border_color(cx.theme().border)
|
.border_color(cx.theme().border)
|
||||||
.bg(cx.theme().surface_background)
|
.bg(cx.theme().tab_background)
|
||||||
.px_2()
|
.pl_0p5()
|
||||||
|
.pr_1()
|
||||||
.children(left_dock_button)
|
.children(left_dock_button)
|
||||||
.children(bottom_dock_button),
|
.children(bottom_dock_button),
|
||||||
)
|
)
|
||||||
@@ -682,16 +675,43 @@ impl TabPanel {
|
|||||||
Some(
|
Some(
|
||||||
Tab::new()
|
Tab::new()
|
||||||
.ix(ix)
|
.ix(ix)
|
||||||
.label(panel.title(cx))
|
.tab_bar_prefix(has_extend_dock_button)
|
||||||
.py_2()
|
.child(panel.title(cx))
|
||||||
.selected(active)
|
.selected(active)
|
||||||
.disabled(disabled)
|
.disabled(disabled)
|
||||||
|
.suffix(
|
||||||
|
Button::new("close-{ix}")
|
||||||
|
.icon(IconName::Close)
|
||||||
|
.tooltip("Close panel")
|
||||||
|
.ghost()
|
||||||
|
.xsmall()
|
||||||
|
.on_click(cx.listener({
|
||||||
|
let panel = panel.clone();
|
||||||
|
move |view, _ev, window, cx| {
|
||||||
|
view.remove_panel(&panel, window, cx);
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.on_click(cx.listener({
|
||||||
|
let is_collapsed = self.collapsed;
|
||||||
|
let dock_area = self.dock_area.clone();
|
||||||
|
move |view, _, window, cx| {
|
||||||
|
view.set_active_ix(ix, window, cx);
|
||||||
|
|
||||||
|
// Open dock if clicked on the collapsed bottom dock
|
||||||
|
if is_bottom_dock && is_collapsed {
|
||||||
|
_ = dock_area.update(cx, |dock_area, cx| {
|
||||||
|
dock_area.toggle_dock(DockPlacement::Bottom, window, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
.when(!disabled, |this| {
|
.when(!disabled, |this| {
|
||||||
this.on_mouse_down(
|
this.on_mouse_down(
|
||||||
MouseButton::Middle,
|
MouseButton::Middle,
|
||||||
cx.listener({
|
cx.listener({
|
||||||
let panel = panel.clone();
|
let panel = panel.clone();
|
||||||
move |view, _, window, cx| {
|
move |view, _ev, window, cx| {
|
||||||
view.remove_panel(&panel, window, cx);
|
view.remove_panel(&panel, window, cx);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -730,7 +750,7 @@ impl TabPanel {
|
|||||||
div()
|
div()
|
||||||
.id("tab-bar-empty-space")
|
.id("tab-bar-empty-space")
|
||||||
.h_full()
|
.h_full()
|
||||||
.flex_grow()
|
.flex_grow_1()
|
||||||
.min_w_16()
|
.min_w_16()
|
||||||
.when(state.droppable, |this| {
|
.when(state.droppable, |this| {
|
||||||
let view = cx.entity();
|
let view = cx.entity();
|
||||||
@@ -757,14 +777,15 @@ impl TabPanel {
|
|||||||
this.suffix(
|
this.suffix(
|
||||||
h_flex()
|
h_flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
.px_2()
|
|
||||||
.gap_1()
|
|
||||||
.top_0()
|
.top_0()
|
||||||
.right_0()
|
.right_0()
|
||||||
.h_full()
|
.h_full()
|
||||||
.border_color(cx.theme().border)
|
|
||||||
.border_l_1()
|
.border_l_1()
|
||||||
.border_b_1()
|
.border_b_1()
|
||||||
|
.px_0p5()
|
||||||
|
.gap_1()
|
||||||
|
.border_color(cx.theme().border)
|
||||||
|
.bg(cx.theme().tab_background)
|
||||||
.child(self.render_toolbar(state, window, cx))
|
.child(self.render_toolbar(state, window, cx))
|
||||||
.when_some(right_dock_button, |this, btn| this.child(btn)),
|
.when_some(right_dock_button, |this, btn| this.child(btn)),
|
||||||
)
|
)
|
||||||
@@ -1080,12 +1101,12 @@ impl TabPanel {
|
|||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
if self.panels.len() > 1 {
|
if self.panels.len() > 1
|
||||||
if let Some(panel) = self.active_panel(cx) {
|
&& let Some(panel) = self.active_panel(cx)
|
||||||
|
{
|
||||||
self.remove_panel(&panel, window, cx);
|
self.remove_panel(&panel, window, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Focusable for TabPanel {
|
impl Focusable for TabPanel {
|
||||||
@@ -1099,6 +1120,7 @@ impl Focusable for TabPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<DismissEvent> for TabPanel {}
|
impl EventEmitter<DismissEvent> for TabPanel {}
|
||||||
|
|
||||||
impl EventEmitter<PanelEvent> for TabPanel {}
|
impl EventEmitter<PanelEvent> for TabPanel {}
|
||||||
|
|
||||||
impl Render for TabPanel {
|
impl Render for TabPanel {
|
||||||
@@ -1,294 +0,0 @@
|
|||||||
use std::fmt::{self, Debug, Display, Formatter};
|
|
||||||
|
|
||||||
use gpui::{AbsoluteLength, Axis, Corner, Length, Pixels};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
/// A enum for defining the placement of the element.
|
|
||||||
///
|
|
||||||
/// See also: [`Side`] if you need to define the left, right side.
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
pub enum Placement {
|
|
||||||
#[serde(rename = "top")]
|
|
||||||
Top,
|
|
||||||
#[serde(rename = "bottom")]
|
|
||||||
Bottom,
|
|
||||||
#[serde(rename = "left")]
|
|
||||||
Left,
|
|
||||||
#[serde(rename = "right")]
|
|
||||||
Right,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Placement {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
Placement::Top => write!(f, "Top"),
|
|
||||||
Placement::Bottom => write!(f, "Bottom"),
|
|
||||||
Placement::Left => write!(f, "Left"),
|
|
||||||
Placement::Right => write!(f, "Right"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Placement {
|
|
||||||
#[inline]
|
|
||||||
pub fn is_horizontal(&self) -> bool {
|
|
||||||
matches!(self, Placement::Left | Placement::Right)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn is_vertical(&self) -> bool {
|
|
||||||
matches!(self, Placement::Top | Placement::Bottom)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn axis(&self) -> Axis {
|
|
||||||
match self {
|
|
||||||
Placement::Top | Placement::Bottom => Axis::Vertical,
|
|
||||||
Placement::Left | Placement::Right => Axis::Horizontal,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The anchor position of an element.
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
|
|
||||||
pub enum Anchor {
|
|
||||||
#[default]
|
|
||||||
#[serde(rename = "top-left")]
|
|
||||||
TopLeft,
|
|
||||||
#[serde(rename = "top-center")]
|
|
||||||
TopCenter,
|
|
||||||
#[serde(rename = "top-right")]
|
|
||||||
TopRight,
|
|
||||||
#[serde(rename = "bottom-left")]
|
|
||||||
BottomLeft,
|
|
||||||
#[serde(rename = "bottom-center")]
|
|
||||||
BottomCenter,
|
|
||||||
#[serde(rename = "bottom-right")]
|
|
||||||
BottomRight,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Anchor {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
Anchor::TopLeft => write!(f, "TopLeft"),
|
|
||||||
Anchor::TopCenter => write!(f, "TopCenter"),
|
|
||||||
Anchor::TopRight => write!(f, "TopRight"),
|
|
||||||
Anchor::BottomLeft => write!(f, "BottomLeft"),
|
|
||||||
Anchor::BottomCenter => write!(f, "BottomCenter"),
|
|
||||||
Anchor::BottomRight => write!(f, "BottomRight"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Anchor {
|
|
||||||
/// Returns true if the anchor is at the top.
|
|
||||||
#[inline]
|
|
||||||
pub fn is_top(&self) -> bool {
|
|
||||||
matches!(self, Self::TopLeft | Self::TopCenter | Self::TopRight)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if the anchor is at the bottom.
|
|
||||||
#[inline]
|
|
||||||
pub fn is_bottom(&self) -> bool {
|
|
||||||
matches!(
|
|
||||||
self,
|
|
||||||
Self::BottomLeft | Self::BottomCenter | Self::BottomRight
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if the anchor is at the left.
|
|
||||||
#[inline]
|
|
||||||
pub fn is_left(&self) -> bool {
|
|
||||||
matches!(self, Self::TopLeft | Self::BottomLeft)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if the anchor is at the right.
|
|
||||||
#[inline]
|
|
||||||
pub fn is_right(&self) -> bool {
|
|
||||||
matches!(self, Self::TopRight | Self::BottomRight)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if the anchor is at the center.
|
|
||||||
#[inline]
|
|
||||||
pub fn is_center(&self) -> bool {
|
|
||||||
matches!(self, Self::TopCenter | Self::BottomCenter)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Swaps the vertical position of the anchor.
|
|
||||||
pub fn swap_vertical(&self) -> Self {
|
|
||||||
match self {
|
|
||||||
Anchor::TopLeft => Anchor::BottomLeft,
|
|
||||||
Anchor::TopCenter => Anchor::BottomCenter,
|
|
||||||
Anchor::TopRight => Anchor::BottomRight,
|
|
||||||
Anchor::BottomLeft => Anchor::TopLeft,
|
|
||||||
Anchor::BottomCenter => Anchor::TopCenter,
|
|
||||||
Anchor::BottomRight => Anchor::TopRight,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Swaps the horizontal position of the anchor.
|
|
||||||
pub fn swap_horizontal(&self) -> Self {
|
|
||||||
match self {
|
|
||||||
Anchor::TopLeft => Anchor::TopRight,
|
|
||||||
Anchor::TopCenter => Anchor::TopCenter,
|
|
||||||
Anchor::TopRight => Anchor::TopLeft,
|
|
||||||
Anchor::BottomLeft => Anchor::BottomRight,
|
|
||||||
Anchor::BottomCenter => Anchor::BottomCenter,
|
|
||||||
Anchor::BottomRight => Anchor::BottomLeft,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn other_side_corner_along(&self, axis: Axis) -> Anchor {
|
|
||||||
match axis {
|
|
||||||
Axis::Vertical => match self {
|
|
||||||
Self::TopLeft => Self::BottomLeft,
|
|
||||||
Self::TopCenter => Self::BottomCenter,
|
|
||||||
Self::TopRight => Self::BottomRight,
|
|
||||||
Self::BottomLeft => Self::TopLeft,
|
|
||||||
Self::BottomCenter => Self::TopCenter,
|
|
||||||
Self::BottomRight => Self::TopRight,
|
|
||||||
},
|
|
||||||
Axis::Horizontal => match self {
|
|
||||||
Self::TopLeft => Self::TopRight,
|
|
||||||
Self::TopCenter => Self::TopCenter,
|
|
||||||
Self::TopRight => Self::TopLeft,
|
|
||||||
Self::BottomLeft => Self::BottomRight,
|
|
||||||
Self::BottomCenter => Self::BottomCenter,
|
|
||||||
Self::BottomRight => Self::BottomLeft,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Corner> for Anchor {
|
|
||||||
fn from(corner: Corner) -> Self {
|
|
||||||
match corner {
|
|
||||||
Corner::TopLeft => Anchor::TopLeft,
|
|
||||||
Corner::TopRight => Anchor::TopRight,
|
|
||||||
Corner::BottomLeft => Anchor::BottomLeft,
|
|
||||||
Corner::BottomRight => Anchor::BottomRight,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Anchor> for Corner {
|
|
||||||
fn from(anchor: Anchor) -> Self {
|
|
||||||
match anchor {
|
|
||||||
Anchor::TopLeft => Corner::TopLeft,
|
|
||||||
Anchor::TopRight => Corner::TopRight,
|
|
||||||
Anchor::BottomLeft => Corner::BottomLeft,
|
|
||||||
Anchor::BottomRight => Corner::BottomRight,
|
|
||||||
Anchor::TopCenter => Corner::TopLeft,
|
|
||||||
Anchor::BottomCenter => Corner::BottomLeft,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A enum for defining the side of the element.
|
|
||||||
///
|
|
||||||
/// See also: [`Placement`] if you need to define the 4 edges.
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
pub enum Side {
|
|
||||||
#[serde(rename = "left")]
|
|
||||||
Left,
|
|
||||||
#[serde(rename = "right")]
|
|
||||||
Right,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Side {
|
|
||||||
/// Returns true if the side is left.
|
|
||||||
#[inline]
|
|
||||||
pub fn is_left(&self) -> bool {
|
|
||||||
matches!(self, Self::Left)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if the side is right.
|
|
||||||
#[inline]
|
|
||||||
pub fn is_right(&self) -> bool {
|
|
||||||
matches!(self, Self::Right)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A trait to extend the [`Axis`] enum with utility methods.
|
|
||||||
pub trait AxisExt {
|
|
||||||
#[allow(clippy::wrong_self_convention)]
|
|
||||||
fn is_horizontal(self) -> bool;
|
|
||||||
#[allow(clippy::wrong_self_convention)]
|
|
||||||
fn is_vertical(self) -> bool;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AxisExt for Axis {
|
|
||||||
#[inline]
|
|
||||||
fn is_horizontal(self) -> bool {
|
|
||||||
self == Axis::Horizontal
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn is_vertical(self) -> bool {
|
|
||||||
self == Axis::Vertical
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A trait for converting [`Pixels`] to `f32` and `f64`.
|
|
||||||
pub trait PixelsExt {
|
|
||||||
fn as_f32(&self) -> f32;
|
|
||||||
#[allow(clippy::wrong_self_convention)]
|
|
||||||
fn as_f64(self) -> f64;
|
|
||||||
}
|
|
||||||
impl PixelsExt for Pixels {
|
|
||||||
fn as_f32(&self) -> f32 {
|
|
||||||
f32::from(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn as_f64(self) -> f64 {
|
|
||||||
f64::from(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A trait to extend the [`Length`] enum with utility methods.
|
|
||||||
pub trait LengthExt {
|
|
||||||
/// Converts the [`Length`] to [`Pixels`] based on a given `base_size` and `rem_size`.
|
|
||||||
///
|
|
||||||
/// If the [`Length`] is [`Length::Auto`], it returns `None`.
|
|
||||||
fn to_pixels(&self, base_size: AbsoluteLength, rem_size: Pixels) -> Option<Pixels>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LengthExt for Length {
|
|
||||||
fn to_pixels(&self, base_size: AbsoluteLength, rem_size: Pixels) -> Option<Pixels> {
|
|
||||||
match self {
|
|
||||||
Length::Auto => None,
|
|
||||||
Length::Definite(len) => Some(len.to_pixels(base_size, rem_size)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A struct for defining the edges of an element.
|
|
||||||
///
|
|
||||||
/// A extend version of [`gpui::Edges`] to serialize/deserialize.
|
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, Eq, PartialEq)]
|
|
||||||
#[repr(C)]
|
|
||||||
pub struct Edges<T: Clone + Debug + Default + PartialEq> {
|
|
||||||
/// The size of the top edge.
|
|
||||||
pub top: T,
|
|
||||||
/// The size of the right edge.
|
|
||||||
pub right: T,
|
|
||||||
/// The size of the bottom edge.
|
|
||||||
pub bottom: T,
|
|
||||||
/// The size of the left edge.
|
|
||||||
pub left: T,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Edges<T>
|
|
||||||
where
|
|
||||||
T: Clone + Debug + Default + PartialEq,
|
|
||||||
{
|
|
||||||
/// Creates a new `Edges` instance with all edges set to the same value.
|
|
||||||
pub fn all(value: T) -> Self {
|
|
||||||
Self {
|
|
||||||
top: value.clone(),
|
|
||||||
right: value.clone(),
|
|
||||||
bottom: value.clone(),
|
|
||||||
left: value,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::time::{Duration, Instant};
|
use instant::{Duration, Instant};
|
||||||
|
|
||||||
|
/// A HistoryItem represents a single change in the history.
|
||||||
|
/// It must implement Clone and PartialEq to be used in the History.
|
||||||
pub trait HistoryItem: Clone + PartialEq {
|
pub trait HistoryItem: Clone + PartialEq {
|
||||||
fn version(&self) -> usize;
|
fn version(&self) -> usize;
|
||||||
fn set_version(&mut self, version: usize);
|
fn set_version(&mut self, version: usize);
|
||||||
@@ -22,10 +24,11 @@ pub struct History<I: HistoryItem> {
|
|||||||
redos: Vec<I>,
|
redos: Vec<I>,
|
||||||
last_changed_at: Instant,
|
last_changed_at: Instant,
|
||||||
version: usize,
|
version: usize,
|
||||||
max_undo: usize,
|
pub(crate) ignore: bool,
|
||||||
|
max_undos: usize,
|
||||||
group_interval: Option<Duration>,
|
group_interval: Option<Duration>,
|
||||||
|
grouping: bool,
|
||||||
unique: bool,
|
unique: bool,
|
||||||
pub ignore: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<I> History<I>
|
impl<I> History<I>
|
||||||
@@ -39,15 +42,16 @@ where
|
|||||||
ignore: false,
|
ignore: false,
|
||||||
last_changed_at: Instant::now(),
|
last_changed_at: Instant::now(),
|
||||||
version: 0,
|
version: 0,
|
||||||
max_undo: 1000,
|
max_undos: 1000,
|
||||||
group_interval: None,
|
group_interval: None,
|
||||||
|
grouping: false,
|
||||||
unique: false,
|
unique: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the maximum number of undo steps to keep, defaults to 1000.
|
/// Set the maximum number of undo steps to keep, defaults to 1000.
|
||||||
pub fn max_undo(mut self, max_undo: usize) -> Self {
|
pub fn max_undos(mut self, max_undos: usize) -> Self {
|
||||||
self.max_undo = max_undo;
|
self.max_undos = max_undos;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,10 +68,20 @@ where
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Start grouping changes, this will prevent the version from being incremented until `end_grouping` is called.
|
||||||
|
pub fn start_grouping(&mut self) {
|
||||||
|
self.grouping = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// End grouping changes, this will allow the version to be incremented again.
|
||||||
|
pub fn end_grouping(&mut self) {
|
||||||
|
self.grouping = false;
|
||||||
|
}
|
||||||
|
|
||||||
/// Increment the version number if the last change was made more than `GROUP_INTERVAL` milliseconds ago.
|
/// Increment the version number if the last change was made more than `GROUP_INTERVAL` milliseconds ago.
|
||||||
fn inc_version(&mut self) -> usize {
|
fn inc_version(&mut self) -> usize {
|
||||||
let t = Instant::now();
|
let t = Instant::now();
|
||||||
if Some(self.last_changed_at.elapsed()) > self.group_interval {
|
if !self.grouping && Some(self.last_changed_at.elapsed()) > self.group_interval {
|
||||||
self.version += 1;
|
self.version += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,10 +94,11 @@ where
|
|||||||
self.version
|
self.version
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Push a new change to the history.
|
||||||
pub fn push(&mut self, item: I) {
|
pub fn push(&mut self, item: I) {
|
||||||
let version = self.inc_version();
|
let version = self.inc_version();
|
||||||
|
|
||||||
if self.undos.len() >= self.max_undo {
|
if self.undos.len() >= self.max_undos {
|
||||||
self.undos.remove(0);
|
self.undos.remove(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,6 +128,7 @@ where
|
|||||||
self.redos.clear();
|
self.redos.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Undo the last change and return the changes that were undone.
|
||||||
pub fn undo(&mut self) -> Option<Vec<I>> {
|
pub fn undo(&mut self) -> Option<Vec<I>> {
|
||||||
if let Some(first_change) = self.undos.pop() {
|
if let Some(first_change) = self.undos.pop() {
|
||||||
let mut changes = vec![first_change.clone()];
|
let mut changes = vec![first_change.clone()];
|
||||||
@@ -135,6 +151,7 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Redo the last undone change and return the changes that were redone.
|
||||||
pub fn redo(&mut self) -> Option<Vec<I>> {
|
pub fn redo(&mut self) -> Option<Vec<I>> {
|
||||||
if let Some(first_change) = self.redos.pop() {
|
if let Some(first_change) = self.redos.pop() {
|
||||||
let mut changes = vec![first_change.clone()];
|
let mut changes = vec![first_change.clone()];
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
svg, AnyElement, App, AppContext, Context, Entity, Hsla, IntoElement, Radians, Render,
|
AnyElement, App, AppContext, Context, Entity, Hsla, IntoElement, Radians, Render, RenderOnce,
|
||||||
RenderOnce, SharedString, StyleRefinement, Styled, Svg, Transformation, Window,
|
SharedString, StyleRefinement, Styled, Svg, Transformation, Window, svg,
|
||||||
};
|
};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
@@ -39,6 +39,7 @@ pub enum IconName {
|
|||||||
Ellipsis,
|
Ellipsis,
|
||||||
Emoji,
|
Emoji,
|
||||||
Eye,
|
Eye,
|
||||||
|
Input,
|
||||||
Info,
|
Info,
|
||||||
Invite,
|
Invite,
|
||||||
Inbox,
|
Inbox,
|
||||||
@@ -110,6 +111,7 @@ impl IconNamed for IconName {
|
|||||||
Self::Ellipsis => "icons/ellipsis.svg",
|
Self::Ellipsis => "icons/ellipsis.svg",
|
||||||
Self::Emoji => "icons/emoji.svg",
|
Self::Emoji => "icons/emoji.svg",
|
||||||
Self::Eye => "icons/eye.svg",
|
Self::Eye => "icons/eye.svg",
|
||||||
|
Self::Input => "icons/input.svg",
|
||||||
Self::Info => "icons/info.svg",
|
Self::Info => "icons/info.svg",
|
||||||
Self::Invite => "icons/invite.svg",
|
Self::Invite => "icons/invite.svg",
|
||||||
Self::Inbox => "icons/inbox.svg",
|
Self::Inbox => "icons/inbox.svg",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::time::Duration;
|
use instant::Duration;
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
use std::time::Duration;
|
use instant::Duration;
|
||||||
|
|
||||||
use gpui::{px, Context, Pixels};
|
use gpui::{Context, Pixels, Task, px};
|
||||||
|
|
||||||
static INTERVAL: Duration = Duration::from_millis(500);
|
static INTERVAL: Duration = Duration::from_millis(500);
|
||||||
static PAUSE_DELAY: Duration = Duration::from_millis(300);
|
static PAUSE_DELAY: Duration = Duration::from_millis(300);
|
||||||
|
|
||||||
|
// On Windows, Linux, we should use integer to avoid blurry cursor.
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
pub(super) const CURSOR_WIDTH: Pixels = px(2.);
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
pub(super) const CURSOR_WIDTH: Pixels = px(1.5);
|
pub(super) const CURSOR_WIDTH: Pixels = px(1.5);
|
||||||
|
|
||||||
/// To manage the Input cursor blinking.
|
/// To manage the Input cursor blinking.
|
||||||
@@ -12,10 +17,12 @@ pub(super) const CURSOR_WIDTH: Pixels = px(1.5);
|
|||||||
/// Every loop will notify the view to update the `visible`, and Input will observe this update to touch repaint.
|
/// Every loop will notify the view to update the `visible`, and Input will observe this update to touch repaint.
|
||||||
///
|
///
|
||||||
/// The input painter will check if this in visible state, then it will draw the cursor.
|
/// The input painter will check if this in visible state, then it will draw the cursor.
|
||||||
pub struct BlinkCursor {
|
pub(crate) struct BlinkCursor {
|
||||||
visible: bool,
|
visible: bool,
|
||||||
paused: bool,
|
paused: bool,
|
||||||
epoch: usize,
|
epoch: usize,
|
||||||
|
|
||||||
|
_task: Task<()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BlinkCursor {
|
impl BlinkCursor {
|
||||||
@@ -24,6 +31,7 @@ impl BlinkCursor {
|
|||||||
visible: false,
|
visible: false,
|
||||||
paused: false,
|
paused: false,
|
||||||
epoch: 0,
|
epoch: 0,
|
||||||
|
_task: Task::ready(()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,14 +61,12 @@ impl BlinkCursor {
|
|||||||
|
|
||||||
// Schedule the next blink
|
// Schedule the next blink
|
||||||
let epoch = self.next_epoch();
|
let epoch = self.next_epoch();
|
||||||
cx.spawn(async move |this, cx| {
|
self._task = cx.spawn(async move |this, cx| {
|
||||||
cx.background_executor().timer(INTERVAL).await;
|
cx.background_executor().timer(INTERVAL).await;
|
||||||
|
|
||||||
if let Some(this) = this.upgrade() {
|
if let Some(this) = this.upgrade() {
|
||||||
this.update(cx, |this, cx| this.blink(epoch, cx));
|
this.update(cx, |this, cx| this.blink(epoch, cx));
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
.detach();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn visible(&self) -> bool {
|
pub fn visible(&self) -> bool {
|
||||||
@@ -76,7 +82,7 @@ impl BlinkCursor {
|
|||||||
|
|
||||||
// delay 500ms to start the blinking
|
// delay 500ms to start the blinking
|
||||||
let epoch = self.next_epoch();
|
let epoch = self.next_epoch();
|
||||||
cx.spawn(async move |this, cx| {
|
self._task = cx.spawn(async move |this, cx| {
|
||||||
cx.background_executor().timer(PAUSE_DELAY).await;
|
cx.background_executor().timer(PAUSE_DELAY).await;
|
||||||
|
|
||||||
if let Some(this) = this.upgrade() {
|
if let Some(this) = this.upgrade() {
|
||||||
@@ -85,13 +91,6 @@ impl BlinkCursor {
|
|||||||
this.blink(epoch, cx);
|
this.blink(epoch, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for BlinkCursor {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
|
||||||
use crate::history::HistoryItem;
|
use crate::{history::HistoryItem, input::Selection};
|
||||||
use crate::input::cursor::Selection;
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone)]
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
pub struct Change {
|
pub struct Change {
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
use gpui::{App, Styled};
|
use gpui::{App, Styled};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
use crate::button::{Button, ButtonVariants};
|
use crate::button::{Button, ButtonVariants as _};
|
||||||
use crate::{Icon, IconName, Sizable};
|
use crate::{Icon, IconName, Sizable as _};
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn clear_button(cx: &App) -> Button {
|
pub(crate) fn clear_button(cx: &App) -> Button {
|
||||||
Button::new("clean")
|
Button::new("clean")
|
||||||
.icon(Icon::new(IconName::CloseCircle))
|
.icon(Icon::new(IconName::CloseCircle))
|
||||||
.tooltip("Clear")
|
.ghost()
|
||||||
.small()
|
.xsmall()
|
||||||
.transparent()
|
.tab_stop(false)
|
||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().icon_muted)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::ops::Range;
|
use std::ops::{Range, RangeBounds};
|
||||||
|
|
||||||
/// A selection in the text, represented by start and end byte indices.
|
/// A selection in the text, represented by start and end byte indices.
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
|
||||||
@@ -42,5 +42,12 @@ impl From<Selection> for Range<usize> {
|
|||||||
value.start..value.end
|
value.start..value.end
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
impl RangeBounds<usize> for Selection {
|
||||||
|
fn start_bound(&self) -> std::ops::Bound<&usize> {
|
||||||
|
std::ops::Bound::Included(&self.start)
|
||||||
|
}
|
||||||
|
|
||||||
pub type Position = lsp_types::Position;
|
fn end_bound(&self) -> std::ops::Bound<&usize> {
|
||||||
|
std::ops::Bound::Excluded(&self.end)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
172
crates/ui/src/input/display_map/display_map.rs
Normal file
172
crates/ui/src/input/display_map/display_map.rs
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
use std::ops::Range;
|
||||||
|
|
||||||
|
use gpui::{App, Font, Pixels};
|
||||||
|
use ropey::Rope;
|
||||||
|
|
||||||
|
use super::text_wrapper::{LineItem, WrapDisplayPoint};
|
||||||
|
use super::wrap_map::WrapMap;
|
||||||
|
use crate::input::Point as TreeSitterPoint;
|
||||||
|
|
||||||
|
/// DisplayMap is the main interface for Input coordinate mapping.
|
||||||
|
pub struct DisplayMap {
|
||||||
|
wrap_map: WrapMap,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DisplayMap {
|
||||||
|
pub fn new(font: Font, font_size: Pixels, wrap_width: Option<Pixels>) -> Self {
|
||||||
|
Self {
|
||||||
|
wrap_map: WrapMap::new(font, font_size, wrap_width),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get total number of display rows (same as wrap rows without folding)
|
||||||
|
#[inline]
|
||||||
|
pub fn display_row_count(&self) -> usize {
|
||||||
|
self.wrap_map.wrap_row_count()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the buffer line for a given display row
|
||||||
|
pub fn display_row_to_buffer_line(&self, display_row: usize) -> usize {
|
||||||
|
self.wrap_map.wrap_row_to_buffer_line(display_row)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the display row range for a buffer line: [start, end)
|
||||||
|
pub fn buffer_line_to_display_row_range(&self, line: usize) -> Option<Range<usize>> {
|
||||||
|
let range = self.wrap_map.buffer_line_to_wrap_row_range(line);
|
||||||
|
if range.is_empty() { None } else { Some(range) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a buffer line is completely hidden (never true without folding)
|
||||||
|
#[inline]
|
||||||
|
pub fn is_buffer_line_hidden(&self, _line: usize) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All wrap rows are visible since there's no folding.
|
||||||
|
#[inline]
|
||||||
|
pub fn folded_ranges(&self) -> &[()] {
|
||||||
|
&[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adjust folds for edit (no-op without folding)
|
||||||
|
pub fn adjust_folds_for_edit(
|
||||||
|
&mut self,
|
||||||
|
_old_text: &Rope,
|
||||||
|
_range: &Range<usize>,
|
||||||
|
_new_text: &str,
|
||||||
|
) {
|
||||||
|
// No-op: no folding
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update text (incremental or full)
|
||||||
|
pub fn on_text_changed(
|
||||||
|
&mut self,
|
||||||
|
changed_text: &Rope,
|
||||||
|
range: &Range<usize>,
|
||||||
|
new_text: &Rope,
|
||||||
|
cx: &mut App,
|
||||||
|
) {
|
||||||
|
self.wrap_map
|
||||||
|
.on_text_changed(changed_text, range, new_text, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update layout parameters (wrap width or font)
|
||||||
|
pub fn on_layout_changed(&mut self, wrap_width: Option<Pixels>, cx: &mut App) {
|
||||||
|
self.wrap_map.on_layout_changed(wrap_width, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set font parameters
|
||||||
|
pub fn set_font(&mut self, font: Font, font_size: Pixels, cx: &mut App) {
|
||||||
|
self.wrap_map.set_font(font, font_size, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensure text is prepared (initializes wrapper if needed)
|
||||||
|
pub fn ensure_text_prepared(&mut self, text: &Rope, cx: &mut App) {
|
||||||
|
self.wrap_map.ensure_text_prepared(text, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize with text
|
||||||
|
pub fn set_text(&mut self, text: &Rope, cx: &mut App) {
|
||||||
|
self.wrap_map.set_text(text, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert byte offset to wrap display point (with soft wrap info).
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn offset_to_wrap_display_point(&self, offset: usize) -> WrapDisplayPoint {
|
||||||
|
self.wrap_map.wrapper().offset_to_display_point(offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert wrap display point to byte offset.
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn wrap_display_point_to_offset(&self, point: WrapDisplayPoint) -> usize {
|
||||||
|
self.wrap_map.wrapper().display_point_to_offset(point)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert wrap display point to TreeSitterPoint (buffer line/col).
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn wrap_display_point_to_point(&self, point: WrapDisplayPoint) -> TreeSitterPoint {
|
||||||
|
self.wrap_map.wrapper().display_point_to_point(point)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Since there's no folding, wrap row == display row.
|
||||||
|
#[inline]
|
||||||
|
pub fn wrap_row_to_display_row(&self, wrap_row: usize) -> Option<usize> {
|
||||||
|
if wrap_row < self.wrap_row_count() {
|
||||||
|
Some(wrap_row)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Since there's no folding, nearest visible row is the row itself.
|
||||||
|
#[inline]
|
||||||
|
pub fn nearest_visible_display_row(&self, wrap_row: usize) -> usize {
|
||||||
|
wrap_row.min(self.wrap_row_count().saturating_sub(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Since there's no folding, display row == wrap row.
|
||||||
|
#[inline]
|
||||||
|
pub fn display_row_to_wrap_row(&self, display_row: usize) -> Option<usize> {
|
||||||
|
if display_row < self.wrap_row_count() {
|
||||||
|
Some(display_row)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the longest row index (by byte length).
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn longest_row(&self) -> usize {
|
||||||
|
self.wrap_map.wrapper().longest_row.row
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get access to line items (for rendering)
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn lines(&self) -> &[LineItem] {
|
||||||
|
self.wrap_map.lines()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the rope text
|
||||||
|
#[inline]
|
||||||
|
pub fn text(&self) -> &Rope {
|
||||||
|
self.wrap_map.text()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculate how many wrap rows of a buffer line are visible
|
||||||
|
#[inline]
|
||||||
|
pub fn visible_wrap_row_count_for_buffer_line(&self, line: usize) -> usize {
|
||||||
|
self.wrap_map.visible_wrap_row_count_for_buffer_line(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the wrap row count
|
||||||
|
#[inline]
|
||||||
|
pub fn wrap_row_count(&self) -> usize {
|
||||||
|
self.wrap_map.wrap_row_count()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the buffer line count (logical lines)
|
||||||
|
#[inline]
|
||||||
|
pub fn buffer_line_count(&self) -> usize {
|
||||||
|
self.wrap_map.buffer_line_count()
|
||||||
|
}
|
||||||
|
}
|
||||||
7
crates/ui/src/input/display_map/mod.rs
Normal file
7
crates/ui/src/input/display_map/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#[allow(clippy::module_inception)]
|
||||||
|
mod display_map;
|
||||||
|
mod text_wrapper;
|
||||||
|
mod wrap_map;
|
||||||
|
|
||||||
|
pub use self::display_map::DisplayMap;
|
||||||
|
pub(crate) use self::text_wrapper::LineLayout;
|
||||||
582
crates/ui/src/input/display_map/text_wrapper.rs
Normal file
582
crates/ui/src/input/display_map/text_wrapper.rs
Normal file
@@ -0,0 +1,582 @@
|
|||||||
|
use std::ops::Range;
|
||||||
|
|
||||||
|
use gpui::{
|
||||||
|
App, Font, Half, LineFragment, Pixels, Point, ShapedLine, Size, TextAlign, Window, point, px,
|
||||||
|
size,
|
||||||
|
};
|
||||||
|
use ropey::Rope;
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
|
use crate::input::{LastLayout, Point as TreeSitterPoint, RopeExt, WhitespaceIndicators};
|
||||||
|
|
||||||
|
/// A line with soft wrapped lines info.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub(crate) struct LineItem {
|
||||||
|
/// The original line text, without end `\n`.
|
||||||
|
line: Rope,
|
||||||
|
/// The soft wrapped lines relative byte range (0..line.len) of this line (Include first line).
|
||||||
|
///
|
||||||
|
/// Not contains the line end `\n`.
|
||||||
|
pub(crate) wrapped_lines: Vec<Range<usize>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LineItem {
|
||||||
|
/// Get the bytes length of this line.
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn len(&self) -> usize {
|
||||||
|
self.line.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get number of soft wrapped lines of this line (include the first line).
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn lines_len(&self) -> usize {
|
||||||
|
self.wrapped_lines.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub(crate) struct LongestRow {
|
||||||
|
/// The 0-based row index.
|
||||||
|
pub row: usize,
|
||||||
|
/// The bytes length of the longest line.
|
||||||
|
pub len: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Used to prepare the text with soft wrap to be get lines to displayed in the Editor.
|
||||||
|
///
|
||||||
|
/// After use lines to calculate the scroll size of the Editor.
|
||||||
|
pub(crate) struct TextWrapper {
|
||||||
|
text: Rope,
|
||||||
|
/// Total wrapped lines (Inlucde the first line), value is start and end index of the line.
|
||||||
|
soft_lines: usize,
|
||||||
|
font: Font,
|
||||||
|
font_size: Pixels,
|
||||||
|
/// If is none, it means the text is not wrapped
|
||||||
|
wrap_width: Option<Pixels>,
|
||||||
|
/// The longest (row, bytes len) in characters, used to calculate the horizontal scroll width.
|
||||||
|
pub(crate) longest_row: LongestRow,
|
||||||
|
/// The lines by split \n
|
||||||
|
pub(crate) lines: Vec<LineItem>,
|
||||||
|
|
||||||
|
_initialized: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
impl TextWrapper {
|
||||||
|
pub(crate) fn new(font: Font, font_size: Pixels, wrap_width: Option<Pixels>) -> Self {
|
||||||
|
Self {
|
||||||
|
text: Rope::new(),
|
||||||
|
font,
|
||||||
|
font_size,
|
||||||
|
wrap_width,
|
||||||
|
soft_lines: 0,
|
||||||
|
longest_row: LongestRow::default(),
|
||||||
|
lines: Vec::new(),
|
||||||
|
_initialized: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn set_default_text(&mut self, text: &Rope) {
|
||||||
|
self.text = text.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get reference to the rope text.
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn text(&self) -> &Rope {
|
||||||
|
&self.text
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the total number of lines including wrapped lines.
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn len(&self) -> usize {
|
||||||
|
self.soft_lines
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the line item by row index.
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn line(&self, row: usize) -> Option<&LineItem> {
|
||||||
|
self.lines.get(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_wrap_width(&mut self, wrap_width: Option<Pixels>, cx: &mut App) {
|
||||||
|
if wrap_width == self.wrap_width {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.wrap_width = wrap_width;
|
||||||
|
self.update_all(&self.text.clone(), cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_font(&mut self, font: Font, font_size: Pixels, cx: &mut App) {
|
||||||
|
if self.font.eq(&font) && self.font_size == font_size {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.font = font;
|
||||||
|
self.font_size = font_size;
|
||||||
|
self.update_all(&self.text.clone(), cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn prepare_if_need(&mut self, text: &Rope, cx: &mut App) -> bool {
|
||||||
|
if self._initialized {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self._initialized = true;
|
||||||
|
self.update_all(text, cx);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the text wrapper and recalculate the wrapped lines.
|
||||||
|
///
|
||||||
|
/// If the `text` is the same as the current text, do nothing.
|
||||||
|
///
|
||||||
|
/// - `changed_text`: The text [`Rope`] that has changed.
|
||||||
|
/// - `range`: The `selected_range` before change.
|
||||||
|
/// - `new_text`: The inserted text.
|
||||||
|
/// - `force`: Whether to force the update, if false, the update will be skipped if the text is the same.
|
||||||
|
/// - `cx`: The application context.
|
||||||
|
pub(crate) fn update(
|
||||||
|
&mut self,
|
||||||
|
changed_text: &Rope,
|
||||||
|
range: &Range<usize>,
|
||||||
|
new_text: &Rope,
|
||||||
|
cx: &mut App,
|
||||||
|
) {
|
||||||
|
let mut line_wrapper = cx
|
||||||
|
.text_system()
|
||||||
|
.line_wrapper(self.font.clone(), self.font_size);
|
||||||
|
self._update(
|
||||||
|
changed_text,
|
||||||
|
range,
|
||||||
|
new_text,
|
||||||
|
&mut |line_str, wrap_width| {
|
||||||
|
line_wrapper
|
||||||
|
.wrap_line(&[LineFragment::text(line_str)], wrap_width)
|
||||||
|
.collect()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _update<F>(
|
||||||
|
&mut self,
|
||||||
|
changed_text: &Rope,
|
||||||
|
range: &Range<usize>,
|
||||||
|
new_text: &Rope,
|
||||||
|
wrap_line: &mut F,
|
||||||
|
) where
|
||||||
|
F: FnMut(&str, Pixels) -> Vec<gpui::Boundary>,
|
||||||
|
{
|
||||||
|
// Remove the old changed lines.
|
||||||
|
let start_row = self.text.offset_to_point(range.start).row;
|
||||||
|
let start_row = start_row.min(self.lines.len().saturating_sub(1));
|
||||||
|
let end_row = self.text.offset_to_point(range.end).row;
|
||||||
|
let end_row = end_row.min(self.lines.len().saturating_sub(1));
|
||||||
|
let rows_range = start_row..=end_row;
|
||||||
|
|
||||||
|
if rows_range.contains(&self.longest_row.row) {
|
||||||
|
self.longest_row = LongestRow::default();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut longest_row_ix = self.longest_row.row;
|
||||||
|
let mut longest_row_len = self.longest_row.len;
|
||||||
|
|
||||||
|
// To add the new lines.
|
||||||
|
let new_start_row = changed_text.offset_to_point(range.start).row;
|
||||||
|
let new_start_offset = changed_text.line_start_offset(new_start_row);
|
||||||
|
let new_end_row = changed_text
|
||||||
|
.offset_to_point(range.start + new_text.len())
|
||||||
|
.row;
|
||||||
|
let new_end_offset = changed_text.line_end_offset(new_end_row);
|
||||||
|
let new_range = new_start_offset..new_end_offset;
|
||||||
|
|
||||||
|
let mut new_lines = vec![];
|
||||||
|
let wrap_width = self.wrap_width;
|
||||||
|
|
||||||
|
// line not contains `\n`.
|
||||||
|
for (ix, line) in Rope::from(changed_text.slice(new_range))
|
||||||
|
.iter_lines()
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
let line_str = line.to_string();
|
||||||
|
let mut wrapped_lines = vec![];
|
||||||
|
let mut prev_boundary_ix = 0;
|
||||||
|
|
||||||
|
if line_str.len() > longest_row_len {
|
||||||
|
longest_row_ix = new_start_row + ix;
|
||||||
|
longest_row_len = line_str.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If wrap_width is Pixels::MAX, skip wrapping to disable word wrap
|
||||||
|
if let Some(wrap_width) = wrap_width {
|
||||||
|
// Here only have wrapped line, if there is no wrap meet, the `line_wraps` result will empty.
|
||||||
|
for boundary in wrap_line(&line_str, wrap_width) {
|
||||||
|
wrapped_lines.push(prev_boundary_ix..boundary.ix);
|
||||||
|
prev_boundary_ix = boundary.ix;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset of the line
|
||||||
|
if !line_str[prev_boundary_ix..].is_empty() || prev_boundary_ix == 0 {
|
||||||
|
wrapped_lines.push(prev_boundary_ix..line.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
new_lines.push(LineItem {
|
||||||
|
line: Rope::from(line),
|
||||||
|
wrapped_lines,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.lines.is_empty() {
|
||||||
|
self.lines = new_lines;
|
||||||
|
} else {
|
||||||
|
self.lines.splice(rows_range, new_lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.text = changed_text.clone();
|
||||||
|
self.soft_lines = self.lines.iter().map(|l| l.lines_len()).sum();
|
||||||
|
self.longest_row = LongestRow {
|
||||||
|
row: longest_row_ix,
|
||||||
|
len: longest_row_len,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the text wrapper and recalculate the wrapped lines.
|
||||||
|
///
|
||||||
|
/// If the `text` is the same as the current text, do nothing.
|
||||||
|
fn update_all(&mut self, text: &Rope, cx: &mut App) {
|
||||||
|
self.update(text, &(0..text.len()), text, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return display point (with soft wrap) from the given byte offset in the text.
|
||||||
|
///
|
||||||
|
/// Panics if the `offset` is out of bounds.
|
||||||
|
pub(crate) fn offset_to_display_point(&self, offset: usize) -> WrapDisplayPoint {
|
||||||
|
let row = self.text.offset_to_point(offset).row;
|
||||||
|
let start = self.text.line_start_offset(row);
|
||||||
|
let line = &self.lines[row];
|
||||||
|
|
||||||
|
let mut wrapped_row = self
|
||||||
|
.lines
|
||||||
|
.iter()
|
||||||
|
.take(row)
|
||||||
|
.map(|l| l.lines_len())
|
||||||
|
.sum::<usize>();
|
||||||
|
|
||||||
|
let local_offset = offset.saturating_sub(start);
|
||||||
|
for (ix, range) in line.wrapped_lines.iter().enumerate() {
|
||||||
|
if range.contains(&local_offset) {
|
||||||
|
return WrapDisplayPoint::new(
|
||||||
|
wrapped_row + ix,
|
||||||
|
ix,
|
||||||
|
local_offset.saturating_sub(range.start),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise return the eof of the line.
|
||||||
|
let last_range = line.wrapped_lines.last().unwrap_or(&(0..0));
|
||||||
|
let ix = line.lines_len().saturating_sub(1);
|
||||||
|
|
||||||
|
WrapDisplayPoint::new(wrapped_row + ix, ix, last_range.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return byte offset in the text from the given display point (with soft wrap).
|
||||||
|
///
|
||||||
|
/// Panics if the `point.row` is out of bounds.
|
||||||
|
pub(crate) fn display_point_to_offset(&self, point: WrapDisplayPoint) -> usize {
|
||||||
|
let mut wrapped_row = 0;
|
||||||
|
for (row, line) in self.lines.iter().enumerate() {
|
||||||
|
if wrapped_row + line.lines_len() > point.row {
|
||||||
|
let line_start = self.text.line_start_offset(row);
|
||||||
|
let local_row = point.row.saturating_sub(wrapped_row);
|
||||||
|
if let Some(range) = line.wrapped_lines.get(local_row) {
|
||||||
|
return line_start + (range.start + point.column).min(range.end);
|
||||||
|
} else {
|
||||||
|
// If not found, return the end of the line.
|
||||||
|
return line_start + line.len();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapped_row += line.lines_len();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.text.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn display_point_to_point(&self, point: WrapDisplayPoint) -> TreeSitterPoint {
|
||||||
|
let offset = self.display_point_to_offset(point);
|
||||||
|
self.text.offset_to_point(offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn point_to_display_point(&self, point: TreeSitterPoint) -> WrapDisplayPoint {
|
||||||
|
let offset = self.text.point_to_offset(point);
|
||||||
|
self.offset_to_display_point(offset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A display point within the soft-wrapped text.
|
||||||
|
///
|
||||||
|
/// This represents a position in the text after soft-wrapping,
|
||||||
|
/// with an additional `local_row` field tracking the wrap line
|
||||||
|
/// within the original buffer line.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub(crate) struct WrapDisplayPoint {
|
||||||
|
/// The 0-based soft wrapped row index in the text.
|
||||||
|
pub row: usize,
|
||||||
|
/// The 0-based row index in local line (include first line).
|
||||||
|
///
|
||||||
|
/// This value only valid when return from [`TextWrapper::offset_to_display_point`], otherwise it will be ignored.
|
||||||
|
pub local_row: usize,
|
||||||
|
/// The 0-based column byte index in the display line (with soft wrap).
|
||||||
|
pub column: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WrapDisplayPoint {
|
||||||
|
pub fn new(row: usize, local_row: usize, column: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
row,
|
||||||
|
local_row,
|
||||||
|
column,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The layout info of a line with soft wrapped lines.
|
||||||
|
pub(crate) struct LineLayout {
|
||||||
|
/// Total bytes length of this line.
|
||||||
|
len: usize,
|
||||||
|
/// The soft wrapped lines of this line (Include the first line).
|
||||||
|
pub(crate) wrapped_lines: SmallVec<[ShapedLine; 1]>,
|
||||||
|
pub(crate) longest_width: Pixels,
|
||||||
|
pub(crate) whitespace_indicators: Option<WhitespaceIndicators>,
|
||||||
|
/// Whitespace indicators: (line_index, x_position, is_tab)
|
||||||
|
pub(crate) whitespace_chars: Vec<(usize, Pixels, bool)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LineLayout {
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
len: 0,
|
||||||
|
longest_width: px(0.),
|
||||||
|
wrapped_lines: SmallVec::new(),
|
||||||
|
whitespace_chars: Vec::new(),
|
||||||
|
whitespace_indicators: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn lines(mut self, wrapped_lines: SmallVec<[ShapedLine; 1]>) -> Self {
|
||||||
|
self.set_wrapped_lines(wrapped_lines);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_wrapped_lines(&mut self, wrapped_lines: SmallVec<[ShapedLine; 1]>) {
|
||||||
|
self.len = wrapped_lines.iter().map(|l| l.len).sum();
|
||||||
|
let width = wrapped_lines
|
||||||
|
.iter()
|
||||||
|
.map(|l| l.width)
|
||||||
|
.max()
|
||||||
|
.unwrap_or_default();
|
||||||
|
self.longest_width = width;
|
||||||
|
self.wrapped_lines = wrapped_lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn with_whitespaces(mut self, indicators: Option<WhitespaceIndicators>) -> Self {
|
||||||
|
self.whitespace_indicators = indicators;
|
||||||
|
let Some(indicators) = self.whitespace_indicators.as_ref() else {
|
||||||
|
return self;
|
||||||
|
};
|
||||||
|
|
||||||
|
let space_indicator_offset = indicators.space.width.half();
|
||||||
|
|
||||||
|
for (line_index, wrapped_line) in self.wrapped_lines.iter().enumerate() {
|
||||||
|
for (relative_offset, c) in wrapped_line.text.char_indices() {
|
||||||
|
if matches!(c, ' ' | '\t') {
|
||||||
|
let is_tab = c == '\t';
|
||||||
|
let start_x = wrapped_line.x_for_index(relative_offset);
|
||||||
|
let end_x = wrapped_line.x_for_index(relative_offset + c.len_utf8());
|
||||||
|
// Center the indicator in the actual character's space
|
||||||
|
let x_position = if c == ' ' {
|
||||||
|
(start_x + end_x).half() - space_indicator_offset
|
||||||
|
} else {
|
||||||
|
start_x
|
||||||
|
};
|
||||||
|
|
||||||
|
self.whitespace_chars.push((line_index, x_position, is_tab));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn len(&self) -> usize {
|
||||||
|
self.len
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the position (x, y) for the given index in this line layout.
|
||||||
|
///
|
||||||
|
/// - The `offset` is a local byte index in this line layout.
|
||||||
|
/// - When `line_end_affinity` is true, an offset at a soft wrap boundary is placed at
|
||||||
|
/// the end of the current visual line rather than the start of the next one.
|
||||||
|
/// - The return value is relative to the top-left corner of this line layout, start from (0, 0)
|
||||||
|
pub(crate) fn position_for_index(
|
||||||
|
&self,
|
||||||
|
offset: usize,
|
||||||
|
last_layout: &LastLayout,
|
||||||
|
line_end_affinity: bool,
|
||||||
|
) -> Option<Point<Pixels>> {
|
||||||
|
let mut acc_len = 0;
|
||||||
|
let mut offset_y = px(0.);
|
||||||
|
|
||||||
|
let x_offset = last_layout.alignment_offset(self.longest_width);
|
||||||
|
|
||||||
|
for (i, line) in self.wrapped_lines.iter().enumerate() {
|
||||||
|
let is_last = i + 1 == self.wrapped_lines.len();
|
||||||
|
|
||||||
|
let matches = if line.len == 0 {
|
||||||
|
// Empty visual lines still own their boundary offset.
|
||||||
|
offset == acc_len
|
||||||
|
} else if is_last || line_end_affinity {
|
||||||
|
// Inclusive: cursor can sit at end of this visual line.
|
||||||
|
offset >= acc_len && offset <= acc_len + line.len
|
||||||
|
} else {
|
||||||
|
// Exclusive: boundary offset belongs to the next visual line.
|
||||||
|
offset >= acc_len && offset < acc_len + line.len
|
||||||
|
};
|
||||||
|
|
||||||
|
if matches {
|
||||||
|
let x = line.x_for_index(offset.saturating_sub(acc_len)) + x_offset;
|
||||||
|
return Some(point(x, offset_y));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always advance by actual line length. The last line gets +1 so the
|
||||||
|
// cursor can be placed after the final character.
|
||||||
|
acc_len += if is_last { line.len + 1 } else { line.len };
|
||||||
|
offset_y += last_layout.line_height;
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the closest index for the given x in this line layout.
|
||||||
|
pub(crate) fn closest_index_for_x(&self, x: Pixels, last_layout: &LastLayout) -> usize {
|
||||||
|
let mut acc_len = 0;
|
||||||
|
let x_offset = last_layout.alignment_offset(self.longest_width);
|
||||||
|
let x = x - x_offset;
|
||||||
|
|
||||||
|
for (i, line) in self.wrapped_lines.iter().enumerate() {
|
||||||
|
let is_last = i + 1 == self.wrapped_lines.len();
|
||||||
|
if x <= line.width {
|
||||||
|
let mut ix = line.closest_index_for_x(x);
|
||||||
|
if !is_last && ix == line.text.len() {
|
||||||
|
// For soft wrap line, we can't put the cursor at the end of the line.
|
||||||
|
let c_len = line.text.chars().last().map(|c| c.len_utf8()).unwrap_or(0);
|
||||||
|
ix = ix.saturating_sub(c_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc_len + ix;
|
||||||
|
}
|
||||||
|
acc_len += line.text.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
acc_len
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the index for the given position (x, y) in this line layout.
|
||||||
|
///
|
||||||
|
/// The `pos` is relative to the top-left corner of this line layout, start from (0, 0)
|
||||||
|
/// The return value is a local byte index in this line layout, start from 0.
|
||||||
|
pub(crate) fn closest_index_for_position(
|
||||||
|
&self,
|
||||||
|
pos: Point<Pixels>,
|
||||||
|
last_layout: &LastLayout,
|
||||||
|
) -> Option<usize> {
|
||||||
|
let mut offset = 0;
|
||||||
|
let mut line_top = px(0.);
|
||||||
|
let x_offset = last_layout.alignment_offset(self.longest_width);
|
||||||
|
for (i, line) in self.wrapped_lines.iter().enumerate() {
|
||||||
|
let is_last = i + 1 == self.wrapped_lines.len();
|
||||||
|
let line_bottom = line_top + last_layout.line_height;
|
||||||
|
if pos.y >= line_top && pos.y < line_bottom {
|
||||||
|
let mut ix = line.closest_index_for_x(pos.x - x_offset);
|
||||||
|
if !is_last && ix == line.text.len() {
|
||||||
|
// For soft wrap line, we can't put the cursor at the end of the line.
|
||||||
|
let c_len = line.text.chars().last().map(|c| c.len_utf8()).unwrap_or(0);
|
||||||
|
ix = ix.saturating_sub(c_len);
|
||||||
|
}
|
||||||
|
return Some(offset + ix);
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += line.text.len();
|
||||||
|
line_top = line_bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn index_for_position(
|
||||||
|
&self,
|
||||||
|
pos: Point<Pixels>,
|
||||||
|
last_layout: &LastLayout,
|
||||||
|
) -> Option<usize> {
|
||||||
|
let mut offset = 0;
|
||||||
|
let mut line_top = px(0.);
|
||||||
|
let x_offset = last_layout.alignment_offset(self.longest_width);
|
||||||
|
for line in self.wrapped_lines.iter() {
|
||||||
|
let line_bottom = line_top + last_layout.line_height;
|
||||||
|
if pos.y >= line_top && pos.y < line_bottom {
|
||||||
|
let ix = line.index_for_x(pos.x - x_offset)?;
|
||||||
|
return Some(offset + ix);
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += line.text.len();
|
||||||
|
line_top = line_bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn size(&self, line_height: Pixels) -> Size<Pixels> {
|
||||||
|
size(self.longest_width, self.wrapped_lines.len() * line_height)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn paint(
|
||||||
|
&self,
|
||||||
|
pos: Point<Pixels>,
|
||||||
|
line_height: Pixels,
|
||||||
|
text_align: TextAlign,
|
||||||
|
align_width: Option<Pixels>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) {
|
||||||
|
for (ix, line) in self.wrapped_lines.iter().enumerate() {
|
||||||
|
_ = line.paint(
|
||||||
|
pos + point(px(0.), ix * line_height),
|
||||||
|
line_height,
|
||||||
|
text_align,
|
||||||
|
align_width,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paint whitespace indicators
|
||||||
|
if let Some(indicators) = self.whitespace_indicators.as_ref() {
|
||||||
|
for (line_index, x_position, is_tab) in &self.whitespace_chars {
|
||||||
|
let invisible = if *is_tab {
|
||||||
|
indicators.tab.clone()
|
||||||
|
} else {
|
||||||
|
indicators.space.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
let origin = point(
|
||||||
|
pos.x + *x_position,
|
||||||
|
pos.y + *line_index as f32 * line_height,
|
||||||
|
);
|
||||||
|
|
||||||
|
_ = invisible.paint(origin, line_height, text_align, align_width, window, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
172
crates/ui/src/input/display_map/wrap_map.rs
Normal file
172
crates/ui/src/input/display_map/wrap_map.rs
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
/// WrapMap: Soft-wrapping layer (Buffer → Wrap rows).
|
||||||
|
///
|
||||||
|
/// This module wraps the existing TextWrapper and provides:
|
||||||
|
/// - BufferPoint ↔ WrapPoint mapping
|
||||||
|
/// - Efficient buffer_line → wrap_row queries via prefix sum cache
|
||||||
|
/// - Incremental updates when text or layout changes
|
||||||
|
use std::ops::Range;
|
||||||
|
|
||||||
|
use gpui::{App, Font, Pixels};
|
||||||
|
use ropey::Rope;
|
||||||
|
|
||||||
|
use super::text_wrapper::{LineItem, TextWrapper};
|
||||||
|
|
||||||
|
/// WrapMap manages soft-wrapping and provides buffer ↔ wrap coordinate mapping.
|
||||||
|
pub struct WrapMap {
|
||||||
|
/// The underlying text wrapper (reuses existing implementation)
|
||||||
|
wrapper: TextWrapper,
|
||||||
|
|
||||||
|
/// Prefix sum cache: buffer_line_starts[line] = first wrap_row for buffer line `line`
|
||||||
|
/// This allows O(1) lookup of buffer_line → wrap_row
|
||||||
|
buffer_line_starts: Vec<usize>,
|
||||||
|
|
||||||
|
/// Cached line count from last rebuild
|
||||||
|
cached_line_count: usize,
|
||||||
|
|
||||||
|
/// Cached total wrap row count from last rebuild.
|
||||||
|
/// Used together with `cached_line_count` to detect if the cache is stale.
|
||||||
|
/// When soft wrap changes a line's wrap count without changing buffer line count,
|
||||||
|
/// this catches the staleness.
|
||||||
|
cached_wrap_row_count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WrapMap {
|
||||||
|
pub fn new(font: Font, font_size: Pixels, wrap_width: Option<Pixels>) -> Self {
|
||||||
|
Self {
|
||||||
|
wrapper: TextWrapper::new(font, font_size, wrap_width),
|
||||||
|
buffer_line_starts: Vec::new(),
|
||||||
|
cached_line_count: 0,
|
||||||
|
cached_wrap_row_count: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get total number of wrap rows (visual rows after soft-wrapping)
|
||||||
|
#[inline]
|
||||||
|
pub fn wrap_row_count(&self) -> usize {
|
||||||
|
self.wrapper.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get total number of buffer lines (logical lines)
|
||||||
|
#[inline]
|
||||||
|
pub fn buffer_line_count(&self) -> usize {
|
||||||
|
self.wrapper.lines.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the buffer line for a given wrap row
|
||||||
|
pub fn wrap_row_to_buffer_line(&self, wrap_row: usize) -> usize {
|
||||||
|
if wrap_row >= self.wrap_row_count() {
|
||||||
|
return self.buffer_line_count().saturating_sub(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Binary search in prefix sum cache
|
||||||
|
match self.buffer_line_starts.binary_search(&wrap_row) {
|
||||||
|
Ok(line) => line,
|
||||||
|
Err(insert_pos) => insert_pos.saturating_sub(1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the first wrap row for a given buffer line
|
||||||
|
pub fn buffer_line_to_first_wrap_row(&self, line: usize) -> usize {
|
||||||
|
if line >= self.buffer_line_starts.len() {
|
||||||
|
return self.wrap_row_count();
|
||||||
|
}
|
||||||
|
self.buffer_line_starts[line]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the wrap row range for a buffer line: [start, end)
|
||||||
|
pub fn buffer_line_to_wrap_row_range(&self, line: usize) -> Range<usize> {
|
||||||
|
let start = self.buffer_line_to_first_wrap_row(line);
|
||||||
|
let end = if line + 1 < self.buffer_line_starts.len() {
|
||||||
|
self.buffer_line_starts[line + 1]
|
||||||
|
} else {
|
||||||
|
self.wrap_row_count()
|
||||||
|
};
|
||||||
|
start..end
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update text (incremental or full)
|
||||||
|
pub fn on_text_changed(
|
||||||
|
&mut self,
|
||||||
|
changed_text: &Rope,
|
||||||
|
range: &Range<usize>,
|
||||||
|
new_text: &Rope,
|
||||||
|
cx: &mut App,
|
||||||
|
) {
|
||||||
|
self.wrapper.update(changed_text, range, new_text, cx);
|
||||||
|
self.rebuild_cache();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update layout parameters (wrap width or font)
|
||||||
|
pub fn on_layout_changed(&mut self, wrap_width: Option<Pixels>, cx: &mut App) {
|
||||||
|
self.wrapper.set_wrap_width(wrap_width, cx);
|
||||||
|
self.rebuild_cache();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set font parameters
|
||||||
|
pub fn set_font(&mut self, font: Font, font_size: Pixels, cx: &mut App) {
|
||||||
|
self.wrapper.set_font(font, font_size, cx);
|
||||||
|
self.rebuild_cache();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensure text is prepared (initializes wrapper if needed)
|
||||||
|
pub fn ensure_text_prepared(&mut self, text: &Rope, cx: &mut App) -> bool {
|
||||||
|
let did_initialize = self.wrapper.prepare_if_need(text, cx);
|
||||||
|
if did_initialize {
|
||||||
|
self.rebuild_cache();
|
||||||
|
}
|
||||||
|
did_initialize
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize with text
|
||||||
|
pub fn set_text(&mut self, text: &Rope, cx: &mut App) {
|
||||||
|
self.wrapper.set_default_text(text);
|
||||||
|
self.wrapper.prepare_if_need(text, cx);
|
||||||
|
self.rebuild_cache();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rebuild the prefix sum cache: buffer_line_starts
|
||||||
|
fn rebuild_cache(&mut self) {
|
||||||
|
let line_count = self.wrapper.lines.len();
|
||||||
|
let wrap_row_count = self.wrapper.len();
|
||||||
|
|
||||||
|
// Skip if nothing changed: both buffer line count and total wrap row count must match.
|
||||||
|
if line_count == self.cached_line_count
|
||||||
|
&& wrap_row_count == self.cached_wrap_row_count
|
||||||
|
&& !self.buffer_line_starts.is_empty()
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.buffer_line_starts.clear();
|
||||||
|
|
||||||
|
let mut wrap_row = 0;
|
||||||
|
for line_item in &self.wrapper.lines {
|
||||||
|
self.buffer_line_starts.push(wrap_row);
|
||||||
|
wrap_row += line_item.lines_len();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.cached_line_count = line_count;
|
||||||
|
self.cached_wrap_row_count = wrap_row_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get access to the underlying wrapper (for rendering/hit-testing)
|
||||||
|
pub(crate) fn wrapper(&self) -> &TextWrapper {
|
||||||
|
&self.wrapper
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get access to line items (for rendering)
|
||||||
|
pub(crate) fn lines(&self) -> &[LineItem] {
|
||||||
|
&self.wrapper.lines
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the rope text
|
||||||
|
pub fn text(&self) -> &Rope {
|
||||||
|
self.wrapper.text()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculate how many wrap rows of a buffer line are visible.
|
||||||
|
/// Without folding, all wrap rows are visible.
|
||||||
|
pub fn visible_wrap_row_count_for_buffer_line(&self, line: usize) -> usize {
|
||||||
|
self.buffer_line_to_wrap_row_range(line).len()
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
269
crates/ui/src/input/indent.rs
Normal file
269
crates/ui/src/input/indent.rs
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
use gpui::{Context, EntityInputHandler, SharedString, Window};
|
||||||
|
use ropey::RopeSlice;
|
||||||
|
|
||||||
|
use crate::input::mode::InputMode;
|
||||||
|
use crate::input::{Indent, IndentInline, InputState, Outdent, OutdentInline};
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub struct TabSize {
|
||||||
|
/// Default is 2
|
||||||
|
pub tab_size: usize,
|
||||||
|
/// Set true to use `\t` as tab indent, default is false
|
||||||
|
pub hard_tabs: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TabSize {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
tab_size: 2,
|
||||||
|
hard_tabs: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TabSize {
|
||||||
|
pub(super) fn to_string(self) -> SharedString {
|
||||||
|
if self.hard_tabs {
|
||||||
|
"\t".into()
|
||||||
|
} else {
|
||||||
|
" ".repeat(self.tab_size).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Count the indent size of the line in spaces.
|
||||||
|
pub fn indent_count(&self, line: &RopeSlice) -> usize {
|
||||||
|
let mut count = 0;
|
||||||
|
for ch in line.chars() {
|
||||||
|
match ch {
|
||||||
|
'\t' => count += self.tab_size,
|
||||||
|
' ' => count += 1,
|
||||||
|
_ => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputState {
|
||||||
|
/// Set the tab size for the input.
|
||||||
|
///
|
||||||
|
/// Only for [`InputMode::PlainText`] mode with multi_line.
|
||||||
|
pub fn tab_size(mut self, tab: TabSize) -> Self {
|
||||||
|
debug_assert!(self.mode.is_multi_line());
|
||||||
|
if let InputMode::PlainText { tab: t, .. } = &mut self.mode {
|
||||||
|
*t = tab;
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn indent_inline(
|
||||||
|
&mut self,
|
||||||
|
_: &IndentInline,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.indent(false, window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn indent_block(&mut self, _: &Indent, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.indent(true, window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn outdent_inline(
|
||||||
|
&mut self,
|
||||||
|
_: &OutdentInline,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.outdent(false, window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn outdent_block(
|
||||||
|
&mut self,
|
||||||
|
_: &Outdent,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.outdent(true, window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn indent(&mut self, block: bool, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if !self.mode.is_indentable() {
|
||||||
|
cx.propagate();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let tab_indent = self.mode.tab_size().to_string();
|
||||||
|
let selected_range = self.selected_range;
|
||||||
|
let mut added_len = 0;
|
||||||
|
let is_selected = !self.selected_range.is_empty();
|
||||||
|
|
||||||
|
if is_selected || block {
|
||||||
|
let start_offset = self.start_of_line_of_selection(window, cx);
|
||||||
|
let mut offset = start_offset;
|
||||||
|
|
||||||
|
let selected_text = self
|
||||||
|
.text_for_range(
|
||||||
|
self.range_to_utf16(&(offset..selected_range.end)),
|
||||||
|
&mut None,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.unwrap_or("".into());
|
||||||
|
|
||||||
|
for line in selected_text.split('\n') {
|
||||||
|
self.replace_text_in_range_silent(
|
||||||
|
Some(self.range_to_utf16(&(offset..offset))),
|
||||||
|
&tab_indent,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
added_len += tab_indent.len();
|
||||||
|
// +1 for "\n", the `\r` is included in the `line`.
|
||||||
|
offset += line.len() + tab_indent.len() + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_selected {
|
||||||
|
self.selected_range = (start_offset..selected_range.end + added_len).into();
|
||||||
|
} else {
|
||||||
|
self.selected_range =
|
||||||
|
(selected_range.start + added_len..selected_range.end + added_len).into();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Selected none
|
||||||
|
let offset = self.selected_range.start;
|
||||||
|
self.replace_text_in_range_silent(
|
||||||
|
Some(self.range_to_utf16(&(offset..offset))),
|
||||||
|
&tab_indent,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
added_len = tab_indent.len();
|
||||||
|
|
||||||
|
self.selected_range =
|
||||||
|
(selected_range.start + added_len..selected_range.end + added_len).into();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn outdent(&mut self, block: bool, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if !self.mode.is_indentable() {
|
||||||
|
cx.propagate();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let tab_indent = self.mode.tab_size().to_string();
|
||||||
|
let selected_range = self.selected_range;
|
||||||
|
let mut removed_len = 0;
|
||||||
|
let is_selected = !self.selected_range.is_empty();
|
||||||
|
|
||||||
|
if is_selected || block {
|
||||||
|
let start_offset = self.start_of_line_of_selection(window, cx);
|
||||||
|
let mut offset = start_offset;
|
||||||
|
|
||||||
|
let selected_text = self
|
||||||
|
.text_for_range(
|
||||||
|
self.range_to_utf16(&(offset..selected_range.end)),
|
||||||
|
&mut None,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.unwrap_or("".into());
|
||||||
|
|
||||||
|
for line in selected_text.split('\n') {
|
||||||
|
if line.starts_with(tab_indent.as_ref()) {
|
||||||
|
self.replace_text_in_range_silent(
|
||||||
|
Some(self.range_to_utf16(&(offset..offset + tab_indent.len()))),
|
||||||
|
"",
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
removed_len += tab_indent.len();
|
||||||
|
|
||||||
|
// +1 for "\n"
|
||||||
|
offset += line.len().saturating_sub(tab_indent.len()) + 1;
|
||||||
|
} else {
|
||||||
|
offset += line.len() + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_selected {
|
||||||
|
self.selected_range =
|
||||||
|
(start_offset..selected_range.end.saturating_sub(removed_len)).into();
|
||||||
|
} else {
|
||||||
|
self.selected_range = (selected_range.start.saturating_sub(removed_len)
|
||||||
|
..selected_range.end.saturating_sub(removed_len))
|
||||||
|
.into();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Selected none
|
||||||
|
let start_offset = self.selected_range.start;
|
||||||
|
let offset = self.start_of_line_of_selection(window, cx);
|
||||||
|
let offset = self.offset_from_utf16(self.offset_to_utf16(offset));
|
||||||
|
// FIXME: To improve performance
|
||||||
|
if self
|
||||||
|
.text
|
||||||
|
.slice(offset..self.text.len())
|
||||||
|
.to_string()
|
||||||
|
.starts_with(tab_indent.as_ref())
|
||||||
|
{
|
||||||
|
self.replace_text_in_range_silent(
|
||||||
|
Some(self.range_to_utf16(&(offset..offset + tab_indent.len()))),
|
||||||
|
"",
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
removed_len = tab_indent.len();
|
||||||
|
let new_offset = start_offset.saturating_sub(removed_len);
|
||||||
|
self.selected_range = (new_offset..new_offset).into();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use ropey::RopeSlice;
|
||||||
|
|
||||||
|
use super::TabSize;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tab_size() {
|
||||||
|
let tab = TabSize {
|
||||||
|
tab_size: 2,
|
||||||
|
hard_tabs: false,
|
||||||
|
};
|
||||||
|
assert_eq!(tab.to_string(), " ");
|
||||||
|
let tab = TabSize {
|
||||||
|
tab_size: 4,
|
||||||
|
hard_tabs: false,
|
||||||
|
};
|
||||||
|
assert_eq!(tab.to_string(), " ");
|
||||||
|
|
||||||
|
let tab = TabSize {
|
||||||
|
tab_size: 2,
|
||||||
|
hard_tabs: true,
|
||||||
|
};
|
||||||
|
assert_eq!(tab.to_string(), "\t");
|
||||||
|
let tab = TabSize {
|
||||||
|
tab_size: 4,
|
||||||
|
hard_tabs: true,
|
||||||
|
};
|
||||||
|
assert_eq!(tab.to_string(), "\t");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tab_size_indent_count() {
|
||||||
|
let tab = TabSize {
|
||||||
|
tab_size: 4,
|
||||||
|
hard_tabs: false,
|
||||||
|
};
|
||||||
|
assert_eq!(tab.indent_count(&RopeSlice::from("abc")), 0);
|
||||||
|
assert_eq!(tab.indent_count(&RopeSlice::from(" abc")), 2);
|
||||||
|
assert_eq!(tab.indent_count(&RopeSlice::from(" abc")), 4);
|
||||||
|
assert_eq!(tab.indent_count(&RopeSlice::from("\tabc")), 4);
|
||||||
|
assert_eq!(tab.indent_count(&RopeSlice::from(" \tabc")), 6);
|
||||||
|
assert_eq!(tab.indent_count(&RopeSlice::from(" \t abc ")), 6);
|
||||||
|
assert_eq!(tab.indent_count(&RopeSlice::from("abc")), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,30 @@
|
|||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, relative, AnyElement, App, DefiniteLength, Entity, InteractiveElement as _,
|
AnyElement, App, DefiniteLength, Edges, EdgesRefinement, Entity, Hsla, InteractiveElement as _,
|
||||||
IntoElement, MouseButton, ParentElement as _, Rems, RenderOnce, StyleRefinement, Styled,
|
IntoElement, MouseButton, ParentElement as _, Rems, RenderOnce, StyleRefinement, Styled,
|
||||||
Window,
|
TextAlign, Window, div, px, relative,
|
||||||
};
|
};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
use super::clear_button::clear_button;
|
use super::InputState;
|
||||||
use super::state::{InputState, CONTEXT};
|
use super::element::EditorScrollbar;
|
||||||
use crate::button::{Button, ButtonVariants};
|
use crate::button::{Button, ButtonVariants as _};
|
||||||
use crate::indicator::Indicator;
|
use crate::indicator::Indicator;
|
||||||
use crate::{h_flex, IconName, Sizable, Size, StyleSized, StyledExt};
|
use crate::input::clear_button;
|
||||||
|
use crate::{IconName, Selectable, Sizable, Size, StyleSized, StyledExt, h_flex, v_flex};
|
||||||
|
|
||||||
|
/// Returns `(background, foreground)` colors for input-like components.
|
||||||
|
pub(crate) fn input_style(disabled: bool, cx: &App) -> (Hsla, Hsla) {
|
||||||
|
if disabled {
|
||||||
|
(cx.theme().surface_background, cx.theme().text_muted)
|
||||||
|
} else {
|
||||||
|
(cx.theme().elevated_surface_background, cx.theme().text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A text input element bind to an [`InputState`].
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct TextInput {
|
pub struct Input {
|
||||||
state: Entity<InputState>,
|
state: Entity<InputState>,
|
||||||
style: StyleRefinement,
|
style: StyleRefinement,
|
||||||
size: Size,
|
size: Size,
|
||||||
@@ -26,17 +37,30 @@ pub struct TextInput {
|
|||||||
disabled: bool,
|
disabled: bool,
|
||||||
bordered: bool,
|
bordered: bool,
|
||||||
focus_bordered: bool,
|
focus_bordered: bool,
|
||||||
|
tab_index: isize,
|
||||||
|
selected: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Sizable for TextInput {
|
impl Sizable for Input {
|
||||||
fn with_size(mut self, size: impl Into<Size>) -> Self {
|
fn with_size(mut self, size: impl Into<Size>) -> Self {
|
||||||
self.size = size.into();
|
self.size = size.into();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TextInput {
|
impl Selectable for Input {
|
||||||
/// Create a new [`TextInput`] element bind to the [`InputState`].
|
fn selected(mut self, selected: bool) -> Self {
|
||||||
|
self.selected = selected;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_selected(&self) -> bool {
|
||||||
|
self.selected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Input {
|
||||||
|
/// Create a new [`Input`] element bind to the [`InputState`].
|
||||||
pub fn new(state: &Entity<InputState>) -> Self {
|
pub fn new(state: &Entity<InputState>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
state: state.clone(),
|
state: state.clone(),
|
||||||
@@ -51,6 +75,8 @@ impl TextInput {
|
|||||||
disabled: false,
|
disabled: false,
|
||||||
bordered: true,
|
bordered: true,
|
||||||
focus_bordered: true,
|
focus_bordered: true,
|
||||||
|
tab_index: 0,
|
||||||
|
selected: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,9 +120,9 @@ impl TextInput {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set true to show the clear button when the input field is not empty.
|
/// Set whether to show the clear button when the input field is not empty, default is false.
|
||||||
pub fn cleanable(mut self) -> Self {
|
pub fn cleanable(mut self, cleanable: bool) -> Self {
|
||||||
self.cleanable = true;
|
self.cleanable = cleanable;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,79 +138,117 @@ impl TextInput {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_toggle_mask_button(state: Entity<InputState>) -> impl IntoElement {
|
/// Set the tab index for the input, default is 0.
|
||||||
|
pub fn tab_index(mut self, index: isize) -> Self {
|
||||||
|
self.tab_index = index;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_toggle_mask_button(state: &Entity<InputState>, cx: &App) -> impl IntoElement {
|
||||||
|
let _masked = state.read(cx).masked;
|
||||||
Button::new("toggle-mask")
|
Button::new("toggle-mask")
|
||||||
.icon(IconName::Eye)
|
.icon(IconName::Eye)
|
||||||
.xsmall()
|
.xsmall()
|
||||||
.ghost()
|
.ghost()
|
||||||
.on_mouse_down(MouseButton::Left, {
|
.tab_stop(false)
|
||||||
|
.on_click({
|
||||||
let state = state.clone();
|
let state = state.clone();
|
||||||
move |_, window, cx| {
|
move |_, window, cx| {
|
||||||
state.update(cx, |state, cx| {
|
state.update(cx, |state, cx| {
|
||||||
state.set_masked(false, window, cx);
|
state.set_masked(!state.masked, window, cx);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on_mouse_up(MouseButton::Left, {
|
|
||||||
let state = state.clone();
|
|
||||||
move |_, window, cx| {
|
|
||||||
state.update(cx, |state, cx| {
|
|
||||||
state.set_masked(true, window, cx);
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
/// This method must after the refine_style.
|
||||||
|
fn render_editor(
|
||||||
|
paddings: EdgesRefinement<DefiniteLength>,
|
||||||
|
input_state: &Entity<InputState>,
|
||||||
|
state: &InputState,
|
||||||
|
window: &Window,
|
||||||
|
) -> impl IntoElement {
|
||||||
|
let base_size = window.text_style().font_size;
|
||||||
|
let rem_size = window.rem_size();
|
||||||
|
|
||||||
|
let paddings = Edges {
|
||||||
|
left: paddings
|
||||||
|
.left
|
||||||
|
.map(|v| v.to_pixels(base_size, rem_size))
|
||||||
|
.unwrap_or(px(0.)),
|
||||||
|
right: paddings
|
||||||
|
.right
|
||||||
|
.map(|v| v.to_pixels(base_size, rem_size))
|
||||||
|
.unwrap_or(px(0.)),
|
||||||
|
top: paddings
|
||||||
|
.top
|
||||||
|
.map(|v| v.to_pixels(base_size, rem_size))
|
||||||
|
.unwrap_or(px(0.)),
|
||||||
|
bottom: paddings
|
||||||
|
.bottom
|
||||||
|
.map(|v| v.to_pixels(base_size, rem_size))
|
||||||
|
.unwrap_or(px(0.)),
|
||||||
|
};
|
||||||
|
|
||||||
|
state.editor_scrollbar_paddings.set(paddings);
|
||||||
|
state.editor_scrollbar_snapshot.set(None);
|
||||||
|
|
||||||
|
v_flex().size_full().child(
|
||||||
|
div()
|
||||||
|
.relative()
|
||||||
|
.flex_1()
|
||||||
|
.child(input_state.clone())
|
||||||
|
.child(EditorScrollbar::new(input_state.clone())),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Styled for TextInput {
|
impl Styled for Input {
|
||||||
fn style(&mut self) -> &mut StyleRefinement {
|
fn style(&mut self) -> &mut StyleRefinement {
|
||||||
&mut self.style
|
&mut self.style
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for TextInput {
|
impl RenderOnce for Input {
|
||||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
const LINE_HEIGHT: Rems = Rems(1.25);
|
const LINE_HEIGHT: Rems = Rems(1.25);
|
||||||
|
let text_align = self.style.text.text_align.unwrap_or(TextAlign::Left);
|
||||||
|
|
||||||
let font = window.text_style().font();
|
self.state.update(cx, |state, _| {
|
||||||
let font_size = window.text_style().font_size.to_pixels(window.rem_size());
|
|
||||||
|
|
||||||
self.state.update(cx, |state, cx| {
|
|
||||||
state.text_wrapper.set_font(font, font_size, cx);
|
|
||||||
state.text_wrapper.prepare_if_need(&state.text, cx);
|
|
||||||
state.disabled = self.disabled;
|
state.disabled = self.disabled;
|
||||||
|
state.size = self.size;
|
||||||
|
// Only for single line mode
|
||||||
|
if state.mode.is_single_line() {
|
||||||
|
state.text_align = text_align;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let state = self.state.read(cx);
|
let state = self.state.read(cx);
|
||||||
let focused = state.focus_handle.is_focused(window) && !state.disabled;
|
let _focused = state.focus_handle.is_focused(window) && !state.disabled;
|
||||||
|
|
||||||
let gap_x = match self.size {
|
let gap_x = match self.size {
|
||||||
Size::Small => px(4.),
|
Size::Small => px(4.),
|
||||||
Size::Large => px(8.),
|
Size::Large => px(8.),
|
||||||
_ => px(4.),
|
_ => px(6.),
|
||||||
};
|
};
|
||||||
|
|
||||||
let bg = if state.disabled {
|
let (bg, _) = input_style(state.disabled, cx);
|
||||||
cx.theme().surface_background
|
|
||||||
} else {
|
|
||||||
cx.theme().elevated_surface_background
|
|
||||||
};
|
|
||||||
|
|
||||||
let prefix = self.prefix;
|
let prefix = self.prefix;
|
||||||
let suffix = self.suffix;
|
let suffix = self.suffix;
|
||||||
|
|
||||||
let show_clear_button = self.cleanable
|
let show_clear_button = self.cleanable
|
||||||
|
&& !state.disabled
|
||||||
&& !state.loading
|
&& !state.loading
|
||||||
&& !state.text.is_empty()
|
&& state.text.len() > 0
|
||||||
&& state.mode.is_single_line();
|
&& state.mode.is_single_line();
|
||||||
|
|
||||||
let has_suffix = suffix.is_some() || state.loading || self.mask_toggle || show_clear_button;
|
let has_suffix = suffix.is_some() || state.loading || self.mask_toggle || show_clear_button;
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.id(("input", self.state.entity_id()))
|
.id(("input", self.state.entity_id()))
|
||||||
.flex()
|
.flex()
|
||||||
.key_context(CONTEXT)
|
.key_context(crate::input::CONTEXT)
|
||||||
.track_focus(&state.focus_handle)
|
.track_focus(&state.focus_handle.clone())
|
||||||
|
.tab_index(self.tab_index)
|
||||||
.when(!state.disabled, |this| {
|
.when(!state.disabled, |this| {
|
||||||
this.on_action(window.listener_for(&self.state, InputState::backspace))
|
this.on_action(window.listener_for(&self.state, InputState::backspace))
|
||||||
.on_action(window.listener_for(&self.state, InputState::delete))
|
.on_action(window.listener_for(&self.state, InputState::delete))
|
||||||
@@ -205,9 +269,6 @@ impl RenderOnce for TextInput {
|
|||||||
.on_action(window.listener_for(&self.state, InputState::outdent_inline))
|
.on_action(window.listener_for(&self.state, InputState::outdent_inline))
|
||||||
.on_action(window.listener_for(&self.state, InputState::indent_block))
|
.on_action(window.listener_for(&self.state, InputState::indent_block))
|
||||||
.on_action(window.listener_for(&self.state, InputState::outdent_block))
|
.on_action(window.listener_for(&self.state, InputState::outdent_block))
|
||||||
.on_action(
|
|
||||||
window.listener_for(&self.state, InputState::shift_to_new_line),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.on_action(window.listener_for(&self.state, InputState::left))
|
.on_action(window.listener_for(&self.state, InputState::left))
|
||||||
@@ -260,8 +321,8 @@ impl RenderOnce for TextInput {
|
|||||||
.input_px(self.size)
|
.input_px(self.size)
|
||||||
.input_py(self.size)
|
.input_py(self.size)
|
||||||
.input_h(self.size)
|
.input_h(self.size)
|
||||||
.cursor_text()
|
.input_font_size(self.size)
|
||||||
.text_size(font_size)
|
.when(!self.disabled, |this| this.cursor_text())
|
||||||
.items_center()
|
.items_center()
|
||||||
.when(state.mode.is_multi_line(), |this| {
|
.when(state.mode.is_multi_line(), |this| {
|
||||||
this.h_auto()
|
this.h_auto()
|
||||||
@@ -269,33 +330,34 @@ impl RenderOnce for TextInput {
|
|||||||
})
|
})
|
||||||
.when(self.appearance, |this| {
|
.when(self.appearance, |this| {
|
||||||
this.bg(bg)
|
this.bg(bg)
|
||||||
|
.when(self.disabled, |this| this.opacity(0.5))
|
||||||
.rounded(cx.theme().radius)
|
.rounded(cx.theme().radius)
|
||||||
.when(self.bordered, |this| {
|
.when(self.bordered, |this| {
|
||||||
this.border_color(cx.theme().border)
|
this.border_color(cx.theme().border)
|
||||||
.border_1()
|
.border_1()
|
||||||
.when(cx.theme().shadow, |this| this.shadow_xs())
|
.when(cx.theme().shadow, |this| this.shadow_xs())
|
||||||
.when(focused && self.focus_bordered, |this| {
|
|
||||||
this.border_color(cx.theme().border_focused)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.items_center()
|
.items_center()
|
||||||
.gap(gap_x)
|
.gap(gap_x)
|
||||||
.refine_style(&self.style)
|
.refine_style(&self.style)
|
||||||
.children(prefix)
|
.children(prefix)
|
||||||
.child(self.state.clone())
|
.when(state.mode.is_multi_line(), |mut this| {
|
||||||
|
let paddings = this.style().padding.clone();
|
||||||
|
this.child(Self::render_editor(paddings, &self.state, state, window))
|
||||||
|
})
|
||||||
|
.when(!state.mode.is_multi_line(), |this| {
|
||||||
|
this.child(self.state.clone())
|
||||||
|
})
|
||||||
.when(has_suffix, |this| {
|
.when(has_suffix, |this| {
|
||||||
this.pr_2().child(
|
this.pr_2().child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.id("suffix")
|
.id("suffix")
|
||||||
.gap(gap_x)
|
.gap(gap_x)
|
||||||
.when(self.appearance, |this| this.bg(bg))
|
|
||||||
.items_center()
|
.items_center()
|
||||||
.when(state.loading, |this| {
|
.when(state.loading, |this| this.child(Indicator::new()))
|
||||||
this.child(Indicator::new().color(cx.theme().text_muted))
|
|
||||||
})
|
|
||||||
.when(self.mask_toggle, |this| {
|
.when(self.mask_toggle, |this| {
|
||||||
this.child(Self::render_toggle_mask_button(self.state.clone()))
|
this.child(Self::render_toggle_mask_button(&self.state, cx))
|
||||||
})
|
})
|
||||||
.when(show_clear_button, |this| {
|
.when(show_clear_button, |this| {
|
||||||
this.child(clear_button(cx).on_click({
|
this.child(clear_button(cx).on_click({
|
||||||
@@ -303,6 +365,7 @@ impl RenderOnce for TextInput {
|
|||||||
move |_, window, cx| {
|
move |_, window, cx| {
|
||||||
state.update(cx, |state, cx| {
|
state.update(cx, |state, cx| {
|
||||||
state.clean(window, cx);
|
state.clean(window, cx);
|
||||||
|
state.focus(window, cx);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
@@ -225,14 +225,13 @@ impl MaskPattern {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// check if the fraction part is valid
|
// check if the fraction part is valid
|
||||||
if let Some(frac) = frac_part {
|
if let Some(frac) = frac_part
|
||||||
if !frac
|
&& !frac
|
||||||
.chars()
|
.chars()
|
||||||
.all(|ch| ch.is_ascii_digit() || Some(ch) == *separator)
|
.all(|ch| ch.is_ascii_digit() || Some(ch) == *separator)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -255,13 +254,13 @@ impl MaskPattern {
|
|||||||
|
|
||||||
if token.is_sep() {
|
if token.is_sep() {
|
||||||
// If next token is match, it's valid
|
// If next token is match, it's valid
|
||||||
if let Some(next_token) = tokens.get(pos + 1) {
|
if let Some(next_token) = tokens.get(pos + 1)
|
||||||
if next_token.is_match(ch) {
|
&& next_token.is_match(ch)
|
||||||
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
@@ -320,14 +319,14 @@ impl MaskPattern {
|
|||||||
if fraction == &Some(0) {
|
if fraction == &Some(0) {
|
||||||
int_with_sep
|
int_with_sep
|
||||||
} else {
|
} else {
|
||||||
format!("{int_with_sep}.{frac}")
|
format!("{}.{}", int_with_sep, frac)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
int_with_sep
|
int_with_sep
|
||||||
};
|
};
|
||||||
|
|
||||||
let final_str = if let Some(sign) = maybe_signed {
|
let final_str = if let Some(sign) = maybe_signed {
|
||||||
format!("{sign}{final_str}")
|
format!("{}{}", sign, final_str)
|
||||||
} else {
|
} else {
|
||||||
final_str
|
final_str
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,15 +1,27 @@
|
|||||||
|
pub(super) const MASK_CHAR: char = '*';
|
||||||
|
|
||||||
mod blink_cursor;
|
mod blink_cursor;
|
||||||
mod change;
|
mod change;
|
||||||
|
mod clear_button;
|
||||||
mod cursor;
|
mod cursor;
|
||||||
|
mod display_map;
|
||||||
mod element;
|
mod element;
|
||||||
|
mod indent;
|
||||||
|
#[allow(clippy::module_inception)]
|
||||||
|
mod input;
|
||||||
mod mask_pattern;
|
mod mask_pattern;
|
||||||
mod mode;
|
mod mode;
|
||||||
|
mod movement;
|
||||||
mod rope_ext;
|
mod rope_ext;
|
||||||
|
mod selection;
|
||||||
mod state;
|
mod state;
|
||||||
mod text_input;
|
|
||||||
mod text_wrapper;
|
|
||||||
|
|
||||||
pub(crate) mod clear_button;
|
|
||||||
|
|
||||||
|
pub(crate) use clear_button::*;
|
||||||
|
pub use cursor::*;
|
||||||
|
pub use display_map::DisplayMap;
|
||||||
|
pub use indent::TabSize;
|
||||||
|
pub use input::*;
|
||||||
|
pub use mask_pattern::MaskPattern;
|
||||||
|
pub use rope_ext::{InputEdit, Point, RopeExt, RopeLines};
|
||||||
|
pub use ropey::Rope;
|
||||||
pub use state::*;
|
pub use state::*;
|
||||||
pub use text_input::*;
|
|
||||||
|
|||||||
@@ -1,42 +1,14 @@
|
|||||||
use gpui::SharedString;
|
use super::display_map::DisplayMap;
|
||||||
|
|
||||||
use super::text_wrapper::TextWrapper;
|
#[derive(Clone)]
|
||||||
|
pub(crate) enum InputMode {
|
||||||
#[derive(Debug, Copy, Clone)]
|
/// A plain text input mode.
|
||||||
pub struct TabSize {
|
PlainText {
|
||||||
/// Default is 2
|
multi_line: bool,
|
||||||
pub tab_size: usize,
|
tab: crate::input::indent::TabSize,
|
||||||
/// Set true to use `\t` as tab indent, default is false
|
|
||||||
pub hard_tabs: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for TabSize {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
tab_size: 2,
|
|
||||||
hard_tabs: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TabSize {
|
|
||||||
pub(super) fn to_string(self) -> SharedString {
|
|
||||||
if self.hard_tabs {
|
|
||||||
"\t".into()
|
|
||||||
} else {
|
|
||||||
" ".repeat(self.tab_size).into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
|
||||||
pub enum InputMode {
|
|
||||||
#[default]
|
|
||||||
SingleLine,
|
|
||||||
MultiLine {
|
|
||||||
tab: TabSize,
|
|
||||||
rows: usize,
|
rows: usize,
|
||||||
},
|
},
|
||||||
|
/// An auto grow input mode.
|
||||||
AutoGrow {
|
AutoGrow {
|
||||||
rows: usize,
|
rows: usize,
|
||||||
min_rows: usize,
|
min_rows: usize,
|
||||||
@@ -44,11 +16,43 @@ pub enum InputMode {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for InputMode {
|
||||||
|
fn default() -> Self {
|
||||||
|
InputMode::plain_text()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
impl InputMode {
|
impl InputMode {
|
||||||
|
/// Create a plain input mode with default settings.
|
||||||
|
pub(super) fn plain_text() -> Self {
|
||||||
|
InputMode::PlainText {
|
||||||
|
multi_line: false,
|
||||||
|
tab: crate::input::indent::TabSize::default(),
|
||||||
|
rows: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an auto grow input mode with given min and max rows.
|
||||||
|
pub(super) fn auto_grow(min_rows: usize, max_rows: usize) -> Self {
|
||||||
|
InputMode::AutoGrow {
|
||||||
|
rows: min_rows,
|
||||||
|
min_rows,
|
||||||
|
max_rows,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn multi_line(mut self, multi_line: bool) -> Self {
|
||||||
|
match &mut self {
|
||||||
|
InputMode::PlainText { multi_line: ml, .. } => *ml = multi_line,
|
||||||
|
InputMode::AutoGrow { .. } => {}
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(super) fn is_single_line(&self) -> bool {
|
pub(super) fn is_single_line(&self) -> bool {
|
||||||
matches!(self, InputMode::SingleLine)
|
!self.is_multi_line()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -58,15 +62,15 @@ impl InputMode {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(super) fn is_multi_line(&self) -> bool {
|
pub(super) fn is_multi_line(&self) -> bool {
|
||||||
matches!(
|
match self {
|
||||||
self,
|
InputMode::PlainText { multi_line, .. } => *multi_line,
|
||||||
InputMode::MultiLine { .. } | InputMode::AutoGrow { .. }
|
InputMode::AutoGrow { max_rows, .. } => *max_rows > 1,
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn set_rows(&mut self, new_rows: usize) {
|
pub(super) fn set_rows(&mut self, new_rows: usize) {
|
||||||
match self {
|
match self {
|
||||||
InputMode::MultiLine { rows, .. } => {
|
InputMode::PlainText { rows, .. } => {
|
||||||
*rows = new_rows;
|
*rows = new_rows;
|
||||||
}
|
}
|
||||||
InputMode::AutoGrow {
|
InputMode::AutoGrow {
|
||||||
@@ -76,25 +80,27 @@ impl InputMode {
|
|||||||
} => {
|
} => {
|
||||||
*rows = new_rows.clamp(*min_rows, *max_rows);
|
*rows = new_rows.clamp(*min_rows, *max_rows);
|
||||||
}
|
}
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn update_auto_grow(&mut self, text_wrapper: &TextWrapper) {
|
pub(super) fn update_auto_grow(&mut self, display_map: &DisplayMap) {
|
||||||
if self.is_single_line() {
|
if self.is_single_line() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let wrapped_lines = text_wrapper.len();
|
let wrapped_lines = display_map.wrap_row_count();
|
||||||
self.set_rows(wrapped_lines);
|
self.set_rows(wrapped_lines);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// At least 1 row be return.
|
/// At least 1 row be return.
|
||||||
pub(super) fn rows(&self) -> usize {
|
pub(super) fn rows(&self) -> usize {
|
||||||
|
if !self.is_multi_line() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
InputMode::MultiLine { rows, .. } => *rows,
|
InputMode::PlainText { rows, .. } => *rows,
|
||||||
InputMode::AutoGrow { rows, .. } => *rows,
|
InputMode::AutoGrow { rows, .. } => *rows,
|
||||||
_ => 1,
|
|
||||||
}
|
}
|
||||||
.max(1)
|
.max(1)
|
||||||
}
|
}
|
||||||
@@ -103,7 +109,6 @@ impl InputMode {
|
|||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
pub(super) fn min_rows(&self) -> usize {
|
pub(super) fn min_rows(&self) -> usize {
|
||||||
match self {
|
match self {
|
||||||
InputMode::MultiLine { .. } => 1,
|
|
||||||
InputMode::AutoGrow { min_rows, .. } => *min_rows,
|
InputMode::AutoGrow { min_rows, .. } => *min_rows,
|
||||||
_ => 1,
|
_ => 1,
|
||||||
}
|
}
|
||||||
@@ -112,18 +117,29 @@ impl InputMode {
|
|||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
pub(super) fn max_rows(&self) -> usize {
|
pub(super) fn max_rows(&self) -> usize {
|
||||||
|
if !self.is_multi_line() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
InputMode::MultiLine { .. } => usize::MAX,
|
|
||||||
InputMode::AutoGrow { max_rows, .. } => *max_rows,
|
InputMode::AutoGrow { max_rows, .. } => *max_rows,
|
||||||
_ => 1,
|
_ => usize::MAX,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(super) fn tab_size(&self) -> Option<&TabSize> {
|
pub(super) fn is_indentable(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
InputMode::MultiLine { tab, .. } => Some(tab),
|
InputMode::PlainText { multi_line, .. } => *multi_line,
|
||||||
_ => None,
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(super) fn tab_size(&self) -> crate::input::indent::TabSize {
|
||||||
|
match self {
|
||||||
|
InputMode::PlainText { tab, .. } => *tab,
|
||||||
|
_ => crate::input::indent::TabSize::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
264
crates/ui/src/input/movement.rs
Normal file
264
crates/ui/src/input/movement.rs
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
use gpui::{Context, Point, Window};
|
||||||
|
|
||||||
|
use crate::input::{
|
||||||
|
InputState, MoveDown, MoveEnd, MoveHome, MoveLeft, MovePageDown, MovePageUp, MoveRight,
|
||||||
|
MoveToEnd, MoveToNextWord, MoveToPreviousWord, MoveToStart, MoveUp, RopeExt as _,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub(crate) enum MoveDirection {
|
||||||
|
Up,
|
||||||
|
Down,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputState {
|
||||||
|
/// Called after moving the cursor. Updates preferred_column if we know where the cursor now is.
|
||||||
|
pub(super) fn update_preferred_column(&mut self) {
|
||||||
|
let Some(last_layout) = &self.last_layout else {
|
||||||
|
self.preferred_column = None;
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let point = self.text.offset_to_point(self.cursor());
|
||||||
|
let Some(line) = last_layout.line(point.row) else {
|
||||||
|
self.preferred_column = None;
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(pos) = line.position_for_index(point.column, last_layout, false) else {
|
||||||
|
self.preferred_column = None;
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.preferred_column = Some((pos.x, point.column));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move the cursor to the given offset.
|
||||||
|
///
|
||||||
|
/// The offset is the UTF-8 offset.
|
||||||
|
///
|
||||||
|
/// Ensure the offset use self.next_boundary or self.previous_boundary to get the correct offset.
|
||||||
|
pub(crate) fn move_to(
|
||||||
|
&mut self,
|
||||||
|
offset: usize,
|
||||||
|
direction: Option<MoveDirection>,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let offset = offset.clamp(0, self.text.len());
|
||||||
|
self.cursor_line_end_affinity = false;
|
||||||
|
self.selected_range = (offset..offset).into();
|
||||||
|
self.scroll_to(offset, direction, cx);
|
||||||
|
self.pause_blink_cursor(cx);
|
||||||
|
self.update_preferred_column();
|
||||||
|
cx.notify()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move the cursor vertically by one line (up or down) while preserving the column if possible.
|
||||||
|
///
|
||||||
|
/// move_lines: Number of lines to move vertically (positive for down, negative for up).
|
||||||
|
pub(super) fn move_vertical(
|
||||||
|
&mut self,
|
||||||
|
move_lines: isize,
|
||||||
|
_: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
if self.mode.is_single_line() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(last_layout) = &self.last_layout else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let offset = self.cursor();
|
||||||
|
let was_preferred_column = self.preferred_column;
|
||||||
|
|
||||||
|
let mut display_point = self.display_map.offset_to_wrap_display_point(offset);
|
||||||
|
|
||||||
|
// Convert wrap row → display row (skips folded rows), move, then convert back
|
||||||
|
let current_display_row = self
|
||||||
|
.display_map
|
||||||
|
.wrap_row_to_display_row(display_point.row)
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
self.display_map
|
||||||
|
.nearest_visible_display_row(display_point.row)
|
||||||
|
});
|
||||||
|
let max_display_row = self.display_map.display_row_count().saturating_sub(1);
|
||||||
|
let target_display_row = current_display_row
|
||||||
|
.saturating_add_signed(move_lines)
|
||||||
|
.min(max_display_row);
|
||||||
|
let target_wrap_row = self
|
||||||
|
.display_map
|
||||||
|
.display_row_to_wrap_row(target_display_row)
|
||||||
|
.unwrap_or(display_point.row);
|
||||||
|
|
||||||
|
display_point.row = target_wrap_row;
|
||||||
|
display_point.column = 0;
|
||||||
|
let mut new_offset = self.display_map.wrap_display_point_to_offset(display_point);
|
||||||
|
|
||||||
|
if let Some((preferred_x, column)) = was_preferred_column {
|
||||||
|
// Get display point again to update local_row.
|
||||||
|
let mut next_display_point = self.display_map.offset_to_wrap_display_point(new_offset);
|
||||||
|
next_display_point.column = 0;
|
||||||
|
let next_point = self
|
||||||
|
.display_map
|
||||||
|
.wrap_display_point_to_point(next_display_point);
|
||||||
|
let line_start_offset = self.text.line_start_offset(next_point.row);
|
||||||
|
|
||||||
|
// If in visible range, prefer to use position to get column.
|
||||||
|
if let Some(line) = last_layout.line(next_point.row) {
|
||||||
|
if let Some(x) = line.closest_index_for_position(
|
||||||
|
Point {
|
||||||
|
x: preferred_x,
|
||||||
|
y: next_display_point.local_row * last_layout.line_height,
|
||||||
|
},
|
||||||
|
last_layout,
|
||||||
|
) {
|
||||||
|
new_offset = line_start_offset + x;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Not in visible range, use column directly.
|
||||||
|
let max_line_len = self.text.slice_line(next_point.row).len();
|
||||||
|
new_offset = line_start_offset + column.min(max_line_len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.pause_blink_cursor(cx);
|
||||||
|
let direction = if move_lines < 0 {
|
||||||
|
MoveDirection::Up
|
||||||
|
} else {
|
||||||
|
MoveDirection::Down
|
||||||
|
};
|
||||||
|
self.move_to(new_offset, Some(direction), cx);
|
||||||
|
// Set back the preferred_column
|
||||||
|
self.preferred_column = was_preferred_column;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn left(&mut self, _: &MoveLeft, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.pause_blink_cursor(cx);
|
||||||
|
if self.selected_range.is_empty() {
|
||||||
|
self.move_to(self.previous_boundary(self.cursor()), None, cx);
|
||||||
|
} else {
|
||||||
|
self.move_to(self.selected_range.start, None, cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn right(&mut self, _: &MoveRight, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.pause_blink_cursor(cx);
|
||||||
|
if self.selected_range.is_empty() {
|
||||||
|
self.move_to(self.next_boundary(self.selected_range.end), None, cx);
|
||||||
|
} else {
|
||||||
|
self.move_to(self.selected_range.end, None, cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn up(&mut self, _action: &MoveUp, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if self.mode.is_single_line() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.selected_range.is_empty() {
|
||||||
|
self.move_to(
|
||||||
|
self.previous_boundary(self.selected_range.start.saturating_sub(1)),
|
||||||
|
Some(MoveDirection::Up),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
self.pause_blink_cursor(cx);
|
||||||
|
self.move_vertical(-1, window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn down(&mut self, _action: &MoveDown, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if self.mode.is_single_line() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.selected_range.is_empty() {
|
||||||
|
self.move_to(
|
||||||
|
self.next_boundary(self.selected_range.end.saturating_sub(1)),
|
||||||
|
Some(MoveDirection::Down),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.pause_blink_cursor(cx);
|
||||||
|
self.move_vertical(1, window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn page_up(&mut self, _: &MovePageUp, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if self.mode.is_single_line() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(last_layout) = &self.last_layout else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let display_lines = (self.input_bounds.size.height / last_layout.line_height) as isize;
|
||||||
|
self.move_vertical(-display_lines, window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn page_down(
|
||||||
|
&mut self,
|
||||||
|
_: &MovePageDown,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
if self.mode.is_single_line() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(last_layout) = &self.last_layout else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let display_lines = (self.input_bounds.size.height / last_layout.line_height) as isize;
|
||||||
|
self.move_vertical(display_lines, window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn home(&mut self, _: &MoveHome, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.pause_blink_cursor(cx);
|
||||||
|
let offset = self.start_of_line();
|
||||||
|
self.move_to(offset, Some(MoveDirection::Up), cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn end(&mut self, _: &MoveEnd, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.pause_blink_cursor(cx);
|
||||||
|
let offset = self.end_of_line();
|
||||||
|
self.move_to(offset, Some(MoveDirection::Down), cx);
|
||||||
|
self.cursor_line_end_affinity = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn move_to_start(
|
||||||
|
&mut self,
|
||||||
|
_: &MoveToStart,
|
||||||
|
_: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.move_to(0, None, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn move_to_end(&mut self, _: &MoveToEnd, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.move_to(self.text.len(), None, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn move_to_previous_word(
|
||||||
|
&mut self,
|
||||||
|
_: &MoveToPreviousWord,
|
||||||
|
_: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let offset = self.previous_start_of_word();
|
||||||
|
self.move_to(offset, None, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn move_to_next_word(
|
||||||
|
&mut self,
|
||||||
|
_: &MoveToNextWord,
|
||||||
|
_: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let offset = self.next_end_of_word();
|
||||||
|
self.move_to(offset, None, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,70 +1,49 @@
|
|||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
|
|
||||||
use rope::{Point, Rope};
|
use ropey::{LineType, Rope, RopeSlice};
|
||||||
|
use sum_tree::Bias;
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
pub use tree_sitter::{InputEdit, Point};
|
||||||
|
|
||||||
use super::cursor::Position;
|
#[cfg(target_family = "wasm")]
|
||||||
|
/// Stub type for tree-sitter Point on WASM (tree-sitter not available).
|
||||||
/// An extension trait for `Rope` to provide additional utility methods.
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub trait RopeExt {
|
pub struct Point {
|
||||||
/// Get the line at the given row (0-based) index, including the `\r` at the end, but not `\n`.
|
pub row: usize,
|
||||||
///
|
pub column: usize,
|
||||||
/// Return empty rope if the row (0-based) is out of bounds.
|
|
||||||
fn line(&self, row: usize) -> Rope;
|
|
||||||
|
|
||||||
/// Start offset of the line at the given row (0-based) index.
|
|
||||||
fn line_start_offset(&self, row: usize) -> usize;
|
|
||||||
|
|
||||||
/// Line the end offset (including `\n`) of the line at the given row (0-based) index.
|
|
||||||
///
|
|
||||||
/// Return the end of the rope if the row is out of bounds.
|
|
||||||
fn line_end_offset(&self, row: usize) -> usize;
|
|
||||||
|
|
||||||
/// Return the number of lines in the rope.
|
|
||||||
fn lines_len(&self) -> usize;
|
|
||||||
|
|
||||||
/// Return the lines iterator.
|
|
||||||
///
|
|
||||||
/// Each line is including the `\r` at the end, but not `\n`.
|
|
||||||
fn lines(&self) -> RopeLines;
|
|
||||||
|
|
||||||
/// Check is equal to another rope.
|
|
||||||
fn eq(&self, other: &Rope) -> bool;
|
|
||||||
|
|
||||||
/// Total number of characters in the rope.
|
|
||||||
fn chars_count(&self) -> usize;
|
|
||||||
|
|
||||||
/// Get char at the given offset (byte).
|
|
||||||
///
|
|
||||||
/// If the offset is in the middle of a multi-byte character will panic.
|
|
||||||
///
|
|
||||||
/// If the offset is out of bounds, return None.
|
|
||||||
fn char_at(&self, offset: usize) -> Option<char>;
|
|
||||||
|
|
||||||
/// Get the byte offset from the given line, column [`Position`] (0-based).
|
|
||||||
fn position_to_offset(&self, line_col: &Position) -> usize;
|
|
||||||
|
|
||||||
/// Get the line, column [`Position`] (0-based) from the given byte offset.
|
|
||||||
fn offset_to_position(&self, offset: usize) -> Position;
|
|
||||||
|
|
||||||
/// Get the word byte range at the given offset (byte).
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn word_range(&self, offset: usize) -> Option<Range<usize>>;
|
|
||||||
|
|
||||||
/// Get word at the given offset (byte).
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn word_at(&self, offset: usize) -> String;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_family = "wasm")]
|
||||||
|
impl Point {
|
||||||
|
pub fn new(row: usize, column: usize) -> Self {
|
||||||
|
Self { row, column }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_family = "wasm")]
|
||||||
|
/// Stub type for tree-sitter InputEdit on WASM (tree-sitter not available).
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct InputEdit {
|
||||||
|
pub start_byte: usize,
|
||||||
|
pub old_end_byte: usize,
|
||||||
|
pub new_end_byte: usize,
|
||||||
|
pub start_position: Point,
|
||||||
|
pub old_end_position: Point,
|
||||||
|
pub new_end_position: Point,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Position = lsp_types::Position;
|
||||||
|
|
||||||
/// An iterator over the lines of a `Rope`.
|
/// An iterator over the lines of a `Rope`.
|
||||||
pub struct RopeLines {
|
pub struct RopeLines<'a> {
|
||||||
|
rope: &'a Rope,
|
||||||
row: usize,
|
row: usize,
|
||||||
end_row: usize,
|
end_row: usize,
|
||||||
rope: Rope,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RopeLines {
|
impl<'a> RopeLines<'a> {
|
||||||
/// Create a new `RopeLines` iterator.
|
/// Create a new `RopeLines` iterator.
|
||||||
pub fn new(rope: Rope) -> Self {
|
pub fn new(rope: &'a Rope) -> Self {
|
||||||
let end_row = rope.lines_len();
|
let end_row = rope.lines_len();
|
||||||
Self {
|
Self {
|
||||||
row: 0,
|
row: 0,
|
||||||
@@ -73,9 +52,8 @@ impl RopeLines {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
impl<'a> Iterator for RopeLines<'a> {
|
||||||
impl Iterator for RopeLines {
|
type Item = RopeSlice<'a>;
|
||||||
type Item = Rope;
|
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
@@ -83,7 +61,7 @@ impl Iterator for RopeLines {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let line = self.rope.line(self.row);
|
let line = self.rope.slice_line(self.row);
|
||||||
self.row += 1;
|
self.row += 1;
|
||||||
Some(line)
|
Some(line)
|
||||||
}
|
}
|
||||||
@@ -101,23 +79,261 @@ impl Iterator for RopeLines {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::iter::ExactSizeIterator for RopeLines {}
|
impl std::iter::ExactSizeIterator for RopeLines<'_> {}
|
||||||
impl std::iter::FusedIterator for RopeLines {}
|
impl std::iter::FusedIterator for RopeLines<'_> {}
|
||||||
|
|
||||||
|
/// An extension trait for [`Rope`] to provide additional utility methods.
|
||||||
|
pub trait RopeExt {
|
||||||
|
/// Start offset of the line at the given row (0-based) index.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use gpui_component::{Rope, RopeExt};
|
||||||
|
///
|
||||||
|
/// let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
|
||||||
|
/// assert_eq!(rope.line_start_offset(0), 0);
|
||||||
|
/// assert_eq!(rope.line_start_offset(1), 6);
|
||||||
|
/// ```
|
||||||
|
fn line_start_offset(&self, row: usize) -> usize;
|
||||||
|
|
||||||
|
/// Line the end offset (including `\n`) of the line at the given row (0-based) index.
|
||||||
|
///
|
||||||
|
/// Return the end of the rope if the row is out of bounds.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use gpui_component::{Rope, RopeExt};
|
||||||
|
/// let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
|
||||||
|
/// assert_eq!(rope.line_end_offset(0), 5); // "Hello\n"
|
||||||
|
/// assert_eq!(rope.line_end_offset(1), 12); // "World\r\n"
|
||||||
|
/// ```
|
||||||
|
fn line_end_offset(&self, row: usize) -> usize;
|
||||||
|
|
||||||
|
/// Return a line slice at the given row (0-based) index. including `\r` if present, but not `\n`.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use gpui_component::{Rope, RopeExt};
|
||||||
|
/// let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
|
||||||
|
/// assert_eq!(rope.slice_line(0).to_string(), "Hello");
|
||||||
|
/// assert_eq!(rope.slice_line(1).to_string(), "World\r");
|
||||||
|
/// assert_eq!(rope.slice_line(2).to_string(), "This is a test 中文");
|
||||||
|
/// assert_eq!(rope.slice_line(6).to_string(), ""); // out of bounds
|
||||||
|
/// ```
|
||||||
|
fn slice_line(&self, row: usize) -> RopeSlice<'_>;
|
||||||
|
|
||||||
|
/// Return a slice of rows in the given range (0-based, end exclusive).
|
||||||
|
///
|
||||||
|
/// If the range is out of bounds, it will be clamped to the valid range.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use gpui_component::{Rope, RopeExt};
|
||||||
|
/// let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
|
||||||
|
/// assert_eq!(rope.slice_lines(0..2).to_string(), "Hello\nWorld\r");
|
||||||
|
/// assert_eq!(rope.slice_lines(1..3).to_string(), "World\r\nThis is a test 中文");
|
||||||
|
/// assert_eq!(rope.slice_lines(2..5).to_string(), "This is a test 中文\nRope");
|
||||||
|
/// assert_eq!(rope.slice_lines(3..10).to_string(), "Rope");
|
||||||
|
/// assert_eq!(rope.slice_lines(5..10).to_string(), ""); // out of bounds
|
||||||
|
/// ```
|
||||||
|
fn slice_lines(&self, rows_range: Range<usize>) -> RopeSlice<'_>;
|
||||||
|
|
||||||
|
/// Return an iterator over all lines in the rope.
|
||||||
|
///
|
||||||
|
/// Each line slice includes `\r` if present, but not `\n`.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use gpui_component::{Rope, RopeExt};
|
||||||
|
/// let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
|
||||||
|
/// let lines: Vec<_> = rope.iter_lines().map(|r| r.to_string()).collect();
|
||||||
|
/// assert_eq!(lines, vec!["Hello", "World\r", "This is a test 中文", "Rope"]);
|
||||||
|
/// ```
|
||||||
|
fn iter_lines(&self) -> RopeLines<'_>;
|
||||||
|
|
||||||
|
/// Return the number of lines in the rope.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use gpui_component::{Rope, RopeExt};
|
||||||
|
/// let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
|
||||||
|
/// assert_eq!(rope.lines_len(), 4);
|
||||||
|
/// ```
|
||||||
|
fn lines_len(&self) -> usize;
|
||||||
|
|
||||||
|
/// Return the length of the row (0-based) in characters, including `\r` if present, but not `\n`.
|
||||||
|
///
|
||||||
|
/// If the row is out of bounds, return 0.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use gpui_component::{Rope, RopeExt};
|
||||||
|
/// let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
|
||||||
|
/// assert_eq!(rope.line_len(0), 5); // "Hello"
|
||||||
|
/// assert_eq!(rope.line_len(1), 6); // "World\r"
|
||||||
|
/// assert_eq!(rope.line_len(2), 21); // "This is a test 中文"
|
||||||
|
/// assert_eq!(rope.line_len(4), 0); // out of bounds
|
||||||
|
/// ```
|
||||||
|
fn line_len(&self, row: usize) -> usize;
|
||||||
|
|
||||||
|
/// Replace the text in the given byte range with new text.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// - If the range is not on char boundary.
|
||||||
|
/// - If the range is out of bounds.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use gpui_component::{Rope, RopeExt};
|
||||||
|
/// let mut rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
|
||||||
|
/// rope.replace(6..11, "Universe");
|
||||||
|
/// assert_eq!(rope.to_string(), "Hello\nUniverse\r\nThis is a test 中文\nRope");
|
||||||
|
/// ```
|
||||||
|
fn replace(&mut self, range: Range<usize>, new_text: &str);
|
||||||
|
|
||||||
|
/// Get char at the given offset (byte).
|
||||||
|
///
|
||||||
|
/// - If the offset is in the middle of a multi-byte character will panic.
|
||||||
|
/// - If the offset is out of bounds, return None.
|
||||||
|
fn char_at(&self, offset: usize) -> Option<char>;
|
||||||
|
|
||||||
|
/// Get the byte offset from the given line, column [`Position`] (0-based).
|
||||||
|
///
|
||||||
|
/// The column is in characters.
|
||||||
|
fn position_to_offset(&self, line_col: &Position) -> usize;
|
||||||
|
|
||||||
|
/// Get the line, column [`Position`] (0-based) from the given byte offset.
|
||||||
|
///
|
||||||
|
/// The column is in characters.
|
||||||
|
fn offset_to_position(&self, offset: usize) -> Position;
|
||||||
|
|
||||||
|
/// Get point (row, column) from the given byte offset.
|
||||||
|
///
|
||||||
|
/// The column is in bytes.
|
||||||
|
fn offset_to_point(&self, offset: usize) -> Point;
|
||||||
|
|
||||||
|
/// Get byte offset from the given point (row, column).
|
||||||
|
///
|
||||||
|
/// The column is 0-based in bytes.
|
||||||
|
fn point_to_offset(&self, point: Point) -> usize;
|
||||||
|
|
||||||
|
/// Get the word byte range at the given byte offset (0-based).
|
||||||
|
fn word_range(&self, offset: usize) -> Option<Range<usize>>;
|
||||||
|
|
||||||
|
/// Get word at the given byte offset (0-based).
|
||||||
|
fn word_at(&self, offset: usize) -> String;
|
||||||
|
|
||||||
|
/// Convert offset in UTF-16 to byte offset (0-based).
|
||||||
|
///
|
||||||
|
/// Runs in O(log N) time.
|
||||||
|
fn offset_utf16_to_offset(&self, offset_utf16: usize) -> usize;
|
||||||
|
|
||||||
|
/// Convert byte offset (0-based) to offset in UTF-16.
|
||||||
|
///
|
||||||
|
/// Runs in O(log N) time.
|
||||||
|
fn offset_to_offset_utf16(&self, offset: usize) -> usize;
|
||||||
|
|
||||||
|
/// Get a clipped offset (avoid in a char boundary).
|
||||||
|
///
|
||||||
|
/// - If Bias::Left and inside the char boundary, return the ix - 1;
|
||||||
|
/// - If Bias::Right and in inside char boundary, return the ix + 1;
|
||||||
|
/// - Otherwise return the ix.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use gpui_component::{Rope, RopeExt};
|
||||||
|
/// use sum_tree::Bias;
|
||||||
|
///
|
||||||
|
/// let rope = Rope::from("Hello 中文🎉 test\nRope");
|
||||||
|
/// assert_eq!(rope.clip_offset(5, Bias::Left), 5);
|
||||||
|
/// // Inside multi-byte character '中' (3 bytes)
|
||||||
|
/// assert_eq!(rope.clip_offset(7, Bias::Left), 6);
|
||||||
|
/// assert_eq!(rope.clip_offset(7, Bias::Right), 9);
|
||||||
|
/// ```
|
||||||
|
fn clip_offset(&self, offset: usize, bias: Bias) -> usize;
|
||||||
|
|
||||||
|
/// Convert offset in characters to byte offset (0-based).
|
||||||
|
///
|
||||||
|
/// Run in O(n) time.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use gpui_component::{Rope, RopeExt};
|
||||||
|
/// let rope = Rope::from("a 中文🎉 test\nRope");
|
||||||
|
/// assert_eq!(rope.char_index_to_offset(0), 0);
|
||||||
|
/// assert_eq!(rope.char_index_to_offset(1), 1);
|
||||||
|
/// assert_eq!(rope.char_index_to_offset(3), "a 中".len());
|
||||||
|
/// assert_eq!(rope.char_index_to_offset(5), "a 中文🎉".len());
|
||||||
|
/// ```
|
||||||
|
fn char_index_to_offset(&self, char_index: usize) -> usize;
|
||||||
|
|
||||||
|
/// Convert byte offset (0-based) to offset in characters.
|
||||||
|
///
|
||||||
|
/// Run in O(n) time.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use gpui_component::{Rope, RopeExt};
|
||||||
|
/// let rope = Rope::from("a 中文🎉 test\nRope");
|
||||||
|
/// assert_eq!(rope.offset_to_char_index(0), 0);
|
||||||
|
/// assert_eq!(rope.offset_to_char_index(1), 1);
|
||||||
|
/// assert_eq!(rope.offset_to_char_index(3), 3);
|
||||||
|
/// assert_eq!(rope.offset_to_char_index(4), 3);
|
||||||
|
/// ```
|
||||||
|
fn offset_to_char_index(&self, offset: usize) -> usize;
|
||||||
|
}
|
||||||
|
|
||||||
impl RopeExt for Rope {
|
impl RopeExt for Rope {
|
||||||
fn line(&self, row: usize) -> Rope {
|
fn slice_line(&self, row: usize) -> RopeSlice<'_> {
|
||||||
let start = self.line_start_offset(row);
|
let total_lines = self.lines_len();
|
||||||
let end = start + self.line_len(row as u32) as usize;
|
if row >= total_lines {
|
||||||
|
return self.slice(0..0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let line = self.line(row, LineType::LF);
|
||||||
|
if line.len() > 0 {
|
||||||
|
let line_end = line.len() - 1;
|
||||||
|
if line.is_char_boundary(line_end) && line.char(line_end) == '\n' {
|
||||||
|
return line.slice(..line_end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
line
|
||||||
|
}
|
||||||
|
|
||||||
|
fn slice_lines(&self, rows_range: Range<usize>) -> RopeSlice<'_> {
|
||||||
|
let start = self.line_start_offset(rows_range.start);
|
||||||
|
let end = self.line_end_offset(rows_range.end.saturating_sub(1));
|
||||||
self.slice(start..end)
|
self.slice(start..end)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn iter_lines(&self) -> RopeLines<'_> {
|
||||||
|
RopeLines::new(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn line_len(&self, row: usize) -> usize {
|
||||||
|
self.slice_line(row).len()
|
||||||
|
}
|
||||||
|
|
||||||
fn line_start_offset(&self, row: usize) -> usize {
|
fn line_start_offset(&self, row: usize) -> usize {
|
||||||
let row = row as u32;
|
|
||||||
self.point_to_offset(Point::new(row, 0))
|
self.point_to_offset(Point::new(row, 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn offset_to_point(&self, offset: usize) -> Point {
|
||||||
|
let offset = self.clip_offset(offset, Bias::Left);
|
||||||
|
let row = self.byte_to_line_idx(offset, LineType::LF);
|
||||||
|
let line_start = self.line_to_byte_idx(row, LineType::LF);
|
||||||
|
let column = offset.saturating_sub(line_start);
|
||||||
|
Point::new(row, column)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn point_to_offset(&self, point: Point) -> usize {
|
||||||
|
if point.row >= self.lines_len() {
|
||||||
|
return self.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
let line_start = self.line_to_byte_idx(point.row, LineType::LF);
|
||||||
|
line_start + point.column
|
||||||
|
}
|
||||||
|
|
||||||
fn position_to_offset(&self, pos: &Position) -> usize {
|
fn position_to_offset(&self, pos: &Position) -> usize {
|
||||||
let line = self.line(pos.line as usize);
|
let line = self.slice_line(pos.line as usize);
|
||||||
self.line_start_offset(pos.line as usize)
|
self.line_start_offset(pos.line as usize)
|
||||||
+ line
|
+ line
|
||||||
.chars()
|
.chars()
|
||||||
@@ -128,34 +344,22 @@ impl RopeExt for Rope {
|
|||||||
|
|
||||||
fn offset_to_position(&self, offset: usize) -> Position {
|
fn offset_to_position(&self, offset: usize) -> Position {
|
||||||
let point = self.offset_to_point(offset);
|
let point = self.offset_to_point(offset);
|
||||||
let line = self.line(point.row as usize);
|
let line = self.slice_line(point.row);
|
||||||
let column = line.clip_offset(point.column as usize, sum_tree::Bias::Left);
|
let offset = line.utf16_to_byte_idx(line.byte_to_utf16_idx(point.column));
|
||||||
let character = line.slice(0..column).chars().count();
|
let character = line.slice(..offset).chars().count();
|
||||||
Position::new(point.row, character as u32)
|
Position::new(point.row as u32, character as u32)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn line_end_offset(&self, row: usize) -> usize {
|
fn line_end_offset(&self, row: usize) -> usize {
|
||||||
if row > self.max_point().row as usize {
|
if row > self.lines_len() {
|
||||||
return self.len();
|
return self.len();
|
||||||
}
|
}
|
||||||
|
|
||||||
self.line_start_offset(row) + self.line_len(row as u32) as usize
|
self.line_start_offset(row) + self.line_len(row)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn lines_len(&self) -> usize {
|
fn lines_len(&self) -> usize {
|
||||||
self.max_point().row as usize + 1
|
self.len_lines(LineType::LF)
|
||||||
}
|
|
||||||
|
|
||||||
fn lines(&self) -> RopeLines {
|
|
||||||
RopeLines::new(self.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn eq(&self, other: &Rope) -> bool {
|
|
||||||
self.summary() == other.summary()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn chars_count(&self) -> usize {
|
|
||||||
self.chars().count()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn char_at(&self, offset: usize) -> Option<char> {
|
fn char_at(&self, offset: usize) -> Option<char> {
|
||||||
@@ -163,8 +367,7 @@ impl RopeExt for Rope {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let offset = self.clip_offset(offset, sum_tree::Bias::Left);
|
self.get_char(offset).ok()
|
||||||
self.slice(offset..self.len()).chars().next()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn word_range(&self, offset: usize) -> Option<Range<usize>> {
|
fn word_range(&self, offset: usize) -> Option<Range<usize>> {
|
||||||
@@ -172,10 +375,9 @@ impl RopeExt for Rope {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let offset = self.clip_offset(offset, sum_tree::Bias::Left);
|
|
||||||
|
|
||||||
let mut left = String::new();
|
let mut left = String::new();
|
||||||
for c in self.reversed_chars_at(offset) {
|
let offset = self.clip_offset(offset, Bias::Left);
|
||||||
|
for c in self.chars_at(offset).reversed() {
|
||||||
if c.is_alphanumeric() || c == '_' {
|
if c.is_alphanumeric() || c == '_' {
|
||||||
left.insert(0, c);
|
left.insert(0, c);
|
||||||
} else {
|
} else {
|
||||||
@@ -191,11 +393,7 @@ impl RopeExt for Rope {
|
|||||||
|
|
||||||
let end = offset + right.len();
|
let end = offset + right.len();
|
||||||
|
|
||||||
if start == end {
|
if start == end { None } else { Some(start..end) }
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(start..end)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn word_at(&self, offset: usize) -> String {
|
fn word_at(&self, offset: usize) -> String {
|
||||||
@@ -205,4 +403,54 @@ impl RopeExt for Rope {
|
|||||||
String::new()
|
String::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn offset_utf16_to_offset(&self, offset_utf16: usize) -> usize {
|
||||||
|
if offset_utf16 > self.len_utf16() {
|
||||||
|
return self.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.utf16_to_byte_idx(offset_utf16)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn offset_to_offset_utf16(&self, offset: usize) -> usize {
|
||||||
|
if offset > self.len() {
|
||||||
|
return self.len_utf16();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.byte_to_utf16_idx(offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replace(&mut self, range: Range<usize>, new_text: &str) {
|
||||||
|
let range =
|
||||||
|
self.clip_offset(range.start, Bias::Left)..self.clip_offset(range.end, Bias::Right);
|
||||||
|
self.remove(range.clone());
|
||||||
|
self.insert(range.start, new_text);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clip_offset(&self, offset: usize, bias: Bias) -> usize {
|
||||||
|
if offset > self.len() {
|
||||||
|
return self.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.is_char_boundary(offset) {
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
if bias == Bias::Left {
|
||||||
|
self.floor_char_boundary(offset)
|
||||||
|
} else {
|
||||||
|
self.ceil_char_boundary(offset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn char_index_to_offset(&self, char_offset: usize) -> usize {
|
||||||
|
self.chars().take(char_offset).map(|c| c.len_utf8()).sum()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn offset_to_char_index(&self, offset: usize) -> usize {
|
||||||
|
let offset = self.clip_offset(offset, Bias::Right);
|
||||||
|
self.slice(..offset).chars().count()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
140
crates/ui/src/input/selection.rs
Normal file
140
crates/ui/src/input/selection.rs
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
use std::ops::Range;
|
||||||
|
|
||||||
|
use gpui::{Context, Window};
|
||||||
|
use ropey::Rope;
|
||||||
|
use sum_tree::Bias;
|
||||||
|
|
||||||
|
use crate::input::{InputState, RopeExt};
|
||||||
|
|
||||||
|
impl InputState {
|
||||||
|
/// Select the word at the given offset on double-click.
|
||||||
|
///
|
||||||
|
/// The offset is the UTF-8 offset.
|
||||||
|
pub(super) fn select_word(&mut self, offset: usize, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let Some(range) = TextSelector::word_range(&self.text, offset) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.selected_range = (range.start..range.end).into();
|
||||||
|
self.selected_word_range = Some(self.selected_range);
|
||||||
|
cx.notify()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Select the line at the given offset on triple-click.
|
||||||
|
///
|
||||||
|
/// The offset is the UTF-8 offset.
|
||||||
|
pub(super) fn select_line(&mut self, offset: usize, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let range = TextSelector::line_range(&self.text, offset);
|
||||||
|
self.selected_range = (range.start..range.end).into();
|
||||||
|
self.selected_word_range = None;
|
||||||
|
cx.notify()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TextSelector;
|
||||||
|
impl TextSelector {
|
||||||
|
/// Select a line in the given text at the specified offset.
|
||||||
|
///
|
||||||
|
/// The offset is the UTF-8 offset.
|
||||||
|
///
|
||||||
|
/// Returns the start and end offsets of the selected line.
|
||||||
|
pub fn line_range(text: &Rope, offset: usize) -> Range<usize> {
|
||||||
|
let offset = text.clip_offset(offset, Bias::Left);
|
||||||
|
let row = text.offset_to_point(offset).row;
|
||||||
|
let start = text.line_start_offset(row);
|
||||||
|
let end = text.line_end_offset(row);
|
||||||
|
|
||||||
|
start..end
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Select a word in the given text at the specified offset.
|
||||||
|
///
|
||||||
|
/// The offset is the UTF-8 offset.
|
||||||
|
///
|
||||||
|
/// Returns the start and end offsets of the selected word.
|
||||||
|
pub fn word_range(text: &Rope, offset: usize) -> Option<Range<usize>> {
|
||||||
|
let offset = text.clip_offset(offset, Bias::Left);
|
||||||
|
let char = text.char_at(offset)?;
|
||||||
|
let end = offset + char.len_utf8();
|
||||||
|
let prev_chars = text.chars_at(offset).reversed().take(128);
|
||||||
|
let next_chars = text.chars_at(end).take(128);
|
||||||
|
|
||||||
|
Some(word_range_from_chars(offset, char, prev_chars, next_chars))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub(crate) enum CharType {
|
||||||
|
/// a-z, A-Z, 0-9, _
|
||||||
|
Word,
|
||||||
|
/// '\t', ' ', '\u{00A0}' etc.
|
||||||
|
Whitespace,
|
||||||
|
/// \n, \r
|
||||||
|
Newline,
|
||||||
|
/// . , ; : ( ) [ ] { } ... or CJK characters: `汉`, `🎉` etc.
|
||||||
|
Other,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<char> for CharType {
|
||||||
|
fn from(c: char) -> Self {
|
||||||
|
match c {
|
||||||
|
c if is_word_char(c) => CharType::Word,
|
||||||
|
c if c == '\n' || c == '\r' => CharType::Newline,
|
||||||
|
c if c.is_whitespace() => CharType::Whitespace,
|
||||||
|
_ => CharType::Other,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CharType {
|
||||||
|
fn is_connectable(self, c: char) -> bool {
|
||||||
|
matches!(
|
||||||
|
(self, CharType::from(c)),
|
||||||
|
(CharType::Word, CharType::Word) | (CharType::Whitespace, CharType::Whitespace)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_word_char(c: char) -> bool {
|
||||||
|
matches!(c, '_')
|
||||||
|
// ASCII alphanumeric characters, for English, numbers: `Hello123`, etc.
|
||||||
|
|| c.is_ascii_alphanumeric()
|
||||||
|
// Latin script in Unicode for French, German, Spanish, etc.
|
||||||
|
|| matches!(c, '\u{00C0}'..='\u{00FF}')
|
||||||
|
|| matches!(c, '\u{0100}'..='\u{017F}')
|
||||||
|
|| matches!(c, '\u{0180}'..='\u{024F}')
|
||||||
|
// Cyrillic for Russian, Ukrainian, etc.
|
||||||
|
|| matches!(c, '\u{0400}'..='\u{04FF}')
|
||||||
|
// Vietnamese
|
||||||
|
|| matches!(c, '\u{1E00}'..='\u{1EFF}')
|
||||||
|
|| matches!(c, '\u{0300}'..='\u{036F}')
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn word_range_from_chars(
|
||||||
|
offset: usize,
|
||||||
|
c: char,
|
||||||
|
prev_chars: impl Iterator<Item = char>,
|
||||||
|
next_chars: impl Iterator<Item = char>,
|
||||||
|
) -> Range<usize> {
|
||||||
|
let char_type = CharType::from(c);
|
||||||
|
let mut start = offset;
|
||||||
|
let mut end = offset + c.len_utf8();
|
||||||
|
|
||||||
|
for prev in prev_chars.take(128) {
|
||||||
|
if char_type.is_connectable(prev) {
|
||||||
|
start -= prev.len_utf8();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for next in next_chars.take(128) {
|
||||||
|
if char_type.is_connectable(next) {
|
||||||
|
end += next.len_utf8();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
start..end
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user