mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-27 10:34:53 +03:00
Merge branch 'main' into add-setting-to-automatically-enable-virtual-environment
This commit is contained in:
commit
0801e5e437
3
.github/workflows/publish_collab_image.yml
vendored
3
.github/workflows/publish_collab_image.yml
vendored
@ -22,6 +22,9 @@ jobs:
|
|||||||
- name: Sign into DigitalOcean docker registry
|
- name: Sign into DigitalOcean docker registry
|
||||||
run: doctl registry login
|
run: doctl registry login
|
||||||
|
|
||||||
|
- name: Prune Docker system
|
||||||
|
run: docker system prune
|
||||||
|
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
|
151
Cargo.lock
generated
151
Cargo.lock
generated
@ -1063,6 +1063,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"async-broadcast",
|
"async-broadcast",
|
||||||
"audio",
|
"audio",
|
||||||
|
"channel",
|
||||||
"client",
|
"client",
|
||||||
"collections",
|
"collections",
|
||||||
"fs",
|
"fs",
|
||||||
@ -1190,6 +1191,41 @@ version = "1.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "channel"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"client",
|
||||||
|
"collections",
|
||||||
|
"db",
|
||||||
|
"futures 0.3.28",
|
||||||
|
"gpui",
|
||||||
|
"image",
|
||||||
|
"language",
|
||||||
|
"lazy_static",
|
||||||
|
"log",
|
||||||
|
"parking_lot 0.11.2",
|
||||||
|
"postage",
|
||||||
|
"rand 0.8.5",
|
||||||
|
"rpc",
|
||||||
|
"schemars",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"settings",
|
||||||
|
"smol",
|
||||||
|
"staff_mode",
|
||||||
|
"sum_tree",
|
||||||
|
"tempfile",
|
||||||
|
"text",
|
||||||
|
"thiserror",
|
||||||
|
"time 0.3.24",
|
||||||
|
"tiny_http",
|
||||||
|
"url",
|
||||||
|
"util",
|
||||||
|
"uuid 1.4.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.26"
|
version = "0.4.26"
|
||||||
@ -1354,6 +1390,7 @@ dependencies = [
|
|||||||
"staff_mode",
|
"staff_mode",
|
||||||
"sum_tree",
|
"sum_tree",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
|
"text",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"time 0.3.24",
|
"time 0.3.24",
|
||||||
"tiny_http",
|
"tiny_http",
|
||||||
@ -1409,7 +1446,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "collab"
|
name = "collab"
|
||||||
version = "0.17.0"
|
version = "0.18.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-tungstenite",
|
"async-tungstenite",
|
||||||
@ -1418,8 +1455,11 @@ dependencies = [
|
|||||||
"axum-extra",
|
"axum-extra",
|
||||||
"base64 0.13.1",
|
"base64 0.13.1",
|
||||||
"call",
|
"call",
|
||||||
|
"channel",
|
||||||
"clap 3.2.25",
|
"clap 3.2.25",
|
||||||
"client",
|
"client",
|
||||||
|
"clock",
|
||||||
|
"collab_ui",
|
||||||
"collections",
|
"collections",
|
||||||
"ctor",
|
"ctor",
|
||||||
"dashmap",
|
"dashmap",
|
||||||
@ -1444,6 +1484,7 @@ dependencies = [
|
|||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"project",
|
"project",
|
||||||
"prometheus",
|
"prometheus",
|
||||||
|
"prost 0.8.0",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rpc",
|
"rpc",
|
||||||
@ -1456,6 +1497,7 @@ dependencies = [
|
|||||||
"settings",
|
"settings",
|
||||||
"sha-1 0.9.8",
|
"sha-1 0.9.8",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
|
"text",
|
||||||
"theme",
|
"theme",
|
||||||
"time 0.3.24",
|
"time 0.3.24",
|
||||||
"tokio",
|
"tokio",
|
||||||
@ -1478,6 +1520,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"auto_update",
|
"auto_update",
|
||||||
"call",
|
"call",
|
||||||
|
"channel",
|
||||||
"client",
|
"client",
|
||||||
"clock",
|
"clock",
|
||||||
"collections",
|
"collections",
|
||||||
@ -1488,6 +1531,7 @@ dependencies = [
|
|||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"language",
|
||||||
"log",
|
"log",
|
||||||
"menu",
|
"menu",
|
||||||
"picker",
|
"picker",
|
||||||
@ -1556,6 +1600,19 @@ dependencies = [
|
|||||||
"workspace",
|
"workspace",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "component_test"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"gpui",
|
||||||
|
"project",
|
||||||
|
"settings",
|
||||||
|
"theme",
|
||||||
|
"util",
|
||||||
|
"workspace",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "concurrent-queue"
|
name = "concurrent-queue"
|
||||||
version = "2.2.0"
|
version = "2.2.0"
|
||||||
@ -2085,6 +2142,15 @@ dependencies = [
|
|||||||
"syn 1.0.109",
|
"syn 1.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_refineable"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 1.0.109",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dhat"
|
name = "dhat"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
@ -3067,6 +3133,7 @@ dependencies = [
|
|||||||
"png",
|
"png",
|
||||||
"postage",
|
"postage",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
|
"refineable",
|
||||||
"resvg",
|
"resvg",
|
||||||
"schemars",
|
"schemars",
|
||||||
"seahash",
|
"seahash",
|
||||||
@ -3078,6 +3145,7 @@ dependencies = [
|
|||||||
"smol",
|
"smol",
|
||||||
"sqlez",
|
"sqlez",
|
||||||
"sum_tree",
|
"sum_tree",
|
||||||
|
"taffy",
|
||||||
"time 0.3.24",
|
"time 0.3.24",
|
||||||
"tiny-skia",
|
"tiny-skia",
|
||||||
"usvg",
|
"usvg",
|
||||||
@ -3090,11 +3158,18 @@ dependencies = [
|
|||||||
name = "gpui_macros"
|
name = "gpui_macros"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"lazy_static",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 1.0.109",
|
"syn 1.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "grid"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eec1c01eb1de97451ee0d60de7d81cf1e72aabefb021616027f3d1c3ec1c723c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h2"
|
name = "h2"
|
||||||
version = "0.3.20"
|
version = "0.3.20"
|
||||||
@ -5087,6 +5162,33 @@ version = "0.3.27"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
|
checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "playground"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"derive_more",
|
||||||
|
"gpui",
|
||||||
|
"log",
|
||||||
|
"parking_lot 0.11.2",
|
||||||
|
"playground_macros",
|
||||||
|
"refineable",
|
||||||
|
"serde",
|
||||||
|
"simplelog",
|
||||||
|
"smallvec",
|
||||||
|
"taffy",
|
||||||
|
"util",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "playground_macros"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 1.0.109",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "plist"
|
name = "plist"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@ -5764,6 +5866,16 @@ dependencies = [
|
|||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "refineable"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"derive_refineable",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 1.0.109",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regalloc2"
|
name = "regalloc2"
|
||||||
version = "0.2.3"
|
version = "0.2.3"
|
||||||
@ -6075,9 +6187,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust-embed"
|
name = "rust-embed"
|
||||||
version = "6.8.1"
|
version = "8.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a36224c3276f8c4ebc8c20f158eca7ca4359c8db89991c4925132aaaf6702661"
|
checksum = "b1e7d90385b59f0a6bf3d3b757f3ca4ece2048265d70db20a2016043d4509a40"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rust-embed-impl",
|
"rust-embed-impl",
|
||||||
"rust-embed-utils",
|
"rust-embed-utils",
|
||||||
@ -6086,9 +6198,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust-embed-impl"
|
name = "rust-embed-impl"
|
||||||
version = "6.8.1"
|
version = "8.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "49b94b81e5b2c284684141a2fb9e2a31be90638caf040bf9afbc5a0416afe1ac"
|
checksum = "3c3d8c6fd84090ae348e63a84336b112b5c3918b3bf0493a581f7bd8ee623c29"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -6099,9 +6211,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust-embed-utils"
|
name = "rust-embed-utils"
|
||||||
version = "7.8.1"
|
version = "8.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9d38ff6bf570dc3bb7100fce9f7b60c33fa71d80e88da3f2580df4ff2bdded74"
|
checksum = "873feff8cb7bf86fdf0a71bb21c95159f4e4a37dd7a4bd1855a940909b583ada"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"globset",
|
"globset",
|
||||||
"sha2 0.10.7",
|
"sha2 0.10.7",
|
||||||
@ -6929,6 +7041,15 @@ version = "0.3.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "826167069c09b99d56f31e9ae5c99049e932a98c9dc2dac47645b08dbbf76ba7"
|
checksum = "826167069c09b99d56f31e9ae5c99049e932a98c9dc2dac47645b08dbbf76ba7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "slotmap"
|
||||||
|
version = "1.0.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342"
|
||||||
|
dependencies = [
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sluice"
|
name = "sluice"
|
||||||
version = "0.5.5"
|
version = "0.5.5"
|
||||||
@ -7368,6 +7489,17 @@ dependencies = [
|
|||||||
"winx",
|
"winx",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "taffy"
|
||||||
|
version = "0.3.11"
|
||||||
|
source = "git+https://github.com/DioxusLabs/taffy?rev=dab541d6104d58e2e10ce90c4a1dad0b703160cd#dab541d6104d58e2e10ce90c4a1dad0b703160cd"
|
||||||
|
dependencies = [
|
||||||
|
"arrayvec 0.7.4",
|
||||||
|
"grid",
|
||||||
|
"num-traits",
|
||||||
|
"slotmap",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "take-until"
|
name = "take-until"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@ -9446,6 +9578,7 @@ dependencies = [
|
|||||||
"async-recursion 1.0.4",
|
"async-recursion 1.0.4",
|
||||||
"bincode",
|
"bincode",
|
||||||
"call",
|
"call",
|
||||||
|
"channel",
|
||||||
"client",
|
"client",
|
||||||
"collections",
|
"collections",
|
||||||
"context_menu",
|
"context_menu",
|
||||||
@ -9557,7 +9690,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zed"
|
name = "zed"
|
||||||
version = "0.101.0"
|
version = "0.102.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"activity_indicator",
|
"activity_indicator",
|
||||||
"ai",
|
"ai",
|
||||||
@ -9571,6 +9704,7 @@ dependencies = [
|
|||||||
"backtrace",
|
"backtrace",
|
||||||
"breadcrumbs",
|
"breadcrumbs",
|
||||||
"call",
|
"call",
|
||||||
|
"channel",
|
||||||
"chrono",
|
"chrono",
|
||||||
"cli",
|
"cli",
|
||||||
"client",
|
"client",
|
||||||
@ -9578,6 +9712,7 @@ dependencies = [
|
|||||||
"collab_ui",
|
"collab_ui",
|
||||||
"collections",
|
"collections",
|
||||||
"command_palette",
|
"command_palette",
|
||||||
|
"component_test",
|
||||||
"context_menu",
|
"context_menu",
|
||||||
"copilot",
|
"copilot",
|
||||||
"copilot_button",
|
"copilot_button",
|
||||||
|
10
Cargo.toml
10
Cargo.toml
@ -6,6 +6,7 @@ members = [
|
|||||||
"crates/auto_update",
|
"crates/auto_update",
|
||||||
"crates/breadcrumbs",
|
"crates/breadcrumbs",
|
||||||
"crates/call",
|
"crates/call",
|
||||||
|
"crates/channel",
|
||||||
"crates/cli",
|
"crates/cli",
|
||||||
"crates/client",
|
"crates/client",
|
||||||
"crates/clock",
|
"crates/clock",
|
||||||
@ -13,10 +14,13 @@ members = [
|
|||||||
"crates/collab_ui",
|
"crates/collab_ui",
|
||||||
"crates/collections",
|
"crates/collections",
|
||||||
"crates/command_palette",
|
"crates/command_palette",
|
||||||
|
"crates/component_test",
|
||||||
"crates/context_menu",
|
"crates/context_menu",
|
||||||
"crates/copilot",
|
"crates/copilot",
|
||||||
"crates/copilot_button",
|
"crates/copilot_button",
|
||||||
"crates/db",
|
"crates/db",
|
||||||
|
"crates/refineable",
|
||||||
|
"crates/refineable/derive_refineable",
|
||||||
"crates/diagnostics",
|
"crates/diagnostics",
|
||||||
"crates/drag_and_drop",
|
"crates/drag_and_drop",
|
||||||
"crates/editor",
|
"crates/editor",
|
||||||
@ -28,6 +32,8 @@ members = [
|
|||||||
"crates/git",
|
"crates/git",
|
||||||
"crates/go_to_line",
|
"crates/go_to_line",
|
||||||
"crates/gpui",
|
"crates/gpui",
|
||||||
|
"crates/gpui/playground",
|
||||||
|
"crates/gpui/playground_macros",
|
||||||
"crates/gpui_macros",
|
"crates/gpui_macros",
|
||||||
"crates/install_cli",
|
"crates/install_cli",
|
||||||
"crates/journal",
|
"crates/journal",
|
||||||
@ -91,9 +97,11 @@ log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
|||||||
ordered-float = { version = "2.1.1" }
|
ordered-float = { version = "2.1.1" }
|
||||||
parking_lot = { version = "0.11.1" }
|
parking_lot = { version = "0.11.1" }
|
||||||
postage = { version = "0.5", features = ["futures-traits"] }
|
postage = { version = "0.5", features = ["futures-traits"] }
|
||||||
|
prost = { version = "0.8" }
|
||||||
rand = { version = "0.8.5" }
|
rand = { version = "0.8.5" }
|
||||||
|
refineable = { path = "./crates/refineable" }
|
||||||
regex = { version = "1.5" }
|
regex = { version = "1.5" }
|
||||||
rust-embed = { version = "6.3", features = ["include-exclude"] }
|
rust-embed = { version = "8.0", features = ["include-exclude"] }
|
||||||
schemars = { version = "0.8" }
|
schemars = { version = "0.8" }
|
||||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||||
serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
|
serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
|
||||||
|
@ -543,6 +543,8 @@
|
|||||||
"bindings": {
|
"bindings": {
|
||||||
"left": "project_panel::CollapseSelectedEntry",
|
"left": "project_panel::CollapseSelectedEntry",
|
||||||
"right": "project_panel::ExpandSelectedEntry",
|
"right": "project_panel::ExpandSelectedEntry",
|
||||||
|
"cmd-n": "project_panel::NewFile",
|
||||||
|
"alt-cmd-n": "project_panel::NewDirectory",
|
||||||
"cmd-x": "project_panel::Cut",
|
"cmd-x": "project_panel::Cut",
|
||||||
"cmd-c": "project_panel::Copy",
|
"cmd-c": "project_panel::Copy",
|
||||||
"cmd-v": "project_panel::Paste",
|
"cmd-v": "project_panel::Paste",
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
{
|
{
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"cmd-shift-o": "projects::OpenRecent",
|
"cmd-shift-o": "projects::OpenRecent",
|
||||||
"cmd-shift-b": "branches::OpenRecent",
|
|
||||||
"cmd-alt-tab": "project_panel::ToggleFocus"
|
"cmd-alt-tab": "project_panel::ToggleFocus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -12,8 +11,9 @@
|
|||||||
"cmd-l": "go_to_line::Toggle",
|
"cmd-l": "go_to_line::Toggle",
|
||||||
"ctrl-shift-d": "editor::DuplicateLine",
|
"ctrl-shift-d": "editor::DuplicateLine",
|
||||||
"cmd-b": "editor::GoToDefinition",
|
"cmd-b": "editor::GoToDefinition",
|
||||||
"alt-cmd-b": "editor::GoToDefinition",
|
|
||||||
"cmd-j": "editor::ScrollCursorCenter",
|
"cmd-j": "editor::ScrollCursorCenter",
|
||||||
|
"cmd-enter": "editor::NewlineBelow",
|
||||||
|
"cmd-alt-enter": "editor::NewLineAbove",
|
||||||
"cmd-shift-l": "editor::SelectLine",
|
"cmd-shift-l": "editor::SelectLine",
|
||||||
"cmd-shift-t": "outline::Toggle",
|
"cmd-shift-t": "outline::Toggle",
|
||||||
"alt-backspace": "editor::DeleteToPreviousWordStart",
|
"alt-backspace": "editor::DeleteToPreviousWordStart",
|
||||||
@ -51,14 +51,17 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"ctrl-shift-left": "editor::SelectToPreviousSubwordStart",
|
"ctrl-shift-left": "editor::SelectToPreviousSubwordStart",
|
||||||
"ctrl-shift-right": "editor::SelectToNextSubwordEnd"
|
"ctrl-shift-right": "editor::SelectToNextSubwordEnd",
|
||||||
|
"ctrl-w": "editor::SelectNext",
|
||||||
|
"ctrl-u": "editor::ConvertToUpperCase",
|
||||||
|
"ctrl-shift-u": "editor::ConvertToLowerCase",
|
||||||
|
"ctrl-alt-u": "editor::ConvertToUpperCamelCase",
|
||||||
|
"ctrl-_": "editor::ConvertToSnakeCase"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && mode == full",
|
"context": "Editor && mode == full",
|
||||||
"bindings": {
|
"bindings": {}
|
||||||
"cmd-alt-enter": "editor::NewlineAbove"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "BufferSearchBar",
|
"context": "BufferSearchBar",
|
||||||
@ -85,5 +88,9 @@
|
|||||||
{
|
{
|
||||||
"context": "ProjectPanel",
|
"context": "ProjectPanel",
|
||||||
"bindings": {}
|
"bindings": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": "Dock",
|
||||||
|
"bindings": {}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -287,6 +287,12 @@
|
|||||||
"shift-o": "vim::InsertLineAbove",
|
"shift-o": "vim::InsertLineAbove",
|
||||||
"~": "vim::ChangeCase",
|
"~": "vim::ChangeCase",
|
||||||
"p": "vim::Paste",
|
"p": "vim::Paste",
|
||||||
|
"shift-p": [
|
||||||
|
"vim::Paste",
|
||||||
|
{
|
||||||
|
"before": true
|
||||||
|
}
|
||||||
|
],
|
||||||
"u": "editor::Undo",
|
"u": "editor::Undo",
|
||||||
"ctrl-r": "editor::Redo",
|
"ctrl-r": "editor::Redo",
|
||||||
"/": "vim::Search",
|
"/": "vim::Search",
|
||||||
@ -375,7 +381,13 @@
|
|||||||
"d": "vim::VisualDelete",
|
"d": "vim::VisualDelete",
|
||||||
"x": "vim::VisualDelete",
|
"x": "vim::VisualDelete",
|
||||||
"y": "vim::VisualYank",
|
"y": "vim::VisualYank",
|
||||||
"p": "vim::VisualPaste",
|
"p": "vim::Paste",
|
||||||
|
"shift-p": [
|
||||||
|
"vim::Paste",
|
||||||
|
{
|
||||||
|
"preserveClipboard": true
|
||||||
|
}
|
||||||
|
],
|
||||||
"s": "vim::Substitute",
|
"s": "vim::Substitute",
|
||||||
"c": "vim::Substitute",
|
"c": "vim::Substitute",
|
||||||
"~": "vim::ChangeCase",
|
"~": "vim::ChangeCase",
|
||||||
@ -421,7 +433,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && vim_mode == insert",
|
"context": "Editor && vim_mode == insert && !menu",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"escape": "vim::NormalBefore",
|
"escape": "vim::NormalBefore",
|
||||||
"ctrl-c": "vim::NormalBefore",
|
"ctrl-c": "vim::NormalBefore",
|
||||||
|
@ -20,6 +20,7 @@ test-support = [
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
audio = { path = "../audio" }
|
audio = { path = "../audio" }
|
||||||
|
channel = { path = "../channel" }
|
||||||
client = { path = "../client" }
|
client = { path = "../client" }
|
||||||
collections = { path = "../collections" }
|
collections = { path = "../collections" }
|
||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui" }
|
||||||
|
@ -7,9 +7,8 @@ use std::sync::Arc;
|
|||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use audio::Audio;
|
use audio::Audio;
|
||||||
use call_settings::CallSettings;
|
use call_settings::CallSettings;
|
||||||
use client::{
|
use channel::ChannelId;
|
||||||
proto, ChannelId, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore,
|
use client::{proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore};
|
||||||
};
|
|
||||||
use collections::HashSet;
|
use collections::HashSet;
|
||||||
use futures::{future::Shared, FutureExt};
|
use futures::{future::Shared, FutureExt};
|
||||||
use postage::watch;
|
use postage::watch;
|
||||||
|
51
crates/channel/Cargo.toml
Normal file
51
crates/channel/Cargo.toml
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
[package]
|
||||||
|
name = "channel"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/channel.rs"
|
||||||
|
doctest = false
|
||||||
|
|
||||||
|
[features]
|
||||||
|
test-support = ["collections/test-support", "gpui/test-support", "rpc/test-support"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
client = { path = "../client" }
|
||||||
|
collections = { path = "../collections" }
|
||||||
|
db = { path = "../db" }
|
||||||
|
gpui = { path = "../gpui" }
|
||||||
|
util = { path = "../util" }
|
||||||
|
rpc = { path = "../rpc" }
|
||||||
|
text = { path = "../text" }
|
||||||
|
language = { path = "../language" }
|
||||||
|
settings = { path = "../settings" }
|
||||||
|
staff_mode = { path = "../staff_mode" }
|
||||||
|
sum_tree = { path = "../sum_tree" }
|
||||||
|
|
||||||
|
anyhow.workspace = true
|
||||||
|
futures.workspace = true
|
||||||
|
image = "0.23"
|
||||||
|
lazy_static.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
parking_lot.workspace = true
|
||||||
|
postage.workspace = true
|
||||||
|
rand.workspace = true
|
||||||
|
schemars.workspace = true
|
||||||
|
smol.workspace = true
|
||||||
|
thiserror.workspace = true
|
||||||
|
time.workspace = true
|
||||||
|
tiny_http = "0.8"
|
||||||
|
uuid = { version = "1.1.2", features = ["v4"] }
|
||||||
|
url = "2.2"
|
||||||
|
serde.workspace = true
|
||||||
|
serde_derive.workspace = true
|
||||||
|
tempfile = "3"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
collections = { path = "../collections", features = ["test-support"] }
|
||||||
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
|
rpc = { path = "../rpc", features = ["test-support"] }
|
||||||
|
settings = { path = "../settings", features = ["test-support"] }
|
||||||
|
util = { path = "../util", features = ["test-support"] }
|
14
crates/channel/src/channel.rs
Normal file
14
crates/channel/src/channel.rs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
mod channel_store;
|
||||||
|
|
||||||
|
pub mod channel_buffer;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
pub use channel_store::*;
|
||||||
|
use client::Client;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod channel_store_tests;
|
||||||
|
|
||||||
|
pub fn init(client: &Arc<Client>) {
|
||||||
|
channel_buffer::init(client);
|
||||||
|
}
|
197
crates/channel/src/channel_buffer.rs
Normal file
197
crates/channel/src/channel_buffer.rs
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
use crate::Channel;
|
||||||
|
use anyhow::Result;
|
||||||
|
use client::Client;
|
||||||
|
use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle};
|
||||||
|
use rpc::{proto, TypedEnvelope};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use util::ResultExt;
|
||||||
|
|
||||||
|
pub(crate) fn init(client: &Arc<Client>) {
|
||||||
|
client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer);
|
||||||
|
client.add_model_message_handler(ChannelBuffer::handle_add_channel_buffer_collaborator);
|
||||||
|
client.add_model_message_handler(ChannelBuffer::handle_remove_channel_buffer_collaborator);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ChannelBuffer {
|
||||||
|
pub(crate) channel: Arc<Channel>,
|
||||||
|
connected: bool,
|
||||||
|
collaborators: Vec<proto::Collaborator>,
|
||||||
|
buffer: ModelHandle<language::Buffer>,
|
||||||
|
client: Arc<Client>,
|
||||||
|
subscription: Option<client::Subscription>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Event {
|
||||||
|
CollaboratorsChanged,
|
||||||
|
Disconnected,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entity for ChannelBuffer {
|
||||||
|
type Event = Event;
|
||||||
|
|
||||||
|
fn release(&mut self, _: &mut AppContext) {
|
||||||
|
if self.connected {
|
||||||
|
self.client
|
||||||
|
.send(proto::LeaveChannelBuffer {
|
||||||
|
channel_id: self.channel.id,
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChannelBuffer {
|
||||||
|
pub(crate) async fn new(
|
||||||
|
channel: Arc<Channel>,
|
||||||
|
client: Arc<Client>,
|
||||||
|
mut cx: AsyncAppContext,
|
||||||
|
) -> Result<ModelHandle<Self>> {
|
||||||
|
let response = client
|
||||||
|
.request(proto::JoinChannelBuffer {
|
||||||
|
channel_id: channel.id,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let base_text = response.base_text;
|
||||||
|
let operations = response
|
||||||
|
.operations
|
||||||
|
.into_iter()
|
||||||
|
.map(language::proto::deserialize_operation)
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
|
||||||
|
let collaborators = response.collaborators;
|
||||||
|
|
||||||
|
let buffer = cx.add_model(|_| {
|
||||||
|
language::Buffer::remote(response.buffer_id, response.replica_id as u16, base_text)
|
||||||
|
});
|
||||||
|
buffer.update(&mut cx, |buffer, cx| buffer.apply_ops(operations, cx))?;
|
||||||
|
|
||||||
|
let subscription = client.subscribe_to_entity(channel.id)?;
|
||||||
|
|
||||||
|
anyhow::Ok(cx.add_model(|cx| {
|
||||||
|
cx.subscribe(&buffer, Self::on_buffer_update).detach();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
buffer,
|
||||||
|
client,
|
||||||
|
connected: true,
|
||||||
|
collaborators,
|
||||||
|
channel,
|
||||||
|
subscription: Some(subscription.set_model(&cx.handle(), &mut cx.to_async())),
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_update_channel_buffer(
|
||||||
|
this: ModelHandle<Self>,
|
||||||
|
update_channel_buffer: TypedEnvelope<proto::UpdateChannelBuffer>,
|
||||||
|
_: Arc<Client>,
|
||||||
|
mut cx: AsyncAppContext,
|
||||||
|
) -> Result<()> {
|
||||||
|
let ops = update_channel_buffer
|
||||||
|
.payload
|
||||||
|
.operations
|
||||||
|
.into_iter()
|
||||||
|
.map(language::proto::deserialize_operation)
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
cx.notify();
|
||||||
|
this.buffer
|
||||||
|
.update(cx, |buffer, cx| buffer.apply_ops(ops, cx))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_add_channel_buffer_collaborator(
|
||||||
|
this: ModelHandle<Self>,
|
||||||
|
envelope: TypedEnvelope<proto::AddChannelBufferCollaborator>,
|
||||||
|
_: Arc<Client>,
|
||||||
|
mut cx: AsyncAppContext,
|
||||||
|
) -> Result<()> {
|
||||||
|
let collaborator = envelope.payload.collaborator.ok_or_else(|| {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"Should have gotten a collaborator in the AddChannelBufferCollaborator message"
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.collaborators.push(collaborator);
|
||||||
|
cx.emit(Event::CollaboratorsChanged);
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_remove_channel_buffer_collaborator(
|
||||||
|
this: ModelHandle<Self>,
|
||||||
|
message: TypedEnvelope<proto::RemoveChannelBufferCollaborator>,
|
||||||
|
_: Arc<Client>,
|
||||||
|
mut cx: AsyncAppContext,
|
||||||
|
) -> Result<()> {
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.collaborators.retain(|collaborator| {
|
||||||
|
if collaborator.peer_id == message.payload.peer_id {
|
||||||
|
this.buffer.update(cx, |buffer, cx| {
|
||||||
|
buffer.remove_peer(collaborator.replica_id as u16, cx)
|
||||||
|
});
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cx.emit(Event::CollaboratorsChanged);
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_buffer_update(
|
||||||
|
&mut self,
|
||||||
|
_: ModelHandle<language::Buffer>,
|
||||||
|
event: &language::Event,
|
||||||
|
_: &mut ModelContext<Self>,
|
||||||
|
) {
|
||||||
|
if let language::Event::Operation(operation) = event {
|
||||||
|
let operation = language::proto::serialize_operation(operation);
|
||||||
|
self.client
|
||||||
|
.send(proto::UpdateChannelBuffer {
|
||||||
|
channel_id: self.channel.id,
|
||||||
|
operations: vec![operation],
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn buffer(&self) -> ModelHandle<language::Buffer> {
|
||||||
|
self.buffer.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn collaborators(&self) -> &[proto::Collaborator] {
|
||||||
|
&self.collaborators
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn channel(&self) -> Arc<Channel> {
|
||||||
|
self.channel.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn disconnect(&mut self, cx: &mut ModelContext<Self>) {
|
||||||
|
if self.connected {
|
||||||
|
self.connected = false;
|
||||||
|
self.subscription.take();
|
||||||
|
cx.emit(Event::Disconnected);
|
||||||
|
cx.notify()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_connected(&self) -> bool {
|
||||||
|
self.connected
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn replica_id(&self, cx: &AppContext) -> u16 {
|
||||||
|
self.buffer.read(cx).replica_id()
|
||||||
|
}
|
||||||
|
}
|
@ -1,19 +1,14 @@
|
|||||||
use crate::Status;
|
use crate::channel_buffer::ChannelBuffer;
|
||||||
use crate::{Client, Subscription, User, UserStore};
|
use anyhow::{anyhow, Result};
|
||||||
use anyhow::anyhow;
|
use client::{Client, Status, Subscription, User, UserId, UserStore};
|
||||||
use anyhow::Result;
|
use collections::{hash_map, HashMap, HashSet};
|
||||||
use collections::HashMap;
|
use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
|
||||||
use collections::HashSet;
|
use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle};
|
||||||
use futures::channel::mpsc;
|
|
||||||
use futures::Future;
|
|
||||||
use futures::StreamExt;
|
|
||||||
use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, Task};
|
|
||||||
use rpc::{proto, TypedEnvelope};
|
use rpc::{proto, TypedEnvelope};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
|
||||||
pub type ChannelId = u64;
|
pub type ChannelId = u64;
|
||||||
pub type UserId = u64;
|
|
||||||
|
|
||||||
pub struct ChannelStore {
|
pub struct ChannelStore {
|
||||||
channels_by_id: HashMap<ChannelId, Arc<Channel>>,
|
channels_by_id: HashMap<ChannelId, Arc<Channel>>,
|
||||||
@ -23,6 +18,7 @@ pub struct ChannelStore {
|
|||||||
channels_with_admin_privileges: HashSet<ChannelId>,
|
channels_with_admin_privileges: HashSet<ChannelId>,
|
||||||
outgoing_invites: HashSet<(ChannelId, UserId)>,
|
outgoing_invites: HashSet<(ChannelId, UserId)>,
|
||||||
update_channels_tx: mpsc::UnboundedSender<proto::UpdateChannels>,
|
update_channels_tx: mpsc::UnboundedSender<proto::UpdateChannels>,
|
||||||
|
opened_buffers: HashMap<ChannelId, OpenedChannelBuffer>,
|
||||||
client: Arc<Client>,
|
client: Arc<Client>,
|
||||||
user_store: ModelHandle<UserStore>,
|
user_store: ModelHandle<UserStore>,
|
||||||
_rpc_subscription: Subscription,
|
_rpc_subscription: Subscription,
|
||||||
@ -57,6 +53,11 @@ pub enum ChannelMemberStatus {
|
|||||||
NotMember,
|
NotMember,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum OpenedChannelBuffer {
|
||||||
|
Open(WeakModelHandle<ChannelBuffer>),
|
||||||
|
Loading(Shared<Task<Result<ModelHandle<ChannelBuffer>, Arc<anyhow::Error>>>>),
|
||||||
|
}
|
||||||
|
|
||||||
impl ChannelStore {
|
impl ChannelStore {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
client: Arc<Client>,
|
client: Arc<Client>,
|
||||||
@ -70,16 +71,14 @@ impl ChannelStore {
|
|||||||
let mut connection_status = client.status();
|
let mut connection_status = client.status();
|
||||||
let watch_connection_status = cx.spawn_weak(|this, mut cx| async move {
|
let watch_connection_status = cx.spawn_weak(|this, mut cx| async move {
|
||||||
while let Some(status) = connection_status.next().await {
|
while let Some(status) = connection_status.next().await {
|
||||||
if matches!(status, Status::ConnectionLost | Status::SignedOut) {
|
if !status.is_connected() {
|
||||||
if let Some(this) = this.upgrade(&cx) {
|
if let Some(this) = this.upgrade(&cx) {
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
this.channels_by_id.clear();
|
if matches!(status, Status::ConnectionLost | Status::SignedOut) {
|
||||||
this.channel_invitations.clear();
|
this.handle_disconnect(cx);
|
||||||
this.channel_participants.clear();
|
} else {
|
||||||
this.channels_with_admin_privileges.clear();
|
this.disconnect_buffers(cx);
|
||||||
this.channel_paths.clear();
|
}
|
||||||
this.outgoing_invites.clear();
|
|
||||||
cx.notify();
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
@ -87,6 +86,7 @@ impl ChannelStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
channels_by_id: HashMap::default(),
|
channels_by_id: HashMap::default(),
|
||||||
channel_invitations: Vec::default(),
|
channel_invitations: Vec::default(),
|
||||||
@ -94,6 +94,7 @@ impl ChannelStore {
|
|||||||
channel_participants: Default::default(),
|
channel_participants: Default::default(),
|
||||||
channels_with_admin_privileges: Default::default(),
|
channels_with_admin_privileges: Default::default(),
|
||||||
outgoing_invites: Default::default(),
|
outgoing_invites: Default::default(),
|
||||||
|
opened_buffers: Default::default(),
|
||||||
update_channels_tx,
|
update_channels_tx,
|
||||||
client,
|
client,
|
||||||
user_store,
|
user_store,
|
||||||
@ -114,6 +115,16 @@ impl ChannelStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn has_children(&self, channel_id: ChannelId) -> bool {
|
||||||
|
self.channel_paths.iter().any(|path| {
|
||||||
|
if let Some(ix) = path.iter().position(|id| *id == channel_id) {
|
||||||
|
path.len() > ix + 1
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn channel_count(&self) -> usize {
|
pub fn channel_count(&self) -> usize {
|
||||||
self.channel_paths.len()
|
self.channel_paths.len()
|
||||||
}
|
}
|
||||||
@ -141,6 +152,74 @@ impl ChannelStore {
|
|||||||
self.channels_by_id.get(&channel_id)
|
self.channels_by_id.get(&channel_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn open_channel_buffer(
|
||||||
|
&mut self,
|
||||||
|
channel_id: ChannelId,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Task<Result<ModelHandle<ChannelBuffer>>> {
|
||||||
|
// Make sure that a given channel buffer is only opened once per
|
||||||
|
// app instance, even if this method is called multiple times
|
||||||
|
// with the same channel id while the first task is still running.
|
||||||
|
let task = loop {
|
||||||
|
match self.opened_buffers.entry(channel_id) {
|
||||||
|
hash_map::Entry::Occupied(e) => match e.get() {
|
||||||
|
OpenedChannelBuffer::Open(buffer) => {
|
||||||
|
if let Some(buffer) = buffer.upgrade(cx) {
|
||||||
|
break Task::ready(Ok(buffer)).shared();
|
||||||
|
} else {
|
||||||
|
self.opened_buffers.remove(&channel_id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OpenedChannelBuffer::Loading(task) => break task.clone(),
|
||||||
|
},
|
||||||
|
hash_map::Entry::Vacant(e) => {
|
||||||
|
let client = self.client.clone();
|
||||||
|
let task = cx
|
||||||
|
.spawn(|this, cx| async move {
|
||||||
|
let channel = this.read_with(&cx, |this, _| {
|
||||||
|
this.channel_for_id(channel_id).cloned().ok_or_else(|| {
|
||||||
|
Arc::new(anyhow!("no channel for id: {}", channel_id))
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
|
||||||
|
ChannelBuffer::new(channel, client, cx)
|
||||||
|
.await
|
||||||
|
.map_err(Arc::new)
|
||||||
|
})
|
||||||
|
.shared();
|
||||||
|
e.insert(OpenedChannelBuffer::Loading(task.clone()));
|
||||||
|
cx.spawn({
|
||||||
|
let task = task.clone();
|
||||||
|
|this, mut cx| async move {
|
||||||
|
let result = task.await;
|
||||||
|
this.update(&mut cx, |this, cx| match result {
|
||||||
|
Ok(buffer) => {
|
||||||
|
cx.observe_release(&buffer, move |this, _, _| {
|
||||||
|
this.opened_buffers.remove(&channel_id);
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
this.opened_buffers.insert(
|
||||||
|
channel_id,
|
||||||
|
OpenedChannelBuffer::Open(buffer.downgrade()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
log::error!("failed to open channel buffer {error:?}");
|
||||||
|
this.opened_buffers.remove(&channel_id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
break task;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
cx.foreground()
|
||||||
|
.spawn(async move { task.await.map_err(|error| anyhow!("{}", error)) })
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_user_admin(&self, channel_id: ChannelId) -> bool {
|
pub fn is_user_admin(&self, channel_id: ChannelId) -> bool {
|
||||||
self.channel_paths.iter().any(|path| {
|
self.channel_paths.iter().any(|path| {
|
||||||
if let Some(ix) = path.iter().position(|id| *id == channel_id) {
|
if let Some(ix) = path.iter().position(|id| *id == channel_id) {
|
||||||
@ -403,6 +482,27 @@ impl ChannelStore {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_disconnect(&mut self, cx: &mut ModelContext<'_, ChannelStore>) {
|
||||||
|
self.disconnect_buffers(cx);
|
||||||
|
self.channels_by_id.clear();
|
||||||
|
self.channel_invitations.clear();
|
||||||
|
self.channel_participants.clear();
|
||||||
|
self.channels_with_admin_privileges.clear();
|
||||||
|
self.channel_paths.clear();
|
||||||
|
self.outgoing_invites.clear();
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn disconnect_buffers(&mut self, cx: &mut ModelContext<ChannelStore>) {
|
||||||
|
for (_, buffer) in self.opened_buffers.drain() {
|
||||||
|
if let OpenedChannelBuffer::Open(buffer) = buffer {
|
||||||
|
if let Some(buffer) = buffer.upgrade(cx) {
|
||||||
|
buffer.update(cx, |buffer, cx| buffer.disconnect(cx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn update_channels(
|
pub(crate) fn update_channels(
|
||||||
&mut self,
|
&mut self,
|
||||||
payload: proto::UpdateChannels,
|
payload: proto::UpdateChannels,
|
||||||
@ -437,25 +537,30 @@ impl ChannelStore {
|
|||||||
.retain(|channel_id, _| !payload.remove_channels.contains(channel_id));
|
.retain(|channel_id, _| !payload.remove_channels.contains(channel_id));
|
||||||
self.channels_with_admin_privileges
|
self.channels_with_admin_privileges
|
||||||
.retain(|channel_id| !payload.remove_channels.contains(channel_id));
|
.retain(|channel_id| !payload.remove_channels.contains(channel_id));
|
||||||
|
|
||||||
|
for channel_id in &payload.remove_channels {
|
||||||
|
let channel_id = *channel_id;
|
||||||
|
if let Some(OpenedChannelBuffer::Open(buffer)) =
|
||||||
|
self.opened_buffers.remove(&channel_id)
|
||||||
|
{
|
||||||
|
if let Some(buffer) = buffer.upgrade(cx) {
|
||||||
|
buffer.update(cx, ChannelBuffer::disconnect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for channel in payload.channels {
|
for channel_proto in payload.channels {
|
||||||
if let Some(existing_channel) = self.channels_by_id.get_mut(&channel.id) {
|
if let Some(existing_channel) = self.channels_by_id.get_mut(&channel_proto.id) {
|
||||||
// FIXME: We may be missing a path for this existing channel in certain cases
|
Arc::make_mut(existing_channel).name = channel_proto.name;
|
||||||
let existing_channel = Arc::make_mut(existing_channel);
|
} else {
|
||||||
existing_channel.name = channel.name;
|
let channel = Arc::new(Channel {
|
||||||
continue;
|
id: channel_proto.id,
|
||||||
}
|
name: channel_proto.name,
|
||||||
|
});
|
||||||
|
self.channels_by_id.insert(channel.id, channel.clone());
|
||||||
|
|
||||||
self.channels_by_id.insert(
|
if let Some(parent_id) = channel_proto.parent_id {
|
||||||
channel.id,
|
|
||||||
Arc::new(Channel {
|
|
||||||
id: channel.id,
|
|
||||||
name: channel.name,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(parent_id) = channel.parent_id {
|
|
||||||
let mut ix = 0;
|
let mut ix = 0;
|
||||||
while ix < self.channel_paths.len() {
|
while ix < self.channel_paths.len() {
|
||||||
let path = &self.channel_paths[ix];
|
let path = &self.channel_paths[ix];
|
||||||
@ -471,6 +576,7 @@ impl ChannelStore {
|
|||||||
self.channel_paths.push(vec![channel.id]);
|
self.channel_paths.push(vec![channel.id]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.channel_paths.sort_by(|a, b| {
|
self.channel_paths.sort_by(|a, b| {
|
||||||
let a = Self::channel_path_sorting_key(a, &self.channels_by_id);
|
let a = Self::channel_path_sorting_key(a, &self.channels_by_id);
|
@ -1,4 +1,7 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
use client::{Client, UserStore};
|
||||||
|
use gpui::{AppContext, ModelHandle};
|
||||||
|
use rpc::proto;
|
||||||
use util::http::FakeHttpClient;
|
use util::http::FakeHttpClient;
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
@ -17,6 +17,7 @@ db = { path = "../db" }
|
|||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui" }
|
||||||
util = { path = "../util" }
|
util = { path = "../util" }
|
||||||
rpc = { path = "../rpc" }
|
rpc = { path = "../rpc" }
|
||||||
|
text = { path = "../text" }
|
||||||
settings = { path = "../settings" }
|
settings = { path = "../settings" }
|
||||||
staff_mode = { path = "../staff_mode" }
|
staff_mode = { path = "../staff_mode" }
|
||||||
sum_tree = { path = "../sum_tree" }
|
sum_tree = { path = "../sum_tree" }
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub mod test;
|
pub mod test;
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod channel_store_tests;
|
|
||||||
|
|
||||||
pub mod channel_store;
|
|
||||||
pub mod telemetry;
|
pub mod telemetry;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
|
||||||
@ -48,7 +44,6 @@ use util::channel::ReleaseChannel;
|
|||||||
use util::http::HttpClient;
|
use util::http::HttpClient;
|
||||||
use util::{ResultExt, TryFutureExt};
|
use util::{ResultExt, TryFutureExt};
|
||||||
|
|
||||||
pub use channel_store::*;
|
|
||||||
pub use rpc::*;
|
pub use rpc::*;
|
||||||
pub use telemetry::ClickhouseEvent;
|
pub use telemetry::ClickhouseEvent;
|
||||||
pub use user::*;
|
pub use user::*;
|
||||||
|
@ -10,9 +10,11 @@ use std::sync::{Arc, Weak};
|
|||||||
use util::http::HttpClient;
|
use util::http::HttpClient;
|
||||||
use util::TryFutureExt as _;
|
use util::TryFutureExt as _;
|
||||||
|
|
||||||
|
pub type UserId = u64;
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
#[derive(Default, Debug)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub id: u64,
|
pub id: UserId,
|
||||||
pub github_login: String,
|
pub github_login: String,
|
||||||
pub avatar: Option<Arc<ImageData>>,
|
pub avatar: Option<Arc<ImageData>>,
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
|
|||||||
default-run = "collab"
|
default-run = "collab"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
name = "collab"
|
name = "collab"
|
||||||
version = "0.17.0"
|
version = "0.18.0"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
@ -14,8 +14,10 @@ name = "seed"
|
|||||||
required-features = ["seed-support"]
|
required-features = ["seed-support"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
clock = { path = "../clock" }
|
||||||
collections = { path = "../collections" }
|
collections = { path = "../collections" }
|
||||||
live_kit_server = { path = "../live_kit_server" }
|
live_kit_server = { path = "../live_kit_server" }
|
||||||
|
text = { path = "../text" }
|
||||||
rpc = { path = "../rpc" }
|
rpc = { path = "../rpc" }
|
||||||
util = { path = "../util" }
|
util = { path = "../util" }
|
||||||
|
|
||||||
@ -35,6 +37,7 @@ log.workspace = true
|
|||||||
nanoid = "0.4"
|
nanoid = "0.4"
|
||||||
parking_lot.workspace = true
|
parking_lot.workspace = true
|
||||||
prometheus = "0.13"
|
prometheus = "0.13"
|
||||||
|
prost.workspace = true
|
||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
reqwest = { version = "0.11", features = ["json"], optional = true }
|
reqwest = { version = "0.11", features = ["json"], optional = true }
|
||||||
scrypt = "0.7"
|
scrypt = "0.7"
|
||||||
@ -62,6 +65,7 @@ collections = { path = "../collections", features = ["test-support"] }
|
|||||||
gpui = { path = "../gpui", features = ["test-support"] }
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
call = { path = "../call", features = ["test-support"] }
|
call = { path = "../call", features = ["test-support"] }
|
||||||
client = { path = "../client", features = ["test-support"] }
|
client = { path = "../client", features = ["test-support"] }
|
||||||
|
channel = { path = "../channel" }
|
||||||
editor = { path = "../editor", features = ["test-support"] }
|
editor = { path = "../editor", features = ["test-support"] }
|
||||||
language = { path = "../language", features = ["test-support"] }
|
language = { path = "../language", features = ["test-support"] }
|
||||||
fs = { path = "../fs", features = ["test-support"] }
|
fs = { path = "../fs", features = ["test-support"] }
|
||||||
@ -74,6 +78,7 @@ rpc = { path = "../rpc", features = ["test-support"] }
|
|||||||
settings = { path = "../settings", features = ["test-support"] }
|
settings = { path = "../settings", features = ["test-support"] }
|
||||||
theme = { path = "../theme" }
|
theme = { path = "../theme" }
|
||||||
workspace = { path = "../workspace", features = ["test-support"] }
|
workspace = { path = "../workspace", features = ["test-support"] }
|
||||||
|
collab_ui = { path = "../collab_ui", features = ["test-support"] }
|
||||||
|
|
||||||
ctor.workspace = true
|
ctor.workspace = true
|
||||||
env_logger.workspace = true
|
env_logger.workspace = true
|
||||||
|
@ -208,3 +208,44 @@ CREATE TABLE "channel_members" (
|
|||||||
);
|
);
|
||||||
|
|
||||||
CREATE UNIQUE INDEX "index_channel_members_on_channel_id_and_user_id" ON "channel_members" ("channel_id", "user_id");
|
CREATE UNIQUE INDEX "index_channel_members_on_channel_id_and_user_id" ON "channel_members" ("channel_id", "user_id");
|
||||||
|
|
||||||
|
CREATE TABLE "buffers" (
|
||||||
|
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,
|
||||||
|
"epoch" INTEGER NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX "index_buffers_on_channel_id" ON "buffers" ("channel_id");
|
||||||
|
|
||||||
|
CREATE TABLE "buffer_operations" (
|
||||||
|
"buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE,
|
||||||
|
"epoch" INTEGER NOT NULL,
|
||||||
|
"replica_id" INTEGER NOT NULL,
|
||||||
|
"lamport_timestamp" INTEGER NOT NULL,
|
||||||
|
"value" BLOB NOT NULL,
|
||||||
|
PRIMARY KEY(buffer_id, epoch, lamport_timestamp, replica_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "buffer_snapshots" (
|
||||||
|
"buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE,
|
||||||
|
"epoch" INTEGER NOT NULL,
|
||||||
|
"text" TEXT NOT NULL,
|
||||||
|
"operation_serialization_version" INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY(buffer_id, epoch)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "channel_buffer_collaborators" (
|
||||||
|
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,
|
||||||
|
"connection_id" INTEGER NOT NULL,
|
||||||
|
"connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,
|
||||||
|
"connection_lost" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
||||||
|
"replica_id" INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX "index_channel_buffer_collaborators_on_channel_id" ON "channel_buffer_collaborators" ("channel_id");
|
||||||
|
CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_and_replica_id" ON "channel_buffer_collaborators" ("channel_id", "replica_id");
|
||||||
|
CREATE INDEX "index_channel_buffer_collaborators_on_connection_server_id" ON "channel_buffer_collaborators" ("connection_server_id");
|
||||||
|
CREATE INDEX "index_channel_buffer_collaborators_on_connection_id" ON "channel_buffer_collaborators" ("connection_id");
|
||||||
|
CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_connection_id_and_server_id" ON "channel_buffer_collaborators" ("channel_id", "connection_id", "connection_server_id");
|
||||||
|
@ -0,0 +1,40 @@
|
|||||||
|
CREATE TABLE "buffers" (
|
||||||
|
"id" SERIAL PRIMARY KEY,
|
||||||
|
"channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,
|
||||||
|
"epoch" INTEGER NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX "index_buffers_on_channel_id" ON "buffers" ("channel_id");
|
||||||
|
|
||||||
|
CREATE TABLE "buffer_operations" (
|
||||||
|
"buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE,
|
||||||
|
"epoch" INTEGER NOT NULL,
|
||||||
|
"replica_id" INTEGER NOT NULL,
|
||||||
|
"lamport_timestamp" INTEGER NOT NULL,
|
||||||
|
"value" BYTEA NOT NULL,
|
||||||
|
PRIMARY KEY(buffer_id, epoch, lamport_timestamp, replica_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "buffer_snapshots" (
|
||||||
|
"buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE,
|
||||||
|
"epoch" INTEGER NOT NULL,
|
||||||
|
"text" TEXT NOT NULL,
|
||||||
|
"operation_serialization_version" INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY(buffer_id, epoch)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "channel_buffer_collaborators" (
|
||||||
|
"id" SERIAL PRIMARY KEY,
|
||||||
|
"channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,
|
||||||
|
"connection_id" INTEGER NOT NULL,
|
||||||
|
"connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,
|
||||||
|
"connection_lost" BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
"user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
||||||
|
"replica_id" INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX "index_channel_buffer_collaborators_on_channel_id" ON "channel_buffer_collaborators" ("channel_id");
|
||||||
|
CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_and_replica_id" ON "channel_buffer_collaborators" ("channel_id", "replica_id");
|
||||||
|
CREATE INDEX "index_channel_buffer_collaborators_on_connection_server_id" ON "channel_buffer_collaborators" ("connection_server_id");
|
||||||
|
CREATE INDEX "index_channel_buffer_collaborators_on_connection_id" ON "channel_buffer_collaborators" ("connection_id");
|
||||||
|
CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_connection_id_and_server_id" ON "channel_buffer_collaborators" ("channel_id", "connection_id", "connection_server_id");
|
@ -1,7 +1,8 @@
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod db_tests;
|
pub mod tests;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub mod test_db;
|
pub use tests::TestDb;
|
||||||
|
|
||||||
mod ids;
|
mod ids;
|
||||||
mod queries;
|
mod queries;
|
||||||
@ -52,6 +53,8 @@ pub struct Database {
|
|||||||
runtime: Option<tokio::runtime::Runtime>,
|
runtime: Option<tokio::runtime::Runtime>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The `Database` type has so many methods that its impl blocks are split into
|
||||||
|
// separate files in the `queries` folder.
|
||||||
impl Database {
|
impl Database {
|
||||||
pub async fn new(options: ConnectOptions, executor: Executor) -> Result<Self> {
|
pub async fn new(options: ConnectOptions, executor: Executor) -> Result<Self> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
@ -110,6 +110,7 @@ fn value_to_integer(v: Value) -> Result<i32, ValueTypeErr> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
id_type!(BufferId);
|
||||||
id_type!(AccessTokenId);
|
id_type!(AccessTokenId);
|
||||||
id_type!(ChannelId);
|
id_type!(ChannelId);
|
||||||
id_type!(ChannelMemberId);
|
id_type!(ChannelMemberId);
|
||||||
@ -123,3 +124,4 @@ id_type!(ReplicaId);
|
|||||||
id_type!(ServerId);
|
id_type!(ServerId);
|
||||||
id_type!(SignupId);
|
id_type!(SignupId);
|
||||||
id_type!(UserId);
|
id_type!(UserId);
|
||||||
|
id_type!(ChannelBufferCollaboratorId);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
pub mod access_tokens;
|
pub mod access_tokens;
|
||||||
|
pub mod buffers;
|
||||||
pub mod channels;
|
pub mod channels;
|
||||||
pub mod contacts;
|
pub mod contacts;
|
||||||
pub mod projects;
|
pub mod projects;
|
||||||
|
588
crates/collab/src/db/queries/buffers.rs
Normal file
588
crates/collab/src/db/queries/buffers.rs
Normal file
@ -0,0 +1,588 @@
|
|||||||
|
use super::*;
|
||||||
|
use prost::Message;
|
||||||
|
use text::{EditOperation, InsertionTimestamp, UndoOperation};
|
||||||
|
|
||||||
|
impl Database {
|
||||||
|
pub async fn join_channel_buffer(
|
||||||
|
&self,
|
||||||
|
channel_id: ChannelId,
|
||||||
|
user_id: UserId,
|
||||||
|
connection: ConnectionId,
|
||||||
|
) -> Result<proto::JoinChannelBufferResponse> {
|
||||||
|
self.transaction(|tx| async move {
|
||||||
|
let tx = tx;
|
||||||
|
|
||||||
|
self.check_user_is_channel_member(channel_id, user_id, &tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let buffer = channel::Model {
|
||||||
|
id: channel_id,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.find_related(buffer::Entity)
|
||||||
|
.one(&*tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let buffer = if let Some(buffer) = buffer {
|
||||||
|
buffer
|
||||||
|
} else {
|
||||||
|
let buffer = buffer::ActiveModel {
|
||||||
|
channel_id: ActiveValue::Set(channel_id),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.insert(&*tx)
|
||||||
|
.await?;
|
||||||
|
buffer_snapshot::ActiveModel {
|
||||||
|
buffer_id: ActiveValue::Set(buffer.id),
|
||||||
|
epoch: ActiveValue::Set(0),
|
||||||
|
text: ActiveValue::Set(String::new()),
|
||||||
|
operation_serialization_version: ActiveValue::Set(
|
||||||
|
storage::SERIALIZATION_VERSION,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
.insert(&*tx)
|
||||||
|
.await?;
|
||||||
|
buffer
|
||||||
|
};
|
||||||
|
|
||||||
|
// Join the collaborators
|
||||||
|
let mut collaborators = channel_buffer_collaborator::Entity::find()
|
||||||
|
.filter(channel_buffer_collaborator::Column::ChannelId.eq(channel_id))
|
||||||
|
.all(&*tx)
|
||||||
|
.await?;
|
||||||
|
let replica_ids = collaborators
|
||||||
|
.iter()
|
||||||
|
.map(|c| c.replica_id)
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
let mut replica_id = ReplicaId(0);
|
||||||
|
while replica_ids.contains(&replica_id) {
|
||||||
|
replica_id.0 += 1;
|
||||||
|
}
|
||||||
|
let collaborator = channel_buffer_collaborator::ActiveModel {
|
||||||
|
channel_id: ActiveValue::Set(channel_id),
|
||||||
|
connection_id: ActiveValue::Set(connection.id as i32),
|
||||||
|
connection_server_id: ActiveValue::Set(ServerId(connection.owner_id as i32)),
|
||||||
|
user_id: ActiveValue::Set(user_id),
|
||||||
|
replica_id: ActiveValue::Set(replica_id),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.insert(&*tx)
|
||||||
|
.await?;
|
||||||
|
collaborators.push(collaborator);
|
||||||
|
|
||||||
|
// Assemble the buffer state
|
||||||
|
let (base_text, operations) = self.get_buffer_state(&buffer, &tx).await?;
|
||||||
|
|
||||||
|
Ok(proto::JoinChannelBufferResponse {
|
||||||
|
buffer_id: buffer.id.to_proto(),
|
||||||
|
replica_id: replica_id.to_proto() as u32,
|
||||||
|
base_text,
|
||||||
|
operations,
|
||||||
|
collaborators: collaborators
|
||||||
|
.into_iter()
|
||||||
|
.map(|collaborator| proto::Collaborator {
|
||||||
|
peer_id: Some(collaborator.connection().into()),
|
||||||
|
user_id: collaborator.user_id.to_proto(),
|
||||||
|
replica_id: collaborator.replica_id.0 as u32,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn leave_channel_buffer(
|
||||||
|
&self,
|
||||||
|
channel_id: ChannelId,
|
||||||
|
connection: ConnectionId,
|
||||||
|
) -> Result<Vec<ConnectionId>> {
|
||||||
|
self.transaction(|tx| async move {
|
||||||
|
self.leave_channel_buffer_internal(channel_id, connection, &*tx)
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn leave_channel_buffer_internal(
|
||||||
|
&self,
|
||||||
|
channel_id: ChannelId,
|
||||||
|
connection: ConnectionId,
|
||||||
|
tx: &DatabaseTransaction,
|
||||||
|
) -> Result<Vec<ConnectionId>> {
|
||||||
|
let result = channel_buffer_collaborator::Entity::delete_many()
|
||||||
|
.filter(
|
||||||
|
Condition::all()
|
||||||
|
.add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id))
|
||||||
|
.add(channel_buffer_collaborator::Column::ConnectionId.eq(connection.id as i32))
|
||||||
|
.add(
|
||||||
|
channel_buffer_collaborator::Column::ConnectionServerId
|
||||||
|
.eq(connection.owner_id as i32),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.exec(&*tx)
|
||||||
|
.await?;
|
||||||
|
if result.rows_affected == 0 {
|
||||||
|
Err(anyhow!("not a collaborator on this project"))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut connections = Vec::new();
|
||||||
|
let mut rows = channel_buffer_collaborator::Entity::find()
|
||||||
|
.filter(
|
||||||
|
Condition::all().add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)),
|
||||||
|
)
|
||||||
|
.stream(&*tx)
|
||||||
|
.await?;
|
||||||
|
while let Some(row) = rows.next().await {
|
||||||
|
let row = row?;
|
||||||
|
connections.push(ConnectionId {
|
||||||
|
id: row.connection_id as u32,
|
||||||
|
owner_id: row.connection_server_id.0 as u32,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
drop(rows);
|
||||||
|
|
||||||
|
if connections.is_empty() {
|
||||||
|
self.snapshot_buffer(channel_id, &tx).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(connections)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn leave_channel_buffers(
|
||||||
|
&self,
|
||||||
|
connection: ConnectionId,
|
||||||
|
) -> Result<Vec<(ChannelId, Vec<ConnectionId>)>> {
|
||||||
|
self.transaction(|tx| async move {
|
||||||
|
#[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)]
|
||||||
|
enum QueryChannelIds {
|
||||||
|
ChannelId,
|
||||||
|
}
|
||||||
|
|
||||||
|
let channel_ids: Vec<ChannelId> = channel_buffer_collaborator::Entity::find()
|
||||||
|
.select_only()
|
||||||
|
.column(channel_buffer_collaborator::Column::ChannelId)
|
||||||
|
.filter(Condition::all().add(
|
||||||
|
channel_buffer_collaborator::Column::ConnectionId.eq(connection.id as i32),
|
||||||
|
))
|
||||||
|
.into_values::<_, QueryChannelIds>()
|
||||||
|
.all(&*tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut result = Vec::new();
|
||||||
|
for channel_id in channel_ids {
|
||||||
|
let collaborators = self
|
||||||
|
.leave_channel_buffer_internal(channel_id, connection, &*tx)
|
||||||
|
.await?;
|
||||||
|
result.push((channel_id, collaborators));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
pub async fn get_channel_buffer_collaborators(
|
||||||
|
&self,
|
||||||
|
channel_id: ChannelId,
|
||||||
|
) -> Result<Vec<UserId>> {
|
||||||
|
self.transaction(|tx| async move {
|
||||||
|
#[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)]
|
||||||
|
enum QueryUserIds {
|
||||||
|
UserId,
|
||||||
|
}
|
||||||
|
|
||||||
|
let users: Vec<UserId> = channel_buffer_collaborator::Entity::find()
|
||||||
|
.select_only()
|
||||||
|
.column(channel_buffer_collaborator::Column::UserId)
|
||||||
|
.filter(
|
||||||
|
Condition::all()
|
||||||
|
.add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)),
|
||||||
|
)
|
||||||
|
.into_values::<_, QueryUserIds>()
|
||||||
|
.all(&*tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(users)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_channel_buffer(
|
||||||
|
&self,
|
||||||
|
channel_id: ChannelId,
|
||||||
|
user: UserId,
|
||||||
|
operations: &[proto::Operation],
|
||||||
|
) -> Result<Vec<ConnectionId>> {
|
||||||
|
self.transaction(move |tx| async move {
|
||||||
|
self.check_user_is_channel_member(channel_id, user, &*tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let buffer = buffer::Entity::find()
|
||||||
|
.filter(buffer::Column::ChannelId.eq(channel_id))
|
||||||
|
.one(&*tx)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| anyhow!("no such buffer"))?;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)]
|
||||||
|
enum QueryVersion {
|
||||||
|
OperationSerializationVersion,
|
||||||
|
}
|
||||||
|
|
||||||
|
let serialization_version: i32 = buffer
|
||||||
|
.find_related(buffer_snapshot::Entity)
|
||||||
|
.select_only()
|
||||||
|
.column(buffer_snapshot::Column::OperationSerializationVersion)
|
||||||
|
.filter(buffer_snapshot::Column::Epoch.eq(buffer.epoch))
|
||||||
|
.into_values::<_, QueryVersion>()
|
||||||
|
.one(&*tx)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| anyhow!("missing buffer snapshot"))?;
|
||||||
|
|
||||||
|
let operations = operations
|
||||||
|
.iter()
|
||||||
|
.filter_map(|op| operation_to_storage(op, &buffer, serialization_version))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if !operations.is_empty() {
|
||||||
|
buffer_operation::Entity::insert_many(operations)
|
||||||
|
.exec(&*tx)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut connections = Vec::new();
|
||||||
|
let mut rows = channel_buffer_collaborator::Entity::find()
|
||||||
|
.filter(
|
||||||
|
Condition::all()
|
||||||
|
.add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)),
|
||||||
|
)
|
||||||
|
.stream(&*tx)
|
||||||
|
.await?;
|
||||||
|
while let Some(row) = rows.next().await {
|
||||||
|
let row = row?;
|
||||||
|
connections.push(ConnectionId {
|
||||||
|
id: row.connection_id as u32,
|
||||||
|
owner_id: row.connection_server_id.0 as u32,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(connections)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_buffer_state(
|
||||||
|
&self,
|
||||||
|
buffer: &buffer::Model,
|
||||||
|
tx: &DatabaseTransaction,
|
||||||
|
) -> Result<(String, Vec<proto::Operation>)> {
|
||||||
|
let id = buffer.id;
|
||||||
|
let (base_text, version) = if buffer.epoch > 0 {
|
||||||
|
let snapshot = buffer_snapshot::Entity::find()
|
||||||
|
.filter(
|
||||||
|
buffer_snapshot::Column::BufferId
|
||||||
|
.eq(id)
|
||||||
|
.and(buffer_snapshot::Column::Epoch.eq(buffer.epoch)),
|
||||||
|
)
|
||||||
|
.one(&*tx)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| anyhow!("no such snapshot"))?;
|
||||||
|
|
||||||
|
let version = snapshot.operation_serialization_version;
|
||||||
|
(snapshot.text, version)
|
||||||
|
} else {
|
||||||
|
(String::new(), storage::SERIALIZATION_VERSION)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut rows = buffer_operation::Entity::find()
|
||||||
|
.filter(
|
||||||
|
buffer_operation::Column::BufferId
|
||||||
|
.eq(id)
|
||||||
|
.and(buffer_operation::Column::Epoch.eq(buffer.epoch)),
|
||||||
|
)
|
||||||
|
.stream(&*tx)
|
||||||
|
.await?;
|
||||||
|
let mut operations = Vec::new();
|
||||||
|
while let Some(row) = rows.next().await {
|
||||||
|
let row = row?;
|
||||||
|
|
||||||
|
let operation = operation_from_storage(row, version)?;
|
||||||
|
operations.push(proto::Operation {
|
||||||
|
variant: Some(operation),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((base_text, operations))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn snapshot_buffer(&self, channel_id: ChannelId, tx: &DatabaseTransaction) -> Result<()> {
|
||||||
|
let buffer = channel::Model {
|
||||||
|
id: channel_id,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.find_related(buffer::Entity)
|
||||||
|
.one(&*tx)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| anyhow!("no such buffer"))?;
|
||||||
|
|
||||||
|
let (base_text, operations) = self.get_buffer_state(&buffer, tx).await?;
|
||||||
|
if operations.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut text_buffer = text::Buffer::new(0, 0, base_text);
|
||||||
|
text_buffer
|
||||||
|
.apply_ops(operations.into_iter().filter_map(operation_from_wire))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let base_text = text_buffer.text();
|
||||||
|
let epoch = buffer.epoch + 1;
|
||||||
|
|
||||||
|
buffer_snapshot::Model {
|
||||||
|
buffer_id: buffer.id,
|
||||||
|
epoch,
|
||||||
|
text: base_text,
|
||||||
|
operation_serialization_version: storage::SERIALIZATION_VERSION,
|
||||||
|
}
|
||||||
|
.into_active_model()
|
||||||
|
.insert(tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
buffer::ActiveModel {
|
||||||
|
id: ActiveValue::Unchanged(buffer.id),
|
||||||
|
epoch: ActiveValue::Set(epoch),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.save(tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn operation_to_storage(
|
||||||
|
operation: &proto::Operation,
|
||||||
|
buffer: &buffer::Model,
|
||||||
|
_format: i32,
|
||||||
|
) -> Option<buffer_operation::ActiveModel> {
|
||||||
|
let (replica_id, lamport_timestamp, value) = match operation.variant.as_ref()? {
|
||||||
|
proto::operation::Variant::Edit(operation) => (
|
||||||
|
operation.replica_id,
|
||||||
|
operation.lamport_timestamp,
|
||||||
|
storage::Operation {
|
||||||
|
local_timestamp: operation.local_timestamp,
|
||||||
|
version: version_to_storage(&operation.version),
|
||||||
|
is_undo: false,
|
||||||
|
edit_ranges: operation
|
||||||
|
.ranges
|
||||||
|
.iter()
|
||||||
|
.map(|range| storage::Range {
|
||||||
|
start: range.start,
|
||||||
|
end: range.end,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
edit_texts: operation.new_text.clone(),
|
||||||
|
undo_counts: Vec::new(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
proto::operation::Variant::Undo(operation) => (
|
||||||
|
operation.replica_id,
|
||||||
|
operation.lamport_timestamp,
|
||||||
|
storage::Operation {
|
||||||
|
local_timestamp: operation.local_timestamp,
|
||||||
|
version: version_to_storage(&operation.version),
|
||||||
|
is_undo: true,
|
||||||
|
edit_ranges: Vec::new(),
|
||||||
|
edit_texts: Vec::new(),
|
||||||
|
undo_counts: operation
|
||||||
|
.counts
|
||||||
|
.iter()
|
||||||
|
.map(|entry| storage::UndoCount {
|
||||||
|
replica_id: entry.replica_id,
|
||||||
|
local_timestamp: entry.local_timestamp,
|
||||||
|
count: entry.count,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_ => None?,
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(buffer_operation::ActiveModel {
|
||||||
|
buffer_id: ActiveValue::Set(buffer.id),
|
||||||
|
epoch: ActiveValue::Set(buffer.epoch),
|
||||||
|
replica_id: ActiveValue::Set(replica_id as i32),
|
||||||
|
lamport_timestamp: ActiveValue::Set(lamport_timestamp as i32),
|
||||||
|
value: ActiveValue::Set(value.encode_to_vec()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn operation_from_storage(
|
||||||
|
row: buffer_operation::Model,
|
||||||
|
_format_version: i32,
|
||||||
|
) -> Result<proto::operation::Variant, Error> {
|
||||||
|
let operation =
|
||||||
|
storage::Operation::decode(row.value.as_slice()).map_err(|error| anyhow!("{}", error))?;
|
||||||
|
let version = version_from_storage(&operation.version);
|
||||||
|
Ok(if operation.is_undo {
|
||||||
|
proto::operation::Variant::Undo(proto::operation::Undo {
|
||||||
|
replica_id: row.replica_id as u32,
|
||||||
|
local_timestamp: operation.local_timestamp as u32,
|
||||||
|
lamport_timestamp: row.lamport_timestamp as u32,
|
||||||
|
version,
|
||||||
|
counts: operation
|
||||||
|
.undo_counts
|
||||||
|
.iter()
|
||||||
|
.map(|entry| proto::UndoCount {
|
||||||
|
replica_id: entry.replica_id,
|
||||||
|
local_timestamp: entry.local_timestamp,
|
||||||
|
count: entry.count,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
proto::operation::Variant::Edit(proto::operation::Edit {
|
||||||
|
replica_id: row.replica_id as u32,
|
||||||
|
local_timestamp: operation.local_timestamp as u32,
|
||||||
|
lamport_timestamp: row.lamport_timestamp as u32,
|
||||||
|
version,
|
||||||
|
ranges: operation
|
||||||
|
.edit_ranges
|
||||||
|
.into_iter()
|
||||||
|
.map(|range| proto::Range {
|
||||||
|
start: range.start,
|
||||||
|
end: range.end,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
new_text: operation.edit_texts,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn version_to_storage(version: &Vec<proto::VectorClockEntry>) -> Vec<storage::VectorClockEntry> {
|
||||||
|
version
|
||||||
|
.iter()
|
||||||
|
.map(|entry| storage::VectorClockEntry {
|
||||||
|
replica_id: entry.replica_id,
|
||||||
|
timestamp: entry.timestamp,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn version_from_storage(version: &Vec<storage::VectorClockEntry>) -> Vec<proto::VectorClockEntry> {
|
||||||
|
version
|
||||||
|
.iter()
|
||||||
|
.map(|entry| proto::VectorClockEntry {
|
||||||
|
replica_id: entry.replica_id,
|
||||||
|
timestamp: entry.timestamp,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is currently a manual copy of the deserialization code in the client's langauge crate
|
||||||
|
pub fn operation_from_wire(operation: proto::Operation) -> Option<text::Operation> {
|
||||||
|
match operation.variant? {
|
||||||
|
proto::operation::Variant::Edit(edit) => Some(text::Operation::Edit(EditOperation {
|
||||||
|
timestamp: InsertionTimestamp {
|
||||||
|
replica_id: edit.replica_id as text::ReplicaId,
|
||||||
|
local: edit.local_timestamp,
|
||||||
|
lamport: edit.lamport_timestamp,
|
||||||
|
},
|
||||||
|
version: version_from_wire(&edit.version),
|
||||||
|
ranges: edit
|
||||||
|
.ranges
|
||||||
|
.into_iter()
|
||||||
|
.map(|range| {
|
||||||
|
text::FullOffset(range.start as usize)..text::FullOffset(range.end as usize)
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
new_text: edit.new_text.into_iter().map(Arc::from).collect(),
|
||||||
|
})),
|
||||||
|
proto::operation::Variant::Undo(undo) => Some(text::Operation::Undo {
|
||||||
|
lamport_timestamp: clock::Lamport {
|
||||||
|
replica_id: undo.replica_id as text::ReplicaId,
|
||||||
|
value: undo.lamport_timestamp,
|
||||||
|
},
|
||||||
|
undo: UndoOperation {
|
||||||
|
id: clock::Local {
|
||||||
|
replica_id: undo.replica_id as text::ReplicaId,
|
||||||
|
value: undo.local_timestamp,
|
||||||
|
},
|
||||||
|
version: version_from_wire(&undo.version),
|
||||||
|
counts: undo
|
||||||
|
.counts
|
||||||
|
.into_iter()
|
||||||
|
.map(|c| {
|
||||||
|
(
|
||||||
|
clock::Local {
|
||||||
|
replica_id: c.replica_id as text::ReplicaId,
|
||||||
|
value: c.local_timestamp,
|
||||||
|
},
|
||||||
|
c.count,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn version_from_wire(message: &[proto::VectorClockEntry]) -> clock::Global {
|
||||||
|
let mut version = clock::Global::new();
|
||||||
|
for entry in message {
|
||||||
|
version.observe(clock::Local {
|
||||||
|
replica_id: entry.replica_id as text::ReplicaId,
|
||||||
|
value: entry.timestamp,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
version
|
||||||
|
}
|
||||||
|
|
||||||
|
mod storage {
|
||||||
|
#![allow(non_snake_case)]
|
||||||
|
use prost::Message;
|
||||||
|
pub const SERIALIZATION_VERSION: i32 = 1;
|
||||||
|
|
||||||
|
#[derive(Message)]
|
||||||
|
pub struct Operation {
|
||||||
|
#[prost(uint32, tag = "1")]
|
||||||
|
pub local_timestamp: u32,
|
||||||
|
#[prost(message, repeated, tag = "2")]
|
||||||
|
pub version: Vec<VectorClockEntry>,
|
||||||
|
#[prost(bool, tag = "3")]
|
||||||
|
pub is_undo: bool,
|
||||||
|
#[prost(message, repeated, tag = "4")]
|
||||||
|
pub edit_ranges: Vec<Range>,
|
||||||
|
#[prost(string, repeated, tag = "5")]
|
||||||
|
pub edit_texts: Vec<String>,
|
||||||
|
#[prost(message, repeated, tag = "6")]
|
||||||
|
pub undo_counts: Vec<UndoCount>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Message)]
|
||||||
|
pub struct VectorClockEntry {
|
||||||
|
#[prost(uint32, tag = "1")]
|
||||||
|
pub replica_id: u32,
|
||||||
|
#[prost(uint32, tag = "2")]
|
||||||
|
pub timestamp: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Message)]
|
||||||
|
pub struct Range {
|
||||||
|
#[prost(uint64, tag = "1")]
|
||||||
|
pub start: u64,
|
||||||
|
#[prost(uint64, tag = "2")]
|
||||||
|
pub end: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Message)]
|
||||||
|
pub struct UndoCount {
|
||||||
|
#[prost(uint32, tag = "1")]
|
||||||
|
pub replica_id: u32,
|
||||||
|
#[prost(uint32, tag = "2")]
|
||||||
|
pub local_timestamp: u32,
|
||||||
|
#[prost(uint32, tag = "3")]
|
||||||
|
pub count: u32,
|
||||||
|
}
|
||||||
|
}
|
@ -903,15 +903,35 @@ impl Database {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.one(&*tx)
|
.one(&*tx)
|
||||||
.await?
|
.await?;
|
||||||
.ok_or_else(|| anyhow!("not a participant in any room"))?;
|
|
||||||
|
|
||||||
|
if let Some(participant) = participant {
|
||||||
room_participant::Entity::update(room_participant::ActiveModel {
|
room_participant::Entity::update(room_participant::ActiveModel {
|
||||||
answering_connection_lost: ActiveValue::set(true),
|
answering_connection_lost: ActiveValue::set(true),
|
||||||
..participant.into_active_model()
|
..participant.into_active_model()
|
||||||
})
|
})
|
||||||
.exec(&*tx)
|
.exec(&*tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
channel_buffer_collaborator::Entity::update_many()
|
||||||
|
.filter(
|
||||||
|
Condition::all()
|
||||||
|
.add(
|
||||||
|
channel_buffer_collaborator::Column::ConnectionId
|
||||||
|
.eq(connection.id as i32),
|
||||||
|
)
|
||||||
|
.add(
|
||||||
|
channel_buffer_collaborator::Column::ConnectionServerId
|
||||||
|
.eq(connection.owner_id as i32),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.set(channel_buffer_collaborator::ActiveModel {
|
||||||
|
connection_lost: ActiveValue::set(true),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.exec(&*tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
pub mod access_token;
|
pub mod access_token;
|
||||||
|
pub mod buffer;
|
||||||
|
pub mod buffer_operation;
|
||||||
|
pub mod buffer_snapshot;
|
||||||
pub mod channel;
|
pub mod channel;
|
||||||
|
pub mod channel_buffer_collaborator;
|
||||||
pub mod channel_member;
|
pub mod channel_member;
|
||||||
pub mod channel_path;
|
pub mod channel_path;
|
||||||
pub mod contact;
|
pub mod contact;
|
||||||
|
45
crates/collab/src/db/tables/buffer.rs
Normal file
45
crates/collab/src/db/tables/buffer.rs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
use crate::db::{BufferId, ChannelId};
|
||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||||
|
#[sea_orm(table_name = "buffers")]
|
||||||
|
pub struct Model {
|
||||||
|
#[sea_orm(primary_key)]
|
||||||
|
pub id: BufferId,
|
||||||
|
pub epoch: i32,
|
||||||
|
pub channel_id: ChannelId,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
pub enum Relation {
|
||||||
|
#[sea_orm(has_many = "super::buffer_operation::Entity")]
|
||||||
|
Operations,
|
||||||
|
#[sea_orm(has_many = "super::buffer_snapshot::Entity")]
|
||||||
|
Snapshots,
|
||||||
|
#[sea_orm(
|
||||||
|
belongs_to = "super::channel::Entity",
|
||||||
|
from = "Column::ChannelId",
|
||||||
|
to = "super::channel::Column::Id"
|
||||||
|
)]
|
||||||
|
Channel,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::buffer_operation::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::Operations.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::buffer_snapshot::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::Snapshots.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::channel::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::Channel.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActiveModelBehavior for ActiveModel {}
|
34
crates/collab/src/db/tables/buffer_operation.rs
Normal file
34
crates/collab/src/db/tables/buffer_operation.rs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
use crate::db::BufferId;
|
||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||||
|
#[sea_orm(table_name = "buffer_operations")]
|
||||||
|
pub struct Model {
|
||||||
|
#[sea_orm(primary_key)]
|
||||||
|
pub buffer_id: BufferId,
|
||||||
|
#[sea_orm(primary_key)]
|
||||||
|
pub epoch: i32,
|
||||||
|
#[sea_orm(primary_key)]
|
||||||
|
pub lamport_timestamp: i32,
|
||||||
|
#[sea_orm(primary_key)]
|
||||||
|
pub replica_id: i32,
|
||||||
|
pub value: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
pub enum Relation {
|
||||||
|
#[sea_orm(
|
||||||
|
belongs_to = "super::buffer::Entity",
|
||||||
|
from = "Column::BufferId",
|
||||||
|
to = "super::buffer::Column::Id"
|
||||||
|
)]
|
||||||
|
Buffer,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::buffer::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::Buffer.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActiveModelBehavior for ActiveModel {}
|
31
crates/collab/src/db/tables/buffer_snapshot.rs
Normal file
31
crates/collab/src/db/tables/buffer_snapshot.rs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
use crate::db::BufferId;
|
||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||||
|
#[sea_orm(table_name = "buffer_snapshots")]
|
||||||
|
pub struct Model {
|
||||||
|
#[sea_orm(primary_key)]
|
||||||
|
pub buffer_id: BufferId,
|
||||||
|
#[sea_orm(primary_key)]
|
||||||
|
pub epoch: i32,
|
||||||
|
pub text: String,
|
||||||
|
pub operation_serialization_version: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
pub enum Relation {
|
||||||
|
#[sea_orm(
|
||||||
|
belongs_to = "super::buffer::Entity",
|
||||||
|
from = "Column::BufferId",
|
||||||
|
to = "super::buffer::Column::Id"
|
||||||
|
)]
|
||||||
|
Buffer,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::buffer::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::Buffer.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActiveModelBehavior for ActiveModel {}
|
@ -15,8 +15,12 @@ impl ActiveModelBehavior for ActiveModel {}
|
|||||||
pub enum Relation {
|
pub enum Relation {
|
||||||
#[sea_orm(has_one = "super::room::Entity")]
|
#[sea_orm(has_one = "super::room::Entity")]
|
||||||
Room,
|
Room,
|
||||||
|
#[sea_orm(has_one = "super::buffer::Entity")]
|
||||||
|
Buffer,
|
||||||
#[sea_orm(has_many = "super::channel_member::Entity")]
|
#[sea_orm(has_many = "super::channel_member::Entity")]
|
||||||
Member,
|
Member,
|
||||||
|
#[sea_orm(has_many = "super::channel_buffer_collaborator::Entity")]
|
||||||
|
BufferCollaborators,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Related<super::channel_member::Entity> for Entity {
|
impl Related<super::channel_member::Entity> for Entity {
|
||||||
@ -30,3 +34,15 @@ impl Related<super::room::Entity> for Entity {
|
|||||||
Relation::Room.def()
|
Relation::Room.def()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Related<super::buffer::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::Buffer.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::channel_buffer_collaborator::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::BufferCollaborators.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
43
crates/collab/src/db/tables/channel_buffer_collaborator.rs
Normal file
43
crates/collab/src/db/tables/channel_buffer_collaborator.rs
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
use crate::db::{ChannelBufferCollaboratorId, ChannelId, ReplicaId, ServerId, UserId};
|
||||||
|
use rpc::ConnectionId;
|
||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||||
|
#[sea_orm(table_name = "channel_buffer_collaborators")]
|
||||||
|
pub struct Model {
|
||||||
|
#[sea_orm(primary_key)]
|
||||||
|
pub id: ChannelBufferCollaboratorId,
|
||||||
|
pub channel_id: ChannelId,
|
||||||
|
pub connection_id: i32,
|
||||||
|
pub connection_server_id: ServerId,
|
||||||
|
pub connection_lost: bool,
|
||||||
|
pub user_id: UserId,
|
||||||
|
pub replica_id: ReplicaId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Model {
|
||||||
|
pub fn connection(&self) -> ConnectionId {
|
||||||
|
ConnectionId {
|
||||||
|
owner_id: self.connection_server_id.0 as u32,
|
||||||
|
id: self.connection_id as u32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
pub enum Relation {
|
||||||
|
#[sea_orm(
|
||||||
|
belongs_to = "super::channel::Entity",
|
||||||
|
from = "Column::ChannelId",
|
||||||
|
to = "super::channel::Column::Id"
|
||||||
|
)]
|
||||||
|
Channel,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::channel::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::Channel.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActiveModelBehavior for ActiveModel {}
|
@ -1,3 +1,6 @@
|
|||||||
|
mod buffer_tests;
|
||||||
|
mod db_tests;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use gpui::executor::Background;
|
use gpui::executor::Background;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
@ -91,6 +94,26 @@ impl TestDb {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! test_both_dbs {
|
||||||
|
($test_name:ident, $postgres_test_name:ident, $sqlite_test_name:ident) => {
|
||||||
|
#[gpui::test]
|
||||||
|
async fn $postgres_test_name() {
|
||||||
|
let test_db = crate::db::TestDb::postgres(
|
||||||
|
gpui::executor::Deterministic::new(0).build_background(),
|
||||||
|
);
|
||||||
|
$test_name(test_db.db()).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn $sqlite_test_name() {
|
||||||
|
let test_db =
|
||||||
|
crate::db::TestDb::sqlite(gpui::executor::Deterministic::new(0).build_background());
|
||||||
|
$test_name(test_db.db()).await;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
impl Drop for TestDb {
|
impl Drop for TestDb {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
let db = self.db.take().unwrap();
|
let db = self.db.take().unwrap();
|
165
crates/collab/src/db/tests/buffer_tests.rs
Normal file
165
crates/collab/src/db/tests/buffer_tests.rs
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
use super::*;
|
||||||
|
use crate::test_both_dbs;
|
||||||
|
use language::proto;
|
||||||
|
use text::Buffer;
|
||||||
|
|
||||||
|
test_both_dbs!(
|
||||||
|
test_channel_buffers,
|
||||||
|
test_channel_buffers_postgres,
|
||||||
|
test_channel_buffers_sqlite
|
||||||
|
);
|
||||||
|
|
||||||
|
async fn test_channel_buffers(db: &Arc<Database>) {
|
||||||
|
let a_id = db
|
||||||
|
.create_user(
|
||||||
|
"user_a@example.com",
|
||||||
|
false,
|
||||||
|
NewUserParams {
|
||||||
|
github_login: "user_a".into(),
|
||||||
|
github_user_id: 101,
|
||||||
|
invite_count: 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.user_id;
|
||||||
|
let b_id = db
|
||||||
|
.create_user(
|
||||||
|
"user_b@example.com",
|
||||||
|
false,
|
||||||
|
NewUserParams {
|
||||||
|
github_login: "user_b".into(),
|
||||||
|
github_user_id: 102,
|
||||||
|
invite_count: 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.user_id;
|
||||||
|
|
||||||
|
// This user will not be a part of the channel
|
||||||
|
let c_id = db
|
||||||
|
.create_user(
|
||||||
|
"user_c@example.com",
|
||||||
|
false,
|
||||||
|
NewUserParams {
|
||||||
|
github_login: "user_c".into(),
|
||||||
|
github_user_id: 102,
|
||||||
|
invite_count: 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.user_id;
|
||||||
|
|
||||||
|
let owner_id = db.create_server("production").await.unwrap().0 as u32;
|
||||||
|
|
||||||
|
let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap();
|
||||||
|
|
||||||
|
db.invite_channel_member(zed_id, b_id, a_id, false)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
db.respond_to_channel_invite(zed_id, b_id, true)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let connection_id_a = ConnectionId { owner_id, id: 1 };
|
||||||
|
let _ = db
|
||||||
|
.join_channel_buffer(zed_id, a_id, connection_id_a)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut buffer_a = Buffer::new(0, 0, "".to_string());
|
||||||
|
let mut operations = Vec::new();
|
||||||
|
operations.push(buffer_a.edit([(0..0, "hello world")]));
|
||||||
|
operations.push(buffer_a.edit([(5..5, ", cruel")]));
|
||||||
|
operations.push(buffer_a.edit([(0..5, "goodbye")]));
|
||||||
|
operations.push(buffer_a.undo().unwrap().1);
|
||||||
|
assert_eq!(buffer_a.text(), "hello, cruel world");
|
||||||
|
|
||||||
|
let operations = operations
|
||||||
|
.into_iter()
|
||||||
|
.map(|op| proto::serialize_operation(&language::Operation::Buffer(op)))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
db.update_channel_buffer(zed_id, a_id, &operations)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let connection_id_b = ConnectionId { owner_id, id: 2 };
|
||||||
|
let buffer_response_b = db
|
||||||
|
.join_channel_buffer(zed_id, b_id, connection_id_b)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut buffer_b = Buffer::new(0, 0, buffer_response_b.base_text);
|
||||||
|
buffer_b
|
||||||
|
.apply_ops(buffer_response_b.operations.into_iter().map(|operation| {
|
||||||
|
let operation = proto::deserialize_operation(operation).unwrap();
|
||||||
|
if let language::Operation::Buffer(operation) = operation {
|
||||||
|
operation
|
||||||
|
} else {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(buffer_b.text(), "hello, cruel world");
|
||||||
|
|
||||||
|
// Ensure that C fails to open the buffer
|
||||||
|
assert!(db
|
||||||
|
.join_channel_buffer(zed_id, c_id, ConnectionId { owner_id, id: 3 })
|
||||||
|
.await
|
||||||
|
.is_err());
|
||||||
|
|
||||||
|
// Ensure that both collaborators have shown up
|
||||||
|
assert_eq!(
|
||||||
|
buffer_response_b.collaborators,
|
||||||
|
&[
|
||||||
|
rpc::proto::Collaborator {
|
||||||
|
user_id: a_id.to_proto(),
|
||||||
|
peer_id: Some(rpc::proto::PeerId { id: 1, owner_id }),
|
||||||
|
replica_id: 0,
|
||||||
|
},
|
||||||
|
rpc::proto::Collaborator {
|
||||||
|
user_id: b_id.to_proto(),
|
||||||
|
peer_id: Some(rpc::proto::PeerId { id: 2, owner_id }),
|
||||||
|
replica_id: 1,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ensure that get_channel_buffer_collaborators works
|
||||||
|
let zed_collaborats = db.get_channel_buffer_collaborators(zed_id).await.unwrap();
|
||||||
|
assert_eq!(zed_collaborats, &[a_id, b_id]);
|
||||||
|
|
||||||
|
let collaborators = db
|
||||||
|
.leave_channel_buffer(zed_id, connection_id_b)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(collaborators, &[connection_id_a],);
|
||||||
|
|
||||||
|
let cargo_id = db.create_root_channel("cargo", "2", a_id).await.unwrap();
|
||||||
|
let _ = db
|
||||||
|
.join_channel_buffer(cargo_id, a_id, connection_id_a)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
db.leave_channel_buffers(connection_id_a).await.unwrap();
|
||||||
|
|
||||||
|
let zed_collaborators = db.get_channel_buffer_collaborators(zed_id).await.unwrap();
|
||||||
|
let cargo_collaborators = db.get_channel_buffer_collaborators(cargo_id).await.unwrap();
|
||||||
|
assert_eq!(zed_collaborators, &[]);
|
||||||
|
assert_eq!(cargo_collaborators, &[]);
|
||||||
|
|
||||||
|
// When everyone has left the channel, the operations are collapsed into
|
||||||
|
// a new base text.
|
||||||
|
let buffer_response_b = db
|
||||||
|
.join_channel_buffer(zed_id, b_id, connection_id_b)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(buffer_response_b.base_text, "hello, cruel world");
|
||||||
|
assert_eq!(buffer_response_b.operations, &[]);
|
||||||
|
}
|
@ -1,32 +1,17 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::test_both_dbs;
|
||||||
use gpui::executor::{Background, Deterministic};
|
use gpui::executor::{Background, Deterministic};
|
||||||
use pretty_assertions::{assert_eq, assert_ne};
|
use pretty_assertions::{assert_eq, assert_ne};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use test_db::TestDb;
|
use tests::TestDb;
|
||||||
|
|
||||||
macro_rules! test_both_dbs {
|
|
||||||
($postgres_test_name:ident, $sqlite_test_name:ident, $db:ident, $body:block) => {
|
|
||||||
#[gpui::test]
|
|
||||||
async fn $postgres_test_name() {
|
|
||||||
let test_db = TestDb::postgres(Deterministic::new(0).build_background());
|
|
||||||
let $db = test_db.db();
|
|
||||||
$body
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn $sqlite_test_name() {
|
|
||||||
let test_db = TestDb::sqlite(Deterministic::new(0).build_background());
|
|
||||||
let $db = test_db.db();
|
|
||||||
$body
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
test_both_dbs!(
|
test_both_dbs!(
|
||||||
|
test_get_users,
|
||||||
test_get_users_by_ids_postgres,
|
test_get_users_by_ids_postgres,
|
||||||
test_get_users_by_ids_sqlite,
|
test_get_users_by_ids_sqlite
|
||||||
db,
|
);
|
||||||
{
|
|
||||||
|
async fn test_get_users(db: &Arc<Database>) {
|
||||||
let mut user_ids = Vec::new();
|
let mut user_ids = Vec::new();
|
||||||
let mut user_metric_ids = Vec::new();
|
let mut user_metric_ids = Vec::new();
|
||||||
for i in 1..=4 {
|
for i in 1..=4 {
|
||||||
@ -88,13 +73,14 @@ test_both_dbs!(
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
test_both_dbs!(
|
test_both_dbs!(
|
||||||
|
test_get_or_create_user_by_github_account,
|
||||||
test_get_or_create_user_by_github_account_postgres,
|
test_get_or_create_user_by_github_account_postgres,
|
||||||
test_get_or_create_user_by_github_account_sqlite,
|
test_get_or_create_user_by_github_account_sqlite
|
||||||
db,
|
);
|
||||||
{
|
|
||||||
|
async fn test_get_or_create_user_by_github_account(db: &Arc<Database>) {
|
||||||
let user_id1 = db
|
let user_id1 = db
|
||||||
.create_user(
|
.create_user(
|
||||||
"user1@example.com",
|
"user1@example.com",
|
||||||
@ -155,13 +141,14 @@ test_both_dbs!(
|
|||||||
assert_eq!(user.github_user_id, Some(103));
|
assert_eq!(user.github_user_id, Some(103));
|
||||||
assert_eq!(user.email_address, Some("user3@example.com".into()));
|
assert_eq!(user.email_address, Some("user3@example.com".into()));
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
test_both_dbs!(
|
test_both_dbs!(
|
||||||
|
test_create_access_tokens,
|
||||||
test_create_access_tokens_postgres,
|
test_create_access_tokens_postgres,
|
||||||
test_create_access_tokens_sqlite,
|
test_create_access_tokens_sqlite
|
||||||
db,
|
);
|
||||||
{
|
|
||||||
|
async fn test_create_access_tokens(db: &Arc<Database>) {
|
||||||
let user = db
|
let user = db
|
||||||
.create_user(
|
.create_user(
|
||||||
"u1@example.com",
|
"u1@example.com",
|
||||||
@ -234,9 +221,14 @@ test_both_dbs!(
|
|||||||
assert!(db.get_access_token(token_2).await.is_err());
|
assert!(db.get_access_token(token_2).await.is_err());
|
||||||
assert!(db.get_access_token(token_1).await.is_err());
|
assert!(db.get_access_token(token_1).await.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test_both_dbs!(
|
||||||
|
test_add_contacts,
|
||||||
|
test_add_contacts_postgres,
|
||||||
|
test_add_contacts_sqlite
|
||||||
);
|
);
|
||||||
|
|
||||||
test_both_dbs!(test_add_contacts_postgres, test_add_contacts_sqlite, db, {
|
async fn test_add_contacts(db: &Arc<Database>) {
|
||||||
let mut user_ids = Vec::new();
|
let mut user_ids = Vec::new();
|
||||||
for i in 0..3 {
|
for i in 0..3 {
|
||||||
user_ids.push(
|
user_ids.push(
|
||||||
@ -403,9 +395,15 @@ test_both_dbs!(test_add_contacts_postgres, test_add_contacts_sqlite, db, {
|
|||||||
busy: false,
|
busy: false,
|
||||||
}],
|
}],
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
|
||||||
test_both_dbs!(test_metrics_id_postgres, test_metrics_id_sqlite, db, {
|
test_both_dbs!(
|
||||||
|
test_metrics_id,
|
||||||
|
test_metrics_id_postgres,
|
||||||
|
test_metrics_id_sqlite
|
||||||
|
);
|
||||||
|
|
||||||
|
async fn test_metrics_id(db: &Arc<Database>) {
|
||||||
let NewUserResult {
|
let NewUserResult {
|
||||||
user_id: user1,
|
user_id: user1,
|
||||||
metrics_id: metrics_id1,
|
metrics_id: metrics_id1,
|
||||||
@ -444,13 +442,15 @@ test_both_dbs!(test_metrics_id_postgres, test_metrics_id_sqlite, db, {
|
|||||||
assert_eq!(metrics_id1.len(), 36);
|
assert_eq!(metrics_id1.len(), 36);
|
||||||
assert_eq!(metrics_id2.len(), 36);
|
assert_eq!(metrics_id2.len(), 36);
|
||||||
assert_ne!(metrics_id1, metrics_id2);
|
assert_ne!(metrics_id1, metrics_id2);
|
||||||
});
|
}
|
||||||
|
|
||||||
test_both_dbs!(
|
test_both_dbs!(
|
||||||
|
test_project_count,
|
||||||
test_project_count_postgres,
|
test_project_count_postgres,
|
||||||
test_project_count_sqlite,
|
test_project_count_sqlite
|
||||||
db,
|
);
|
||||||
{
|
|
||||||
|
async fn test_project_count(db: &Arc<Database>) {
|
||||||
let owner_id = db.create_server("test").await.unwrap().0 as u32;
|
let owner_id = db.create_server("test").await.unwrap().0 as u32;
|
||||||
|
|
||||||
let user1 = db
|
let user1 = db
|
||||||
@ -519,7 +519,6 @@ test_both_dbs!(
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0);
|
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0);
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_fuzzy_like_string() {
|
fn test_fuzzy_like_string() {
|
||||||
@ -878,7 +877,9 @@ async fn test_invite_codes() {
|
|||||||
assert!(db.has_contact(user5, user1).await.unwrap());
|
assert!(db.has_contact(user5, user1).await.unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
test_both_dbs!(test_channels_postgres, test_channels_sqlite, db, {
|
test_both_dbs!(test_channels, test_channels_postgres, test_channels_sqlite);
|
||||||
|
|
||||||
|
async fn test_channels(db: &Arc<Database>) {
|
||||||
let a_id = db
|
let a_id = db
|
||||||
.create_user(
|
.create_user(
|
||||||
"user1@example.com",
|
"user1@example.com",
|
||||||
@ -1063,13 +1064,15 @@ test_both_dbs!(test_channels_postgres, test_channels_sqlite, db, {
|
|||||||
assert!(db.get_channel(rust_id, a_id).await.unwrap().is_none());
|
assert!(db.get_channel(rust_id, a_id).await.unwrap().is_none());
|
||||||
assert!(db.get_channel(cargo_id, a_id).await.unwrap().is_none());
|
assert!(db.get_channel(cargo_id, a_id).await.unwrap().is_none());
|
||||||
assert!(db.get_channel(cargo_ra_id, a_id).await.unwrap().is_none());
|
assert!(db.get_channel(cargo_ra_id, a_id).await.unwrap().is_none());
|
||||||
});
|
}
|
||||||
|
|
||||||
test_both_dbs!(
|
test_both_dbs!(
|
||||||
|
test_joining_channels,
|
||||||
test_joining_channels_postgres,
|
test_joining_channels_postgres,
|
||||||
test_joining_channels_sqlite,
|
test_joining_channels_sqlite
|
||||||
db,
|
);
|
||||||
{
|
|
||||||
|
async fn test_joining_channels(db: &Arc<Database>) {
|
||||||
let owner_id = db.create_server("test").await.unwrap().0 as u32;
|
let owner_id = db.create_server("test").await.unwrap().0 as u32;
|
||||||
|
|
||||||
let user_1 = db
|
let user_1 = db
|
||||||
@ -1119,13 +1122,14 @@ test_both_dbs!(
|
|||||||
.await
|
.await
|
||||||
.is_err());
|
.is_err());
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
test_both_dbs!(
|
test_both_dbs!(
|
||||||
|
test_channel_invites,
|
||||||
test_channel_invites_postgres,
|
test_channel_invites_postgres,
|
||||||
test_channel_invites_sqlite,
|
test_channel_invites_sqlite
|
||||||
db,
|
);
|
||||||
{
|
|
||||||
|
async fn test_channel_invites(db: &Arc<Database>) {
|
||||||
db.create_server("test").await.unwrap();
|
db.create_server("test").await.unwrap();
|
||||||
|
|
||||||
let user_1 = db
|
let user_1 = db
|
||||||
@ -1263,13 +1267,14 @@ test_both_dbs!(
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
test_both_dbs!(
|
test_both_dbs!(
|
||||||
|
test_channel_renames,
|
||||||
test_channel_renames_postgres,
|
test_channel_renames_postgres,
|
||||||
test_channel_renames_sqlite,
|
test_channel_renames_sqlite
|
||||||
db,
|
);
|
||||||
{
|
|
||||||
|
async fn test_channel_renames(db: &Arc<Database>) {
|
||||||
db.create_server("test").await.unwrap();
|
db.create_server("test").await.unwrap();
|
||||||
|
|
||||||
let user_1 = db
|
let user_1 = db
|
||||||
@ -1323,7 +1328,6 @@ test_both_dbs!(
|
|||||||
let bad_name_rename = db.rename_channel(zed_id, user_1, "#").await;
|
let bad_name_rename = db.rename_channel(zed_id, user_1, "#").await;
|
||||||
assert!(bad_name_rename.is_err())
|
assert!(bad_name_rename.is_err())
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_multiple_signup_overwrite() {
|
async fn test_multiple_signup_overwrite() {
|
@ -35,8 +35,8 @@ use lazy_static::lazy_static;
|
|||||||
use prometheus::{register_int_gauge, IntGauge};
|
use prometheus::{register_int_gauge, IntGauge};
|
||||||
use rpc::{
|
use rpc::{
|
||||||
proto::{
|
proto::{
|
||||||
self, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, LiveKitConnectionInfo,
|
self, Ack, AddChannelBufferCollaborator, AnyTypedEnvelope, EntityMessage, EnvelopedMessage,
|
||||||
RequestMessage,
|
LiveKitConnectionInfo, RequestMessage,
|
||||||
},
|
},
|
||||||
Connection, ConnectionId, Peer, Receipt, TypedEnvelope,
|
Connection, ConnectionId, Peer, Receipt, TypedEnvelope,
|
||||||
};
|
};
|
||||||
@ -248,6 +248,9 @@ impl Server {
|
|||||||
.add_request_handler(remove_channel_member)
|
.add_request_handler(remove_channel_member)
|
||||||
.add_request_handler(set_channel_member_admin)
|
.add_request_handler(set_channel_member_admin)
|
||||||
.add_request_handler(rename_channel)
|
.add_request_handler(rename_channel)
|
||||||
|
.add_request_handler(join_channel_buffer)
|
||||||
|
.add_request_handler(leave_channel_buffer)
|
||||||
|
.add_message_handler(update_channel_buffer)
|
||||||
.add_request_handler(get_channel_members)
|
.add_request_handler(get_channel_members)
|
||||||
.add_request_handler(respond_to_channel_invite)
|
.add_request_handler(respond_to_channel_invite)
|
||||||
.add_request_handler(join_channel)
|
.add_request_handler(join_channel)
|
||||||
@ -851,6 +854,10 @@ async fn connection_lost(
|
|||||||
.await
|
.await
|
||||||
.trace_err();
|
.trace_err();
|
||||||
|
|
||||||
|
leave_channel_buffers_for_session(&session)
|
||||||
|
.await
|
||||||
|
.trace_err();
|
||||||
|
|
||||||
futures::select_biased! {
|
futures::select_biased! {
|
||||||
_ = executor.sleep(RECONNECT_TIMEOUT).fuse() => {
|
_ = executor.sleep(RECONNECT_TIMEOUT).fuse() => {
|
||||||
leave_room_for_session(&session).await.trace_err();
|
leave_room_for_session(&session).await.trace_err();
|
||||||
@ -866,6 +873,8 @@ async fn connection_lost(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
update_user_contacts(session.user_id, &session).await?;
|
update_user_contacts(session.user_id, &session).await?;
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
_ = teardown.changed().fuse() => {}
|
_ = teardown.changed().fuse() => {}
|
||||||
}
|
}
|
||||||
@ -2478,6 +2487,104 @@ async fn join_channel(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn join_channel_buffer(
|
||||||
|
request: proto::JoinChannelBuffer,
|
||||||
|
response: Response<proto::JoinChannelBuffer>,
|
||||||
|
session: Session,
|
||||||
|
) -> Result<()> {
|
||||||
|
let db = session.db().await;
|
||||||
|
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||||
|
|
||||||
|
let open_response = db
|
||||||
|
.join_channel_buffer(channel_id, session.user_id, session.connection_id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let replica_id = open_response.replica_id;
|
||||||
|
let collaborators = open_response.collaborators.clone();
|
||||||
|
|
||||||
|
response.send(open_response)?;
|
||||||
|
|
||||||
|
let update = AddChannelBufferCollaborator {
|
||||||
|
channel_id: channel_id.to_proto(),
|
||||||
|
collaborator: Some(proto::Collaborator {
|
||||||
|
user_id: session.user_id.to_proto(),
|
||||||
|
peer_id: Some(session.connection_id.into()),
|
||||||
|
replica_id,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
channel_buffer_updated(
|
||||||
|
session.connection_id,
|
||||||
|
collaborators
|
||||||
|
.iter()
|
||||||
|
.filter_map(|collaborator| Some(collaborator.peer_id?.into())),
|
||||||
|
&update,
|
||||||
|
&session.peer,
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_channel_buffer(
|
||||||
|
request: proto::UpdateChannelBuffer,
|
||||||
|
session: Session,
|
||||||
|
) -> Result<()> {
|
||||||
|
let db = session.db().await;
|
||||||
|
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||||
|
|
||||||
|
let collaborators = db
|
||||||
|
.update_channel_buffer(channel_id, session.user_id, &request.operations)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
channel_buffer_updated(
|
||||||
|
session.connection_id,
|
||||||
|
collaborators,
|
||||||
|
&proto::UpdateChannelBuffer {
|
||||||
|
channel_id: channel_id.to_proto(),
|
||||||
|
operations: request.operations,
|
||||||
|
},
|
||||||
|
&session.peer,
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn leave_channel_buffer(
|
||||||
|
request: proto::LeaveChannelBuffer,
|
||||||
|
response: Response<proto::LeaveChannelBuffer>,
|
||||||
|
session: Session,
|
||||||
|
) -> Result<()> {
|
||||||
|
let db = session.db().await;
|
||||||
|
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||||
|
|
||||||
|
let collaborators_to_notify = db
|
||||||
|
.leave_channel_buffer(channel_id, session.connection_id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
response.send(Ack {})?;
|
||||||
|
|
||||||
|
channel_buffer_updated(
|
||||||
|
session.connection_id,
|
||||||
|
collaborators_to_notify,
|
||||||
|
&proto::RemoveChannelBufferCollaborator {
|
||||||
|
channel_id: channel_id.to_proto(),
|
||||||
|
peer_id: Some(session.connection_id.into()),
|
||||||
|
},
|
||||||
|
&session.peer,
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn channel_buffer_updated<T: EnvelopedMessage>(
|
||||||
|
sender_id: ConnectionId,
|
||||||
|
collaborators: impl IntoIterator<Item = ConnectionId>,
|
||||||
|
message: &T,
|
||||||
|
peer: &Peer,
|
||||||
|
) {
|
||||||
|
broadcast(Some(sender_id), collaborators.into_iter(), |peer_id| {
|
||||||
|
peer.send(peer_id.into(), message.clone())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async fn update_diff_base(request: proto::UpdateDiffBase, session: Session) -> Result<()> {
|
async fn update_diff_base(request: proto::UpdateDiffBase, session: Session) -> Result<()> {
|
||||||
let project_id = ProjectId::from_proto(request.project_id);
|
let project_id = ProjectId::from_proto(request.project_id);
|
||||||
let project_connection_ids = session
|
let project_connection_ids = session
|
||||||
@ -2803,6 +2910,28 @@ async fn leave_room_for_session(session: &Session) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn leave_channel_buffers_for_session(session: &Session) -> Result<()> {
|
||||||
|
let left_channel_buffers = session
|
||||||
|
.db()
|
||||||
|
.await
|
||||||
|
.leave_channel_buffers(session.connection_id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
for (channel_id, connections) in left_channel_buffers {
|
||||||
|
channel_buffer_updated(
|
||||||
|
session.connection_id,
|
||||||
|
connections,
|
||||||
|
&proto::RemoveChannelBufferCollaborator {
|
||||||
|
channel_id: channel_id.to_proto(),
|
||||||
|
peer_id: Some(session.connection_id.into()),
|
||||||
|
},
|
||||||
|
&session.peer,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn project_left(project: &db::LeftProject, session: &Session) {
|
fn project_left(project: &db::LeftProject, session: &Session) {
|
||||||
for connection_id in &project.connection_ids {
|
for connection_id in &project.connection_ids {
|
||||||
if project.host_user_id == session.user_id {
|
if project.host_user_id == session.user_id {
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
db::{test_db::TestDb, NewUserParams, UserId},
|
db::{tests::TestDb, NewUserParams, UserId},
|
||||||
executor::Executor,
|
executor::Executor,
|
||||||
rpc::{Server, CLEANUP_TIMEOUT},
|
rpc::{Server, CLEANUP_TIMEOUT},
|
||||||
AppState,
|
AppState,
|
||||||
};
|
};
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use call::{ActiveCall, Room};
|
use call::{ActiveCall, Room};
|
||||||
|
use channel::ChannelStore;
|
||||||
use client::{
|
use client::{
|
||||||
self, proto::PeerId, ChannelStore, Client, Connection, Credentials, EstablishConnectionError,
|
self, proto::PeerId, Client, Connection, Credentials, EstablishConnectionError, UserStore,
|
||||||
UserStore,
|
|
||||||
};
|
};
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
use fs::FakeFs;
|
use fs::FakeFs;
|
||||||
@ -31,6 +31,7 @@ use std::{
|
|||||||
use util::http::FakeHttpClient;
|
use util::http::FakeHttpClient;
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
|
mod channel_buffer_tests;
|
||||||
mod channel_tests;
|
mod channel_tests;
|
||||||
mod integration_tests;
|
mod integration_tests;
|
||||||
mod randomized_integration_tests;
|
mod randomized_integration_tests;
|
||||||
@ -210,6 +211,7 @@ impl TestServer {
|
|||||||
workspace::init(app_state.clone(), cx);
|
workspace::init(app_state.clone(), cx);
|
||||||
audio::init((), cx);
|
audio::init((), cx);
|
||||||
call::init(client.clone(), user_store.clone(), cx);
|
call::init(client.clone(), user_store.clone(), cx);
|
||||||
|
channel::init(&client);
|
||||||
});
|
});
|
||||||
|
|
||||||
client
|
client
|
||||||
|
426
crates/collab/src/tests/channel_buffer_tests.rs
Normal file
426
crates/collab/src/tests/channel_buffer_tests.rs
Normal file
@ -0,0 +1,426 @@
|
|||||||
|
use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};
|
||||||
|
use call::ActiveCall;
|
||||||
|
use channel::Channel;
|
||||||
|
use client::UserId;
|
||||||
|
use collab_ui::channel_view::ChannelView;
|
||||||
|
use collections::HashMap;
|
||||||
|
use futures::future;
|
||||||
|
use gpui::{executor::Deterministic, ModelHandle, TestAppContext};
|
||||||
|
use rpc::{proto, RECEIVE_TIMEOUT};
|
||||||
|
use serde_json::json;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_core_channel_buffers(
|
||||||
|
deterministic: Arc<Deterministic>,
|
||||||
|
cx_a: &mut TestAppContext,
|
||||||
|
cx_b: &mut TestAppContext,
|
||||||
|
) {
|
||||||
|
deterministic.forbid_parking();
|
||||||
|
let mut server = TestServer::start(&deterministic).await;
|
||||||
|
let client_a = server.create_client(cx_a, "user_a").await;
|
||||||
|
let client_b = server.create_client(cx_b, "user_b").await;
|
||||||
|
|
||||||
|
let zed_id = server
|
||||||
|
.make_channel("zed", (&client_a, cx_a), &mut [(&client_b, cx_b)])
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Client A joins the channel buffer
|
||||||
|
let channel_buffer_a = client_a
|
||||||
|
.channel_store()
|
||||||
|
.update(cx_a, |channel, cx| channel.open_channel_buffer(zed_id, cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Client A edits the buffer
|
||||||
|
let buffer_a = channel_buffer_a.read_with(cx_a, |buffer, _| buffer.buffer());
|
||||||
|
|
||||||
|
buffer_a.update(cx_a, |buffer, cx| {
|
||||||
|
buffer.edit([(0..0, "hello world")], None, cx)
|
||||||
|
});
|
||||||
|
buffer_a.update(cx_a, |buffer, cx| {
|
||||||
|
buffer.edit([(5..5, ", cruel")], None, cx)
|
||||||
|
});
|
||||||
|
buffer_a.update(cx_a, |buffer, cx| {
|
||||||
|
buffer.edit([(0..5, "goodbye")], None, cx)
|
||||||
|
});
|
||||||
|
buffer_a.update(cx_a, |buffer, cx| buffer.undo(cx));
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
|
||||||
|
assert_eq!(buffer_text(&buffer_a, cx_a), "hello, cruel world");
|
||||||
|
|
||||||
|
// Client B joins the channel buffer
|
||||||
|
let channel_buffer_b = client_b
|
||||||
|
.channel_store()
|
||||||
|
.update(cx_b, |channel, cx| channel.open_channel_buffer(zed_id, cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
channel_buffer_b.read_with(cx_b, |buffer, _| {
|
||||||
|
assert_collaborators(
|
||||||
|
buffer.collaborators(),
|
||||||
|
&[client_a.user_id(), client_b.user_id()],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Client B sees the correct text, and then edits it
|
||||||
|
let buffer_b = channel_buffer_b.read_with(cx_b, |buffer, _| buffer.buffer());
|
||||||
|
assert_eq!(
|
||||||
|
buffer_b.read_with(cx_b, |buffer, _| buffer.remote_id()),
|
||||||
|
buffer_a.read_with(cx_a, |buffer, _| buffer.remote_id())
|
||||||
|
);
|
||||||
|
assert_eq!(buffer_text(&buffer_b, cx_b), "hello, cruel world");
|
||||||
|
buffer_b.update(cx_b, |buffer, cx| {
|
||||||
|
buffer.edit([(7..12, "beautiful")], None, cx)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Both A and B see the new edit
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
assert_eq!(buffer_text(&buffer_a, cx_a), "hello, beautiful world");
|
||||||
|
assert_eq!(buffer_text(&buffer_b, cx_b), "hello, beautiful world");
|
||||||
|
|
||||||
|
// Client A closes the channel buffer.
|
||||||
|
cx_a.update(|_| drop(channel_buffer_a));
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
|
||||||
|
// Client B sees that client A is gone from the channel buffer.
|
||||||
|
channel_buffer_b.read_with(cx_b, |buffer, _| {
|
||||||
|
assert_collaborators(&buffer.collaborators(), &[client_b.user_id()]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Client A rejoins the channel buffer
|
||||||
|
let _channel_buffer_a = client_a
|
||||||
|
.channel_store()
|
||||||
|
.update(cx_a, |channels, cx| {
|
||||||
|
channels.open_channel_buffer(zed_id, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
|
||||||
|
// Sanity test, make sure we saw A rejoining
|
||||||
|
channel_buffer_b.read_with(cx_b, |buffer, _| {
|
||||||
|
assert_collaborators(
|
||||||
|
&buffer.collaborators(),
|
||||||
|
&[client_b.user_id(), client_a.user_id()],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Client A loses connection.
|
||||||
|
server.forbid_connections();
|
||||||
|
server.disconnect_client(client_a.peer_id().unwrap());
|
||||||
|
deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
|
||||||
|
|
||||||
|
// Client B observes A disconnect
|
||||||
|
channel_buffer_b.read_with(cx_b, |buffer, _| {
|
||||||
|
assert_collaborators(&buffer.collaborators(), &[client_b.user_id()]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO:
|
||||||
|
// - Test synchronizing offline updates, what happens to A's channel buffer when A disconnects
|
||||||
|
// - Test interaction with channel deletion while buffer is open
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_channel_buffer_replica_ids(
|
||||||
|
deterministic: Arc<Deterministic>,
|
||||||
|
cx_a: &mut TestAppContext,
|
||||||
|
cx_b: &mut TestAppContext,
|
||||||
|
cx_c: &mut TestAppContext,
|
||||||
|
) {
|
||||||
|
deterministic.forbid_parking();
|
||||||
|
let mut server = TestServer::start(&deterministic).await;
|
||||||
|
let client_a = server.create_client(cx_a, "user_a").await;
|
||||||
|
let client_b = server.create_client(cx_b, "user_b").await;
|
||||||
|
let client_c = server.create_client(cx_c, "user_c").await;
|
||||||
|
|
||||||
|
let channel_id = server
|
||||||
|
.make_channel(
|
||||||
|
"zed",
|
||||||
|
(&client_a, cx_a),
|
||||||
|
&mut [(&client_b, cx_b), (&client_c, cx_c)],
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let active_call_a = cx_a.read(ActiveCall::global);
|
||||||
|
let active_call_b = cx_b.read(ActiveCall::global);
|
||||||
|
let active_call_c = cx_c.read(ActiveCall::global);
|
||||||
|
|
||||||
|
// Clients A and B join a channel.
|
||||||
|
active_call_a
|
||||||
|
.update(cx_a, |call, cx| call.join_channel(channel_id, cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
active_call_b
|
||||||
|
.update(cx_b, |call, cx| call.join_channel(channel_id, cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Clients A, B, and C join a channel buffer
|
||||||
|
// C first so that the replica IDs in the project and the channel buffer are different
|
||||||
|
let channel_buffer_c = client_c
|
||||||
|
.channel_store()
|
||||||
|
.update(cx_c, |channel, cx| {
|
||||||
|
channel.open_channel_buffer(channel_id, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let channel_buffer_b = client_b
|
||||||
|
.channel_store()
|
||||||
|
.update(cx_b, |channel, cx| {
|
||||||
|
channel.open_channel_buffer(channel_id, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let channel_buffer_a = client_a
|
||||||
|
.channel_store()
|
||||||
|
.update(cx_a, |channel, cx| {
|
||||||
|
channel.open_channel_buffer(channel_id, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Client B shares a project
|
||||||
|
client_b
|
||||||
|
.fs()
|
||||||
|
.insert_tree("/dir", json!({ "file.txt": "contents" }))
|
||||||
|
.await;
|
||||||
|
let (project_b, _) = client_b.build_local_project("/dir", cx_b).await;
|
||||||
|
let shared_project_id = active_call_b
|
||||||
|
.update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Client A joins the project
|
||||||
|
let project_a = client_a.build_remote_project(shared_project_id, cx_a).await;
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
|
||||||
|
// Client C is in a separate project.
|
||||||
|
client_c.fs().insert_tree("/dir", json!({})).await;
|
||||||
|
let (separate_project_c, _) = client_c.build_local_project("/dir", cx_c).await;
|
||||||
|
|
||||||
|
// Note that each user has a different replica id in the projects vs the
|
||||||
|
// channel buffer.
|
||||||
|
channel_buffer_a.read_with(cx_a, |channel_buffer, cx| {
|
||||||
|
assert_eq!(project_a.read(cx).replica_id(), 1);
|
||||||
|
assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 2);
|
||||||
|
});
|
||||||
|
channel_buffer_b.read_with(cx_b, |channel_buffer, cx| {
|
||||||
|
assert_eq!(project_b.read(cx).replica_id(), 0);
|
||||||
|
assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 1);
|
||||||
|
});
|
||||||
|
channel_buffer_c.read_with(cx_c, |channel_buffer, cx| {
|
||||||
|
// C is not in the project
|
||||||
|
assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
let channel_window_a =
|
||||||
|
cx_a.add_window(|cx| ChannelView::new(project_a.clone(), channel_buffer_a.clone(), cx));
|
||||||
|
let channel_window_b =
|
||||||
|
cx_b.add_window(|cx| ChannelView::new(project_b.clone(), channel_buffer_b.clone(), cx));
|
||||||
|
let channel_window_c = cx_c.add_window(|cx| {
|
||||||
|
ChannelView::new(separate_project_c.clone(), channel_buffer_c.clone(), cx)
|
||||||
|
});
|
||||||
|
|
||||||
|
let channel_view_a = channel_window_a.root(cx_a);
|
||||||
|
let channel_view_b = channel_window_b.root(cx_b);
|
||||||
|
let channel_view_c = channel_window_c.root(cx_c);
|
||||||
|
|
||||||
|
// For clients A and B, the replica ids in the channel buffer are mapped
|
||||||
|
// so that they match the same users' replica ids in their shared project.
|
||||||
|
channel_view_a.read_with(cx_a, |view, cx| {
|
||||||
|
assert_eq!(
|
||||||
|
view.editor.read(cx).replica_id_map().unwrap(),
|
||||||
|
&[(1, 0), (2, 1)].into_iter().collect::<HashMap<_, _>>()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
channel_view_b.read_with(cx_b, |view, cx| {
|
||||||
|
assert_eq!(
|
||||||
|
view.editor.read(cx).replica_id_map().unwrap(),
|
||||||
|
&[(1, 0), (2, 1)].into_iter().collect::<HashMap<u16, u16>>(),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Client C only sees themself, as they're not part of any shared project
|
||||||
|
channel_view_c.read_with(cx_c, |view, cx| {
|
||||||
|
assert_eq!(
|
||||||
|
view.editor.read(cx).replica_id_map().unwrap(),
|
||||||
|
&[(0, 0)].into_iter().collect::<HashMap<u16, u16>>(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Client C joins the project that clients A and B are in.
|
||||||
|
active_call_c
|
||||||
|
.update(cx_c, |call, cx| call.join_channel(channel_id, cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let project_c = client_c.build_remote_project(shared_project_id, cx_c).await;
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
project_c.read_with(cx_c, |project, _| {
|
||||||
|
assert_eq!(project.replica_id(), 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
// For clients A and B, client C's replica id in the channel buffer is
|
||||||
|
// now mapped to their replica id in the shared project.
|
||||||
|
channel_view_a.read_with(cx_a, |view, cx| {
|
||||||
|
assert_eq!(
|
||||||
|
view.editor.read(cx).replica_id_map().unwrap(),
|
||||||
|
&[(1, 0), (2, 1), (0, 2)]
|
||||||
|
.into_iter()
|
||||||
|
.collect::<HashMap<_, _>>()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
channel_view_b.read_with(cx_b, |view, cx| {
|
||||||
|
assert_eq!(
|
||||||
|
view.editor.read(cx).replica_id_map().unwrap(),
|
||||||
|
&[(1, 0), (2, 1), (0, 2)]
|
||||||
|
.into_iter()
|
||||||
|
.collect::<HashMap<_, _>>(),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_reopen_channel_buffer(deterministic: Arc<Deterministic>, cx_a: &mut TestAppContext) {
|
||||||
|
deterministic.forbid_parking();
|
||||||
|
let mut server = TestServer::start(&deterministic).await;
|
||||||
|
let client_a = server.create_client(cx_a, "user_a").await;
|
||||||
|
|
||||||
|
let zed_id = server.make_channel("zed", (&client_a, cx_a), &mut []).await;
|
||||||
|
|
||||||
|
let channel_buffer_1 = client_a
|
||||||
|
.channel_store()
|
||||||
|
.update(cx_a, |channel, cx| channel.open_channel_buffer(zed_id, cx));
|
||||||
|
let channel_buffer_2 = client_a
|
||||||
|
.channel_store()
|
||||||
|
.update(cx_a, |channel, cx| channel.open_channel_buffer(zed_id, cx));
|
||||||
|
let channel_buffer_3 = client_a
|
||||||
|
.channel_store()
|
||||||
|
.update(cx_a, |channel, cx| channel.open_channel_buffer(zed_id, cx));
|
||||||
|
|
||||||
|
// All concurrent tasks for opening a channel buffer return the same model handle.
|
||||||
|
let (channel_buffer_1, channel_buffer_2, channel_buffer_3) =
|
||||||
|
future::try_join3(channel_buffer_1, channel_buffer_2, channel_buffer_3)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let model_id = channel_buffer_1.id();
|
||||||
|
assert_eq!(channel_buffer_1, channel_buffer_2);
|
||||||
|
assert_eq!(channel_buffer_1, channel_buffer_3);
|
||||||
|
|
||||||
|
channel_buffer_1.update(cx_a, |buffer, cx| {
|
||||||
|
buffer.buffer().update(cx, |buffer, cx| {
|
||||||
|
buffer.edit([(0..0, "hello")], None, cx);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
|
||||||
|
cx_a.update(|_| {
|
||||||
|
drop(channel_buffer_1);
|
||||||
|
drop(channel_buffer_2);
|
||||||
|
drop(channel_buffer_3);
|
||||||
|
});
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
|
||||||
|
// The channel buffer can be reopened after dropping it.
|
||||||
|
let channel_buffer = client_a
|
||||||
|
.channel_store()
|
||||||
|
.update(cx_a, |channel, cx| channel.open_channel_buffer(zed_id, cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_ne!(channel_buffer.id(), model_id);
|
||||||
|
channel_buffer.update(cx_a, |buffer, cx| {
|
||||||
|
buffer.buffer().update(cx, |buffer, _| {
|
||||||
|
assert_eq!(buffer.text(), "hello");
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_channel_buffer_disconnect(
|
||||||
|
deterministic: Arc<Deterministic>,
|
||||||
|
cx_a: &mut TestAppContext,
|
||||||
|
cx_b: &mut TestAppContext,
|
||||||
|
) {
|
||||||
|
deterministic.forbid_parking();
|
||||||
|
let mut server = TestServer::start(&deterministic).await;
|
||||||
|
let client_a = server.create_client(cx_a, "user_a").await;
|
||||||
|
let client_b = server.create_client(cx_b, "user_b").await;
|
||||||
|
|
||||||
|
let channel_id = server
|
||||||
|
.make_channel("zed", (&client_a, cx_a), &mut [(&client_b, cx_b)])
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let channel_buffer_a = client_a
|
||||||
|
.channel_store()
|
||||||
|
.update(cx_a, |channel, cx| {
|
||||||
|
channel.open_channel_buffer(channel_id, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let channel_buffer_b = client_b
|
||||||
|
.channel_store()
|
||||||
|
.update(cx_b, |channel, cx| {
|
||||||
|
channel.open_channel_buffer(channel_id, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
server.forbid_connections();
|
||||||
|
server.disconnect_client(client_a.peer_id().unwrap());
|
||||||
|
deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
|
||||||
|
|
||||||
|
channel_buffer_a.update(cx_a, |buffer, _| {
|
||||||
|
assert_eq!(
|
||||||
|
buffer.channel().as_ref(),
|
||||||
|
&Channel {
|
||||||
|
id: channel_id,
|
||||||
|
name: "zed".to_string()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert!(!buffer.is_connected());
|
||||||
|
});
|
||||||
|
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
|
||||||
|
server.allow_connections();
|
||||||
|
deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
|
||||||
|
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
|
||||||
|
client_a
|
||||||
|
.channel_store()
|
||||||
|
.update(cx_a, |channel_store, _| {
|
||||||
|
channel_store.remove_channel(channel_id)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
|
||||||
|
// Channel buffer observed the deletion
|
||||||
|
channel_buffer_b.update(cx_b, |buffer, _| {
|
||||||
|
assert_eq!(
|
||||||
|
buffer.channel().as_ref(),
|
||||||
|
&Channel {
|
||||||
|
id: channel_id,
|
||||||
|
name: "zed".to_string()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert!(!buffer.is_connected());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
fn assert_collaborators(collaborators: &[proto::Collaborator], ids: &[Option<UserId>]) {
|
||||||
|
assert_eq!(
|
||||||
|
collaborators
|
||||||
|
.into_iter()
|
||||||
|
.map(|collaborator| collaborator.user_id)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
ids.into_iter().map(|id| id.unwrap()).collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn buffer_text(channel_buffer: &ModelHandle<language::Buffer>, cx: &mut TestAppContext) -> String {
|
||||||
|
channel_buffer.read_with(cx, |buffer, _| buffer.text())
|
||||||
|
}
|
@ -3,7 +3,8 @@ use crate::{
|
|||||||
tests::{room_participants, RoomParticipants, TestServer},
|
tests::{room_participants, RoomParticipants, TestServer},
|
||||||
};
|
};
|
||||||
use call::ActiveCall;
|
use call::ActiveCall;
|
||||||
use client::{ChannelId, ChannelMembership, ChannelStore, User};
|
use channel::{ChannelId, ChannelMembership, ChannelStore};
|
||||||
|
use client::User;
|
||||||
use gpui::{executor::Deterministic, ModelHandle, TestAppContext};
|
use gpui::{executor::Deterministic, ModelHandle, TestAppContext};
|
||||||
use rpc::{proto, RECEIVE_TIMEOUT};
|
use rpc::{proto, RECEIVE_TIMEOUT};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@ -798,7 +799,7 @@ async fn test_lost_channel_creation(
|
|||||||
|
|
||||||
deterministic.run_until_parked();
|
deterministic.run_until_parked();
|
||||||
|
|
||||||
// Sanity check
|
// Sanity check, B has the invitation
|
||||||
assert_channel_invitations(
|
assert_channel_invitations(
|
||||||
client_b.channel_store(),
|
client_b.channel_store(),
|
||||||
cx_b,
|
cx_b,
|
||||||
@ -810,6 +811,7 @@ async fn test_lost_channel_creation(
|
|||||||
}],
|
}],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// A creates a subchannel while the invite is still pending.
|
||||||
let subchannel_id = client_a
|
let subchannel_id = client_a
|
||||||
.channel_store()
|
.channel_store()
|
||||||
.update(cx_a, |channel_store, cx| {
|
.update(cx_a, |channel_store, cx| {
|
||||||
@ -840,7 +842,7 @@ async fn test_lost_channel_creation(
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Accept the invite
|
// Client B accepts the invite
|
||||||
client_b
|
client_b
|
||||||
.channel_store()
|
.channel_store()
|
||||||
.update(cx_b, |channel_store, _| {
|
.update(cx_b, |channel_store, _| {
|
||||||
@ -851,7 +853,7 @@ async fn test_lost_channel_creation(
|
|||||||
|
|
||||||
deterministic.run_until_parked();
|
deterministic.run_until_parked();
|
||||||
|
|
||||||
// B should now see the channel
|
// Client B should now see the channel
|
||||||
assert_channels(
|
assert_channels(
|
||||||
client_b.channel_store(),
|
client_b.channel_store(),
|
||||||
cx_b,
|
cx_b,
|
||||||
|
@ -26,6 +26,7 @@ auto_update = { path = "../auto_update" }
|
|||||||
db = { path = "../db" }
|
db = { path = "../db" }
|
||||||
call = { path = "../call" }
|
call = { path = "../call" }
|
||||||
client = { path = "../client" }
|
client = { path = "../client" }
|
||||||
|
channel = { path = "../channel" }
|
||||||
clock = { path = "../clock" }
|
clock = { path = "../clock" }
|
||||||
collections = { path = "../collections" }
|
collections = { path = "../collections" }
|
||||||
context_menu = { path = "../context_menu" }
|
context_menu = { path = "../context_menu" }
|
||||||
@ -33,6 +34,7 @@ editor = { path = "../editor" }
|
|||||||
feedback = { path = "../feedback" }
|
feedback = { path = "../feedback" }
|
||||||
fuzzy = { path = "../fuzzy" }
|
fuzzy = { path = "../fuzzy" }
|
||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui" }
|
||||||
|
language = { path = "../language" }
|
||||||
menu = { path = "../menu" }
|
menu = { path = "../menu" }
|
||||||
picker = { path = "../picker" }
|
picker = { path = "../picker" }
|
||||||
project = { path = "../project" }
|
project = { path = "../project" }
|
||||||
|
351
crates/collab_ui/src/channel_view.rs
Normal file
351
crates/collab_ui/src/channel_view.rs
Normal file
@ -0,0 +1,351 @@
|
|||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use channel::{
|
||||||
|
channel_buffer::{self, ChannelBuffer},
|
||||||
|
ChannelId,
|
||||||
|
};
|
||||||
|
use client::proto;
|
||||||
|
use clock::ReplicaId;
|
||||||
|
use collections::HashMap;
|
||||||
|
use editor::Editor;
|
||||||
|
use gpui::{
|
||||||
|
actions,
|
||||||
|
elements::{ChildView, Label},
|
||||||
|
geometry::vector::Vector2F,
|
||||||
|
AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Subscription, Task, View,
|
||||||
|
ViewContext, ViewHandle,
|
||||||
|
};
|
||||||
|
use project::Project;
|
||||||
|
use std::any::Any;
|
||||||
|
use workspace::{
|
||||||
|
item::{FollowableItem, Item, ItemHandle},
|
||||||
|
register_followable_item,
|
||||||
|
searchable::SearchableItemHandle,
|
||||||
|
ItemNavHistory, Pane, ViewId, Workspace, WorkspaceId,
|
||||||
|
};
|
||||||
|
|
||||||
|
actions!(channel_view, [Deploy]);
|
||||||
|
|
||||||
|
pub(crate) fn init(cx: &mut AppContext) {
|
||||||
|
register_followable_item::<ChannelView>(cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ChannelView {
|
||||||
|
pub editor: ViewHandle<Editor>,
|
||||||
|
project: ModelHandle<Project>,
|
||||||
|
channel_buffer: ModelHandle<ChannelBuffer>,
|
||||||
|
remote_id: Option<ViewId>,
|
||||||
|
_editor_event_subscription: Subscription,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChannelView {
|
||||||
|
pub fn open(
|
||||||
|
channel_id: ChannelId,
|
||||||
|
pane: ViewHandle<Pane>,
|
||||||
|
workspace: ViewHandle<Workspace>,
|
||||||
|
cx: &mut AppContext,
|
||||||
|
) -> Task<Result<ViewHandle<Self>>> {
|
||||||
|
let workspace = workspace.read(cx);
|
||||||
|
let project = workspace.project().to_owned();
|
||||||
|
let channel_store = workspace.app_state().channel_store.clone();
|
||||||
|
let markdown = workspace
|
||||||
|
.app_state()
|
||||||
|
.languages
|
||||||
|
.language_for_name("Markdown");
|
||||||
|
let channel_buffer =
|
||||||
|
channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx));
|
||||||
|
|
||||||
|
cx.spawn(|mut cx| async move {
|
||||||
|
let channel_buffer = channel_buffer.await?;
|
||||||
|
let markdown = markdown.await?;
|
||||||
|
channel_buffer.update(&mut cx, |buffer, cx| {
|
||||||
|
buffer.buffer().update(cx, |buffer, cx| {
|
||||||
|
buffer.set_language(Some(markdown), cx);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
pane.update(&mut cx, |pane, cx| {
|
||||||
|
pane.items_of_type::<Self>()
|
||||||
|
.find(|channel_view| channel_view.read(cx).channel_buffer == channel_buffer)
|
||||||
|
.unwrap_or_else(|| cx.add_view(|cx| Self::new(project, channel_buffer, cx)))
|
||||||
|
})
|
||||||
|
.ok_or_else(|| anyhow!("pane was dropped"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(
|
||||||
|
project: ModelHandle<Project>,
|
||||||
|
channel_buffer: ModelHandle<ChannelBuffer>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Self {
|
||||||
|
let buffer = channel_buffer.read(cx).buffer();
|
||||||
|
// buffer.update(cx, |buffer, cx| buffer.set_language(language, cx));
|
||||||
|
let editor = cx.add_view(|cx| Editor::for_buffer(buffer, None, cx));
|
||||||
|
let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone()));
|
||||||
|
|
||||||
|
cx.subscribe(&project, Self::handle_project_event).detach();
|
||||||
|
cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event)
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
let this = Self {
|
||||||
|
editor,
|
||||||
|
project,
|
||||||
|
channel_buffer,
|
||||||
|
remote_id: None,
|
||||||
|
_editor_event_subscription,
|
||||||
|
};
|
||||||
|
this.refresh_replica_id_map(cx);
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_project_event(
|
||||||
|
&mut self,
|
||||||
|
_: ModelHandle<Project>,
|
||||||
|
event: &project::Event,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
match event {
|
||||||
|
project::Event::RemoteIdChanged(_) => {}
|
||||||
|
project::Event::DisconnectedFromHost => {}
|
||||||
|
project::Event::Closed => {}
|
||||||
|
project::Event::CollaboratorUpdated { .. } => {}
|
||||||
|
project::Event::CollaboratorLeft(_) => {}
|
||||||
|
project::Event::CollaboratorJoined(_) => {}
|
||||||
|
_ => return,
|
||||||
|
}
|
||||||
|
self.refresh_replica_id_map(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_channel_buffer_event(
|
||||||
|
&mut self,
|
||||||
|
_: ModelHandle<ChannelBuffer>,
|
||||||
|
event: &channel_buffer::Event,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
match event {
|
||||||
|
channel_buffer::Event::CollaboratorsChanged => {
|
||||||
|
self.refresh_replica_id_map(cx);
|
||||||
|
}
|
||||||
|
channel_buffer::Event::Disconnected => self.editor.update(cx, |editor, cx| {
|
||||||
|
editor.set_read_only(true);
|
||||||
|
cx.notify();
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a mapping of channel buffer replica ids to the corresponding
|
||||||
|
/// replica ids in the current project.
|
||||||
|
///
|
||||||
|
/// Using this mapping, a given user can be displayed with the same color
|
||||||
|
/// in the channel buffer as in other files in the project. Users who are
|
||||||
|
/// in the channel buffer but not the project will not have a color.
|
||||||
|
fn refresh_replica_id_map(&self, cx: &mut ViewContext<Self>) {
|
||||||
|
let mut project_replica_ids_by_channel_buffer_replica_id = HashMap::default();
|
||||||
|
let project = self.project.read(cx);
|
||||||
|
let channel_buffer = self.channel_buffer.read(cx);
|
||||||
|
project_replica_ids_by_channel_buffer_replica_id
|
||||||
|
.insert(channel_buffer.replica_id(cx), project.replica_id());
|
||||||
|
project_replica_ids_by_channel_buffer_replica_id.extend(
|
||||||
|
channel_buffer
|
||||||
|
.collaborators()
|
||||||
|
.iter()
|
||||||
|
.filter_map(|channel_buffer_collaborator| {
|
||||||
|
project
|
||||||
|
.collaborators()
|
||||||
|
.values()
|
||||||
|
.find_map(|project_collaborator| {
|
||||||
|
(project_collaborator.user_id == channel_buffer_collaborator.user_id)
|
||||||
|
.then_some((
|
||||||
|
channel_buffer_collaborator.replica_id as ReplicaId,
|
||||||
|
project_collaborator.replica_id,
|
||||||
|
))
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
self.editor.update(cx, |editor, cx| {
|
||||||
|
editor.set_replica_id_map(Some(project_replica_ids_by_channel_buffer_replica_id), cx)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entity for ChannelView {
|
||||||
|
type Event = editor::Event;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for ChannelView {
|
||||||
|
fn ui_name() -> &'static str {
|
||||||
|
"ChannelView"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||||
|
ChildView::new(self.editor.as_any(), cx).into_any()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||||
|
if cx.is_self_focused() {
|
||||||
|
cx.focus(self.editor.as_any())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Item for ChannelView {
|
||||||
|
fn tab_content<V: 'static>(
|
||||||
|
&self,
|
||||||
|
_: Option<usize>,
|
||||||
|
style: &theme::Tab,
|
||||||
|
cx: &gpui::AppContext,
|
||||||
|
) -> AnyElement<V> {
|
||||||
|
let channel_name = &self.channel_buffer.read(cx).channel().name;
|
||||||
|
let label = if self.channel_buffer.read(cx).is_connected() {
|
||||||
|
format!("#{}", channel_name)
|
||||||
|
} else {
|
||||||
|
format!("#{} (disconnected)", channel_name)
|
||||||
|
};
|
||||||
|
Label::new(label, style.label.to_owned()).into_any()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<Self> {
|
||||||
|
Some(Self::new(
|
||||||
|
self.project.clone(),
|
||||||
|
self.channel_buffer.clone(),
|
||||||
|
cx,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_singleton(&self, _cx: &AppContext) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
|
||||||
|
self.editor
|
||||||
|
.update(cx, |editor, cx| editor.navigate(data, cx))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
self.editor
|
||||||
|
.update(cx, |editor, cx| Item::deactivated(editor, cx))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext<Self>) {
|
||||||
|
self.editor
|
||||||
|
.update(cx, |editor, cx| Item::set_nav_history(editor, history, cx))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_searchable(&self, _: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
|
||||||
|
Some(Box::new(self.editor.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_toolbar(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Vector2F> {
|
||||||
|
self.editor.read(cx).pixel_position_of_cursor(cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FollowableItem for ChannelView {
|
||||||
|
fn remote_id(&self) -> Option<workspace::ViewId> {
|
||||||
|
self.remote_id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
|
||||||
|
let channel = self.channel_buffer.read(cx).channel();
|
||||||
|
Some(proto::view::Variant::ChannelView(
|
||||||
|
proto::view::ChannelView {
|
||||||
|
channel_id: channel.id,
|
||||||
|
editor: if let Some(proto::view::Variant::Editor(proto)) =
|
||||||
|
self.editor.read(cx).to_state_proto(cx)
|
||||||
|
{
|
||||||
|
Some(proto)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_state_proto(
|
||||||
|
pane: ViewHandle<workspace::Pane>,
|
||||||
|
workspace: ViewHandle<workspace::Workspace>,
|
||||||
|
remote_id: workspace::ViewId,
|
||||||
|
state: &mut Option<proto::view::Variant>,
|
||||||
|
cx: &mut AppContext,
|
||||||
|
) -> Option<gpui::Task<anyhow::Result<ViewHandle<Self>>>> {
|
||||||
|
let Some(proto::view::Variant::ChannelView(_)) = state else { return None };
|
||||||
|
let Some(proto::view::Variant::ChannelView(state)) = state.take() else { unreachable!() };
|
||||||
|
|
||||||
|
let open = ChannelView::open(state.channel_id, pane, workspace, cx);
|
||||||
|
|
||||||
|
Some(cx.spawn(|mut cx| async move {
|
||||||
|
let this = open.await?;
|
||||||
|
|
||||||
|
let task = this
|
||||||
|
.update(&mut cx, |this, cx| {
|
||||||
|
this.remote_id = Some(remote_id);
|
||||||
|
|
||||||
|
if let Some(state) = state.editor {
|
||||||
|
Some(this.editor.update(cx, |editor, cx| {
|
||||||
|
editor.apply_update_proto(
|
||||||
|
&this.project,
|
||||||
|
proto::update_view::Variant::Editor(proto::update_view::Editor {
|
||||||
|
selections: state.selections,
|
||||||
|
pending_selection: state.pending_selection,
|
||||||
|
scroll_top_anchor: state.scroll_top_anchor,
|
||||||
|
scroll_x: state.scroll_x,
|
||||||
|
scroll_y: state.scroll_y,
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.ok_or_else(|| anyhow!("window was closed"))?;
|
||||||
|
|
||||||
|
if let Some(task) = task {
|
||||||
|
task.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(this)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_event_to_update_proto(
|
||||||
|
&self,
|
||||||
|
event: &Self::Event,
|
||||||
|
update: &mut Option<proto::update_view::Variant>,
|
||||||
|
cx: &AppContext,
|
||||||
|
) -> bool {
|
||||||
|
self.editor
|
||||||
|
.read(cx)
|
||||||
|
.add_event_to_update_proto(event, update, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_update_proto(
|
||||||
|
&mut self,
|
||||||
|
project: &ModelHandle<Project>,
|
||||||
|
message: proto::update_view::Variant,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> gpui::Task<anyhow::Result<()>> {
|
||||||
|
self.editor.update(cx, |editor, cx| {
|
||||||
|
editor.apply_update_proto(project, message, cx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_leader_replica_id(
|
||||||
|
&mut self,
|
||||||
|
leader_replica_id: Option<u16>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
self.editor.update(cx, |editor, cx| {
|
||||||
|
editor.set_leader_replica_id(leader_replica_id, cx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool {
|
||||||
|
Editor::should_unfollow_on_event(event, cx)
|
||||||
|
}
|
||||||
|
}
|
@ -4,10 +4,8 @@ mod panel_settings;
|
|||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use call::ActiveCall;
|
use call::ActiveCall;
|
||||||
use client::{
|
use channel::{Channel, ChannelEvent, ChannelId, ChannelStore};
|
||||||
proto::PeerId, Channel, ChannelEvent, ChannelId, ChannelStore, Client, Contact, User, UserStore,
|
use client::{proto::PeerId, Client, Contact, User, UserStore};
|
||||||
};
|
|
||||||
|
|
||||||
use context_menu::{ContextMenu, ContextMenuItem};
|
use context_menu::{ContextMenu, ContextMenuItem};
|
||||||
use db::kvp::KEY_VALUE_STORE;
|
use db::kvp::KEY_VALUE_STORE;
|
||||||
use editor::{Cancel, Editor};
|
use editor::{Cancel, Editor};
|
||||||
@ -16,16 +14,18 @@ use fuzzy::{match_strings, StringMatchCandidate};
|
|||||||
use gpui::{
|
use gpui::{
|
||||||
actions,
|
actions,
|
||||||
elements::{
|
elements::{
|
||||||
Canvas, ChildView, Empty, Flex, Image, Label, List, ListOffset, ListState,
|
Canvas, ChildView, Component, Empty, Flex, Image, Label, List, ListOffset, ListState,
|
||||||
MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement, Stack, Svg,
|
MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement, SafeStylable,
|
||||||
|
Stack, Svg,
|
||||||
},
|
},
|
||||||
|
fonts::TextStyle,
|
||||||
geometry::{
|
geometry::{
|
||||||
rect::RectF,
|
rect::RectF,
|
||||||
vector::{vec2f, Vector2F},
|
vector::{vec2f, Vector2F},
|
||||||
},
|
},
|
||||||
impl_actions,
|
impl_actions,
|
||||||
platform::{CursorStyle, MouseButton, PromptLevel},
|
platform::{CursorStyle, MouseButton, PromptLevel},
|
||||||
serde_json, AnyElement, AppContext, AsyncAppContext, Element, Entity, ModelHandle,
|
serde_json, AnyElement, AppContext, AsyncAppContext, Element, Entity, FontCache, ModelHandle,
|
||||||
Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||||
};
|
};
|
||||||
use menu::{Confirm, SelectNext, SelectPrev};
|
use menu::{Confirm, SelectNext, SelectPrev};
|
||||||
@ -35,7 +35,7 @@ use serde_derive::{Deserialize, Serialize};
|
|||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
use staff_mode::StaffMode;
|
use staff_mode::StaffMode;
|
||||||
use std::{borrow::Cow, mem, sync::Arc};
|
use std::{borrow::Cow, mem, sync::Arc};
|
||||||
use theme::IconButton;
|
use theme::{components::ComponentExt, IconButton};
|
||||||
use util::{iife, ResultExt, TryFutureExt};
|
use util::{iife, ResultExt, TryFutureExt};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
dock::{DockPosition, Panel},
|
dock::{DockPosition, Panel},
|
||||||
@ -43,7 +43,10 @@ use workspace::{
|
|||||||
Workspace,
|
Workspace,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::face_pile::FacePile;
|
use crate::{
|
||||||
|
channel_view::{self, ChannelView},
|
||||||
|
face_pile::FacePile,
|
||||||
|
};
|
||||||
use channel_modal::ChannelModal;
|
use channel_modal::ChannelModal;
|
||||||
|
|
||||||
use self::contact_finder::ContactFinder;
|
use self::contact_finder::ContactFinder;
|
||||||
@ -53,6 +56,11 @@ struct RemoveChannel {
|
|||||||
channel_id: u64,
|
channel_id: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
|
struct ToggleCollapse {
|
||||||
|
channel_id: u64,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
struct NewChannel {
|
struct NewChannel {
|
||||||
channel_id: u64,
|
channel_id: u64,
|
||||||
@ -73,7 +81,21 @@ struct RenameChannel {
|
|||||||
channel_id: u64,
|
channel_id: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
actions!(collab_panel, [ToggleFocus, Remove, Secondary]);
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
|
struct OpenChannelBuffer {
|
||||||
|
channel_id: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
actions!(
|
||||||
|
collab_panel,
|
||||||
|
[
|
||||||
|
ToggleFocus,
|
||||||
|
Remove,
|
||||||
|
Secondary,
|
||||||
|
CollapseSelectedChannel,
|
||||||
|
ExpandSelectedChannel
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
impl_actions!(
|
impl_actions!(
|
||||||
collab_panel,
|
collab_panel,
|
||||||
@ -82,7 +104,9 @@ impl_actions!(
|
|||||||
NewChannel,
|
NewChannel,
|
||||||
InviteMembers,
|
InviteMembers,
|
||||||
ManageMembers,
|
ManageMembers,
|
||||||
RenameChannel
|
RenameChannel,
|
||||||
|
ToggleCollapse,
|
||||||
|
OpenChannelBuffer
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -92,6 +116,7 @@ pub fn init(_client: Arc<Client>, cx: &mut AppContext) {
|
|||||||
settings::register::<panel_settings::CollaborationPanelSettings>(cx);
|
settings::register::<panel_settings::CollaborationPanelSettings>(cx);
|
||||||
contact_finder::init(cx);
|
contact_finder::init(cx);
|
||||||
channel_modal::init(cx);
|
channel_modal::init(cx);
|
||||||
|
channel_view::init(cx);
|
||||||
|
|
||||||
cx.add_action(CollabPanel::cancel);
|
cx.add_action(CollabPanel::cancel);
|
||||||
cx.add_action(CollabPanel::select_next);
|
cx.add_action(CollabPanel::select_next);
|
||||||
@ -105,6 +130,10 @@ pub fn init(_client: Arc<Client>, cx: &mut AppContext) {
|
|||||||
cx.add_action(CollabPanel::manage_members);
|
cx.add_action(CollabPanel::manage_members);
|
||||||
cx.add_action(CollabPanel::rename_selected_channel);
|
cx.add_action(CollabPanel::rename_selected_channel);
|
||||||
cx.add_action(CollabPanel::rename_channel);
|
cx.add_action(CollabPanel::rename_channel);
|
||||||
|
cx.add_action(CollabPanel::toggle_channel_collapsed);
|
||||||
|
cx.add_action(CollabPanel::collapse_selected_channel);
|
||||||
|
cx.add_action(CollabPanel::expand_selected_channel);
|
||||||
|
cx.add_action(CollabPanel::open_channel_buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -147,6 +176,7 @@ pub struct CollabPanel {
|
|||||||
list_state: ListState<Self>,
|
list_state: ListState<Self>,
|
||||||
subscriptions: Vec<Subscription>,
|
subscriptions: Vec<Subscription>,
|
||||||
collapsed_sections: Vec<Section>,
|
collapsed_sections: Vec<Section>,
|
||||||
|
collapsed_channels: Vec<ChannelId>,
|
||||||
workspace: WeakViewHandle<Workspace>,
|
workspace: WeakViewHandle<Workspace>,
|
||||||
context_menu_on_selected: bool,
|
context_menu_on_selected: bool,
|
||||||
}
|
}
|
||||||
@ -154,6 +184,7 @@ pub struct CollabPanel {
|
|||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
struct SerializedChannelsPanel {
|
struct SerializedChannelsPanel {
|
||||||
width: Option<f32>,
|
width: Option<f32>,
|
||||||
|
collapsed_channels: Vec<ChannelId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -198,6 +229,9 @@ enum ListEntry {
|
|||||||
channel: Arc<Channel>,
|
channel: Arc<Channel>,
|
||||||
depth: usize,
|
depth: usize,
|
||||||
},
|
},
|
||||||
|
ChannelNotes {
|
||||||
|
channel_id: ChannelId,
|
||||||
|
},
|
||||||
ChannelEditor {
|
ChannelEditor {
|
||||||
depth: usize,
|
depth: usize,
|
||||||
},
|
},
|
||||||
@ -341,6 +375,12 @@ impl CollabPanel {
|
|||||||
return channel_row;
|
return channel_row;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ListEntry::ChannelNotes { channel_id } => this.render_channel_notes(
|
||||||
|
*channel_id,
|
||||||
|
&theme.collab_panel,
|
||||||
|
is_selected,
|
||||||
|
cx,
|
||||||
|
),
|
||||||
ListEntry::ChannelInvite(channel) => Self::render_channel_invite(
|
ListEntry::ChannelInvite(channel) => Self::render_channel_invite(
|
||||||
channel.clone(),
|
channel.clone(),
|
||||||
this.channel_store.clone(),
|
this.channel_store.clone(),
|
||||||
@ -398,6 +438,7 @@ impl CollabPanel {
|
|||||||
subscriptions: Vec::default(),
|
subscriptions: Vec::default(),
|
||||||
match_candidates: Vec::default(),
|
match_candidates: Vec::default(),
|
||||||
collapsed_sections: vec![Section::Offline],
|
collapsed_sections: vec![Section::Offline],
|
||||||
|
collapsed_channels: Vec::default(),
|
||||||
workspace: workspace.weak_handle(),
|
workspace: workspace.weak_handle(),
|
||||||
client: workspace.app_state().client.clone(),
|
client: workspace.app_state().client.clone(),
|
||||||
context_menu_on_selected: true,
|
context_menu_on_selected: true,
|
||||||
@ -479,6 +520,7 @@ impl CollabPanel {
|
|||||||
if let Some(serialized_panel) = serialized_panel {
|
if let Some(serialized_panel) = serialized_panel {
|
||||||
panel.update(cx, |panel, cx| {
|
panel.update(cx, |panel, cx| {
|
||||||
panel.width = serialized_panel.width;
|
panel.width = serialized_panel.width;
|
||||||
|
panel.collapsed_channels = serialized_panel.collapsed_channels;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -489,12 +531,16 @@ impl CollabPanel {
|
|||||||
|
|
||||||
fn serialize(&mut self, cx: &mut ViewContext<Self>) {
|
fn serialize(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
let width = self.width;
|
let width = self.width;
|
||||||
|
let collapsed_channels = self.collapsed_channels.clone();
|
||||||
self.pending_serialization = cx.background().spawn(
|
self.pending_serialization = cx.background().spawn(
|
||||||
async move {
|
async move {
|
||||||
KEY_VALUE_STORE
|
KEY_VALUE_STORE
|
||||||
.write_kvp(
|
.write_kvp(
|
||||||
COLLABORATION_PANEL_KEY.into(),
|
COLLABORATION_PANEL_KEY.into(),
|
||||||
serde_json::to_string(&SerializedChannelsPanel { width })?,
|
serde_json::to_string(&SerializedChannelsPanel {
|
||||||
|
width,
|
||||||
|
collapsed_channels,
|
||||||
|
})?,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
anyhow::Ok(())
|
anyhow::Ok(())
|
||||||
@ -518,6 +564,10 @@ impl CollabPanel {
|
|||||||
if !self.collapsed_sections.contains(&Section::ActiveCall) {
|
if !self.collapsed_sections.contains(&Section::ActiveCall) {
|
||||||
let room = room.read(cx);
|
let room = room.read(cx);
|
||||||
|
|
||||||
|
if let Some(channel_id) = room.channel_id() {
|
||||||
|
self.entries.push(ListEntry::ChannelNotes { channel_id })
|
||||||
|
}
|
||||||
|
|
||||||
// Populate the active user.
|
// Populate the active user.
|
||||||
if let Some(user) = user_store.current_user() {
|
if let Some(user) = user_store.current_user() {
|
||||||
self.match_candidates.clear();
|
self.match_candidates.clear();
|
||||||
@ -657,10 +707,24 @@ impl CollabPanel {
|
|||||||
self.entries.push(ListEntry::ChannelEditor { depth: 0 });
|
self.entries.push(ListEntry::ChannelEditor { depth: 0 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let mut collapse_depth = None;
|
||||||
for mat in matches {
|
for mat in matches {
|
||||||
let (depth, channel) =
|
let (depth, channel) =
|
||||||
channel_store.channel_at_index(mat.candidate_id).unwrap();
|
channel_store.channel_at_index(mat.candidate_id).unwrap();
|
||||||
|
|
||||||
|
if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) {
|
||||||
|
collapse_depth = Some(depth);
|
||||||
|
} else if let Some(collapsed_depth) = collapse_depth {
|
||||||
|
if depth > collapsed_depth {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if self.is_channel_collapsed(channel.id) {
|
||||||
|
collapse_depth = Some(depth);
|
||||||
|
} else {
|
||||||
|
collapse_depth = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
match &self.channel_editing_state {
|
match &self.channel_editing_state {
|
||||||
Some(ChannelEditingState::Create { parent_id, .. })
|
Some(ChannelEditingState::Create { parent_id, .. })
|
||||||
if *parent_id == Some(channel.id) =>
|
if *parent_id == Some(channel.id) =>
|
||||||
@ -963,25 +1027,19 @@ impl CollabPanel {
|
|||||||
) -> AnyElement<Self> {
|
) -> AnyElement<Self> {
|
||||||
enum JoinProject {}
|
enum JoinProject {}
|
||||||
|
|
||||||
let font_cache = cx.font_cache();
|
let host_avatar_width = theme
|
||||||
let host_avatar_height = theme
|
|
||||||
.contact_avatar
|
.contact_avatar
|
||||||
.width
|
.width
|
||||||
.or(theme.contact_avatar.height)
|
.or(theme.contact_avatar.height)
|
||||||
.unwrap_or(0.);
|
.unwrap_or(0.);
|
||||||
let row = &theme.project_row.inactive_state().default;
|
|
||||||
let tree_branch = theme.tree_branch;
|
let tree_branch = theme.tree_branch;
|
||||||
let line_height = row.name.text.line_height(font_cache);
|
|
||||||
let cap_height = row.name.text.cap_height(font_cache);
|
|
||||||
let baseline_offset =
|
|
||||||
row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.;
|
|
||||||
let project_name = if worktree_root_names.is_empty() {
|
let project_name = if worktree_root_names.is_empty() {
|
||||||
"untitled".to_string()
|
"untitled".to_string()
|
||||||
} else {
|
} else {
|
||||||
worktree_root_names.join(", ")
|
worktree_root_names.join(", ")
|
||||||
};
|
};
|
||||||
|
|
||||||
MouseEventHandler::new::<JoinProject, _>(project_id as usize, cx, |mouse_state, _| {
|
MouseEventHandler::new::<JoinProject, _>(project_id as usize, cx, |mouse_state, cx| {
|
||||||
let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
|
let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
|
||||||
let row = theme
|
let row = theme
|
||||||
.project_row
|
.project_row
|
||||||
@ -989,39 +1047,20 @@ impl CollabPanel {
|
|||||||
.style_for(mouse_state);
|
.style_for(mouse_state);
|
||||||
|
|
||||||
Flex::row()
|
Flex::row()
|
||||||
|
.with_child(render_tree_branch(
|
||||||
|
tree_branch,
|
||||||
|
&row.name.text,
|
||||||
|
is_last,
|
||||||
|
vec2f(host_avatar_width, theme.row_height),
|
||||||
|
cx.font_cache(),
|
||||||
|
))
|
||||||
.with_child(
|
.with_child(
|
||||||
Stack::new()
|
Svg::new("icons/file_icons/folder.svg")
|
||||||
.with_child(Canvas::new(move |scene, bounds, _, _, _| {
|
.with_color(theme.channel_hash.color)
|
||||||
let start_x =
|
|
||||||
bounds.min_x() + (bounds.width() / 2.) - (tree_branch.width / 2.);
|
|
||||||
let end_x = bounds.max_x();
|
|
||||||
let start_y = bounds.min_y();
|
|
||||||
let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
|
|
||||||
|
|
||||||
scene.push_quad(gpui::Quad {
|
|
||||||
bounds: RectF::from_points(
|
|
||||||
vec2f(start_x, start_y),
|
|
||||||
vec2f(
|
|
||||||
start_x + tree_branch.width,
|
|
||||||
if is_last { end_y } else { bounds.max_y() },
|
|
||||||
),
|
|
||||||
),
|
|
||||||
background: Some(tree_branch.color),
|
|
||||||
border: gpui::Border::default(),
|
|
||||||
corner_radii: (0.).into(),
|
|
||||||
});
|
|
||||||
scene.push_quad(gpui::Quad {
|
|
||||||
bounds: RectF::from_points(
|
|
||||||
vec2f(start_x, end_y),
|
|
||||||
vec2f(end_x, end_y + tree_branch.width),
|
|
||||||
),
|
|
||||||
background: Some(tree_branch.color),
|
|
||||||
border: gpui::Border::default(),
|
|
||||||
corner_radii: (0.).into(),
|
|
||||||
});
|
|
||||||
}))
|
|
||||||
.constrained()
|
.constrained()
|
||||||
.with_width(host_avatar_height),
|
.with_width(theme.channel_hash.width)
|
||||||
|
.aligned()
|
||||||
|
.left(),
|
||||||
)
|
)
|
||||||
.with_child(
|
.with_child(
|
||||||
Label::new(project_name, row.name.text.clone())
|
Label::new(project_name, row.name.text.clone())
|
||||||
@ -1196,7 +1235,7 @@ impl CollabPanel {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if let Some(name) = channel_name {
|
if let Some(name) = channel_name {
|
||||||
Cow::Owned(format!("Current Call - #{}", name))
|
Cow::Owned(format!("#{}", name))
|
||||||
} else {
|
} else {
|
||||||
Cow::Borrowed("Current Call")
|
Cow::Borrowed("Current Call")
|
||||||
}
|
}
|
||||||
@ -1332,7 +1371,7 @@ impl CollabPanel {
|
|||||||
.with_cursor_style(CursorStyle::PointingHand)
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||||
if can_collapse {
|
if can_collapse {
|
||||||
this.toggle_expanded(section, cx);
|
this.toggle_section_expanded(section, cx);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -1479,6 +1518,11 @@ impl CollabPanel {
|
|||||||
cx: &AppContext,
|
cx: &AppContext,
|
||||||
) -> AnyElement<Self> {
|
) -> AnyElement<Self> {
|
||||||
Flex::row()
|
Flex::row()
|
||||||
|
.with_child(
|
||||||
|
Empty::new()
|
||||||
|
.constrained()
|
||||||
|
.with_width(theme.collab_panel.disclosure.button_space()),
|
||||||
|
)
|
||||||
.with_child(
|
.with_child(
|
||||||
Svg::new("icons/hash.svg")
|
Svg::new("icons/hash.svg")
|
||||||
.with_color(theme.collab_panel.channel_hash.color)
|
.with_color(theme.collab_panel.channel_hash.color)
|
||||||
@ -1537,6 +1581,10 @@ impl CollabPanel {
|
|||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> AnyElement<Self> {
|
) -> AnyElement<Self> {
|
||||||
let channel_id = channel.id;
|
let channel_id = channel.id;
|
||||||
|
let has_children = self.channel_store.read(cx).has_children(channel_id);
|
||||||
|
let disclosed =
|
||||||
|
has_children.then(|| !self.collapsed_channels.binary_search(&channel_id).is_ok());
|
||||||
|
|
||||||
let is_active = iife!({
|
let is_active = iife!({
|
||||||
let call_channel = ActiveCall::global(cx)
|
let call_channel = ActiveCall::global(cx)
|
||||||
.read(cx)
|
.read(cx)
|
||||||
@ -1550,7 +1598,7 @@ impl CollabPanel {
|
|||||||
const FACEPILE_LIMIT: usize = 3;
|
const FACEPILE_LIMIT: usize = 3;
|
||||||
|
|
||||||
MouseEventHandler::new::<Channel, _>(channel.id as usize, cx, |state, cx| {
|
MouseEventHandler::new::<Channel, _>(channel.id as usize, cx, |state, cx| {
|
||||||
Flex::row()
|
Flex::<Self>::row()
|
||||||
.with_child(
|
.with_child(
|
||||||
Svg::new("icons/hash.svg")
|
Svg::new("icons/hash.svg")
|
||||||
.with_color(theme.channel_hash.color)
|
.with_color(theme.channel_hash.color)
|
||||||
@ -1599,6 +1647,11 @@ impl CollabPanel {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.align_children_center()
|
.align_children_center()
|
||||||
|
.styleable_component()
|
||||||
|
.disclosable(disclosed, Box::new(ToggleCollapse { channel_id }))
|
||||||
|
.with_id(channel_id as usize)
|
||||||
|
.with_style(theme.disclosure.clone())
|
||||||
|
.element()
|
||||||
.constrained()
|
.constrained()
|
||||||
.with_height(theme.row_height)
|
.with_height(theme.row_height)
|
||||||
.contained()
|
.contained()
|
||||||
@ -1618,6 +1671,61 @@ impl CollabPanel {
|
|||||||
.into_any()
|
.into_any()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_channel_notes(
|
||||||
|
&self,
|
||||||
|
channel_id: ChannelId,
|
||||||
|
theme: &theme::CollabPanel,
|
||||||
|
is_selected: bool,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> AnyElement<Self> {
|
||||||
|
enum ChannelNotes {}
|
||||||
|
let host_avatar_width = theme
|
||||||
|
.contact_avatar
|
||||||
|
.width
|
||||||
|
.or(theme.contact_avatar.height)
|
||||||
|
.unwrap_or(0.);
|
||||||
|
|
||||||
|
MouseEventHandler::new::<ChannelNotes, _>(channel_id as usize, cx, |state, cx| {
|
||||||
|
let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state);
|
||||||
|
let row = theme.project_row.in_state(is_selected).style_for(state);
|
||||||
|
|
||||||
|
Flex::<Self>::row()
|
||||||
|
.with_child(render_tree_branch(
|
||||||
|
tree_branch,
|
||||||
|
&row.name.text,
|
||||||
|
true,
|
||||||
|
vec2f(host_avatar_width, theme.row_height),
|
||||||
|
cx.font_cache(),
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
Svg::new("icons/radix/file.svg")
|
||||||
|
.with_color(theme.channel_hash.color)
|
||||||
|
.constrained()
|
||||||
|
.with_width(theme.channel_hash.width)
|
||||||
|
.aligned()
|
||||||
|
.left(),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
Label::new("notes", theme.channel_name.text.clone())
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.channel_name.container)
|
||||||
|
.aligned()
|
||||||
|
.left()
|
||||||
|
.flex(1., true),
|
||||||
|
)
|
||||||
|
.constrained()
|
||||||
|
.with_height(theme.row_height)
|
||||||
|
.contained()
|
||||||
|
.with_style(*theme.channel_row.style_for(is_selected, state))
|
||||||
|
.with_padding_left(theme.channel_row.default_style().padding.left)
|
||||||
|
})
|
||||||
|
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||||
|
this.open_channel_buffer(&OpenChannelBuffer { channel_id }, cx);
|
||||||
|
})
|
||||||
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
|
.into_any()
|
||||||
|
}
|
||||||
|
|
||||||
fn render_channel_invite(
|
fn render_channel_invite(
|
||||||
channel: Arc<Channel>,
|
channel: Arc<Channel>,
|
||||||
channel_store: ModelHandle<ChannelStore>,
|
channel_store: ModelHandle<ChannelStore>,
|
||||||
@ -1815,7 +1923,6 @@ impl CollabPanel {
|
|||||||
channel_id: u64,
|
channel_id: u64,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
if self.channel_store.read(cx).is_user_admin(channel_id) {
|
|
||||||
self.context_menu_on_selected = position.is_none();
|
self.context_menu_on_selected = position.is_none();
|
||||||
|
|
||||||
self.context_menu.update(cx, |context_menu, cx| {
|
self.context_menu.update(cx, |context_menu, cx| {
|
||||||
@ -1825,6 +1932,30 @@ impl CollabPanel {
|
|||||||
OverlayPositionMode::Window
|
OverlayPositionMode::Window
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let expand_action_name = if self.is_channel_collapsed(channel_id) {
|
||||||
|
"Expand Subchannels"
|
||||||
|
} else {
|
||||||
|
"Collapse Subchannels"
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut items = vec![
|
||||||
|
ContextMenuItem::action(expand_action_name, ToggleCollapse { channel_id }),
|
||||||
|
ContextMenuItem::action("Open Notes", OpenChannelBuffer { channel_id }),
|
||||||
|
];
|
||||||
|
|
||||||
|
if self.channel_store.read(cx).is_user_admin(channel_id) {
|
||||||
|
items.extend([
|
||||||
|
ContextMenuItem::Separator,
|
||||||
|
ContextMenuItem::action("New Subchannel", NewChannel { channel_id }),
|
||||||
|
ContextMenuItem::action("Rename", RenameChannel { channel_id }),
|
||||||
|
ContextMenuItem::Separator,
|
||||||
|
ContextMenuItem::action("Invite Members", InviteMembers { channel_id }),
|
||||||
|
ContextMenuItem::action("Manage Members", ManageMembers { channel_id }),
|
||||||
|
ContextMenuItem::Separator,
|
||||||
|
ContextMenuItem::action("Delete", RemoveChannel { channel_id }),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
context_menu.show(
|
context_menu.show(
|
||||||
position.unwrap_or_default(),
|
position.unwrap_or_default(),
|
||||||
if self.context_menu_on_selected {
|
if self.context_menu_on_selected {
|
||||||
@ -1832,23 +1963,13 @@ impl CollabPanel {
|
|||||||
} else {
|
} else {
|
||||||
gpui::elements::AnchorCorner::BottomLeft
|
gpui::elements::AnchorCorner::BottomLeft
|
||||||
},
|
},
|
||||||
vec![
|
items,
|
||||||
ContextMenuItem::action("New Subchannel", NewChannel { channel_id }),
|
|
||||||
ContextMenuItem::Separator,
|
|
||||||
ContextMenuItem::action("Invite to Channel", InviteMembers { channel_id }),
|
|
||||||
ContextMenuItem::Separator,
|
|
||||||
ContextMenuItem::action("Rename", RenameChannel { channel_id }),
|
|
||||||
ContextMenuItem::action("Manage", ManageMembers { channel_id }),
|
|
||||||
ContextMenuItem::Separator,
|
|
||||||
ContextMenuItem::action("Delete", RemoveChannel { channel_id }),
|
|
||||||
],
|
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
|
fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
|
||||||
if self.take_editing_state(cx) {
|
if self.take_editing_state(cx) {
|
||||||
@ -1912,7 +2033,7 @@ impl CollabPanel {
|
|||||||
| Section::Online
|
| Section::Online
|
||||||
| Section::Offline
|
| Section::Offline
|
||||||
| Section::ChannelInvites => {
|
| Section::ChannelInvites => {
|
||||||
self.toggle_expanded(*section, cx);
|
self.toggle_section_expanded(*section, cx);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
ListEntry::Contact { contact, calling } => {
|
ListEntry::Contact { contact, calling } => {
|
||||||
@ -2000,7 +2121,7 @@ impl CollabPanel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toggle_expanded(&mut self, section: Section, cx: &mut ViewContext<Self>) {
|
fn toggle_section_expanded(&mut self, section: Section, cx: &mut ViewContext<Self>) {
|
||||||
if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) {
|
if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) {
|
||||||
self.collapsed_sections.remove(ix);
|
self.collapsed_sections.remove(ix);
|
||||||
} else {
|
} else {
|
||||||
@ -2009,6 +2130,55 @@ impl CollabPanel {
|
|||||||
self.update_entries(false, cx);
|
self.update_entries(false, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn collapse_selected_channel(
|
||||||
|
&mut self,
|
||||||
|
_: &CollapseSelectedChannel,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.is_channel_collapsed(channel_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.toggle_channel_collapsed(&ToggleCollapse { channel_id }, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext<Self>) {
|
||||||
|
let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if !self.is_channel_collapsed(channel_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.toggle_channel_collapsed(&ToggleCollapse { channel_id }, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggle_channel_collapsed(&mut self, action: &ToggleCollapse, cx: &mut ViewContext<Self>) {
|
||||||
|
let channel_id = action.channel_id;
|
||||||
|
|
||||||
|
match self.collapsed_channels.binary_search(&channel_id) {
|
||||||
|
Ok(ix) => {
|
||||||
|
self.collapsed_channels.remove(ix);
|
||||||
|
}
|
||||||
|
Err(ix) => {
|
||||||
|
self.collapsed_channels.insert(ix, channel_id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.serialize(cx);
|
||||||
|
self.update_entries(true, cx);
|
||||||
|
cx.notify();
|
||||||
|
cx.focus_self();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_channel_collapsed(&self, channel: ChannelId) -> bool {
|
||||||
|
self.collapsed_channels.binary_search(&channel).is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
fn leave_call(cx: &mut ViewContext<Self>) {
|
fn leave_call(cx: &mut ViewContext<Self>) {
|
||||||
ActiveCall::global(cx)
|
ActiveCall::global(cx)
|
||||||
.update(cx, |call, cx| call.hang_up(cx))
|
.update(cx, |call, cx| call.hang_up(cx))
|
||||||
@ -2048,6 +2218,8 @@ impl CollabPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn new_subchannel(&mut self, action: &NewChannel, cx: &mut ViewContext<Self>) {
|
fn new_subchannel(&mut self, action: &NewChannel, cx: &mut ViewContext<Self>) {
|
||||||
|
self.collapsed_channels
|
||||||
|
.retain(|&channel| channel != action.channel_id);
|
||||||
self.channel_editing_state = Some(ChannelEditingState::Create {
|
self.channel_editing_state = Some(ChannelEditingState::Create {
|
||||||
parent_id: Some(action.channel_id),
|
parent_id: Some(action.channel_id),
|
||||||
pending_name: None,
|
pending_name: None,
|
||||||
@ -2103,6 +2275,21 @@ impl CollabPanel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn open_channel_buffer(&mut self, action: &OpenChannelBuffer, cx: &mut ViewContext<Self>) {
|
||||||
|
if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||||
|
let pane = workspace.read(cx).active_pane().clone();
|
||||||
|
let channel_view = ChannelView::open(action.channel_id, pane.clone(), workspace, cx);
|
||||||
|
cx.spawn(|_, mut cx| async move {
|
||||||
|
let channel_view = channel_view.await?;
|
||||||
|
pane.update(&mut cx, |pane, cx| {
|
||||||
|
pane.add_item(Box::new(channel_view), true, true, None, cx)
|
||||||
|
});
|
||||||
|
anyhow::Ok(())
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext<Self>) {
|
fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext<Self>) {
|
||||||
let Some(channel) = self.selected_channel() else {
|
let Some(channel) = self.selected_channel() else {
|
||||||
return;
|
return;
|
||||||
@ -2261,6 +2448,51 @@ impl CollabPanel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_tree_branch(
|
||||||
|
branch_style: theme::TreeBranch,
|
||||||
|
row_style: &TextStyle,
|
||||||
|
is_last: bool,
|
||||||
|
size: Vector2F,
|
||||||
|
font_cache: &FontCache,
|
||||||
|
) -> gpui::elements::ConstrainedBox<CollabPanel> {
|
||||||
|
let line_height = row_style.line_height(font_cache);
|
||||||
|
let cap_height = row_style.cap_height(font_cache);
|
||||||
|
let baseline_offset = row_style.baseline_offset(font_cache) + (size.y() - line_height) / 2.;
|
||||||
|
|
||||||
|
Canvas::new(move |scene, bounds, _, _, _| {
|
||||||
|
scene.paint_layer(None, |scene| {
|
||||||
|
let start_x = bounds.min_x() + (bounds.width() / 2.) - (branch_style.width / 2.);
|
||||||
|
let end_x = bounds.max_x();
|
||||||
|
let start_y = bounds.min_y();
|
||||||
|
let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
|
||||||
|
|
||||||
|
scene.push_quad(gpui::Quad {
|
||||||
|
bounds: RectF::from_points(
|
||||||
|
vec2f(start_x, start_y),
|
||||||
|
vec2f(
|
||||||
|
start_x + branch_style.width,
|
||||||
|
if is_last { end_y } else { bounds.max_y() },
|
||||||
|
),
|
||||||
|
),
|
||||||
|
background: Some(branch_style.color),
|
||||||
|
border: gpui::Border::default(),
|
||||||
|
corner_radii: (0.).into(),
|
||||||
|
});
|
||||||
|
scene.push_quad(gpui::Quad {
|
||||||
|
bounds: RectF::from_points(
|
||||||
|
vec2f(start_x, end_y),
|
||||||
|
vec2f(end_x, end_y + branch_style.width),
|
||||||
|
),
|
||||||
|
background: Some(branch_style.color),
|
||||||
|
border: gpui::Border::default(),
|
||||||
|
corner_radii: (0.).into(),
|
||||||
|
});
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.constrained()
|
||||||
|
.with_width(size.x())
|
||||||
|
}
|
||||||
|
|
||||||
impl View for CollabPanel {
|
impl View for CollabPanel {
|
||||||
fn ui_name() -> &'static str {
|
fn ui_name() -> &'static str {
|
||||||
"CollabPanel"
|
"CollabPanel"
|
||||||
@ -2470,6 +2702,14 @@ impl PartialEq for ListEntry {
|
|||||||
return channel_1.id == channel_2.id && depth_1 == depth_2;
|
return channel_1.id == channel_2.id && depth_1 == depth_2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ListEntry::ChannelNotes { channel_id } => {
|
||||||
|
if let ListEntry::ChannelNotes {
|
||||||
|
channel_id: other_id,
|
||||||
|
} = other
|
||||||
|
{
|
||||||
|
return channel_id == other_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
ListEntry::ChannelInvite(channel_1) => {
|
ListEntry::ChannelInvite(channel_1) => {
|
||||||
if let ListEntry::ChannelInvite(channel_2) = other {
|
if let ListEntry::ChannelInvite(channel_2) = other {
|
||||||
return channel_1.id == channel_2.id;
|
return channel_1.id == channel_2.id;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use client::{proto, ChannelId, ChannelMembership, ChannelStore, User, UserId, UserStore};
|
use channel::{ChannelId, ChannelMembership, ChannelStore};
|
||||||
|
use client::{proto, User, UserId, UserStore};
|
||||||
use context_menu::{ContextMenu, ContextMenuItem};
|
use context_menu::{ContextMenu, ContextMenuItem};
|
||||||
use fuzzy::{match_strings, StringMatchCandidate};
|
use fuzzy::{match_strings, StringMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
|
@ -1096,7 +1096,7 @@ impl CollabTitlebarItem {
|
|||||||
style
|
style
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_face<V: View>(
|
fn render_face<V: 'static>(
|
||||||
avatar: Arc<ImageData>,
|
avatar: Arc<ImageData>,
|
||||||
avatar_style: AvatarStyle,
|
avatar_style: AvatarStyle,
|
||||||
background_color: Color,
|
background_color: Color,
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
pub mod channel_view;
|
||||||
pub mod collab_panel;
|
pub mod collab_panel;
|
||||||
mod collab_titlebar_item;
|
mod collab_titlebar_item;
|
||||||
mod contact_notification;
|
mod contact_notification;
|
||||||
|
@ -2,14 +2,14 @@ use client::User;
|
|||||||
use gpui::{
|
use gpui::{
|
||||||
elements::*,
|
elements::*,
|
||||||
platform::{CursorStyle, MouseButton},
|
platform::{CursorStyle, MouseButton},
|
||||||
AnyElement, Element, View, ViewContext,
|
AnyElement, Element, ViewContext,
|
||||||
};
|
};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
enum Dismiss {}
|
enum Dismiss {}
|
||||||
enum Button {}
|
enum Button {}
|
||||||
|
|
||||||
pub fn render_user_notification<F, V>(
|
pub fn render_user_notification<F, V: 'static>(
|
||||||
user: Arc<User>,
|
user: Arc<User>,
|
||||||
title: &'static str,
|
title: &'static str,
|
||||||
body: Option<&'static str>,
|
body: Option<&'static str>,
|
||||||
@ -19,7 +19,6 @@ pub fn render_user_notification<F, V>(
|
|||||||
) -> AnyElement<V>
|
) -> AnyElement<V>
|
||||||
where
|
where
|
||||||
F: 'static + Fn(&mut V, &mut ViewContext<V>),
|
F: 'static + Fn(&mut V, &mut ViewContext<V>),
|
||||||
V: View,
|
|
||||||
{
|
{
|
||||||
let theme = theme::current(cx).clone();
|
let theme = theme::current(cx).clone();
|
||||||
let theme = &theme.contact_notification;
|
let theme = &theme.contact_notification;
|
||||||
|
18
crates/component_test/Cargo.toml
Normal file
18
crates/component_test/Cargo.toml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
[package]
|
||||||
|
name = "component_test"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/component_test.rs"
|
||||||
|
doctest = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow.workspace = true
|
||||||
|
gpui = { path = "../gpui" }
|
||||||
|
settings = { path = "../settings" }
|
||||||
|
util = { path = "../util" }
|
||||||
|
theme = { path = "../theme" }
|
||||||
|
workspace = { path = "../workspace" }
|
||||||
|
project = { path = "../project" }
|
121
crates/component_test/src/component_test.rs
Normal file
121
crates/component_test/src/component_test.rs
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
use gpui::{
|
||||||
|
actions,
|
||||||
|
elements::{Component, Flex, ParentElement, SafeStylable},
|
||||||
|
AppContext, Element, Entity, ModelHandle, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||||
|
};
|
||||||
|
use project::Project;
|
||||||
|
use theme::components::{action_button::Button, label::Label, ComponentExt};
|
||||||
|
use workspace::{
|
||||||
|
item::Item, register_deserializable_item, ItemId, Pane, PaneBackdrop, Workspace, WorkspaceId,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init(cx: &mut AppContext) {
|
||||||
|
cx.add_action(ComponentTest::toggle_disclosure);
|
||||||
|
cx.add_action(ComponentTest::toggle_toggle);
|
||||||
|
cx.add_action(ComponentTest::deploy);
|
||||||
|
register_deserializable_item::<ComponentTest>(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
actions!(
|
||||||
|
test,
|
||||||
|
[NoAction, ToggleDisclosure, ToggleToggle, NewComponentTest]
|
||||||
|
);
|
||||||
|
|
||||||
|
struct ComponentTest {
|
||||||
|
disclosed: bool,
|
||||||
|
toggled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ComponentTest {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
disclosed: false,
|
||||||
|
toggled: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deploy(workspace: &mut Workspace, _: &NewComponentTest, cx: &mut ViewContext<Workspace>) {
|
||||||
|
workspace.add_item(Box::new(cx.add_view(|_| ComponentTest::new())), cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggle_disclosure(&mut self, _: &ToggleDisclosure, cx: &mut ViewContext<Self>) {
|
||||||
|
self.disclosed = !self.disclosed;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggle_toggle(&mut self, _: &ToggleToggle, cx: &mut ViewContext<Self>) {
|
||||||
|
self.toggled = !self.toggled;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entity for ComponentTest {
|
||||||
|
type Event = ();
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for ComponentTest {
|
||||||
|
fn ui_name() -> &'static str {
|
||||||
|
"Component Test"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> gpui::AnyElement<Self> {
|
||||||
|
let theme = theme::current(cx);
|
||||||
|
|
||||||
|
PaneBackdrop::new(
|
||||||
|
cx.view_id(),
|
||||||
|
Flex::column()
|
||||||
|
.with_spacing(10.)
|
||||||
|
.with_child(
|
||||||
|
Button::action(NoAction)
|
||||||
|
.with_tooltip("Here's what a tooltip looks like", theme.tooltip.clone())
|
||||||
|
.with_contents(Label::new("Click me!"))
|
||||||
|
.with_style(theme.component_test.button.clone())
|
||||||
|
.element(),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
Button::action(ToggleToggle)
|
||||||
|
.with_tooltip("Here's what a tooltip looks like", theme.tooltip.clone())
|
||||||
|
.with_contents(Label::new("Toggle me!"))
|
||||||
|
.toggleable(self.toggled)
|
||||||
|
.with_style(theme.component_test.toggle.clone())
|
||||||
|
.element(),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
Label::new("A disclosure")
|
||||||
|
.disclosable(Some(self.disclosed), Box::new(ToggleDisclosure))
|
||||||
|
.with_style(theme.component_test.disclosure.clone())
|
||||||
|
.element(),
|
||||||
|
)
|
||||||
|
.constrained()
|
||||||
|
.with_width(200.)
|
||||||
|
.aligned()
|
||||||
|
.into_any(),
|
||||||
|
)
|
||||||
|
.into_any()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Item for ComponentTest {
|
||||||
|
fn tab_content<V: 'static>(
|
||||||
|
&self,
|
||||||
|
_: Option<usize>,
|
||||||
|
style: &theme::Tab,
|
||||||
|
_: &AppContext,
|
||||||
|
) -> gpui::AnyElement<V> {
|
||||||
|
gpui::elements::Label::new("Component test", style.label.clone()).into_any()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serialized_item_kind() -> Option<&'static str> {
|
||||||
|
Some("ComponentTest")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize(
|
||||||
|
_project: ModelHandle<Project>,
|
||||||
|
_workspace: WeakViewHandle<Workspace>,
|
||||||
|
_workspace_id: WorkspaceId,
|
||||||
|
_item_id: ItemId,
|
||||||
|
cx: &mut ViewContext<Pane>,
|
||||||
|
) -> Task<anyhow::Result<ViewHandle<Self>>> {
|
||||||
|
Task::ready(Ok(cx.add_view(|_| Self::new())))
|
||||||
|
}
|
||||||
|
}
|
@ -538,7 +538,7 @@ impl ProjectDiagnosticsEditor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Item for ProjectDiagnosticsEditor {
|
impl Item for ProjectDiagnosticsEditor {
|
||||||
fn tab_content<T: View>(
|
fn tab_content<T: 'static>(
|
||||||
&self,
|
&self,
|
||||||
_detail: Option<usize>,
|
_detail: Option<usize>,
|
||||||
style: &theme::Tab,
|
style: &theme::Tab,
|
||||||
@ -735,7 +735,7 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn render_summary<T: View>(
|
pub(crate) fn render_summary<T: 'static>(
|
||||||
summary: &DiagnosticSummary,
|
summary: &DiagnosticSummary,
|
||||||
text_style: &TextStyle,
|
text_style: &TextStyle,
|
||||||
theme: &theme::ProjectDiagnostics,
|
theme: &theme::ProjectDiagnostics,
|
||||||
|
@ -11,7 +11,7 @@ use gpui::{
|
|||||||
|
|
||||||
const DEAD_ZONE: f32 = 4.;
|
const DEAD_ZONE: f32 = 4.;
|
||||||
|
|
||||||
enum State<V: View> {
|
enum State<V> {
|
||||||
Down {
|
Down {
|
||||||
region_offset: Vector2F,
|
region_offset: Vector2F,
|
||||||
region: RectF,
|
region: RectF,
|
||||||
@ -31,7 +31,7 @@ enum State<V: View> {
|
|||||||
Canceled,
|
Canceled,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> Clone for State<V> {
|
impl<V> Clone for State<V> {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
match self {
|
match self {
|
||||||
&State::Down {
|
&State::Down {
|
||||||
@ -68,12 +68,12 @@ impl<V: View> Clone for State<V> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DragAndDrop<V: View> {
|
pub struct DragAndDrop<V> {
|
||||||
containers: HashSet<WeakViewHandle<V>>,
|
containers: HashSet<WeakViewHandle<V>>,
|
||||||
currently_dragged: Option<State<V>>,
|
currently_dragged: Option<State<V>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> Default for DragAndDrop<V> {
|
impl<V> Default for DragAndDrop<V> {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
containers: Default::default(),
|
containers: Default::default(),
|
||||||
@ -82,7 +82,7 @@ impl<V: View> Default for DragAndDrop<V> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> DragAndDrop<V> {
|
impl<V: 'static> DragAndDrop<V> {
|
||||||
pub fn register_container(&mut self, handle: WeakViewHandle<V>) {
|
pub fn register_container(&mut self, handle: WeakViewHandle<V>) {
|
||||||
self.containers.insert(handle);
|
self.containers.insert(handle);
|
||||||
}
|
}
|
||||||
@ -291,7 +291,7 @@ impl<V: View> DragAndDrop<V> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Draggable<V: View> {
|
pub trait Draggable<V> {
|
||||||
fn as_draggable<D: View, P: Any>(
|
fn as_draggable<D: View, P: Any>(
|
||||||
self,
|
self,
|
||||||
payload: P,
|
payload: P,
|
||||||
@ -301,7 +301,7 @@ pub trait Draggable<V: View> {
|
|||||||
Self: Sized;
|
Self: Sized;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> Draggable<V> for MouseEventHandler<V> {
|
impl<V: 'static> Draggable<V> for MouseEventHandler<V> {
|
||||||
fn as_draggable<D: View, P: Any>(
|
fn as_draggable<D: View, P: Any>(
|
||||||
self,
|
self,
|
||||||
payload: P,
|
payload: P,
|
||||||
|
@ -559,6 +559,7 @@ pub struct Editor {
|
|||||||
blink_manager: ModelHandle<BlinkManager>,
|
blink_manager: ModelHandle<BlinkManager>,
|
||||||
show_local_selections: bool,
|
show_local_selections: bool,
|
||||||
mode: EditorMode,
|
mode: EditorMode,
|
||||||
|
replica_id_mapping: Option<HashMap<ReplicaId, ReplicaId>>,
|
||||||
show_gutter: bool,
|
show_gutter: bool,
|
||||||
show_wrap_guides: Option<bool>,
|
show_wrap_guides: Option<bool>,
|
||||||
placeholder_text: Option<Arc<str>>,
|
placeholder_text: Option<Arc<str>>,
|
||||||
@ -1394,6 +1395,7 @@ impl Editor {
|
|||||||
blink_manager: blink_manager.clone(),
|
blink_manager: blink_manager.clone(),
|
||||||
show_local_selections: true,
|
show_local_selections: true,
|
||||||
mode,
|
mode,
|
||||||
|
replica_id_mapping: None,
|
||||||
show_gutter: mode == EditorMode::Full,
|
show_gutter: mode == EditorMode::Full,
|
||||||
show_wrap_guides: None,
|
show_wrap_guides: None,
|
||||||
placeholder_text: None,
|
placeholder_text: None,
|
||||||
@ -1604,6 +1606,19 @@ impl Editor {
|
|||||||
self.read_only = read_only;
|
self.read_only = read_only;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn replica_id_map(&self) -> Option<&HashMap<ReplicaId, ReplicaId>> {
|
||||||
|
self.replica_id_mapping.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_replica_id_map(
|
||||||
|
&mut self,
|
||||||
|
mapping: Option<HashMap<ReplicaId, ReplicaId>>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
self.replica_id_mapping = mapping;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
fn selections_did_change(
|
fn selections_did_change(
|
||||||
&mut self,
|
&mut self,
|
||||||
local: bool,
|
local: bool,
|
||||||
@ -1736,6 +1751,31 @@ impl Editor {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn edit_with_block_indent<I, S, T>(
|
||||||
|
&mut self,
|
||||||
|
edits: I,
|
||||||
|
original_indent_columns: Vec<u32>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) where
|
||||||
|
I: IntoIterator<Item = (Range<S>, T)>,
|
||||||
|
S: ToOffset,
|
||||||
|
T: Into<Arc<str>>,
|
||||||
|
{
|
||||||
|
if self.read_only {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.buffer.update(cx, |buffer, cx| {
|
||||||
|
buffer.edit(
|
||||||
|
edits,
|
||||||
|
Some(AutoindentMode::Block {
|
||||||
|
original_indent_columns,
|
||||||
|
}),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn select(&mut self, phase: SelectPhase, cx: &mut ViewContext<Self>) {
|
fn select(&mut self, phase: SelectPhase, cx: &mut ViewContext<Self>) {
|
||||||
self.hide_context_menu(cx);
|
self.hide_context_menu(cx);
|
||||||
|
|
||||||
@ -2667,7 +2707,6 @@ impl Editor {
|
|||||||
false
|
false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn completion_query(buffer: &MultiBufferSnapshot, position: impl ToOffset) -> Option<String> {
|
fn completion_query(buffer: &MultiBufferSnapshot, position: impl ToOffset) -> Option<String> {
|
||||||
let offset = position.to_offset(buffer);
|
let offset = position.to_offset(buffer);
|
||||||
let (word_range, kind) = buffer.surrounding_word(offset);
|
let (word_range, kind) = buffer.surrounding_word(offset);
|
||||||
@ -4742,6 +4781,7 @@ impl Editor {
|
|||||||
let mut clipboard_selections = Vec::with_capacity(selections.len());
|
let mut clipboard_selections = Vec::with_capacity(selections.len());
|
||||||
{
|
{
|
||||||
let max_point = buffer.max_point();
|
let max_point = buffer.max_point();
|
||||||
|
let mut is_first = true;
|
||||||
for selection in &mut selections {
|
for selection in &mut selections {
|
||||||
let is_entire_line = selection.is_empty() || self.selections.line_mode;
|
let is_entire_line = selection.is_empty() || self.selections.line_mode;
|
||||||
if is_entire_line {
|
if is_entire_line {
|
||||||
@ -4749,6 +4789,11 @@ impl Editor {
|
|||||||
selection.end = cmp::min(max_point, Point::new(selection.end.row + 1, 0));
|
selection.end = cmp::min(max_point, Point::new(selection.end.row + 1, 0));
|
||||||
selection.goal = SelectionGoal::None;
|
selection.goal = SelectionGoal::None;
|
||||||
}
|
}
|
||||||
|
if is_first {
|
||||||
|
is_first = false;
|
||||||
|
} else {
|
||||||
|
text += "\n";
|
||||||
|
}
|
||||||
let mut len = 0;
|
let mut len = 0;
|
||||||
for chunk in buffer.text_for_range(selection.start..selection.end) {
|
for chunk in buffer.text_for_range(selection.start..selection.end) {
|
||||||
text.push_str(chunk);
|
text.push_str(chunk);
|
||||||
@ -4779,6 +4824,7 @@ impl Editor {
|
|||||||
let mut clipboard_selections = Vec::with_capacity(selections.len());
|
let mut clipboard_selections = Vec::with_capacity(selections.len());
|
||||||
{
|
{
|
||||||
let max_point = buffer.max_point();
|
let max_point = buffer.max_point();
|
||||||
|
let mut is_first = true;
|
||||||
for selection in selections.iter() {
|
for selection in selections.iter() {
|
||||||
let mut start = selection.start;
|
let mut start = selection.start;
|
||||||
let mut end = selection.end;
|
let mut end = selection.end;
|
||||||
@ -4787,6 +4833,11 @@ impl Editor {
|
|||||||
start = Point::new(start.row, 0);
|
start = Point::new(start.row, 0);
|
||||||
end = cmp::min(max_point, Point::new(end.row + 1, 0));
|
end = cmp::min(max_point, Point::new(end.row + 1, 0));
|
||||||
}
|
}
|
||||||
|
if is_first {
|
||||||
|
is_first = false;
|
||||||
|
} else {
|
||||||
|
text += "\n";
|
||||||
|
}
|
||||||
let mut len = 0;
|
let mut len = 0;
|
||||||
for chunk in buffer.text_for_range(start..end) {
|
for chunk in buffer.text_for_range(start..end) {
|
||||||
text.push_str(chunk);
|
text.push_str(chunk);
|
||||||
@ -4806,7 +4857,7 @@ impl Editor {
|
|||||||
pub fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
|
pub fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
|
||||||
self.transact(cx, |this, cx| {
|
self.transact(cx, |this, cx| {
|
||||||
if let Some(item) = cx.read_from_clipboard() {
|
if let Some(item) = cx.read_from_clipboard() {
|
||||||
let mut clipboard_text = Cow::Borrowed(item.text());
|
let clipboard_text = Cow::Borrowed(item.text());
|
||||||
if let Some(mut clipboard_selections) = item.metadata::<Vec<ClipboardSelection>>() {
|
if let Some(mut clipboard_selections) = item.metadata::<Vec<ClipboardSelection>>() {
|
||||||
let old_selections = this.selections.all::<usize>(cx);
|
let old_selections = this.selections.all::<usize>(cx);
|
||||||
let all_selections_were_entire_line =
|
let all_selections_were_entire_line =
|
||||||
@ -4814,18 +4865,7 @@ impl Editor {
|
|||||||
let first_selection_indent_column =
|
let first_selection_indent_column =
|
||||||
clipboard_selections.first().map(|s| s.first_line_indent);
|
clipboard_selections.first().map(|s| s.first_line_indent);
|
||||||
if clipboard_selections.len() != old_selections.len() {
|
if clipboard_selections.len() != old_selections.len() {
|
||||||
let mut newline_separated_text = String::new();
|
clipboard_selections.drain(..);
|
||||||
let mut clipboard_selections = clipboard_selections.drain(..).peekable();
|
|
||||||
let mut ix = 0;
|
|
||||||
while let Some(clipboard_selection) = clipboard_selections.next() {
|
|
||||||
newline_separated_text
|
|
||||||
.push_str(&clipboard_text[ix..ix + clipboard_selection.len]);
|
|
||||||
ix += clipboard_selection.len;
|
|
||||||
if clipboard_selections.peek().is_some() {
|
|
||||||
newline_separated_text.push('\n');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
clipboard_text = Cow::Owned(newline_separated_text);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.buffer.update(cx, |buffer, cx| {
|
this.buffer.update(cx, |buffer, cx| {
|
||||||
@ -4841,8 +4881,9 @@ impl Editor {
|
|||||||
if let Some(clipboard_selection) = clipboard_selections.get(ix) {
|
if let Some(clipboard_selection) = clipboard_selections.get(ix) {
|
||||||
let end_offset = start_offset + clipboard_selection.len;
|
let end_offset = start_offset + clipboard_selection.len;
|
||||||
to_insert = &clipboard_text[start_offset..end_offset];
|
to_insert = &clipboard_text[start_offset..end_offset];
|
||||||
|
dbg!(start_offset, end_offset, &clipboard_text, &to_insert);
|
||||||
entire_line = clipboard_selection.is_entire_line;
|
entire_line = clipboard_selection.is_entire_line;
|
||||||
start_offset = end_offset;
|
start_offset = end_offset + 1;
|
||||||
original_indent_column =
|
original_indent_column =
|
||||||
Some(clipboard_selection.first_line_indent);
|
Some(clipboard_selection.first_line_indent);
|
||||||
} else {
|
} else {
|
||||||
@ -8537,6 +8578,7 @@ fn build_style(
|
|||||||
font_size,
|
font_size,
|
||||||
font_properties,
|
font_properties,
|
||||||
underline: Default::default(),
|
underline: Default::default(),
|
||||||
|
soft_wrap: false,
|
||||||
},
|
},
|
||||||
placeholder_text: None,
|
placeholder_text: None,
|
||||||
line_height_scalar,
|
line_height_scalar,
|
||||||
|
@ -6384,7 +6384,7 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
|
|||||||
.update(|cx| {
|
.update(|cx| {
|
||||||
Editor::from_state_proto(
|
Editor::from_state_proto(
|
||||||
pane.clone(),
|
pane.clone(),
|
||||||
project.clone(),
|
workspace.clone(),
|
||||||
ViewId {
|
ViewId {
|
||||||
creator: Default::default(),
|
creator: Default::default(),
|
||||||
id: 0,
|
id: 0,
|
||||||
@ -6479,7 +6479,7 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
|
|||||||
.update(|cx| {
|
.update(|cx| {
|
||||||
Editor::from_state_proto(
|
Editor::from_state_proto(
|
||||||
pane.clone(),
|
pane.clone(),
|
||||||
project.clone(),
|
workspace.clone(),
|
||||||
ViewId {
|
ViewId {
|
||||||
creator: Default::default(),
|
creator: Default::default(),
|
||||||
id: 0,
|
id: 0,
|
||||||
|
@ -62,6 +62,7 @@ struct SelectionLayout {
|
|||||||
head: DisplayPoint,
|
head: DisplayPoint,
|
||||||
cursor_shape: CursorShape,
|
cursor_shape: CursorShape,
|
||||||
is_newest: bool,
|
is_newest: bool,
|
||||||
|
is_local: bool,
|
||||||
range: Range<DisplayPoint>,
|
range: Range<DisplayPoint>,
|
||||||
active_rows: Range<u32>,
|
active_rows: Range<u32>,
|
||||||
}
|
}
|
||||||
@ -73,6 +74,7 @@ impl SelectionLayout {
|
|||||||
cursor_shape: CursorShape,
|
cursor_shape: CursorShape,
|
||||||
map: &DisplaySnapshot,
|
map: &DisplaySnapshot,
|
||||||
is_newest: bool,
|
is_newest: bool,
|
||||||
|
is_local: bool,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let point_selection = selection.map(|p| p.to_point(&map.buffer_snapshot));
|
let point_selection = selection.map(|p| p.to_point(&map.buffer_snapshot));
|
||||||
let display_selection = point_selection.map(|p| p.to_display_point(map));
|
let display_selection = point_selection.map(|p| p.to_display_point(map));
|
||||||
@ -109,6 +111,7 @@ impl SelectionLayout {
|
|||||||
head,
|
head,
|
||||||
cursor_shape,
|
cursor_shape,
|
||||||
is_newest,
|
is_newest,
|
||||||
|
is_local,
|
||||||
range,
|
range,
|
||||||
active_rows,
|
active_rows,
|
||||||
}
|
}
|
||||||
@ -605,7 +608,7 @@ impl EditorElement {
|
|||||||
visible_bounds: RectF,
|
visible_bounds: RectF,
|
||||||
layout: &mut LayoutState,
|
layout: &mut LayoutState,
|
||||||
editor: &mut Editor,
|
editor: &mut Editor,
|
||||||
cx: &mut ViewContext<Editor>,
|
cx: &mut PaintContext<Editor>,
|
||||||
) {
|
) {
|
||||||
let line_height = layout.position_map.line_height;
|
let line_height = layout.position_map.line_height;
|
||||||
|
|
||||||
@ -760,10 +763,9 @@ impl EditorElement {
|
|||||||
visible_bounds: RectF,
|
visible_bounds: RectF,
|
||||||
layout: &mut LayoutState,
|
layout: &mut LayoutState,
|
||||||
editor: &mut Editor,
|
editor: &mut Editor,
|
||||||
cx: &mut ViewContext<Editor>,
|
cx: &mut PaintContext<Editor>,
|
||||||
) {
|
) {
|
||||||
let style = &self.style;
|
let style = &self.style;
|
||||||
let local_replica_id = editor.replica_id(cx);
|
|
||||||
let scroll_position = layout.position_map.snapshot.scroll_position();
|
let scroll_position = layout.position_map.snapshot.scroll_position();
|
||||||
let start_row = layout.visible_display_row_range.start;
|
let start_row = layout.visible_display_row_range.start;
|
||||||
let scroll_top = scroll_position.y() * layout.position_map.line_height;
|
let scroll_top = scroll_position.y() * layout.position_map.line_height;
|
||||||
@ -852,15 +854,13 @@ impl EditorElement {
|
|||||||
|
|
||||||
for (replica_id, selections) in &layout.selections {
|
for (replica_id, selections) in &layout.selections {
|
||||||
let replica_id = *replica_id;
|
let replica_id = *replica_id;
|
||||||
let selection_style = style.replica_selection_style(replica_id);
|
let selection_style = if let Some(replica_id) = replica_id {
|
||||||
|
style.replica_selection_style(replica_id)
|
||||||
|
} else {
|
||||||
|
&style.absent_selection
|
||||||
|
};
|
||||||
|
|
||||||
for selection in selections {
|
for selection in selections {
|
||||||
if !selection.range.is_empty()
|
|
||||||
&& (replica_id == local_replica_id
|
|
||||||
|| Some(replica_id) == editor.leader_replica_id)
|
|
||||||
{
|
|
||||||
invisible_display_ranges.push(selection.range.clone());
|
|
||||||
}
|
|
||||||
self.paint_highlighted_range(
|
self.paint_highlighted_range(
|
||||||
scene,
|
scene,
|
||||||
selection.range.clone(),
|
selection.range.clone(),
|
||||||
@ -874,7 +874,10 @@ impl EditorElement {
|
|||||||
bounds,
|
bounds,
|
||||||
);
|
);
|
||||||
|
|
||||||
if editor.show_local_cursors(cx) || replica_id != local_replica_id {
|
if selection.is_local && !selection.range.is_empty() {
|
||||||
|
invisible_display_ranges.push(selection.range.clone());
|
||||||
|
}
|
||||||
|
if !selection.is_local || editor.show_local_cursors(cx) {
|
||||||
let cursor_position = selection.head;
|
let cursor_position = selection.head;
|
||||||
if layout
|
if layout
|
||||||
.visible_display_row_range
|
.visible_display_row_range
|
||||||
@ -1337,7 +1340,7 @@ impl EditorElement {
|
|||||||
visible_bounds: RectF,
|
visible_bounds: RectF,
|
||||||
layout: &mut LayoutState,
|
layout: &mut LayoutState,
|
||||||
editor: &mut Editor,
|
editor: &mut Editor,
|
||||||
cx: &mut ViewContext<Editor>,
|
cx: &mut PaintContext<Editor>,
|
||||||
) {
|
) {
|
||||||
let scroll_position = layout.position_map.snapshot.scroll_position();
|
let scroll_position = layout.position_map.snapshot.scroll_position();
|
||||||
let scroll_left = scroll_position.x() * layout.position_map.em_width;
|
let scroll_left = scroll_position.x() * layout.position_map.em_width;
|
||||||
@ -2124,7 +2127,7 @@ impl Element<Editor> for EditorElement {
|
|||||||
.anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right))
|
.anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right))
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut selections: Vec<(ReplicaId, Vec<SelectionLayout>)> = Vec::new();
|
let mut selections: Vec<(Option<ReplicaId>, Vec<SelectionLayout>)> = Vec::new();
|
||||||
let mut active_rows = BTreeMap::new();
|
let mut active_rows = BTreeMap::new();
|
||||||
let mut fold_ranges = Vec::new();
|
let mut fold_ranges = Vec::new();
|
||||||
let is_singleton = editor.is_singleton(cx);
|
let is_singleton = editor.is_singleton(cx);
|
||||||
@ -2155,8 +2158,14 @@ impl Element<Editor> for EditorElement {
|
|||||||
.buffer_snapshot
|
.buffer_snapshot
|
||||||
.remote_selections_in_range(&(start_anchor..end_anchor))
|
.remote_selections_in_range(&(start_anchor..end_anchor))
|
||||||
{
|
{
|
||||||
|
let replica_id = if let Some(mapping) = &editor.replica_id_mapping {
|
||||||
|
mapping.get(&replica_id).copied()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
// The local selections match the leader's selections.
|
// The local selections match the leader's selections.
|
||||||
if Some(replica_id) == editor.leader_replica_id {
|
if replica_id.is_some() && replica_id == editor.leader_replica_id {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
remote_selections
|
remote_selections
|
||||||
@ -2168,6 +2177,7 @@ impl Element<Editor> for EditorElement {
|
|||||||
cursor_shape,
|
cursor_shape,
|
||||||
&snapshot.display_snapshot,
|
&snapshot.display_snapshot,
|
||||||
false,
|
false,
|
||||||
|
false,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
selections.extend(remote_selections);
|
selections.extend(remote_selections);
|
||||||
@ -2191,6 +2201,7 @@ impl Element<Editor> for EditorElement {
|
|||||||
editor.cursor_shape,
|
editor.cursor_shape,
|
||||||
&snapshot.display_snapshot,
|
&snapshot.display_snapshot,
|
||||||
is_newest,
|
is_newest,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
if is_newest {
|
if is_newest {
|
||||||
newest_selection_head = Some(layout.head);
|
newest_selection_head = Some(layout.head);
|
||||||
@ -2206,11 +2217,18 @@ impl Element<Editor> for EditorElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Render the local selections in the leader's color when following.
|
// Render the local selections in the leader's color when following.
|
||||||
let local_replica_id = editor
|
let local_replica_id = if let Some(leader_replica_id) = editor.leader_replica_id {
|
||||||
.leader_replica_id
|
leader_replica_id
|
||||||
.unwrap_or_else(|| editor.replica_id(cx));
|
} else {
|
||||||
|
let replica_id = editor.replica_id(cx);
|
||||||
|
if let Some(mapping) = &editor.replica_id_mapping {
|
||||||
|
mapping.get(&replica_id).copied().unwrap_or(replica_id)
|
||||||
|
} else {
|
||||||
|
replica_id
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
selections.push((local_replica_id, layouts));
|
selections.push((Some(local_replica_id), layouts));
|
||||||
}
|
}
|
||||||
|
|
||||||
let scrollbar_settings = &settings::get::<EditorSettings>(cx).scrollbar;
|
let scrollbar_settings = &settings::get::<EditorSettings>(cx).scrollbar;
|
||||||
@ -2591,7 +2609,7 @@ pub struct LayoutState {
|
|||||||
blocks: Vec<BlockLayout>,
|
blocks: Vec<BlockLayout>,
|
||||||
highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
|
highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
|
||||||
fold_ranges: Vec<(BufferRow, Range<DisplayPoint>, Color)>,
|
fold_ranges: Vec<(BufferRow, Range<DisplayPoint>, Color)>,
|
||||||
selections: Vec<(ReplicaId, Vec<SelectionLayout>)>,
|
selections: Vec<(Option<ReplicaId>, Vec<SelectionLayout>)>,
|
||||||
scrollbar_row_range: Range<f32>,
|
scrollbar_row_range: Range<f32>,
|
||||||
show_scrollbars: bool,
|
show_scrollbars: bool,
|
||||||
is_singleton: bool,
|
is_singleton: bool,
|
||||||
|
@ -49,11 +49,12 @@ impl FollowableItem for Editor {
|
|||||||
|
|
||||||
fn from_state_proto(
|
fn from_state_proto(
|
||||||
pane: ViewHandle<workspace::Pane>,
|
pane: ViewHandle<workspace::Pane>,
|
||||||
project: ModelHandle<Project>,
|
workspace: ViewHandle<Workspace>,
|
||||||
remote_id: ViewId,
|
remote_id: ViewId,
|
||||||
state: &mut Option<proto::view::Variant>,
|
state: &mut Option<proto::view::Variant>,
|
||||||
cx: &mut AppContext,
|
cx: &mut AppContext,
|
||||||
) -> Option<Task<Result<ViewHandle<Self>>>> {
|
) -> Option<Task<Result<ViewHandle<Self>>>> {
|
||||||
|
let project = workspace.read(cx).project().to_owned();
|
||||||
let Some(proto::view::Variant::Editor(_)) = state else { return None };
|
let Some(proto::view::Variant::Editor(_)) = state else { return None };
|
||||||
let Some(proto::view::Variant::Editor(state)) = state.take() else { unreachable!() };
|
let Some(proto::view::Variant::Editor(state)) = state.take() else { unreachable!() };
|
||||||
|
|
||||||
@ -561,7 +562,7 @@ impl Item for Editor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tab_content<T: View>(
|
fn tab_content<T: 'static>(
|
||||||
&self,
|
&self,
|
||||||
detail: Option<usize>,
|
detail: Option<usize>,
|
||||||
style: &theme::Tab,
|
style: &theme::Tab,
|
||||||
@ -753,7 +754,7 @@ impl Item for Editor {
|
|||||||
Some(Box::new(handle.clone()))
|
Some(Box::new(handle.clone()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pixel_position_of_cursor(&self) -> Option<Vector2F> {
|
fn pixel_position_of_cursor(&self, _: &AppContext) -> Option<Vector2F> {
|
||||||
self.pixel_position_of_newest_cursor
|
self.pixel_position_of_newest_cursor
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1028,7 +1029,7 @@ impl SearchableItem for Editor {
|
|||||||
if let Some((_, _, excerpt_buffer)) = buffer.as_singleton() {
|
if let Some((_, _, excerpt_buffer)) = buffer.as_singleton() {
|
||||||
ranges.extend(
|
ranges.extend(
|
||||||
query
|
query
|
||||||
.search(excerpt_buffer.as_rope())
|
.search(excerpt_buffer, None)
|
||||||
.await
|
.await
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|range| {
|
.map(|range| {
|
||||||
@ -1038,8 +1039,12 @@ impl SearchableItem for Editor {
|
|||||||
} else {
|
} else {
|
||||||
for excerpt in buffer.excerpt_boundaries_in_range(0..buffer.len()) {
|
for excerpt in buffer.excerpt_boundaries_in_range(0..buffer.len()) {
|
||||||
let excerpt_range = excerpt.range.context.to_offset(&excerpt.buffer);
|
let excerpt_range = excerpt.range.context.to_offset(&excerpt.buffer);
|
||||||
let rope = excerpt.buffer.as_rope().slice(excerpt_range.clone());
|
ranges.extend(
|
||||||
ranges.extend(query.search(&rope).await.into_iter().map(|range| {
|
query
|
||||||
|
.search(&excerpt.buffer, Some(excerpt_range.clone()))
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.map(|range| {
|
||||||
let start = excerpt
|
let start = excerpt
|
||||||
.buffer
|
.buffer
|
||||||
.anchor_after(excerpt_range.start + range.start);
|
.anchor_after(excerpt_range.start + range.start);
|
||||||
@ -1048,7 +1053,8 @@ impl SearchableItem for Editor {
|
|||||||
.anchor_before(excerpt_range.start + range.end);
|
.anchor_before(excerpt_range.start + range.end);
|
||||||
buffer.anchor_in_excerpt(excerpt.id.clone(), start)
|
buffer.anchor_in_excerpt(excerpt.id.clone(), start)
|
||||||
..buffer.anchor_in_excerpt(excerpt.id.clone(), end)
|
..buffer.anchor_in_excerpt(excerpt.id.clone(), end)
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ranges
|
ranges
|
||||||
|
@ -176,14 +176,21 @@ pub fn line_end(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
|
pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
|
||||||
|
let raw_point = point.to_point(map);
|
||||||
|
let language = map.buffer_snapshot.language_at(raw_point);
|
||||||
|
|
||||||
find_preceding_boundary(map, point, |left, right| {
|
find_preceding_boundary(map, point, |left, right| {
|
||||||
(char_kind(left) != char_kind(right) && !right.is_whitespace()) || left == '\n'
|
(char_kind(language, left) != char_kind(language, right) && !right.is_whitespace())
|
||||||
|
|| left == '\n'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
|
pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
|
||||||
|
let raw_point = point.to_point(map);
|
||||||
|
let language = map.buffer_snapshot.language_at(raw_point);
|
||||||
find_preceding_boundary(map, point, |left, right| {
|
find_preceding_boundary(map, point, |left, right| {
|
||||||
let is_word_start = char_kind(left) != char_kind(right) && !right.is_whitespace();
|
let is_word_start =
|
||||||
|
char_kind(language, left) != char_kind(language, right) && !right.is_whitespace();
|
||||||
let is_subword_start =
|
let is_subword_start =
|
||||||
left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
|
left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
|
||||||
is_word_start || is_subword_start || left == '\n'
|
is_word_start || is_subword_start || left == '\n'
|
||||||
@ -191,14 +198,20 @@ pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> Dis
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
|
pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
|
||||||
|
let raw_point = point.to_point(map);
|
||||||
|
let language = map.buffer_snapshot.language_at(raw_point);
|
||||||
find_boundary(map, point, |left, right| {
|
find_boundary(map, point, |left, right| {
|
||||||
(char_kind(left) != char_kind(right) && !left.is_whitespace()) || right == '\n'
|
(char_kind(language, left) != char_kind(language, right) && !left.is_whitespace())
|
||||||
|
|| right == '\n'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
|
pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
|
||||||
|
let raw_point = point.to_point(map);
|
||||||
|
let language = map.buffer_snapshot.language_at(raw_point);
|
||||||
find_boundary(map, point, |left, right| {
|
find_boundary(map, point, |left, right| {
|
||||||
let is_word_end = (char_kind(left) != char_kind(right)) && !left.is_whitespace();
|
let is_word_end =
|
||||||
|
(char_kind(language, left) != char_kind(language, right)) && !left.is_whitespace();
|
||||||
let is_subword_end =
|
let is_subword_end =
|
||||||
left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
|
left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
|
||||||
is_word_end || is_subword_end || right == '\n'
|
is_word_end || is_subword_end || right == '\n'
|
||||||
@ -385,10 +398,15 @@ pub fn find_boundary_in_line(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
|
pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
|
||||||
|
let raw_point = point.to_point(map);
|
||||||
|
let language = map.buffer_snapshot.language_at(raw_point);
|
||||||
let ix = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left);
|
let ix = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left);
|
||||||
let text = &map.buffer_snapshot;
|
let text = &map.buffer_snapshot;
|
||||||
let next_char_kind = text.chars_at(ix).next().map(char_kind);
|
let next_char_kind = text.chars_at(ix).next().map(|c| char_kind(language, c));
|
||||||
let prev_char_kind = text.reversed_chars_at(ix).next().map(char_kind);
|
let prev_char_kind = text
|
||||||
|
.reversed_chars_at(ix)
|
||||||
|
.next()
|
||||||
|
.map(|c| char_kind(language, c));
|
||||||
prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word))
|
prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1346,10 +1346,7 @@ impl MultiBuffer {
|
|||||||
.map(|state| state.buffer.clone())
|
.map(|state| state.buffer.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_completion_trigger<T>(&self, position: T, text: &str, cx: &AppContext) -> bool
|
pub fn is_completion_trigger(&self, position: Anchor, text: &str, cx: &AppContext) -> bool {
|
||||||
where
|
|
||||||
T: ToOffset,
|
|
||||||
{
|
|
||||||
let mut chars = text.chars();
|
let mut chars = text.chars();
|
||||||
let char = if let Some(char) = chars.next() {
|
let char = if let Some(char) = chars.next() {
|
||||||
char
|
char
|
||||||
@ -1360,7 +1357,9 @@ impl MultiBuffer {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if char.is_alphanumeric() || char == '_' {
|
let language = self.language_at(position.clone(), cx);
|
||||||
|
|
||||||
|
if char_kind(language.as_ref(), char) == CharKind::Word {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1865,13 +1864,16 @@ impl MultiBufferSnapshot {
|
|||||||
let mut end = start;
|
let mut end = start;
|
||||||
let mut next_chars = self.chars_at(start).peekable();
|
let mut next_chars = self.chars_at(start).peekable();
|
||||||
let mut prev_chars = self.reversed_chars_at(start).peekable();
|
let mut prev_chars = self.reversed_chars_at(start).peekable();
|
||||||
|
|
||||||
|
let language = self.language_at(start);
|
||||||
|
let kind = |c| char_kind(language, c);
|
||||||
let word_kind = cmp::max(
|
let word_kind = cmp::max(
|
||||||
prev_chars.peek().copied().map(char_kind),
|
prev_chars.peek().copied().map(kind),
|
||||||
next_chars.peek().copied().map(char_kind),
|
next_chars.peek().copied().map(kind),
|
||||||
);
|
);
|
||||||
|
|
||||||
for ch in prev_chars {
|
for ch in prev_chars {
|
||||||
if Some(char_kind(ch)) == word_kind && ch != '\n' {
|
if Some(kind(ch)) == word_kind && ch != '\n' {
|
||||||
start -= ch.len_utf8();
|
start -= ch.len_utf8();
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
@ -1879,7 +1881,7 @@ impl MultiBufferSnapshot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for ch in next_chars {
|
for ch in next_chars {
|
||||||
if Some(char_kind(ch)) == word_kind && ch != '\n' {
|
if Some(kind(ch)) == word_kind && ch != '\n' {
|
||||||
end += ch.len_utf8();
|
end += ch.len_utf8();
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
|
@ -6,6 +6,7 @@ use std::{
|
|||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
|
use collections::HashSet;
|
||||||
use futures::Future;
|
use futures::Future;
|
||||||
use gpui::{json, ViewContext, ViewHandle};
|
use gpui::{json, ViewContext, ViewHandle};
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
@ -154,10 +155,23 @@ impl<'a> EditorLspTestContext<'a> {
|
|||||||
capabilities: lsp::ServerCapabilities,
|
capabilities: lsp::ServerCapabilities,
|
||||||
cx: &'a mut gpui::TestAppContext,
|
cx: &'a mut gpui::TestAppContext,
|
||||||
) -> EditorLspTestContext<'a> {
|
) -> EditorLspTestContext<'a> {
|
||||||
|
let mut word_characters: HashSet<char> = Default::default();
|
||||||
|
word_characters.insert('$');
|
||||||
|
word_characters.insert('#');
|
||||||
let language = Language::new(
|
let language = Language::new(
|
||||||
LanguageConfig {
|
LanguageConfig {
|
||||||
name: "Typescript".into(),
|
name: "Typescript".into(),
|
||||||
path_suffixes: vec!["ts".to_string()],
|
path_suffixes: vec!["ts".to_string()],
|
||||||
|
brackets: language::BracketPairConfig {
|
||||||
|
pairs: vec![language::BracketPair {
|
||||||
|
start: "{".to_string(),
|
||||||
|
end: "}".to_string(),
|
||||||
|
close: true,
|
||||||
|
newline: true,
|
||||||
|
}],
|
||||||
|
disabled_scopes_by_bracket_ix: Default::default(),
|
||||||
|
},
|
||||||
|
word_characters,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
Some(tree_sitter_typescript::language_typescript()),
|
Some(tree_sitter_typescript::language_typescript()),
|
||||||
@ -169,6 +183,23 @@ impl<'a> EditorLspTestContext<'a> {
|
|||||||
("{" @open "}" @close)
|
("{" @open "}" @close)
|
||||||
("<" @open ">" @close)
|
("<" @open ">" @close)
|
||||||
("\"" @open "\"" @close)"#})),
|
("\"" @open "\"" @close)"#})),
|
||||||
|
indents: Some(Cow::from(indoc! {r#"
|
||||||
|
[
|
||||||
|
(call_expression)
|
||||||
|
(assignment_expression)
|
||||||
|
(member_expression)
|
||||||
|
(lexical_declaration)
|
||||||
|
(variable_declaration)
|
||||||
|
(assignment_expression)
|
||||||
|
(if_statement)
|
||||||
|
(for_statement)
|
||||||
|
] @indent
|
||||||
|
|
||||||
|
(_ "[" "]" @end) @indent
|
||||||
|
(_ "<" ">" @end) @indent
|
||||||
|
(_ "{" "}" @end) @indent
|
||||||
|
(_ "(" ")" @end) @indent
|
||||||
|
"#})),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.expect("Could not parse queries");
|
.expect("Could not parse queries");
|
||||||
|
@ -268,7 +268,7 @@ impl Item for FeedbackEditor {
|
|||||||
Some("Send Feedback".into())
|
Some("Send Feedback".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tab_content<T: View>(
|
fn tab_content<T: 'static>(
|
||||||
&self,
|
&self,
|
||||||
_: Option<usize>,
|
_: Option<usize>,
|
||||||
style: &theme::Tab,
|
style: &theme::Tab,
|
||||||
|
@ -39,6 +39,7 @@ pathfinder_color = "0.5"
|
|||||||
pathfinder_geometry = "0.5"
|
pathfinder_geometry = "0.5"
|
||||||
postage.workspace = true
|
postage.workspace = true
|
||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
|
refineable.workspace = true
|
||||||
resvg = "0.14"
|
resvg = "0.14"
|
||||||
schemars = "0.8"
|
schemars = "0.8"
|
||||||
seahash = "4.1"
|
seahash = "4.1"
|
||||||
@ -47,6 +48,7 @@ serde_derive.workspace = true
|
|||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
|
taffy = { git = "https://github.com/DioxusLabs/taffy", rev = "dab541d6104d58e2e10ce90c4a1dad0b703160cd", features = ["flexbox"] }
|
||||||
time.workspace = true
|
time.workspace = true
|
||||||
tiny-skia = "0.5"
|
tiny-skia = "0.5"
|
||||||
usvg = { version = "0.14", features = [] }
|
usvg = { version = "0.14", features = [] }
|
||||||
|
@ -2,7 +2,7 @@ use button_component::Button;
|
|||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
color::Color,
|
color::Color,
|
||||||
elements::{Component, ContainerStyle, Flex, Label, ParentElement},
|
elements::{ContainerStyle, Flex, Label, ParentElement, StatefulComponent},
|
||||||
fonts::{self, TextStyle},
|
fonts::{self, TextStyle},
|
||||||
platform::WindowOptions,
|
platform::WindowOptions,
|
||||||
AnyElement, App, Element, Entity, View, ViewContext,
|
AnyElement, App, Element, Entity, View, ViewContext,
|
||||||
@ -114,7 +114,7 @@ mod theme {
|
|||||||
// Component creation:
|
// Component creation:
|
||||||
mod toggleable_button {
|
mod toggleable_button {
|
||||||
use gpui::{
|
use gpui::{
|
||||||
elements::{Component, ContainerStyle, LabelStyle},
|
elements::{ContainerStyle, LabelStyle, StatefulComponent},
|
||||||
scene::MouseClick,
|
scene::MouseClick,
|
||||||
EventContext, View,
|
EventContext, View,
|
||||||
};
|
};
|
||||||
@ -156,7 +156,7 @@ mod toggleable_button {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> Component<V> for ToggleableButton<V> {
|
impl<V: View> StatefulComponent<V> for ToggleableButton<V> {
|
||||||
fn render(self, v: &mut V, cx: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
|
fn render(self, v: &mut V, cx: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
|
||||||
let button = if let Some(style) = self.style {
|
let button = if let Some(style) = self.style {
|
||||||
self.button.with_style(*style.style_for(self.active))
|
self.button.with_style(*style.style_for(self.active))
|
||||||
@ -171,7 +171,7 @@ mod toggleable_button {
|
|||||||
mod button_component {
|
mod button_component {
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
elements::{Component, ContainerStyle, Label, LabelStyle, MouseEventHandler},
|
elements::{ContainerStyle, Label, LabelStyle, MouseEventHandler, StatefulComponent},
|
||||||
platform::MouseButton,
|
platform::MouseButton,
|
||||||
scene::MouseClick,
|
scene::MouseClick,
|
||||||
AnyElement, Element, EventContext, TypeTag, View, ViewContext,
|
AnyElement, Element, EventContext, TypeTag, View, ViewContext,
|
||||||
@ -212,7 +212,7 @@ mod button_component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> Component<V> for Button<V> {
|
impl<V: View> StatefulComponent<V> for Button<V> {
|
||||||
fn render(self, _: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V> {
|
fn render(self, _: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V> {
|
||||||
let click_handler = self.click_handler;
|
let click_handler = self.click_handler;
|
||||||
|
|
||||||
|
@ -58,6 +58,7 @@ impl gpui::View for TextView {
|
|||||||
font_family_id: family,
|
font_family_id: family,
|
||||||
underline: Default::default(),
|
underline: Default::default(),
|
||||||
font_properties: Default::default(),
|
font_properties: Default::default(),
|
||||||
|
soft_wrap: false,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.with_highlights(vec![(17..26, underline), (34..40, underline)])
|
.with_highlights(vec![(17..26, underline), (34..40, underline)])
|
||||||
|
2919
crates/gpui/playground/Cargo.lock
generated
Normal file
2919
crates/gpui/playground/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
26
crates/gpui/playground/Cargo.toml
Normal file
26
crates/gpui/playground/Cargo.toml
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
[package]
|
||||||
|
name = "playground"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "playground"
|
||||||
|
path = "src/playground.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow.workspace = true
|
||||||
|
derive_more.workspace = true
|
||||||
|
gpui = { path = ".." }
|
||||||
|
log.workspace = true
|
||||||
|
playground_macros = { path = "../playground_macros" }
|
||||||
|
parking_lot.workspace = true
|
||||||
|
refineable.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
simplelog = "0.9"
|
||||||
|
smallvec.workspace = true
|
||||||
|
taffy = { git = "https://github.com/DioxusLabs/taffy", rev = "dab541d6104d58e2e10ce90c4a1dad0b703160cd", features = ["flexbox"] }
|
||||||
|
util = { path = "../../util" }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
gpui = { path = "..", features = ["test-support"] }
|
72
crates/gpui/playground/docs/thoughts.md
Normal file
72
crates/gpui/playground/docs/thoughts.md
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
Much of element styling is now handled by an external engine.
|
||||||
|
|
||||||
|
|
||||||
|
How do I make an element hover.
|
||||||
|
|
||||||
|
There's a hover style.
|
||||||
|
|
||||||
|
Hoverable needs to wrap another element. That element can be styled.
|
||||||
|
|
||||||
|
```rs
|
||||||
|
struct Hoverable<E: Element> {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V> Element<V> for Hoverable {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
```rs
|
||||||
|
#[derive(Styled, Interactive)]
|
||||||
|
pub struct Div {
|
||||||
|
declared_style: StyleRefinement,
|
||||||
|
interactions: Interactions
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Styled {
|
||||||
|
fn declared_style(&mut self) -> &mut StyleRefinement;
|
||||||
|
fn compute_style(&mut self) -> Style {
|
||||||
|
Style::default().refine(self.declared_style())
|
||||||
|
}
|
||||||
|
|
||||||
|
// All the tailwind classes, modifying self.declared_style()
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Style {
|
||||||
|
pub fn paint_background<V>(layout: Layout, cx: &mut PaintContext<V>);
|
||||||
|
pub fn paint_foreground<V>(layout: Layout, cx: &mut PaintContext<V>);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Interactive<V> {
|
||||||
|
fn interactions(&mut self) -> &mut Interactions<V>;
|
||||||
|
|
||||||
|
fn on_click(self, )
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Interactions<V> {
|
||||||
|
click: SmallVec<[<Rc<dyn Fn(&mut V, &dyn Any, )>; 1]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
```rs
|
||||||
|
|
||||||
|
|
||||||
|
trait Stylable {
|
||||||
|
type Style;
|
||||||
|
|
||||||
|
fn with_style(self, style: Self::Style) -> Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
```
|
78
crates/gpui/playground/src/adapter.rs
Normal file
78
crates/gpui/playground/src/adapter.rs
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
use crate::{layout_context::LayoutContext, paint_context::PaintContext};
|
||||||
|
use gpui::{geometry::rect::RectF, LayoutEngine, LayoutId};
|
||||||
|
use util::ResultExt;
|
||||||
|
|
||||||
|
/// Makes a new, playground-style element into a legacy element.
|
||||||
|
pub struct AdapterElement<V>(pub(crate) crate::element::AnyElement<V>);
|
||||||
|
|
||||||
|
impl<V: 'static> gpui::Element<V> for AdapterElement<V> {
|
||||||
|
type LayoutState = Option<(LayoutEngine, LayoutId)>;
|
||||||
|
type PaintState = ();
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
constraint: gpui::SizeConstraint,
|
||||||
|
view: &mut V,
|
||||||
|
cx: &mut gpui::LayoutContext<V>,
|
||||||
|
) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
|
||||||
|
cx.push_layout_engine(LayoutEngine::new());
|
||||||
|
|
||||||
|
let size = constraint.max;
|
||||||
|
let mut cx = LayoutContext::new(cx);
|
||||||
|
let layout_id = self.0.layout(view, &mut cx).log_err();
|
||||||
|
if let Some(layout_id) = layout_id {
|
||||||
|
cx.layout_engine()
|
||||||
|
.unwrap()
|
||||||
|
.compute_layout(layout_id, constraint.max)
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
|
|
||||||
|
let layout_engine = cx.pop_layout_engine();
|
||||||
|
debug_assert!(layout_engine.is_some(),
|
||||||
|
"unexpected layout stack state. is there an unmatched pop_layout_engine in the called code?"
|
||||||
|
);
|
||||||
|
|
||||||
|
(constraint.max, layout_engine.zip(layout_id))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(
|
||||||
|
&mut self,
|
||||||
|
scene: &mut gpui::SceneBuilder,
|
||||||
|
bounds: RectF,
|
||||||
|
visible_bounds: RectF,
|
||||||
|
layout_data: &mut Option<(LayoutEngine, LayoutId)>,
|
||||||
|
view: &mut V,
|
||||||
|
legacy_cx: &mut gpui::PaintContext<V>,
|
||||||
|
) -> Self::PaintState {
|
||||||
|
let (layout_engine, layout_id) = layout_data.take().unwrap();
|
||||||
|
legacy_cx.push_layout_engine(layout_engine);
|
||||||
|
let mut cx = PaintContext::new(legacy_cx, scene);
|
||||||
|
self.0.paint(view, &mut cx);
|
||||||
|
*layout_data = legacy_cx.pop_layout_engine().zip(Some(layout_id));
|
||||||
|
debug_assert!(layout_data.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rect_for_text_range(
|
||||||
|
&self,
|
||||||
|
range_utf16: std::ops::Range<usize>,
|
||||||
|
bounds: RectF,
|
||||||
|
visible_bounds: RectF,
|
||||||
|
layout: &Self::LayoutState,
|
||||||
|
paint: &Self::PaintState,
|
||||||
|
view: &V,
|
||||||
|
cx: &gpui::ViewContext<V>,
|
||||||
|
) -> Option<RectF> {
|
||||||
|
todo!("implement before merging to main")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn debug(
|
||||||
|
&self,
|
||||||
|
bounds: RectF,
|
||||||
|
layout: &Self::LayoutState,
|
||||||
|
paint: &Self::PaintState,
|
||||||
|
view: &V,
|
||||||
|
cx: &gpui::ViewContext<V>,
|
||||||
|
) -> gpui::serde_json::Value {
|
||||||
|
todo!("implement before merging to main")
|
||||||
|
}
|
||||||
|
}
|
276
crates/gpui/playground/src/color.rs
Normal file
276
crates/gpui/playground/src/color.rs
Normal file
@ -0,0 +1,276 @@
|
|||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
|
use std::{num::ParseIntError, ops::Range};
|
||||||
|
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
|
pub fn rgb<C: From<Rgba>>(hex: u32) -> C {
|
||||||
|
let r = ((hex >> 16) & 0xFF) as f32 / 255.0;
|
||||||
|
let g = ((hex >> 8) & 0xFF) as f32 / 255.0;
|
||||||
|
let b = (hex & 0xFF) as f32 / 255.0;
|
||||||
|
Rgba { r, g, b, a: 1.0 }.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Default, Debug)]
|
||||||
|
pub struct Rgba {
|
||||||
|
pub r: f32,
|
||||||
|
pub g: f32,
|
||||||
|
pub b: f32,
|
||||||
|
pub a: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Lerp {
|
||||||
|
fn lerp(&self, level: f32) -> Hsla;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Lerp for Range<Hsla> {
|
||||||
|
fn lerp(&self, level: f32) -> Hsla {
|
||||||
|
let level = level.clamp(0., 1.);
|
||||||
|
Hsla {
|
||||||
|
h: self.start.h + (level * (self.end.h - self.start.h)),
|
||||||
|
s: self.start.s + (level * (self.end.s - self.start.s)),
|
||||||
|
l: self.start.l + (level * (self.end.l - self.start.l)),
|
||||||
|
a: self.start.a + (level * (self.end.a - self.start.a)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<gpui::color::Color> for Rgba {
|
||||||
|
fn from(value: gpui::color::Color) -> Self {
|
||||||
|
Self {
|
||||||
|
r: value.0.r as f32 / 255.0,
|
||||||
|
g: value.0.g as f32 / 255.0,
|
||||||
|
b: value.0.b as f32 / 255.0,
|
||||||
|
a: value.0.a as f32 / 255.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Hsla> for Rgba {
|
||||||
|
fn from(color: Hsla) -> Self {
|
||||||
|
let h = color.h;
|
||||||
|
let s = color.s;
|
||||||
|
let l = color.l;
|
||||||
|
|
||||||
|
let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
|
||||||
|
let x = c * (1.0 - ((h * 6.0) % 2.0 - 1.0).abs());
|
||||||
|
let m = l - c / 2.0;
|
||||||
|
let cm = c + m;
|
||||||
|
let xm = x + m;
|
||||||
|
|
||||||
|
let (r, g, b) = match (h * 6.0).floor() as i32 {
|
||||||
|
0 | 6 => (cm, xm, m),
|
||||||
|
1 => (xm, cm, m),
|
||||||
|
2 => (m, cm, xm),
|
||||||
|
3 => (m, xm, cm),
|
||||||
|
4 => (xm, m, cm),
|
||||||
|
_ => (cm, m, xm),
|
||||||
|
};
|
||||||
|
|
||||||
|
Rgba {
|
||||||
|
r,
|
||||||
|
g,
|
||||||
|
b,
|
||||||
|
a: color.a,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&'_ str> for Rgba {
|
||||||
|
type Error = ParseIntError;
|
||||||
|
|
||||||
|
fn try_from(value: &'_ str) -> Result<Self, Self::Error> {
|
||||||
|
let r = u8::from_str_radix(&value[1..3], 16)? as f32 / 255.0;
|
||||||
|
let g = u8::from_str_radix(&value[3..5], 16)? as f32 / 255.0;
|
||||||
|
let b = u8::from_str_radix(&value[5..7], 16)? as f32 / 255.0;
|
||||||
|
let a = if value.len() > 7 {
|
||||||
|
u8::from_str_radix(&value[7..9], 16)? as f32 / 255.0
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Rgba { r, g, b, a })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<gpui::color::Color> for Rgba {
|
||||||
|
fn into(self) -> gpui::color::Color {
|
||||||
|
gpui::color::rgba(self.r, self.g, self.b, self.a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Copy, Clone, Debug, PartialEq)]
|
||||||
|
pub struct Hsla {
|
||||||
|
pub h: f32,
|
||||||
|
pub s: f32,
|
||||||
|
pub l: f32,
|
||||||
|
pub a: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hsla(h: f32, s: f32, l: f32, a: f32) -> Hsla {
|
||||||
|
Hsla {
|
||||||
|
h: h.clamp(0., 1.),
|
||||||
|
s: s.clamp(0., 1.),
|
||||||
|
l: l.clamp(0., 1.),
|
||||||
|
a: a.clamp(0., 1.),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn black() -> Hsla {
|
||||||
|
Hsla {
|
||||||
|
h: 0.,
|
||||||
|
s: 0.,
|
||||||
|
l: 0.,
|
||||||
|
a: 1.,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Rgba> for Hsla {
|
||||||
|
fn from(color: Rgba) -> Self {
|
||||||
|
let r = color.r;
|
||||||
|
let g = color.g;
|
||||||
|
let b = color.b;
|
||||||
|
|
||||||
|
let max = r.max(g.max(b));
|
||||||
|
let min = r.min(g.min(b));
|
||||||
|
let delta = max - min;
|
||||||
|
|
||||||
|
let l = (max + min) / 2.0;
|
||||||
|
let s = if l == 0.0 || l == 1.0 {
|
||||||
|
0.0
|
||||||
|
} else if l < 0.5 {
|
||||||
|
delta / (2.0 * l)
|
||||||
|
} else {
|
||||||
|
delta / (2.0 - 2.0 * l)
|
||||||
|
};
|
||||||
|
|
||||||
|
let h = if delta == 0.0 {
|
||||||
|
0.0
|
||||||
|
} else if max == r {
|
||||||
|
((g - b) / delta).rem_euclid(6.0) / 6.0
|
||||||
|
} else if max == g {
|
||||||
|
((b - r) / delta + 2.0) / 6.0
|
||||||
|
} else {
|
||||||
|
((r - g) / delta + 4.0) / 6.0
|
||||||
|
};
|
||||||
|
|
||||||
|
Hsla {
|
||||||
|
h,
|
||||||
|
s,
|
||||||
|
l,
|
||||||
|
a: color.a,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hsla {
|
||||||
|
/// Scales the saturation and lightness by the given values, clamping at 1.0.
|
||||||
|
pub fn scale_sl(mut self, s: f32, l: f32) -> Self {
|
||||||
|
self.s = (self.s * s).clamp(0., 1.);
|
||||||
|
self.l = (self.l * l).clamp(0., 1.);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Increases the saturation of the color by a certain amount, with a max
|
||||||
|
/// value of 1.0.
|
||||||
|
pub fn saturate(mut self, amount: f32) -> Self {
|
||||||
|
self.s += amount;
|
||||||
|
self.s = self.s.clamp(0.0, 1.0);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decreases the saturation of the color by a certain amount, with a min
|
||||||
|
/// value of 0.0.
|
||||||
|
pub fn desaturate(mut self, amount: f32) -> Self {
|
||||||
|
self.s -= amount;
|
||||||
|
self.s = self.s.max(0.0);
|
||||||
|
if self.s < 0.0 {
|
||||||
|
self.s = 0.0;
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Brightens the color by increasing the lightness by a certain amount,
|
||||||
|
/// with a max value of 1.0.
|
||||||
|
pub fn brighten(mut self, amount: f32) -> Self {
|
||||||
|
self.l += amount;
|
||||||
|
self.l = self.l.clamp(0.0, 1.0);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Darkens the color by decreasing the lightness by a certain amount,
|
||||||
|
/// with a max value of 0.0.
|
||||||
|
pub fn darken(mut self, amount: f32) -> Self {
|
||||||
|
self.l -= amount;
|
||||||
|
self.l = self.l.clamp(0.0, 1.0);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<gpui::color::Color> for Hsla {
|
||||||
|
fn from(value: gpui::color::Color) -> Self {
|
||||||
|
Rgba::from(value).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<gpui::color::Color> for Hsla {
|
||||||
|
fn into(self) -> gpui::color::Color {
|
||||||
|
Rgba::from(self).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ColorScale {
|
||||||
|
colors: SmallVec<[Hsla; 2]>,
|
||||||
|
positions: SmallVec<[f32; 2]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scale<I, C>(colors: I) -> ColorScale
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = C>,
|
||||||
|
C: Into<Hsla>,
|
||||||
|
{
|
||||||
|
let mut scale = ColorScale {
|
||||||
|
colors: colors.into_iter().map(Into::into).collect(),
|
||||||
|
positions: SmallVec::new(),
|
||||||
|
};
|
||||||
|
let num_colors: f32 = scale.colors.len() as f32 - 1.0;
|
||||||
|
scale.positions = (0..scale.colors.len())
|
||||||
|
.map(|i| i as f32 / num_colors)
|
||||||
|
.collect();
|
||||||
|
scale
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ColorScale {
|
||||||
|
fn at(&self, t: f32) -> Hsla {
|
||||||
|
// Ensure that the input is within [0.0, 1.0]
|
||||||
|
debug_assert!(
|
||||||
|
0.0 <= t && t <= 1.0,
|
||||||
|
"t value {} is out of range. Expected value in range 0.0 to 1.0",
|
||||||
|
t
|
||||||
|
);
|
||||||
|
|
||||||
|
let position = match self
|
||||||
|
.positions
|
||||||
|
.binary_search_by(|a| a.partial_cmp(&t).unwrap())
|
||||||
|
{
|
||||||
|
Ok(index) | Err(index) => index,
|
||||||
|
};
|
||||||
|
let lower_bound = position.saturating_sub(1);
|
||||||
|
let upper_bound = position.min(self.colors.len() - 1);
|
||||||
|
let lower_color = &self.colors[lower_bound];
|
||||||
|
let upper_color = &self.colors[upper_bound];
|
||||||
|
|
||||||
|
match upper_bound.checked_sub(lower_bound) {
|
||||||
|
Some(0) | None => *lower_color,
|
||||||
|
Some(_) => {
|
||||||
|
let interval_t = (t - self.positions[lower_bound])
|
||||||
|
/ (self.positions[upper_bound] - self.positions[lower_bound]);
|
||||||
|
let h = lower_color.h + interval_t * (upper_color.h - lower_color.h);
|
||||||
|
let s = lower_color.s + interval_t * (upper_color.s - lower_color.s);
|
||||||
|
let l = lower_color.l + interval_t * (upper_color.l - lower_color.l);
|
||||||
|
let a = lower_color.a + interval_t * (upper_color.a - lower_color.a);
|
||||||
|
Hsla { h, s, l, a }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
100
crates/gpui/playground/src/components.rs
Normal file
100
crates/gpui/playground/src/components.rs
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
use crate::{
|
||||||
|
div::div,
|
||||||
|
element::{Element, ParentElement},
|
||||||
|
style::StyleHelpers,
|
||||||
|
text::ArcCow,
|
||||||
|
themes::rose_pine,
|
||||||
|
};
|
||||||
|
use gpui::ViewContext;
|
||||||
|
use playground_macros::Element;
|
||||||
|
use std::{marker::PhantomData, rc::Rc};
|
||||||
|
|
||||||
|
struct ButtonHandlers<V, D> {
|
||||||
|
click: Option<Rc<dyn Fn(&mut V, &D, &mut ViewContext<V>)>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V, D> Default for ButtonHandlers<V, D> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self { click: None }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
use crate as playground;
|
||||||
|
#[derive(Element)]
|
||||||
|
pub struct Button<V: 'static, D: 'static> {
|
||||||
|
handlers: ButtonHandlers<V, D>,
|
||||||
|
label: Option<ArcCow<'static, str>>,
|
||||||
|
icon: Option<ArcCow<'static, str>>,
|
||||||
|
data: Rc<D>,
|
||||||
|
view_type: PhantomData<V>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Impl block for buttons without data.
|
||||||
|
// See below for an impl block for any button.
|
||||||
|
impl<V: 'static> Button<V, ()> {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
handlers: ButtonHandlers::default(),
|
||||||
|
label: None,
|
||||||
|
icon: None,
|
||||||
|
data: Rc::new(()),
|
||||||
|
view_type: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn data<D: 'static>(self, data: D) -> Button<V, D> {
|
||||||
|
Button {
|
||||||
|
handlers: ButtonHandlers::default(),
|
||||||
|
label: self.label,
|
||||||
|
icon: self.icon,
|
||||||
|
data: Rc::new(data),
|
||||||
|
view_type: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Impl block for *any* button.
|
||||||
|
impl<V: 'static, D: 'static> Button<V, D> {
|
||||||
|
pub fn label(mut self, label: impl Into<ArcCow<'static, str>>) -> Self {
|
||||||
|
self.label = Some(label.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn icon(mut self, icon: impl Into<ArcCow<'static, str>>) -> Self {
|
||||||
|
self.icon = Some(icon.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
// pub fn click(self, handler: impl Fn(&mut V, &D, &mut ViewContext<V>) + 'static) -> Self {
|
||||||
|
// let data = self.data.clone();
|
||||||
|
// Self::click(self, MouseButton::Left, move |view, _, cx| {
|
||||||
|
// handler(view, data.as_ref(), cx);
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn button<V>() -> Button<V, ()> {
|
||||||
|
Button::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V: 'static, D: 'static> Button<V, D> {
|
||||||
|
fn render(&mut self, view: &mut V, cx: &mut ViewContext<V>) -> impl Element<V> {
|
||||||
|
// TODO: Drive theme from the context
|
||||||
|
let button = div()
|
||||||
|
.fill(rose_pine::dawn().error(0.5))
|
||||||
|
.h_4()
|
||||||
|
.children(self.label.clone());
|
||||||
|
|
||||||
|
button
|
||||||
|
|
||||||
|
// TODO: Event handling
|
||||||
|
// if let Some(handler) = self.handlers.click.clone() {
|
||||||
|
// let data = self.data.clone();
|
||||||
|
// // button.mouse_down(MouseButton::Left, move |view, event, cx| {
|
||||||
|
// // handler(view, data.as_ref(), cx)
|
||||||
|
// // })
|
||||||
|
// } else {
|
||||||
|
// button
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
108
crates/gpui/playground/src/div.rs
Normal file
108
crates/gpui/playground/src/div.rs
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
use crate::{
|
||||||
|
element::{AnyElement, Element, Layout, ParentElement},
|
||||||
|
interactive::{InteractionHandlers, Interactive},
|
||||||
|
layout_context::LayoutContext,
|
||||||
|
paint_context::PaintContext,
|
||||||
|
style::{Style, StyleHelpers, StyleRefinement, Styleable},
|
||||||
|
};
|
||||||
|
use anyhow::Result;
|
||||||
|
use gpui::LayoutId;
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
|
pub struct Div<V: 'static> {
|
||||||
|
style: StyleRefinement,
|
||||||
|
handlers: InteractionHandlers<V>,
|
||||||
|
children: SmallVec<[AnyElement<V>; 2]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn div<V>() -> Div<V> {
|
||||||
|
Div {
|
||||||
|
style: Default::default(),
|
||||||
|
handlers: Default::default(),
|
||||||
|
children: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V: 'static> Element<V> for Div<V> {
|
||||||
|
type Layout = ();
|
||||||
|
|
||||||
|
fn layout(&mut self, view: &mut V, cx: &mut LayoutContext<V>) -> Result<Layout<V, ()>>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
let children = self
|
||||||
|
.children
|
||||||
|
.iter_mut()
|
||||||
|
.map(|child| child.layout(view, cx))
|
||||||
|
.collect::<Result<Vec<LayoutId>>>()?;
|
||||||
|
|
||||||
|
cx.add_layout_node(self.style(), (), children)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(&mut self, view: &mut V, layout: &mut Layout<V, ()>, cx: &mut PaintContext<V>)
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
let style = self.style();
|
||||||
|
|
||||||
|
style.paint_background::<V, Self>(layout, cx);
|
||||||
|
for child in &mut self.children {
|
||||||
|
child.paint(view, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V> Styleable for Div<V> {
|
||||||
|
type Style = Style;
|
||||||
|
|
||||||
|
fn declared_style(&mut self) -> &mut StyleRefinement {
|
||||||
|
&mut self.style
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V> StyleHelpers for Div<V> {}
|
||||||
|
|
||||||
|
impl<V> Interactive<V> for Div<V> {
|
||||||
|
fn interaction_handlers(&mut self) -> &mut InteractionHandlers<V> {
|
||||||
|
&mut self.handlers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V: 'static> ParentElement<V> for Div<V> {
|
||||||
|
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
|
||||||
|
&mut self.children
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test() {
|
||||||
|
// let elt = div().w_auto();
|
||||||
|
}
|
||||||
|
|
||||||
|
// trait Element<V: 'static> {
|
||||||
|
// type Style;
|
||||||
|
|
||||||
|
// fn layout()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// trait Stylable<V: 'static>: Element<V> {
|
||||||
|
// type Style;
|
||||||
|
|
||||||
|
// fn with_style(self, style: Self::Style) -> Self;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub struct HoverStyle<S> {
|
||||||
|
// default: S,
|
||||||
|
// hovered: S,
|
||||||
|
// }
|
||||||
|
|
||||||
|
// struct Hover<V: 'static, C: Stylable<V>> {
|
||||||
|
// child: C,
|
||||||
|
// style: HoverStyle<C::Style>,
|
||||||
|
// }
|
||||||
|
|
||||||
|
// impl<V: 'static, C: Stylable<V>> Hover<V, C> {
|
||||||
|
// fn new(child: C, style: HoverStyle<C::Style>) -> Self {
|
||||||
|
// Self { child, style }
|
||||||
|
// }
|
||||||
|
// }
|
158
crates/gpui/playground/src/element.rs
Normal file
158
crates/gpui/playground/src/element.rs
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use derive_more::{Deref, DerefMut};
|
||||||
|
use gpui::{geometry::rect::RectF, EngineLayout};
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
use std::marker::PhantomData;
|
||||||
|
use util::ResultExt;
|
||||||
|
|
||||||
|
pub use crate::layout_context::LayoutContext;
|
||||||
|
pub use crate::paint_context::PaintContext;
|
||||||
|
|
||||||
|
type LayoutId = gpui::LayoutId;
|
||||||
|
|
||||||
|
pub trait Element<V: 'static>: 'static {
|
||||||
|
type Layout;
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
view: &mut V,
|
||||||
|
cx: &mut LayoutContext<V>,
|
||||||
|
) -> Result<Layout<V, Self::Layout>>
|
||||||
|
where
|
||||||
|
Self: Sized;
|
||||||
|
|
||||||
|
fn paint(
|
||||||
|
&mut self,
|
||||||
|
view: &mut V,
|
||||||
|
layout: &mut Layout<V, Self::Layout>,
|
||||||
|
cx: &mut PaintContext<V>,
|
||||||
|
) where
|
||||||
|
Self: Sized;
|
||||||
|
|
||||||
|
fn into_any(self) -> AnyElement<V>
|
||||||
|
where
|
||||||
|
Self: 'static + Sized,
|
||||||
|
{
|
||||||
|
AnyElement(Box::new(ElementState {
|
||||||
|
element: self,
|
||||||
|
layout: None,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Used to make ElementState<V, E> into a trait object, so we can wrap it in AnyElement<V>.
|
||||||
|
trait ElementStateObject<V> {
|
||||||
|
fn layout(&mut self, view: &mut V, cx: &mut LayoutContext<V>) -> Result<LayoutId>;
|
||||||
|
fn paint(&mut self, view: &mut V, cx: &mut PaintContext<V>);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A wrapper around an element that stores its layout state.
|
||||||
|
struct ElementState<V: 'static, E: Element<V>> {
|
||||||
|
element: E,
|
||||||
|
layout: Option<Layout<V, E::Layout>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// We blanket-implement the object-safe ElementStateObject interface to make ElementStates into trait objects
|
||||||
|
impl<V, E: Element<V>> ElementStateObject<V> for ElementState<V, E> {
|
||||||
|
fn layout(&mut self, view: &mut V, cx: &mut LayoutContext<V>) -> Result<LayoutId> {
|
||||||
|
let layout = self.element.layout(view, cx)?;
|
||||||
|
let layout_id = layout.id;
|
||||||
|
self.layout = Some(layout);
|
||||||
|
Ok(layout_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(&mut self, view: &mut V, cx: &mut PaintContext<V>) {
|
||||||
|
let layout = self.layout.as_mut().expect("paint called before layout");
|
||||||
|
if layout.engine_layout.is_none() {
|
||||||
|
layout.engine_layout = cx.computed_layout(layout.id).log_err()
|
||||||
|
}
|
||||||
|
self.element.paint(view, layout, cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A dynamic element.
|
||||||
|
pub struct AnyElement<V>(Box<dyn ElementStateObject<V>>);
|
||||||
|
|
||||||
|
impl<V> AnyElement<V> {
|
||||||
|
pub fn layout(&mut self, view: &mut V, cx: &mut LayoutContext<V>) -> Result<LayoutId> {
|
||||||
|
self.0.layout(view, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn paint(&mut self, view: &mut V, cx: &mut PaintContext<V>) {
|
||||||
|
self.0.paint(view, cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deref, DerefMut)]
|
||||||
|
pub struct Layout<V, D> {
|
||||||
|
id: LayoutId,
|
||||||
|
engine_layout: Option<EngineLayout>,
|
||||||
|
#[deref]
|
||||||
|
#[deref_mut]
|
||||||
|
element_data: D,
|
||||||
|
view_type: PhantomData<V>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V: 'static, D> Layout<V, D> {
|
||||||
|
pub fn new(id: LayoutId, element_data: D) -> Self {
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
engine_layout: None,
|
||||||
|
element_data: element_data,
|
||||||
|
view_type: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bounds(&mut self, cx: &mut PaintContext<V>) -> RectF {
|
||||||
|
self.engine_layout(cx).bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn order(&mut self, cx: &mut PaintContext<V>) -> u32 {
|
||||||
|
self.engine_layout(cx).order
|
||||||
|
}
|
||||||
|
|
||||||
|
fn engine_layout(&mut self, cx: &mut PaintContext<'_, '_, '_, '_, V>) -> &mut EngineLayout {
|
||||||
|
self.engine_layout
|
||||||
|
.get_or_insert_with(|| cx.computed_layout(self.id).log_err().unwrap_or_default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V: 'static> Layout<V, Option<AnyElement<V>>> {
|
||||||
|
pub fn paint(&mut self, view: &mut V, cx: &mut PaintContext<V>) {
|
||||||
|
let mut element = self.element_data.take().unwrap();
|
||||||
|
element.paint(view, cx);
|
||||||
|
self.element_data = Some(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ParentElement<V: 'static> {
|
||||||
|
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]>;
|
||||||
|
|
||||||
|
fn child(mut self, child: impl IntoElement<V>) -> Self
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
self.children_mut().push(child.into_element().into_any());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn children<I, E>(mut self, children: I) -> Self
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = E>,
|
||||||
|
E: IntoElement<V>,
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
self.children_mut().extend(
|
||||||
|
children
|
||||||
|
.into_iter()
|
||||||
|
.map(|child| child.into_element().into_any()),
|
||||||
|
);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait IntoElement<V: 'static> {
|
||||||
|
type Element: Element<V>;
|
||||||
|
|
||||||
|
fn into_element(self) -> Self::Element;
|
||||||
|
}
|
76
crates/gpui/playground/src/hoverable.rs
Normal file
76
crates/gpui/playground/src/hoverable.rs
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
use crate::{
|
||||||
|
element::{Element, Layout},
|
||||||
|
layout_context::LayoutContext,
|
||||||
|
paint_context::PaintContext,
|
||||||
|
style::{StyleRefinement, Styleable},
|
||||||
|
};
|
||||||
|
use anyhow::Result;
|
||||||
|
use gpui::platform::MouseMovedEvent;
|
||||||
|
use refineable::Refineable;
|
||||||
|
use std::{cell::Cell, marker::PhantomData};
|
||||||
|
|
||||||
|
pub struct Hoverable<V: 'static, E: Element<V> + Styleable> {
|
||||||
|
hovered: Cell<bool>,
|
||||||
|
child_style: StyleRefinement,
|
||||||
|
hovered_style: StyleRefinement,
|
||||||
|
child: E,
|
||||||
|
view_type: PhantomData<V>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hoverable<V, E: Element<V> + Styleable>(mut child: E) -> Hoverable<V, E> {
|
||||||
|
Hoverable {
|
||||||
|
hovered: Cell::new(false),
|
||||||
|
child_style: child.declared_style().clone(),
|
||||||
|
hovered_style: Default::default(),
|
||||||
|
child,
|
||||||
|
view_type: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V, E: Element<V> + Styleable> Styleable for Hoverable<V, E> {
|
||||||
|
type Style = E::Style;
|
||||||
|
|
||||||
|
fn declared_style(&mut self) -> &mut crate::style::StyleRefinement {
|
||||||
|
self.child.declared_style()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V: 'static, E: Element<V> + Styleable> Element<V> for Hoverable<V, E> {
|
||||||
|
type Layout = E::Layout;
|
||||||
|
|
||||||
|
fn layout(&mut self, view: &mut V, cx: &mut LayoutContext<V>) -> Result<Layout<V, Self::Layout>>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
self.child.layout(view, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(
|
||||||
|
&mut self,
|
||||||
|
view: &mut V,
|
||||||
|
layout: &mut Layout<V, Self::Layout>,
|
||||||
|
cx: &mut PaintContext<V>,
|
||||||
|
) where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
if self.hovered.get() {
|
||||||
|
// If hovered, refine the child's style with this element's style.
|
||||||
|
self.child.declared_style().refine(&self.hovered_style);
|
||||||
|
} else {
|
||||||
|
// Otherwise, set the child's style back to its original style.
|
||||||
|
*self.child.declared_style() = self.child_style.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
let bounds = layout.bounds(cx);
|
||||||
|
let order = layout.order(cx);
|
||||||
|
self.hovered.set(bounds.contains_point(cx.mouse_position()));
|
||||||
|
let was_hovered = self.hovered.clone();
|
||||||
|
cx.on_event(order, move |view, event: &MouseMovedEvent, cx| {
|
||||||
|
let is_hovered = bounds.contains_point(event.position);
|
||||||
|
if is_hovered != was_hovered.get() {
|
||||||
|
was_hovered.set(is_hovered);
|
||||||
|
cx.repaint();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
34
crates/gpui/playground/src/interactive.rs
Normal file
34
crates/gpui/playground/src/interactive.rs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
use gpui::{platform::MouseMovedEvent, EventContext};
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
pub trait Interactive<V: 'static> {
|
||||||
|
fn interaction_handlers(&mut self) -> &mut InteractionHandlers<V>;
|
||||||
|
|
||||||
|
fn on_mouse_move<H>(mut self, handler: H) -> Self
|
||||||
|
where
|
||||||
|
H: 'static + Fn(&mut V, &MouseMovedEvent, bool, &mut EventContext<V>),
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
self.interaction_handlers()
|
||||||
|
.mouse_moved
|
||||||
|
.push(Rc::new(move |view, event, hit_test, cx| {
|
||||||
|
handler(view, event, hit_test, cx);
|
||||||
|
cx.bubble
|
||||||
|
}));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct InteractionHandlers<V: 'static> {
|
||||||
|
mouse_moved:
|
||||||
|
SmallVec<[Rc<dyn Fn(&mut V, &MouseMovedEvent, bool, &mut EventContext<V>) -> bool>; 2]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V> Default for InteractionHandlers<V> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
mouse_moved: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
54
crates/gpui/playground/src/layout_context.rs
Normal file
54
crates/gpui/playground/src/layout_context.rs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use derive_more::{Deref, DerefMut};
|
||||||
|
pub use gpui::LayoutContext as LegacyLayoutContext;
|
||||||
|
use gpui::{RenderContext, ViewContext};
|
||||||
|
pub use taffy::tree::NodeId;
|
||||||
|
|
||||||
|
use crate::{element::Layout, style::Style};
|
||||||
|
|
||||||
|
#[derive(Deref, DerefMut)]
|
||||||
|
pub struct LayoutContext<'a, 'b, 'c, 'd, V> {
|
||||||
|
#[deref]
|
||||||
|
#[deref_mut]
|
||||||
|
pub(crate) legacy_cx: &'d mut LegacyLayoutContext<'a, 'b, 'c, V>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b, V> RenderContext<'a, 'b, V> for LayoutContext<'a, 'b, '_, '_, V> {
|
||||||
|
fn text_style(&self) -> gpui::fonts::TextStyle {
|
||||||
|
self.legacy_cx.text_style()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_text_style(&mut self, style: gpui::fonts::TextStyle) {
|
||||||
|
self.legacy_cx.push_text_style(style)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pop_text_style(&mut self) {
|
||||||
|
self.legacy_cx.pop_text_style()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_view_context(&mut self) -> &mut ViewContext<'a, 'b, V> {
|
||||||
|
&mut self.view_context
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b, 'c, 'd, V: 'static> LayoutContext<'a, 'b, 'c, 'd, V> {
|
||||||
|
pub fn new(legacy_cx: &'d mut LegacyLayoutContext<'a, 'b, 'c, V>) -> Self {
|
||||||
|
Self { legacy_cx }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_layout_node<D>(
|
||||||
|
&mut self,
|
||||||
|
style: Style,
|
||||||
|
element_data: D,
|
||||||
|
children: impl IntoIterator<Item = NodeId>,
|
||||||
|
) -> Result<Layout<V, D>> {
|
||||||
|
let rem_size = self.rem_pixels();
|
||||||
|
let id = self
|
||||||
|
.legacy_cx
|
||||||
|
.layout_engine()
|
||||||
|
.ok_or_else(|| anyhow!("no layout engine"))?
|
||||||
|
.add_node(style.to_taffy(rem_size), children)?;
|
||||||
|
|
||||||
|
Ok(Layout::new(id, element_data))
|
||||||
|
}
|
||||||
|
}
|
71
crates/gpui/playground/src/paint_context.rs
Normal file
71
crates/gpui/playground/src/paint_context.rs
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use derive_more::{Deref, DerefMut};
|
||||||
|
use gpui::{scene::EventHandler, EngineLayout, EventContext, LayoutId, RenderContext, ViewContext};
|
||||||
|
pub use gpui::{LayoutContext, PaintContext as LegacyPaintContext};
|
||||||
|
use std::{any::TypeId, rc::Rc};
|
||||||
|
pub use taffy::tree::NodeId;
|
||||||
|
|
||||||
|
#[derive(Deref, DerefMut)]
|
||||||
|
pub struct PaintContext<'a, 'b, 'c, 'd, V> {
|
||||||
|
#[deref]
|
||||||
|
#[deref_mut]
|
||||||
|
pub(crate) legacy_cx: &'d mut LegacyPaintContext<'a, 'b, 'c, V>,
|
||||||
|
pub(crate) scene: &'d mut gpui::SceneBuilder,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b, V> RenderContext<'a, 'b, V> for PaintContext<'a, 'b, '_, '_, V> {
|
||||||
|
fn text_style(&self) -> gpui::fonts::TextStyle {
|
||||||
|
self.legacy_cx.text_style()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_text_style(&mut self, style: gpui::fonts::TextStyle) {
|
||||||
|
self.legacy_cx.push_text_style(style)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pop_text_style(&mut self) {
|
||||||
|
self.legacy_cx.pop_text_style()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_view_context(&mut self) -> &mut ViewContext<'a, 'b, V> {
|
||||||
|
&mut self.view_context
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b, 'c, 'd, V: 'static> PaintContext<'a, 'b, 'c, 'd, V> {
|
||||||
|
pub fn new(
|
||||||
|
legacy_cx: &'d mut LegacyPaintContext<'a, 'b, 'c, V>,
|
||||||
|
scene: &'d mut gpui::SceneBuilder,
|
||||||
|
) -> Self {
|
||||||
|
Self { legacy_cx, scene }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_event<E: 'static>(
|
||||||
|
&mut self,
|
||||||
|
order: u32,
|
||||||
|
handler: impl Fn(&mut V, &E, &mut ViewContext<V>) + 'static,
|
||||||
|
) {
|
||||||
|
let view = self.weak_handle();
|
||||||
|
|
||||||
|
self.scene.event_handlers.push(EventHandler {
|
||||||
|
order,
|
||||||
|
handler: Rc::new(move |event, window_cx| {
|
||||||
|
if let Some(view) = view.upgrade(window_cx) {
|
||||||
|
view.update(window_cx, |view, view_cx| {
|
||||||
|
let mut event_cx = EventContext::new(view_cx);
|
||||||
|
handler(view, event.downcast_ref().unwrap(), &mut event_cx);
|
||||||
|
event_cx.bubble
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
event_type: TypeId::of::<E>(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn computed_layout(&mut self, layout_id: LayoutId) -> Result<EngineLayout> {
|
||||||
|
self.layout_engine()
|
||||||
|
.ok_or_else(|| anyhow!("no layout engine present"))?
|
||||||
|
.computed_layout(layout_id)
|
||||||
|
}
|
||||||
|
}
|
83
crates/gpui/playground/src/playground.rs
Normal file
83
crates/gpui/playground/src/playground.rs
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
#![allow(dead_code, unused_variables)]
|
||||||
|
use crate::{color::black, style::StyleHelpers};
|
||||||
|
use element::Element;
|
||||||
|
use gpui::{
|
||||||
|
geometry::{rect::RectF, vector::vec2f},
|
||||||
|
platform::WindowOptions,
|
||||||
|
};
|
||||||
|
use log::LevelFilter;
|
||||||
|
use simplelog::SimpleLogger;
|
||||||
|
use themes::{rose_pine, ThemeColors};
|
||||||
|
use view::view;
|
||||||
|
|
||||||
|
mod adapter;
|
||||||
|
mod color;
|
||||||
|
mod components;
|
||||||
|
mod div;
|
||||||
|
mod element;
|
||||||
|
mod hoverable;
|
||||||
|
mod interactive;
|
||||||
|
mod layout_context;
|
||||||
|
mod paint_context;
|
||||||
|
mod style;
|
||||||
|
mod text;
|
||||||
|
mod themes;
|
||||||
|
mod view;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
|
||||||
|
|
||||||
|
gpui::App::new(()).unwrap().run(|cx| {
|
||||||
|
cx.add_window(
|
||||||
|
WindowOptions {
|
||||||
|
bounds: gpui::platform::WindowBounds::Fixed(RectF::new(
|
||||||
|
vec2f(0., 0.),
|
||||||
|
vec2f(400., 300.),
|
||||||
|
)),
|
||||||
|
center: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
|_| view(|_| playground(&rose_pine::moon())),
|
||||||
|
);
|
||||||
|
cx.platform().activate(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn playground<V: 'static>(theme: &ThemeColors) -> impl Element<V> {
|
||||||
|
use div::div;
|
||||||
|
|
||||||
|
div()
|
||||||
|
.text_color(black())
|
||||||
|
.h_full()
|
||||||
|
.w_1_2()
|
||||||
|
.fill(theme.success(0.5))
|
||||||
|
// .hover()
|
||||||
|
// .fill(theme.error(0.5))
|
||||||
|
// .child(button().label("Hello").click(|_, _, _| println!("click!")))
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo!()
|
||||||
|
// // column()
|
||||||
|
// // .size(auto())
|
||||||
|
// // .fill(theme.base(0.5))
|
||||||
|
// // .text_color(theme.text(0.5))
|
||||||
|
// // .child(title_bar(theme))
|
||||||
|
// // .child(stage(theme))
|
||||||
|
// // .child(status_bar(theme))
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn title_bar<V: 'static>(theme: &ThemeColors) -> impl Element<V> {
|
||||||
|
// row()
|
||||||
|
// .fill(theme.base(0.2))
|
||||||
|
// .justify(0.)
|
||||||
|
// .width(auto())
|
||||||
|
// .child(text("Zed Playground"))
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn stage<V: 'static>(theme: &ThemeColors) -> impl Element<V> {
|
||||||
|
// row().fill(theme.surface(0.9))
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn status_bar<V: 'static>(theme: &ThemeColors) -> impl Element<V> {
|
||||||
|
// row().fill(theme.surface(0.1))
|
||||||
|
// }
|
286
crates/gpui/playground/src/style.rs
Normal file
286
crates/gpui/playground/src/style.rs
Normal file
@ -0,0 +1,286 @@
|
|||||||
|
use crate::{
|
||||||
|
color::Hsla,
|
||||||
|
element::{Element, Layout},
|
||||||
|
paint_context::PaintContext,
|
||||||
|
};
|
||||||
|
use gpui::{
|
||||||
|
fonts::TextStyleRefinement,
|
||||||
|
geometry::{
|
||||||
|
AbsoluteLength, DefiniteLength, Edges, EdgesRefinement, Length, Point, PointRefinement,
|
||||||
|
Size, SizeRefinement,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use playground_macros::styleable_helpers;
|
||||||
|
use refineable::Refineable;
|
||||||
|
pub use taffy::style::{
|
||||||
|
AlignContent, AlignItems, AlignSelf, Display, FlexDirection, FlexWrap, JustifyContent,
|
||||||
|
Overflow, Position,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Refineable)]
|
||||||
|
pub struct Style {
|
||||||
|
/// What layout strategy should be used?
|
||||||
|
pub display: Display,
|
||||||
|
|
||||||
|
// Overflow properties
|
||||||
|
/// How children overflowing their container should affect layout
|
||||||
|
#[refineable]
|
||||||
|
pub overflow: Point<Overflow>,
|
||||||
|
/// How much space (in points) should be reserved for the scrollbars of `Overflow::Scroll` and `Overflow::Auto` nodes.
|
||||||
|
pub scrollbar_width: f32,
|
||||||
|
|
||||||
|
// Position properties
|
||||||
|
/// What should the `position` value of this struct use as a base offset?
|
||||||
|
pub position: Position,
|
||||||
|
/// How should the position of this element be tweaked relative to the layout defined?
|
||||||
|
#[refineable]
|
||||||
|
pub inset: Edges<Length>,
|
||||||
|
|
||||||
|
// Size properies
|
||||||
|
/// Sets the initial size of the item
|
||||||
|
#[refineable]
|
||||||
|
pub size: Size<Length>,
|
||||||
|
/// Controls the minimum size of the item
|
||||||
|
#[refineable]
|
||||||
|
pub min_size: Size<Length>,
|
||||||
|
/// Controls the maximum size of the item
|
||||||
|
#[refineable]
|
||||||
|
pub max_size: Size<Length>,
|
||||||
|
/// Sets the preferred aspect ratio for the item. The ratio is calculated as width divided by height.
|
||||||
|
pub aspect_ratio: Option<f32>,
|
||||||
|
|
||||||
|
// Spacing Properties
|
||||||
|
/// How large should the margin be on each side?
|
||||||
|
#[refineable]
|
||||||
|
pub margin: Edges<Length>,
|
||||||
|
/// How large should the padding be on each side?
|
||||||
|
#[refineable]
|
||||||
|
pub padding: Edges<DefiniteLength>,
|
||||||
|
/// How large should the border be on each side?
|
||||||
|
#[refineable]
|
||||||
|
pub border: Edges<DefiniteLength>,
|
||||||
|
|
||||||
|
// Alignment properties
|
||||||
|
/// How this node's children aligned in the cross/block axis?
|
||||||
|
pub align_items: Option<AlignItems>,
|
||||||
|
/// How this node should be aligned in the cross/block axis. Falls back to the parents [`AlignItems`] if not set
|
||||||
|
pub align_self: Option<AlignSelf>,
|
||||||
|
/// How should content contained within this item be aligned in the cross/block axis
|
||||||
|
pub align_content: Option<AlignContent>,
|
||||||
|
/// How should contained within this item be aligned in the main/inline axis
|
||||||
|
pub justify_content: Option<JustifyContent>,
|
||||||
|
/// How large should the gaps between items in a flex container be?
|
||||||
|
#[refineable]
|
||||||
|
pub gap: Size<DefiniteLength>,
|
||||||
|
|
||||||
|
// Flexbox properies
|
||||||
|
/// Which direction does the main axis flow in?
|
||||||
|
pub flex_direction: FlexDirection,
|
||||||
|
/// Should elements wrap, or stay in a single line?
|
||||||
|
pub flex_wrap: FlexWrap,
|
||||||
|
/// Sets the initial main axis size of the item
|
||||||
|
pub flex_basis: Length,
|
||||||
|
/// The relative rate at which this item grows when it is expanding to fill space, 0.0 is the default value, and this value must be positive.
|
||||||
|
pub flex_grow: f32,
|
||||||
|
/// The relative rate at which this item shrinks when it is contracting to fit into space, 1.0 is the default value, and this value must be positive.
|
||||||
|
pub flex_shrink: f32,
|
||||||
|
|
||||||
|
/// The fill color of this element
|
||||||
|
pub fill: Option<Fill>,
|
||||||
|
/// The radius of the corners of this element
|
||||||
|
#[refineable]
|
||||||
|
pub corner_radii: CornerRadii,
|
||||||
|
/// The color of text within this element. Cascades to children unless overridden.
|
||||||
|
pub text_color: Option<Hsla>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Style {
|
||||||
|
pub fn to_taffy(&self, rem_size: f32) -> taffy::style::Style {
|
||||||
|
taffy::style::Style {
|
||||||
|
display: self.display,
|
||||||
|
overflow: self.overflow.clone().into(),
|
||||||
|
scrollbar_width: self.scrollbar_width,
|
||||||
|
position: self.position,
|
||||||
|
inset: self.inset.to_taffy(rem_size),
|
||||||
|
size: self.size.to_taffy(rem_size),
|
||||||
|
min_size: self.min_size.to_taffy(rem_size),
|
||||||
|
max_size: self.max_size.to_taffy(rem_size),
|
||||||
|
aspect_ratio: self.aspect_ratio,
|
||||||
|
margin: self.margin.to_taffy(rem_size),
|
||||||
|
padding: self.padding.to_taffy(rem_size),
|
||||||
|
border: self.border.to_taffy(rem_size),
|
||||||
|
align_items: self.align_items,
|
||||||
|
align_self: self.align_self,
|
||||||
|
align_content: self.align_content,
|
||||||
|
justify_content: self.justify_content,
|
||||||
|
gap: self.gap.to_taffy(rem_size),
|
||||||
|
flex_direction: self.flex_direction,
|
||||||
|
flex_wrap: self.flex_wrap,
|
||||||
|
flex_basis: self.flex_basis.to_taffy(rem_size).into(),
|
||||||
|
flex_grow: self.flex_grow,
|
||||||
|
flex_shrink: self.flex_shrink,
|
||||||
|
..Default::default() // Ignore grid properties for now
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Paints the background of an element styled with this style.
|
||||||
|
/// Return the bounds in which to paint the content.
|
||||||
|
pub fn paint_background<V: 'static, E: Element<V>>(
|
||||||
|
&self,
|
||||||
|
layout: &mut Layout<V, E::Layout>,
|
||||||
|
cx: &mut PaintContext<V>,
|
||||||
|
) {
|
||||||
|
let bounds = layout.bounds(cx);
|
||||||
|
let rem_size = cx.rem_pixels();
|
||||||
|
if let Some(color) = self.fill.as_ref().and_then(Fill::color) {
|
||||||
|
cx.scene.push_quad(gpui::Quad {
|
||||||
|
bounds,
|
||||||
|
background: Some(color.into()),
|
||||||
|
corner_radii: self.corner_radii.to_gpui(rem_size),
|
||||||
|
border: Default::default(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Style {
|
||||||
|
fn default() -> Self {
|
||||||
|
Style {
|
||||||
|
display: Display::DEFAULT,
|
||||||
|
overflow: Point {
|
||||||
|
x: Overflow::Visible,
|
||||||
|
y: Overflow::Visible,
|
||||||
|
},
|
||||||
|
scrollbar_width: 0.0,
|
||||||
|
position: Position::Relative,
|
||||||
|
inset: Edges::auto(),
|
||||||
|
margin: Edges::<Length>::zero(),
|
||||||
|
padding: Edges::<DefiniteLength>::zero(),
|
||||||
|
border: Edges::<DefiniteLength>::zero(),
|
||||||
|
size: Size::auto(),
|
||||||
|
min_size: Size::auto(),
|
||||||
|
max_size: Size::auto(),
|
||||||
|
aspect_ratio: None,
|
||||||
|
gap: Size::zero(),
|
||||||
|
// Aligment
|
||||||
|
align_items: None,
|
||||||
|
align_self: None,
|
||||||
|
align_content: None,
|
||||||
|
justify_content: None,
|
||||||
|
// Flexbox
|
||||||
|
flex_direction: FlexDirection::Row,
|
||||||
|
flex_wrap: FlexWrap::NoWrap,
|
||||||
|
flex_grow: 0.0,
|
||||||
|
flex_shrink: 1.0,
|
||||||
|
flex_basis: Length::Auto,
|
||||||
|
fill: None,
|
||||||
|
text_color: None,
|
||||||
|
corner_radii: CornerRadii::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StyleRefinement {
|
||||||
|
pub fn text_style(&self) -> Option<TextStyleRefinement> {
|
||||||
|
self.text_color.map(|color| TextStyleRefinement {
|
||||||
|
color: Some(color.into()),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct OptionalTextStyle {
|
||||||
|
color: Option<Hsla>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OptionalTextStyle {
|
||||||
|
pub fn apply(&self, style: &mut gpui::fonts::TextStyle) {
|
||||||
|
if let Some(color) = self.color {
|
||||||
|
style.color = color.into();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum Fill {
|
||||||
|
Color(Hsla),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Fill {
|
||||||
|
pub fn color(&self) -> Option<Hsla> {
|
||||||
|
match self {
|
||||||
|
Fill::Color(color) => Some(*color),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Fill {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Color(Hsla::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Hsla> for Fill {
|
||||||
|
fn from(color: Hsla) -> Self {
|
||||||
|
Self::Color(color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Refineable, Default)]
|
||||||
|
pub struct CornerRadii {
|
||||||
|
top_left: AbsoluteLength,
|
||||||
|
top_right: AbsoluteLength,
|
||||||
|
bottom_left: AbsoluteLength,
|
||||||
|
bottom_right: AbsoluteLength,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CornerRadii {
|
||||||
|
pub fn to_gpui(&self, rem_size: f32) -> gpui::scene::CornerRadii {
|
||||||
|
gpui::scene::CornerRadii {
|
||||||
|
top_left: self.top_left.to_pixels(rem_size),
|
||||||
|
top_right: self.top_right.to_pixels(rem_size),
|
||||||
|
bottom_left: self.bottom_left.to_pixels(rem_size),
|
||||||
|
bottom_right: self.bottom_right.to_pixels(rem_size),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Styleable {
|
||||||
|
type Style: refineable::Refineable;
|
||||||
|
|
||||||
|
fn declared_style(&mut self) -> &mut playground::style::StyleRefinement;
|
||||||
|
|
||||||
|
fn style(&mut self) -> playground::style::Style {
|
||||||
|
let mut style = playground::style::Style::default();
|
||||||
|
style.refine(self.declared_style());
|
||||||
|
style
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers methods that take and return mut self. This includes tailwind style methods for standard sizes etc.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
// // Sets the padding to 0.5rem, just like class="p-2" in Tailwind.
|
||||||
|
// fn p_2(mut self) -> Self where Self: Sized;
|
||||||
|
use crate as playground; // Macro invocation references this crate as playground.
|
||||||
|
pub trait StyleHelpers: Styleable<Style = Style> {
|
||||||
|
styleable_helpers!();
|
||||||
|
|
||||||
|
fn fill<F>(mut self, fill: F) -> Self
|
||||||
|
where
|
||||||
|
F: Into<Fill>,
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
self.declared_style().fill = Some(fill.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn text_color<C>(mut self, color: C) -> Self
|
||||||
|
where
|
||||||
|
C: Into<Hsla>,
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
self.declared_style().text_color = Some(color.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
151
crates/gpui/playground/src/text.rs
Normal file
151
crates/gpui/playground/src/text.rs
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
use crate::{
|
||||||
|
element::{Element, IntoElement, Layout},
|
||||||
|
layout_context::LayoutContext,
|
||||||
|
paint_context::PaintContext,
|
||||||
|
};
|
||||||
|
use anyhow::Result;
|
||||||
|
use gpui::text_layout::LineLayout;
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
impl<V: 'static, S: Into<ArcCow<'static, str>>> IntoElement<V> for S {
|
||||||
|
type Element = Text;
|
||||||
|
|
||||||
|
fn into_element(self) -> Self::Element {
|
||||||
|
Text { text: self.into() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Text {
|
||||||
|
text: ArcCow<'static, str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V: 'static> Element<V> for Text {
|
||||||
|
type Layout = Arc<Mutex<Option<TextLayout>>>;
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
view: &mut V,
|
||||||
|
cx: &mut LayoutContext<V>,
|
||||||
|
) -> Result<Layout<V, Self::Layout>> {
|
||||||
|
// let rem_size = cx.rem_pixels();
|
||||||
|
// let fonts = cx.platform().fonts();
|
||||||
|
// let text_style = cx.text_style();
|
||||||
|
// let line_height = cx.font_cache().line_height(text_style.font_size);
|
||||||
|
// let layout_engine = cx.layout_engine().expect("no layout engine present");
|
||||||
|
// let text = self.text.clone();
|
||||||
|
// let layout = Arc::new(Mutex::new(None));
|
||||||
|
|
||||||
|
// let style: Style = Style::default().refined(&self.metadata.style);
|
||||||
|
// let node_id = layout_engine.add_measured_node(style.to_taffy(rem_size), {
|
||||||
|
// let layout = layout.clone();
|
||||||
|
// move |params| {
|
||||||
|
// let line_layout = fonts.layout_line(
|
||||||
|
// text.as_ref(),
|
||||||
|
// text_style.font_size,
|
||||||
|
// &[(text.len(), text_style.to_run())],
|
||||||
|
// );
|
||||||
|
|
||||||
|
// let size = Size {
|
||||||
|
// width: line_layout.width,
|
||||||
|
// height: line_height,
|
||||||
|
// };
|
||||||
|
|
||||||
|
// layout.lock().replace(TextLayout {
|
||||||
|
// line_layout: Arc::new(line_layout),
|
||||||
|
// line_height,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// size
|
||||||
|
// }
|
||||||
|
// })?;
|
||||||
|
|
||||||
|
// Ok((node_id, layout))
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint<'a>(
|
||||||
|
&mut self,
|
||||||
|
view: &mut V,
|
||||||
|
layout: &mut Layout<V, Self::Layout>,
|
||||||
|
cx: &mut PaintContext<V>,
|
||||||
|
) {
|
||||||
|
// ) {
|
||||||
|
// let element_layout_lock = layout.from_element.lock();
|
||||||
|
// let element_layout = element_layout_lock
|
||||||
|
// .as_ref()
|
||||||
|
// .expect("layout has not been performed");
|
||||||
|
// let line_layout = element_layout.line_layout.clone();
|
||||||
|
// let line_height = element_layout.line_height;
|
||||||
|
// drop(element_layout_lock);
|
||||||
|
|
||||||
|
// let text_style = cx.text_style();
|
||||||
|
// let line =
|
||||||
|
// gpui::text_layout::Line::new(line_layout, &[(self.text.len(), text_style.to_run())]);
|
||||||
|
// line.paint(
|
||||||
|
// cx.scene,
|
||||||
|
// layout.from_engine.bounds.origin(),
|
||||||
|
// layout.from_engine.bounds,
|
||||||
|
// line_height,
|
||||||
|
// cx.legacy_cx,
|
||||||
|
// );
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TextLayout {
|
||||||
|
line_layout: Arc<LineLayout>,
|
||||||
|
line_height: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum ArcCow<'a, T: ?Sized> {
|
||||||
|
Borrowed(&'a T),
|
||||||
|
Owned(Arc<T>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T: ?Sized> Clone for ArcCow<'a, T> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::Borrowed(borrowed) => Self::Borrowed(borrowed),
|
||||||
|
Self::Owned(owned) => Self::Owned(owned.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T: ?Sized> From<&'a T> for ArcCow<'a, T> {
|
||||||
|
fn from(s: &'a T) -> Self {
|
||||||
|
Self::Borrowed(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<Arc<T>> for ArcCow<'_, T> {
|
||||||
|
fn from(s: Arc<T>) -> Self {
|
||||||
|
Self::Owned(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for ArcCow<'_, str> {
|
||||||
|
fn from(value: String) -> Self {
|
||||||
|
Self::Owned(value.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: ?Sized> std::ops::Deref for ArcCow<'_, T> {
|
||||||
|
type Target = T;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
match self {
|
||||||
|
ArcCow::Borrowed(s) => s,
|
||||||
|
ArcCow::Owned(s) => s.as_ref(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: ?Sized> AsRef<T> for ArcCow<'_, T> {
|
||||||
|
fn as_ref(&self) -> &T {
|
||||||
|
match self {
|
||||||
|
ArcCow::Borrowed(borrowed) => borrowed,
|
||||||
|
ArcCow::Owned(owned) => owned.as_ref(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
84
crates/gpui/playground/src/themes.rs
Normal file
84
crates/gpui/playground/src/themes.rs
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
use crate::color::{Hsla, Lerp};
|
||||||
|
use std::ops::Range;
|
||||||
|
|
||||||
|
pub mod rose_pine;
|
||||||
|
|
||||||
|
pub struct ThemeColors {
|
||||||
|
pub base: Range<Hsla>,
|
||||||
|
pub surface: Range<Hsla>,
|
||||||
|
pub overlay: Range<Hsla>,
|
||||||
|
pub muted: Range<Hsla>,
|
||||||
|
pub subtle: Range<Hsla>,
|
||||||
|
pub text: Range<Hsla>,
|
||||||
|
pub highlight_low: Range<Hsla>,
|
||||||
|
pub highlight_med: Range<Hsla>,
|
||||||
|
pub highlight_high: Range<Hsla>,
|
||||||
|
pub success: Range<Hsla>,
|
||||||
|
pub warning: Range<Hsla>,
|
||||||
|
pub error: Range<Hsla>,
|
||||||
|
pub inserted: Range<Hsla>,
|
||||||
|
pub deleted: Range<Hsla>,
|
||||||
|
pub modified: Range<Hsla>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ThemeColors {
|
||||||
|
pub fn base(&self, level: f32) -> Hsla {
|
||||||
|
self.base.lerp(level)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn surface(&self, level: f32) -> Hsla {
|
||||||
|
self.surface.lerp(level)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn overlay(&self, level: f32) -> Hsla {
|
||||||
|
self.overlay.lerp(level)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn muted(&self, level: f32) -> Hsla {
|
||||||
|
self.muted.lerp(level)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn subtle(&self, level: f32) -> Hsla {
|
||||||
|
self.subtle.lerp(level)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn text(&self, level: f32) -> Hsla {
|
||||||
|
self.text.lerp(level)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn highlight_low(&self, level: f32) -> Hsla {
|
||||||
|
self.highlight_low.lerp(level)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn highlight_med(&self, level: f32) -> Hsla {
|
||||||
|
self.highlight_med.lerp(level)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn highlight_high(&self, level: f32) -> Hsla {
|
||||||
|
self.highlight_high.lerp(level)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn success(&self, level: f32) -> Hsla {
|
||||||
|
self.success.lerp(level)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn warning(&self, level: f32) -> Hsla {
|
||||||
|
self.warning.lerp(level)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn error(&self, level: f32) -> Hsla {
|
||||||
|
self.error.lerp(level)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn inserted(&self, level: f32) -> Hsla {
|
||||||
|
self.inserted.lerp(level)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deleted(&self, level: f32) -> Hsla {
|
||||||
|
self.deleted.lerp(level)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn modified(&self, level: f32) -> Hsla {
|
||||||
|
self.modified.lerp(level)
|
||||||
|
}
|
||||||
|
}
|
133
crates/gpui/playground/src/themes/rose_pine.rs
Normal file
133
crates/gpui/playground/src/themes/rose_pine.rs
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
use std::ops::Range;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
color::{hsla, rgb, Hsla},
|
||||||
|
ThemeColors,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct RosePineThemes {
|
||||||
|
pub default: RosePinePalette,
|
||||||
|
pub dawn: RosePinePalette,
|
||||||
|
pub moon: RosePinePalette,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct RosePinePalette {
|
||||||
|
pub base: Hsla,
|
||||||
|
pub surface: Hsla,
|
||||||
|
pub overlay: Hsla,
|
||||||
|
pub muted: Hsla,
|
||||||
|
pub subtle: Hsla,
|
||||||
|
pub text: Hsla,
|
||||||
|
pub love: Hsla,
|
||||||
|
pub gold: Hsla,
|
||||||
|
pub rose: Hsla,
|
||||||
|
pub pine: Hsla,
|
||||||
|
pub foam: Hsla,
|
||||||
|
pub iris: Hsla,
|
||||||
|
pub highlight_low: Hsla,
|
||||||
|
pub highlight_med: Hsla,
|
||||||
|
pub highlight_high: Hsla,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RosePinePalette {
|
||||||
|
pub fn default() -> RosePinePalette {
|
||||||
|
RosePinePalette {
|
||||||
|
base: rgb(0x191724),
|
||||||
|
surface: rgb(0x1f1d2e),
|
||||||
|
overlay: rgb(0x26233a),
|
||||||
|
muted: rgb(0x6e6a86),
|
||||||
|
subtle: rgb(0x908caa),
|
||||||
|
text: rgb(0xe0def4),
|
||||||
|
love: rgb(0xeb6f92),
|
||||||
|
gold: rgb(0xf6c177),
|
||||||
|
rose: rgb(0xebbcba),
|
||||||
|
pine: rgb(0x31748f),
|
||||||
|
foam: rgb(0x9ccfd8),
|
||||||
|
iris: rgb(0xc4a7e7),
|
||||||
|
highlight_low: rgb(0x21202e),
|
||||||
|
highlight_med: rgb(0x403d52),
|
||||||
|
highlight_high: rgb(0x524f67),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn moon() -> RosePinePalette {
|
||||||
|
RosePinePalette {
|
||||||
|
base: rgb(0x232136),
|
||||||
|
surface: rgb(0x2a273f),
|
||||||
|
overlay: rgb(0x393552),
|
||||||
|
muted: rgb(0x6e6a86),
|
||||||
|
subtle: rgb(0x908caa),
|
||||||
|
text: rgb(0xe0def4),
|
||||||
|
love: rgb(0xeb6f92),
|
||||||
|
gold: rgb(0xf6c177),
|
||||||
|
rose: rgb(0xea9a97),
|
||||||
|
pine: rgb(0x3e8fb0),
|
||||||
|
foam: rgb(0x9ccfd8),
|
||||||
|
iris: rgb(0xc4a7e7),
|
||||||
|
highlight_low: rgb(0x2a283e),
|
||||||
|
highlight_med: rgb(0x44415a),
|
||||||
|
highlight_high: rgb(0x56526e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dawn() -> RosePinePalette {
|
||||||
|
RosePinePalette {
|
||||||
|
base: rgb(0xfaf4ed),
|
||||||
|
surface: rgb(0xfffaf3),
|
||||||
|
overlay: rgb(0xf2e9e1),
|
||||||
|
muted: rgb(0x9893a5),
|
||||||
|
subtle: rgb(0x797593),
|
||||||
|
text: rgb(0x575279),
|
||||||
|
love: rgb(0xb4637a),
|
||||||
|
gold: rgb(0xea9d34),
|
||||||
|
rose: rgb(0xd7827e),
|
||||||
|
pine: rgb(0x286983),
|
||||||
|
foam: rgb(0x56949f),
|
||||||
|
iris: rgb(0x907aa9),
|
||||||
|
highlight_low: rgb(0xf4ede8),
|
||||||
|
highlight_med: rgb(0xdfdad9),
|
||||||
|
highlight_high: rgb(0xcecacd),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default() -> ThemeColors {
|
||||||
|
theme_colors(&RosePinePalette::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn moon() -> ThemeColors {
|
||||||
|
theme_colors(&RosePinePalette::moon())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dawn() -> ThemeColors {
|
||||||
|
theme_colors(&RosePinePalette::dawn())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn theme_colors(p: &RosePinePalette) -> ThemeColors {
|
||||||
|
ThemeColors {
|
||||||
|
base: scale_sl(p.base, (0.8, 0.8), (1.2, 1.2)),
|
||||||
|
surface: scale_sl(p.surface, (0.8, 0.8), (1.2, 1.2)),
|
||||||
|
overlay: scale_sl(p.overlay, (0.8, 0.8), (1.2, 1.2)),
|
||||||
|
muted: scale_sl(p.muted, (0.8, 0.8), (1.2, 1.2)),
|
||||||
|
subtle: scale_sl(p.subtle, (0.8, 0.8), (1.2, 1.2)),
|
||||||
|
text: scale_sl(p.text, (0.8, 0.8), (1.2, 1.2)),
|
||||||
|
highlight_low: scale_sl(p.highlight_low, (0.8, 0.8), (1.2, 1.2)),
|
||||||
|
highlight_med: scale_sl(p.highlight_med, (0.8, 0.8), (1.2, 1.2)),
|
||||||
|
highlight_high: scale_sl(p.highlight_high, (0.8, 0.8), (1.2, 1.2)),
|
||||||
|
success: scale_sl(p.foam, (0.8, 0.8), (1.2, 1.2)),
|
||||||
|
warning: scale_sl(p.gold, (0.8, 0.8), (1.2, 1.2)),
|
||||||
|
error: scale_sl(p.love, (0.8, 0.8), (1.2, 1.2)),
|
||||||
|
inserted: scale_sl(p.foam, (0.8, 0.8), (1.2, 1.2)),
|
||||||
|
deleted: scale_sl(p.love, (0.8, 0.8), (1.2, 1.2)),
|
||||||
|
modified: scale_sl(p.rose, (0.8, 0.8), (1.2, 1.2)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Produces a range by multiplying the saturation and lightness of the base color by the given
|
||||||
|
/// start and end factors.
|
||||||
|
fn scale_sl(base: Hsla, (start_s, start_l): (f32, f32), (end_s, end_l): (f32, f32)) -> Range<Hsla> {
|
||||||
|
let start = hsla(base.h, base.s * start_s, base.l * start_l, base.a);
|
||||||
|
let end = hsla(base.h, base.s * end_s, base.l * end_l, base.a);
|
||||||
|
Range { start, end }
|
||||||
|
}
|
26
crates/gpui/playground/src/view.rs
Normal file
26
crates/gpui/playground/src/view.rs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
use crate::{
|
||||||
|
adapter::AdapterElement,
|
||||||
|
element::{AnyElement, Element},
|
||||||
|
};
|
||||||
|
use gpui::ViewContext;
|
||||||
|
|
||||||
|
pub fn view<F, E>(mut render: F) -> ViewFn
|
||||||
|
where
|
||||||
|
F: 'static + FnMut(&mut ViewContext<ViewFn>) -> E,
|
||||||
|
E: Element<ViewFn>,
|
||||||
|
{
|
||||||
|
ViewFn(Box::new(move |cx| (render)(cx).into_any()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ViewFn(Box<dyn FnMut(&mut ViewContext<ViewFn>) -> AnyElement<ViewFn>>);
|
||||||
|
|
||||||
|
impl gpui::Entity for ViewFn {
|
||||||
|
type Event = ();
|
||||||
|
}
|
||||||
|
|
||||||
|
impl gpui::View for ViewFn {
|
||||||
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> gpui::AnyElement<Self> {
|
||||||
|
use gpui::Element as _;
|
||||||
|
AdapterElement((self.0)(cx)).into_any()
|
||||||
|
}
|
||||||
|
}
|
14
crates/gpui/playground_macros/Cargo.toml
Normal file
14
crates/gpui/playground_macros/Cargo.toml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "playground_macros"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/playground_macros.rs"
|
||||||
|
proc-macro = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
syn = "1.0.72"
|
||||||
|
quote = "1.0.9"
|
||||||
|
proc-macro2 = "1.0.66"
|
91
crates/gpui/playground_macros/src/derive_element.rs
Normal file
91
crates/gpui/playground_macros/src/derive_element.rs
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
use proc_macro::TokenStream;
|
||||||
|
use proc_macro2::Ident;
|
||||||
|
use quote::quote;
|
||||||
|
use syn::{parse_macro_input, parse_quote, DeriveInput, GenericParam, Generics};
|
||||||
|
|
||||||
|
use crate::derive_into_element::impl_into_element;
|
||||||
|
|
||||||
|
pub fn derive_element(input: TokenStream) -> TokenStream {
|
||||||
|
let ast = parse_macro_input!(input as DeriveInput);
|
||||||
|
let type_name = ast.ident;
|
||||||
|
let placeholder_view_generics: Generics = parse_quote! { <V: 'static> };
|
||||||
|
|
||||||
|
let (impl_generics, type_generics, where_clause, view_type_name, lifetimes) =
|
||||||
|
if let Some(first_type_param) = ast.generics.params.iter().find_map(|param| {
|
||||||
|
if let GenericParam::Type(type_param) = param {
|
||||||
|
Some(type_param.ident.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
let mut lifetimes = vec![];
|
||||||
|
for param in ast.generics.params.iter() {
|
||||||
|
if let GenericParam::Lifetime(lifetime_def) = param {
|
||||||
|
lifetimes.push(lifetime_def.lifetime.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let generics = ast.generics.split_for_impl();
|
||||||
|
(
|
||||||
|
generics.0,
|
||||||
|
Some(generics.1),
|
||||||
|
generics.2,
|
||||||
|
first_type_param,
|
||||||
|
lifetimes,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
let generics = placeholder_view_generics.split_for_impl();
|
||||||
|
let placeholder_view_type_name: Ident = parse_quote! { V };
|
||||||
|
(
|
||||||
|
generics.0,
|
||||||
|
None,
|
||||||
|
generics.2,
|
||||||
|
placeholder_view_type_name,
|
||||||
|
vec![],
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let lifetimes = if !lifetimes.is_empty() {
|
||||||
|
quote! { <#(#lifetimes),*> }
|
||||||
|
} else {
|
||||||
|
quote! {}
|
||||||
|
};
|
||||||
|
|
||||||
|
let impl_into_element = impl_into_element(
|
||||||
|
&impl_generics,
|
||||||
|
&view_type_name,
|
||||||
|
&type_name,
|
||||||
|
&type_generics,
|
||||||
|
&where_clause,
|
||||||
|
);
|
||||||
|
|
||||||
|
let gen = quote! {
|
||||||
|
impl #impl_generics playground::element::Element<#view_type_name> for #type_name #type_generics
|
||||||
|
#where_clause
|
||||||
|
{
|
||||||
|
type Layout = Option<playground::element::AnyElement<#view_type_name #lifetimes>>;
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
view: &mut V,
|
||||||
|
cx: &mut playground::element::LayoutContext<V>,
|
||||||
|
) -> anyhow::Result<playground::element::Layout<V, Self::Layout>> {
|
||||||
|
let mut element = self.render(view, cx).into_any();
|
||||||
|
let layout_id = element.layout(view, cx)?;
|
||||||
|
Ok(playground::element::Layout::new(layout_id, Some(element)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(
|
||||||
|
&mut self,
|
||||||
|
view: &mut V,
|
||||||
|
layout: &mut playground::element::Layout<V, Self::Layout>,
|
||||||
|
cx: &mut playground::element::PaintContext<V>,
|
||||||
|
) {
|
||||||
|
layout.paint(view, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#impl_into_element
|
||||||
|
};
|
||||||
|
|
||||||
|
gen.into()
|
||||||
|
}
|
69
crates/gpui/playground_macros/src/derive_into_element.rs
Normal file
69
crates/gpui/playground_macros/src/derive_into_element.rs
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
use proc_macro::TokenStream;
|
||||||
|
use quote::quote;
|
||||||
|
use syn::{
|
||||||
|
parse_macro_input, parse_quote, DeriveInput, GenericParam, Generics, Ident, WhereClause,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn derive_into_element(input: TokenStream) -> TokenStream {
|
||||||
|
let ast = parse_macro_input!(input as DeriveInput);
|
||||||
|
let type_name = ast.ident;
|
||||||
|
|
||||||
|
let placeholder_view_generics: Generics = parse_quote! { <V: 'static> };
|
||||||
|
let placeholder_view_type_name: Ident = parse_quote! { V };
|
||||||
|
let view_type_name: Ident;
|
||||||
|
let impl_generics: syn::ImplGenerics<'_>;
|
||||||
|
let type_generics: Option<syn::TypeGenerics<'_>>;
|
||||||
|
let where_clause: Option<&'_ WhereClause>;
|
||||||
|
|
||||||
|
match ast.generics.params.iter().find_map(|param| {
|
||||||
|
if let GenericParam::Type(type_param) = param {
|
||||||
|
Some(type_param.ident.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Some(type_name) => {
|
||||||
|
view_type_name = type_name;
|
||||||
|
let generics = ast.generics.split_for_impl();
|
||||||
|
impl_generics = generics.0;
|
||||||
|
type_generics = Some(generics.1);
|
||||||
|
where_clause = generics.2;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
view_type_name = placeholder_view_type_name;
|
||||||
|
let generics = placeholder_view_generics.split_for_impl();
|
||||||
|
impl_generics = generics.0;
|
||||||
|
type_generics = None;
|
||||||
|
where_clause = generics.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_into_element(
|
||||||
|
&impl_generics,
|
||||||
|
&view_type_name,
|
||||||
|
&type_name,
|
||||||
|
&type_generics,
|
||||||
|
&where_clause,
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn impl_into_element(
|
||||||
|
impl_generics: &syn::ImplGenerics<'_>,
|
||||||
|
view_type_name: &Ident,
|
||||||
|
type_name: &Ident,
|
||||||
|
type_generics: &Option<syn::TypeGenerics<'_>>,
|
||||||
|
where_clause: &Option<&WhereClause>,
|
||||||
|
) -> proc_macro2::TokenStream {
|
||||||
|
quote! {
|
||||||
|
impl #impl_generics playground::element::IntoElement<#view_type_name> for #type_name #type_generics
|
||||||
|
#where_clause
|
||||||
|
{
|
||||||
|
type Element = Self;
|
||||||
|
|
||||||
|
fn into_element(self) -> Self {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
crates/gpui/playground_macros/src/playground_macros.rs
Normal file
26
crates/gpui/playground_macros/src/playground_macros.rs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
use proc_macro::TokenStream;
|
||||||
|
|
||||||
|
mod derive_element;
|
||||||
|
mod derive_into_element;
|
||||||
|
mod styleable_helpers;
|
||||||
|
mod tailwind_lengths;
|
||||||
|
|
||||||
|
#[proc_macro]
|
||||||
|
pub fn styleable_helpers(args: TokenStream) -> TokenStream {
|
||||||
|
styleable_helpers::styleable_helpers(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[proc_macro_derive(Element, attributes(element_crate))]
|
||||||
|
pub fn derive_element(input: TokenStream) -> TokenStream {
|
||||||
|
derive_element::derive_element(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[proc_macro_derive(IntoElement, attributes(element_crate))]
|
||||||
|
pub fn derive_into_element(input: TokenStream) -> TokenStream {
|
||||||
|
derive_into_element::derive_into_element(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[proc_macro_attribute]
|
||||||
|
pub fn tailwind_lengths(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
|
tailwind_lengths::tailwind_lengths(attr, item)
|
||||||
|
}
|
147
crates/gpui/playground_macros/src/styleable_helpers.rs
Normal file
147
crates/gpui/playground_macros/src/styleable_helpers.rs
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
use proc_macro::TokenStream;
|
||||||
|
use proc_macro2::TokenStream as TokenStream2;
|
||||||
|
use quote::{format_ident, quote};
|
||||||
|
use syn::{
|
||||||
|
parse::{Parse, ParseStream, Result},
|
||||||
|
parse_macro_input,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct StyleableMacroInput;
|
||||||
|
|
||||||
|
impl Parse for StyleableMacroInput {
|
||||||
|
fn parse(_input: ParseStream) -> Result<Self> {
|
||||||
|
Ok(StyleableMacroInput)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn styleable_helpers(input: TokenStream) -> TokenStream {
|
||||||
|
let _ = parse_macro_input!(input as StyleableMacroInput);
|
||||||
|
let methods = generate_methods();
|
||||||
|
let output = quote! {
|
||||||
|
#(#methods)*
|
||||||
|
};
|
||||||
|
output.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_methods() -> Vec<TokenStream2> {
|
||||||
|
let mut methods = Vec::new();
|
||||||
|
|
||||||
|
for (prefix, auto_allowed, fields) in tailwind_prefixes() {
|
||||||
|
for (suffix, length_tokens) in tailwind_lengths() {
|
||||||
|
if !auto_allowed && suffix == "auto" {
|
||||||
|
// Conditional to skip "auto"
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let method_name = format_ident!("{}_{}", prefix, suffix);
|
||||||
|
let field_assignments = fields
|
||||||
|
.iter()
|
||||||
|
.map(|field_tokens| {
|
||||||
|
quote! {
|
||||||
|
style.#field_tokens = Some(gpui::geometry::#length_tokens);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let method = quote! {
|
||||||
|
fn #method_name(mut self) -> Self where Self: std::marker::Sized {
|
||||||
|
let mut style = self.declared_style();
|
||||||
|
#(#field_assignments)*
|
||||||
|
self
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
methods.push(method);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
methods
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tailwind_lengths() -> Vec<(&'static str, TokenStream2)> {
|
||||||
|
vec![
|
||||||
|
("0", quote! { pixels(0.) }),
|
||||||
|
("1", quote! { rems(0.25) }),
|
||||||
|
("2", quote! { rems(0.5) }),
|
||||||
|
("3", quote! { rems(0.75) }),
|
||||||
|
("4", quote! { rems(1.) }),
|
||||||
|
("5", quote! { rems(1.25) }),
|
||||||
|
("6", quote! { rems(1.5) }),
|
||||||
|
("8", quote! { rems(2.0) }),
|
||||||
|
("10", quote! { rems(2.5) }),
|
||||||
|
("12", quote! { rems(3.) }),
|
||||||
|
("16", quote! { rems(4.) }),
|
||||||
|
("20", quote! { rems(5.) }),
|
||||||
|
("24", quote! { rems(6.) }),
|
||||||
|
("32", quote! { rems(8.) }),
|
||||||
|
("40", quote! { rems(10.) }),
|
||||||
|
("48", quote! { rems(12.) }),
|
||||||
|
("56", quote! { rems(14.) }),
|
||||||
|
("64", quote! { rems(16.) }),
|
||||||
|
("72", quote! { rems(18.) }),
|
||||||
|
("80", quote! { rems(20.) }),
|
||||||
|
("96", quote! { rems(24.) }),
|
||||||
|
("auto", quote! { auto() }),
|
||||||
|
("px", quote! { pixels(1.) }),
|
||||||
|
("full", quote! { relative(1.) }),
|
||||||
|
("1_2", quote! { relative(0.5) }),
|
||||||
|
("1_3", quote! { relative(1./3.) }),
|
||||||
|
("2_3", quote! { relative(2./3.) }),
|
||||||
|
("1_4", quote! { relative(0.25) }),
|
||||||
|
("2_4", quote! { relative(0.5) }),
|
||||||
|
("3_4", quote! { relative(0.75) }),
|
||||||
|
("1_5", quote! { relative(0.2) }),
|
||||||
|
("2_5", quote! { relative(0.4) }),
|
||||||
|
("3_5", quote! { relative(0.6) }),
|
||||||
|
("4_5", quote! { relative(0.8) }),
|
||||||
|
("1_6", quote! { relative(1./6.) }),
|
||||||
|
("5_6", quote! { relative(5./6.) }),
|
||||||
|
("1_12", quote! { relative(1./12.) }),
|
||||||
|
// ("screen_50", quote! { DefiniteLength::Vh(50.0) }),
|
||||||
|
// ("screen_75", quote! { DefiniteLength::Vh(75.0) }),
|
||||||
|
// ("screen", quote! { DefiniteLength::Vh(100.0) }),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tailwind_prefixes() -> Vec<(&'static str, bool, Vec<TokenStream2>)> {
|
||||||
|
vec![
|
||||||
|
("w", true, vec![quote! { size.width }]),
|
||||||
|
("h", true, vec![quote! { size.height }]),
|
||||||
|
("min_w", false, vec![quote! { min_size.width }]),
|
||||||
|
("min_h", false, vec![quote! { min_size.height }]),
|
||||||
|
("max_w", false, vec![quote! { max_size.width }]),
|
||||||
|
("max_h", false, vec![quote! { max_size.height }]),
|
||||||
|
(
|
||||||
|
"m",
|
||||||
|
true,
|
||||||
|
vec![quote! { margin.top }, quote! { margin.bottom }],
|
||||||
|
),
|
||||||
|
("mt", true, vec![quote! { margin.top }]),
|
||||||
|
("mb", true, vec![quote! { margin.bottom }]),
|
||||||
|
(
|
||||||
|
"mx",
|
||||||
|
true,
|
||||||
|
vec![quote! { margin.left }, quote! { margin.right }],
|
||||||
|
),
|
||||||
|
("ml", true, vec![quote! { margin.left }]),
|
||||||
|
("mr", true, vec![quote! { margin.right }]),
|
||||||
|
(
|
||||||
|
"p",
|
||||||
|
false,
|
||||||
|
vec![quote! { padding.top }, quote! { padding.bottom }],
|
||||||
|
),
|
||||||
|
("pt", false, vec![quote! { padding.top }]),
|
||||||
|
("pb", false, vec![quote! { padding.bottom }]),
|
||||||
|
(
|
||||||
|
"px",
|
||||||
|
false,
|
||||||
|
vec![quote! { padding.left }, quote! { padding.right }],
|
||||||
|
),
|
||||||
|
("pl", false, vec![quote! { padding.left }]),
|
||||||
|
("pr", false, vec![quote! { padding.right }]),
|
||||||
|
("top", true, vec![quote! { inset.top }]),
|
||||||
|
("bottom", true, vec![quote! { inset.bottom }]),
|
||||||
|
("left", true, vec![quote! { inset.left }]),
|
||||||
|
("right", true, vec![quote! { inset.right }]),
|
||||||
|
]
|
||||||
|
}
|
99
crates/gpui/playground_macros/src/tailwind_lengths.rs
Normal file
99
crates/gpui/playground_macros/src/tailwind_lengths.rs
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
use proc_macro::TokenStream;
|
||||||
|
use proc_macro2::TokenStream as TokenStream2;
|
||||||
|
use quote::{format_ident, quote};
|
||||||
|
use syn::{parse_macro_input, FnArg, ItemFn, PatType};
|
||||||
|
|
||||||
|
pub fn tailwind_lengths(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
|
let input_function = parse_macro_input!(item as ItemFn);
|
||||||
|
|
||||||
|
let visibility = &input_function.vis;
|
||||||
|
let function_signature = input_function.sig.clone();
|
||||||
|
let function_body = input_function.block;
|
||||||
|
let where_clause = &function_signature.generics.where_clause;
|
||||||
|
|
||||||
|
let argument_name = match function_signature.inputs.iter().nth(1) {
|
||||||
|
Some(FnArg::Typed(PatType { pat, .. })) => pat,
|
||||||
|
_ => panic!("Couldn't find the second argument in the function signature"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut output_functions = TokenStream2::new();
|
||||||
|
|
||||||
|
for (length, value) in fixed_lengths() {
|
||||||
|
let function_name = format_ident!("{}{}", function_signature.ident, length);
|
||||||
|
output_functions.extend(quote! {
|
||||||
|
#visibility fn #function_name(mut self) -> Self #where_clause {
|
||||||
|
let #argument_name = #value.into();
|
||||||
|
#function_body
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
output_functions.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fixed_lengths() -> Vec<(&'static str, TokenStream2)> {
|
||||||
|
vec![
|
||||||
|
("0", quote! { DefinedLength::Pixels(0.) }),
|
||||||
|
("px", quote! { DefinedLength::Pixels(1.) }),
|
||||||
|
("0_5", quote! { DefinedLength::Rems(0.125) }),
|
||||||
|
("1", quote! { DefinedLength::Rems(0.25) }),
|
||||||
|
("1_5", quote! { DefinedLength::Rems(0.375) }),
|
||||||
|
("2", quote! { DefinedLength::Rems(0.5) }),
|
||||||
|
("2_5", quote! { DefinedLength::Rems(0.625) }),
|
||||||
|
("3", quote! { DefinedLength::Rems(0.75) }),
|
||||||
|
("3_5", quote! { DefinedLength::Rems(0.875) }),
|
||||||
|
("4", quote! { DefinedLength::Rems(1.) }),
|
||||||
|
("5", quote! { DefinedLength::Rems(1.25) }),
|
||||||
|
("6", quote! { DefinedLength::Rems(1.5) }),
|
||||||
|
("7", quote! { DefinedLength::Rems(1.75) }),
|
||||||
|
("8", quote! { DefinedLength::Rems(2.) }),
|
||||||
|
("9", quote! { DefinedLength::Rems(2.25) }),
|
||||||
|
("10", quote! { DefinedLength::Rems(2.5) }),
|
||||||
|
("11", quote! { DefinedLength::Rems(2.75) }),
|
||||||
|
("12", quote! { DefinedLength::Rems(3.) }),
|
||||||
|
("14", quote! { DefinedLength::Rems(3.5) }),
|
||||||
|
("16", quote! { DefinedLength::Rems(4.) }),
|
||||||
|
("20", quote! { DefinedLength::Rems(5.) }),
|
||||||
|
("24", quote! { DefinedLength::Rems(6.) }),
|
||||||
|
("28", quote! { DefinedLength::Rems(7.) }),
|
||||||
|
("32", quote! { DefinedLength::Rems(8.) }),
|
||||||
|
("36", quote! { DefinedLength::Rems(9.) }),
|
||||||
|
("40", quote! { DefinedLength::Rems(10.) }),
|
||||||
|
("44", quote! { DefinedLength::Rems(11.) }),
|
||||||
|
("48", quote! { DefinedLength::Rems(12.) }),
|
||||||
|
("52", quote! { DefinedLength::Rems(13.) }),
|
||||||
|
("56", quote! { DefinedLength::Rems(14.) }),
|
||||||
|
("60", quote! { DefinedLength::Rems(15.) }),
|
||||||
|
("64", quote! { DefinedLength::Rems(16.) }),
|
||||||
|
("72", quote! { DefinedLength::Rems(18.) }),
|
||||||
|
("80", quote! { DefinedLength::Rems(20.) }),
|
||||||
|
("96", quote! { DefinedLength::Rems(24.) }),
|
||||||
|
("half", quote! { DefinedLength::Percent(50.) }),
|
||||||
|
("1_3rd", quote! { DefinedLength::Percent(33.333333) }),
|
||||||
|
("2_3rd", quote! { DefinedLength::Percent(66.666667) }),
|
||||||
|
("1_4th", quote! { DefinedLength::Percent(25.) }),
|
||||||
|
("2_4th", quote! { DefinedLength::Percent(50.) }),
|
||||||
|
("3_4th", quote! { DefinedLength::Percent(75.) }),
|
||||||
|
("1_5th", quote! { DefinedLength::Percent(20.) }),
|
||||||
|
("2_5th", quote! { DefinedLength::Percent(40.) }),
|
||||||
|
("3_5th", quote! { DefinedLength::Percent(60.) }),
|
||||||
|
("4_5th", quote! { DefinedLength::Percent(80.) }),
|
||||||
|
("1_6th", quote! { DefinedLength::Percent(16.666667) }),
|
||||||
|
("2_6th", quote! { DefinedLength::Percent(33.333333) }),
|
||||||
|
("3_6th", quote! { DefinedLength::Percent(50.) }),
|
||||||
|
("4_6th", quote! { DefinedLength::Percent(66.666667) }),
|
||||||
|
("5_6th", quote! { DefinedLength::Percent(83.333333) }),
|
||||||
|
("1_12th", quote! { DefinedLength::Percent(8.333333) }),
|
||||||
|
("2_12th", quote! { DefinedLength::Percent(16.666667) }),
|
||||||
|
("3_12th", quote! { DefinedLength::Percent(25.) }),
|
||||||
|
("4_12th", quote! { DefinedLength::Percent(33.333333) }),
|
||||||
|
("5_12th", quote! { DefinedLength::Percent(41.666667) }),
|
||||||
|
("6_12th", quote! { DefinedLength::Percent(50.) }),
|
||||||
|
("7_12th", quote! { DefinedLength::Percent(58.333333) }),
|
||||||
|
("8_12th", quote! { DefinedLength::Percent(66.666667) }),
|
||||||
|
("9_12th", quote! { DefinedLength::Percent(75.) }),
|
||||||
|
("10_12th", quote! { DefinedLength::Percent(83.333333) }),
|
||||||
|
("11_12th", quote! { DefinedLength::Percent(91.666667) }),
|
||||||
|
("full", quote! { DefinedLength::Percent(100.) }),
|
||||||
|
]
|
||||||
|
}
|
@ -7,42 +7,6 @@ pub mod test_app_context;
|
|||||||
pub(crate) mod window;
|
pub(crate) mod window;
|
||||||
mod window_input_handler;
|
mod window_input_handler;
|
||||||
|
|
||||||
use std::{
|
|
||||||
any::{type_name, Any, TypeId},
|
|
||||||
cell::RefCell,
|
|
||||||
fmt::{self, Debug},
|
|
||||||
hash::{Hash, Hasher},
|
|
||||||
marker::PhantomData,
|
|
||||||
mem,
|
|
||||||
ops::{Deref, DerefMut, Range},
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
pin::Pin,
|
|
||||||
rc::{self, Rc},
|
|
||||||
sync::{Arc, Weak},
|
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
|
|
||||||
use anyhow::{anyhow, Context, Result};
|
|
||||||
|
|
||||||
use derive_more::Deref;
|
|
||||||
use parking_lot::Mutex;
|
|
||||||
use postage::oneshot;
|
|
||||||
use smallvec::SmallVec;
|
|
||||||
use smol::prelude::*;
|
|
||||||
use util::ResultExt;
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
pub use action::*;
|
|
||||||
use callback_collection::CallbackCollection;
|
|
||||||
use collections::{hash_map::Entry, BTreeMap, HashMap, HashSet, VecDeque};
|
|
||||||
pub use menu::*;
|
|
||||||
use platform::Event;
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
use ref_counts::LeakDetector;
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
pub use test_app_context::{ContextHandle, TestAppContext};
|
|
||||||
use window_input_handler::WindowInputHandler;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
elements::{AnyElement, AnyRootElement, RootElement},
|
elements::{AnyElement, AnyRootElement, RootElement},
|
||||||
executor::{self, Task},
|
executor::{self, Task},
|
||||||
@ -57,8 +21,39 @@ use crate::{
|
|||||||
window::{Window, WindowContext},
|
window::{Window, WindowContext},
|
||||||
AssetCache, AssetSource, ClipboardItem, FontCache, MouseRegionId,
|
AssetCache, AssetSource, ClipboardItem, FontCache, MouseRegionId,
|
||||||
};
|
};
|
||||||
|
pub use action::*;
|
||||||
use self::ref_counts::RefCounts;
|
use anyhow::{anyhow, Context, Result};
|
||||||
|
use callback_collection::CallbackCollection;
|
||||||
|
use collections::{hash_map::Entry, BTreeMap, HashMap, HashSet, VecDeque};
|
||||||
|
use derive_more::Deref;
|
||||||
|
pub use menu::*;
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use platform::Event;
|
||||||
|
use postage::oneshot;
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
use ref_counts::LeakDetector;
|
||||||
|
use ref_counts::RefCounts;
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
use smol::prelude::*;
|
||||||
|
use std::{
|
||||||
|
any::{type_name, Any, TypeId},
|
||||||
|
cell::RefCell,
|
||||||
|
fmt::{self, Debug},
|
||||||
|
hash::{Hash, Hasher},
|
||||||
|
marker::PhantomData,
|
||||||
|
mem,
|
||||||
|
ops::{Deref, DerefMut, Range},
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
pin::Pin,
|
||||||
|
rc::{self, Rc},
|
||||||
|
sync::{Arc, Weak},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub use test_app_context::{ContextHandle, TestAppContext};
|
||||||
|
use util::ResultExt;
|
||||||
|
use uuid::Uuid;
|
||||||
|
use window_input_handler::WindowInputHandler;
|
||||||
|
|
||||||
pub trait Entity: 'static {
|
pub trait Entity: 'static {
|
||||||
type Event;
|
type Event;
|
||||||
@ -73,10 +68,12 @@ pub trait Entity: 'static {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub trait View: Entity + Sized {
|
pub trait View: Entity + Sized {
|
||||||
fn ui_name() -> &'static str;
|
|
||||||
fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> AnyElement<Self>;
|
fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> AnyElement<Self>;
|
||||||
fn focus_in(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {}
|
fn focus_in(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {}
|
||||||
fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {}
|
fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {}
|
||||||
|
fn ui_name() -> &'static str {
|
||||||
|
type_name::<Self>()
|
||||||
|
}
|
||||||
fn key_down(&mut self, _: &KeyDownEvent, _: &mut ViewContext<Self>) -> bool {
|
fn key_down(&mut self, _: &KeyDownEvent, _: &mut ViewContext<Self>) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
@ -640,7 +637,7 @@ impl AppContext {
|
|||||||
pub fn add_action<A, V, F, R>(&mut self, handler: F)
|
pub fn add_action<A, V, F, R>(&mut self, handler: F)
|
||||||
where
|
where
|
||||||
A: Action,
|
A: Action,
|
||||||
V: View,
|
V: 'static,
|
||||||
F: 'static + FnMut(&mut V, &A, &mut ViewContext<V>) -> R,
|
F: 'static + FnMut(&mut V, &A, &mut ViewContext<V>) -> R,
|
||||||
{
|
{
|
||||||
self.add_action_internal(handler, false)
|
self.add_action_internal(handler, false)
|
||||||
@ -649,7 +646,7 @@ impl AppContext {
|
|||||||
pub fn capture_action<A, V, F>(&mut self, handler: F)
|
pub fn capture_action<A, V, F>(&mut self, handler: F)
|
||||||
where
|
where
|
||||||
A: Action,
|
A: Action,
|
||||||
V: View,
|
V: 'static,
|
||||||
F: 'static + FnMut(&mut V, &A, &mut ViewContext<V>),
|
F: 'static + FnMut(&mut V, &A, &mut ViewContext<V>),
|
||||||
{
|
{
|
||||||
self.add_action_internal(handler, true)
|
self.add_action_internal(handler, true)
|
||||||
@ -658,7 +655,7 @@ impl AppContext {
|
|||||||
fn add_action_internal<A, V, F, R>(&mut self, mut handler: F, capture: bool)
|
fn add_action_internal<A, V, F, R>(&mut self, mut handler: F, capture: bool)
|
||||||
where
|
where
|
||||||
A: Action,
|
A: Action,
|
||||||
V: View,
|
V: 'static,
|
||||||
F: 'static + FnMut(&mut V, &A, &mut ViewContext<V>) -> R,
|
F: 'static + FnMut(&mut V, &A, &mut ViewContext<V>) -> R,
|
||||||
{
|
{
|
||||||
let handler = Box::new(
|
let handler = Box::new(
|
||||||
@ -699,7 +696,7 @@ impl AppContext {
|
|||||||
pub fn add_async_action<A, V, F>(&mut self, mut handler: F)
|
pub fn add_async_action<A, V, F>(&mut self, mut handler: F)
|
||||||
where
|
where
|
||||||
A: Action,
|
A: Action,
|
||||||
V: View,
|
V: 'static,
|
||||||
F: 'static + FnMut(&mut V, &A, &mut ViewContext<V>) -> Option<Task<Result<()>>>,
|
F: 'static + FnMut(&mut V, &A, &mut ViewContext<V>) -> Option<Task<Result<()>>>,
|
||||||
{
|
{
|
||||||
self.add_action(move |view, action, cx| {
|
self.add_action(move |view, action, cx| {
|
||||||
@ -898,8 +895,8 @@ impl AppContext {
|
|||||||
|
|
||||||
fn observe_focus<F, V>(&mut self, handle: &ViewHandle<V>, mut callback: F) -> Subscription
|
fn observe_focus<F, V>(&mut self, handle: &ViewHandle<V>, mut callback: F) -> Subscription
|
||||||
where
|
where
|
||||||
|
V: 'static,
|
||||||
F: 'static + FnMut(ViewHandle<V>, bool, &mut WindowContext) -> bool,
|
F: 'static + FnMut(ViewHandle<V>, bool, &mut WindowContext) -> bool,
|
||||||
V: View,
|
|
||||||
{
|
{
|
||||||
let subscription_id = post_inc(&mut self.next_subscription_id);
|
let subscription_id = post_inc(&mut self.next_subscription_id);
|
||||||
let observed = handle.downgrade();
|
let observed = handle.downgrade();
|
||||||
@ -1382,15 +1379,15 @@ impl AppContext {
|
|||||||
self.windows.keys().copied()
|
self.windows.keys().copied()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_view<T: View>(&self, handle: &ViewHandle<T>) -> &T {
|
pub fn read_view<V: 'static>(&self, handle: &ViewHandle<V>) -> &V {
|
||||||
if let Some(view) = self.views.get(&(handle.window, handle.view_id)) {
|
if let Some(view) = self.views.get(&(handle.window, handle.view_id)) {
|
||||||
view.as_any().downcast_ref().expect("downcast is type safe")
|
view.as_any().downcast_ref().expect("downcast is type safe")
|
||||||
} else {
|
} else {
|
||||||
panic!("circular view reference for type {}", type_name::<T>());
|
panic!("circular view reference for type {}", type_name::<V>());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn upgrade_view_handle<T: View>(&self, handle: &WeakViewHandle<T>) -> Option<ViewHandle<T>> {
|
fn upgrade_view_handle<V: 'static>(&self, handle: &WeakViewHandle<V>) -> Option<ViewHandle<V>> {
|
||||||
if self.ref_counts.lock().is_entity_alive(handle.view_id) {
|
if self.ref_counts.lock().is_entity_alive(handle.view_id) {
|
||||||
Some(ViewHandle::new(
|
Some(ViewHandle::new(
|
||||||
handle.window,
|
handle.window,
|
||||||
@ -1659,6 +1656,9 @@ impl AppContext {
|
|||||||
subscription_id,
|
subscription_id,
|
||||||
callback,
|
callback,
|
||||||
),
|
),
|
||||||
|
Effect::RepaintWindow { window } => {
|
||||||
|
self.handle_repaint_window_effect(window)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.pending_notifications.clear();
|
self.pending_notifications.clear();
|
||||||
} else {
|
} else {
|
||||||
@ -1896,6 +1896,15 @@ impl AppContext {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_repaint_window_effect(&mut self, window: AnyWindowHandle) {
|
||||||
|
self.update_window(window, |cx| {
|
||||||
|
cx.layout(false).log_err();
|
||||||
|
if let Some(scene) = cx.paint().log_err() {
|
||||||
|
cx.window.platform_window.present_scene(scene);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_window_activation_effect(&mut self, window: AnyWindowHandle, active: bool) -> bool {
|
fn handle_window_activation_effect(&mut self, window: AnyWindowHandle, active: bool) -> bool {
|
||||||
self.update_window(window, |cx| {
|
self.update_window(window, |cx| {
|
||||||
if cx.window.is_active == active {
|
if cx.window.is_active == active {
|
||||||
@ -2151,7 +2160,7 @@ struct ViewMetadata {
|
|||||||
keymap_context: KeymapContext,
|
keymap_context: KeymapContext,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
#[derive(Default, Clone, Debug)]
|
||||||
pub struct WindowInvalidation {
|
pub struct WindowInvalidation {
|
||||||
pub updated: HashSet<usize>,
|
pub updated: HashSet<usize>,
|
||||||
pub removed: Vec<usize>,
|
pub removed: Vec<usize>,
|
||||||
@ -2255,6 +2264,9 @@ pub enum Effect {
|
|||||||
window: AnyWindowHandle,
|
window: AnyWindowHandle,
|
||||||
is_active: bool,
|
is_active: bool,
|
||||||
},
|
},
|
||||||
|
RepaintWindow {
|
||||||
|
window: AnyWindowHandle,
|
||||||
|
},
|
||||||
WindowActivationObservation {
|
WindowActivationObservation {
|
||||||
window: AnyWindowHandle,
|
window: AnyWindowHandle,
|
||||||
subscription_id: usize,
|
subscription_id: usize,
|
||||||
@ -2448,6 +2460,10 @@ impl Debug for Effect {
|
|||||||
.debug_struct("Effect::ActiveLabeledTasksObservation")
|
.debug_struct("Effect::ActiveLabeledTasksObservation")
|
||||||
.field("subscription_id", subscription_id)
|
.field("subscription_id", subscription_id)
|
||||||
.finish(),
|
.finish(),
|
||||||
|
Effect::RepaintWindow { window } => f
|
||||||
|
.debug_struct("Effect::RepaintWindow")
|
||||||
|
.field("window_id", &window.id())
|
||||||
|
.finish(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2543,10 +2559,7 @@ pub trait AnyView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V> AnyView for V
|
impl<V: View> AnyView for V {
|
||||||
where
|
|
||||||
V: View,
|
|
||||||
{
|
|
||||||
fn as_any(&self) -> &dyn Any {
|
fn as_any(&self) -> &dyn Any {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@ -2878,7 +2891,7 @@ pub struct ViewContext<'a, 'b, T: ?Sized> {
|
|||||||
view_type: PhantomData<T>,
|
view_type: PhantomData<T>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'b, T: View> Deref for ViewContext<'a, 'b, T> {
|
impl<'a, 'b, V> Deref for ViewContext<'a, 'b, V> {
|
||||||
type Target = WindowContext<'a>;
|
type Target = WindowContext<'a>;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
@ -2886,14 +2899,14 @@ impl<'a, 'b, T: View> Deref for ViewContext<'a, 'b, T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: View> DerefMut for ViewContext<'_, '_, T> {
|
impl<'a, 'b, V> DerefMut for ViewContext<'a, 'b, V> {
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
&mut self.window_context
|
&mut self.window_context
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
|
impl<'a, 'b, V: 'static> ViewContext<'a, 'b, V> {
|
||||||
pub(crate) fn mutable(window_context: &'b mut WindowContext<'a>, view_id: usize) -> Self {
|
pub fn mutable(window_context: &'b mut WindowContext<'a>, view_id: usize) -> Self {
|
||||||
Self {
|
Self {
|
||||||
window_context: Reference::Mutable(window_context),
|
window_context: Reference::Mutable(window_context),
|
||||||
view_id,
|
view_id,
|
||||||
@ -2901,7 +2914,7 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn immutable(window_context: &'b WindowContext<'a>, view_id: usize) -> Self {
|
pub fn immutable(window_context: &'b WindowContext<'a>, view_id: usize) -> Self {
|
||||||
Self {
|
Self {
|
||||||
window_context: Reference::Immutable(window_context),
|
window_context: Reference::Immutable(window_context),
|
||||||
view_id,
|
view_id,
|
||||||
@ -2913,6 +2926,12 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
|
|||||||
&mut self.window_context
|
&mut self.window_context
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn notify(&mut self) {
|
||||||
|
let window = self.window_handle;
|
||||||
|
let view_id = self.view_id;
|
||||||
|
self.window_context.notify_view(window, view_id);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn handle(&self) -> ViewHandle<V> {
|
pub fn handle(&self) -> ViewHandle<V> {
|
||||||
ViewHandle::new(
|
ViewHandle::new(
|
||||||
self.window_handle,
|
self.window_handle,
|
||||||
@ -3226,21 +3245,6 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn emit(&mut self, payload: V::Event) {
|
|
||||||
self.window_context
|
|
||||||
.pending_effects
|
|
||||||
.push_back(Effect::Event {
|
|
||||||
entity_id: self.view_id,
|
|
||||||
payload: Box::new(payload),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn notify(&mut self) {
|
|
||||||
let window = self.window_handle;
|
|
||||||
let view_id = self.view_id;
|
|
||||||
self.window_context.notify_view(window, view_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn defer(&mut self, callback: impl 'static + FnOnce(&mut V, &mut ViewContext<V>)) {
|
pub fn defer(&mut self, callback: impl 'static + FnOnce(&mut V, &mut ViewContext<V>)) {
|
||||||
let handle = self.handle();
|
let handle = self.handle();
|
||||||
self.window_context
|
self.window_context
|
||||||
@ -3295,15 +3299,15 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
|
|||||||
let region_id = MouseRegionId::new(tag, self.view_id, region_id);
|
let region_id = MouseRegionId::new(tag, self.view_id, region_id);
|
||||||
MouseState {
|
MouseState {
|
||||||
hovered: self.window.hovered_region_ids.contains(®ion_id),
|
hovered: self.window.hovered_region_ids.contains(®ion_id),
|
||||||
clicked: if let Some((clicked_region_id, button)) = self.window.clicked_region {
|
mouse_down: !self.window.clicked_region_ids.is_empty(),
|
||||||
if region_id == clicked_region_id {
|
clicked: self
|
||||||
Some(button)
|
.window
|
||||||
} else {
|
.clicked_region_ids
|
||||||
None
|
.iter()
|
||||||
}
|
.find(|click_region_id| **click_region_id == region_id)
|
||||||
} else {
|
// If we've gotten here, there should always be a clicked region.
|
||||||
None
|
// But let's be defensive and return None if there isn't.
|
||||||
},
|
.and_then(|_| self.window.clicked_region.map(|(_, button)| button)),
|
||||||
accessed_hovered: false,
|
accessed_hovered: false,
|
||||||
accessed_clicked: false,
|
accessed_clicked: false,
|
||||||
}
|
}
|
||||||
@ -3341,6 +3345,10 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
|
|||||||
self.element_state::<Tag, T>(element_id, T::default())
|
self.element_state::<Tag, T>(element_id, T::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn rem_pixels(&self) -> f32 {
|
||||||
|
16.
|
||||||
|
}
|
||||||
|
|
||||||
pub fn default_element_state_dynamic<T: 'static + Default>(
|
pub fn default_element_state_dynamic<T: 'static + Default>(
|
||||||
&mut self,
|
&mut self,
|
||||||
tag: TypeTag,
|
tag: TypeTag,
|
||||||
@ -3350,6 +3358,17 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<V: View> ViewContext<'_, '_, V> {
|
||||||
|
pub fn emit(&mut self, event: V::Event) {
|
||||||
|
self.window_context
|
||||||
|
.pending_effects
|
||||||
|
.push_back(Effect::Event {
|
||||||
|
entity_id: self.view_id,
|
||||||
|
payload: Box::new(event),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
pub struct TypeTag {
|
pub struct TypeTag {
|
||||||
tag: TypeId,
|
tag: TypeId,
|
||||||
@ -3428,15 +3447,27 @@ impl<V> BorrowWindowContext for ViewContext<'_, '_, V> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct LayoutContext<'a, 'b, 'c, V: View> {
|
/// Methods shared by both LayoutContext and PaintContext
|
||||||
view_context: &'c mut ViewContext<'a, 'b, V>,
|
///
|
||||||
|
/// It's that PaintContext should be implemented in terms of layout context and
|
||||||
|
/// deref to it, in which case we wouldn't need this.
|
||||||
|
pub trait RenderContext<'a, 'b, V> {
|
||||||
|
fn text_style(&self) -> TextStyle;
|
||||||
|
fn push_text_style(&mut self, style: TextStyle);
|
||||||
|
fn pop_text_style(&mut self);
|
||||||
|
fn as_view_context(&mut self) -> &mut ViewContext<'a, 'b, V>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LayoutContext<'a, 'b, 'c, V> {
|
||||||
|
// Nathan: Making this is public while I work on playground.
|
||||||
|
pub view_context: &'c mut ViewContext<'a, 'b, V>,
|
||||||
new_parents: &'c mut HashMap<usize, usize>,
|
new_parents: &'c mut HashMap<usize, usize>,
|
||||||
views_to_notify_if_ancestors_change: &'c mut HashMap<usize, SmallVec<[usize; 2]>>,
|
views_to_notify_if_ancestors_change: &'c mut HashMap<usize, SmallVec<[usize; 2]>>,
|
||||||
text_style_stack: Vec<Arc<TextStyle>>,
|
text_style_stack: Vec<TextStyle>,
|
||||||
pub refreshing: bool,
|
pub refreshing: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'b, 'c, V: View> LayoutContext<'a, 'b, 'c, V> {
|
impl<'a, 'b, 'c, V> LayoutContext<'a, 'b, 'c, V> {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
view_context: &'c mut ViewContext<'a, 'b, V>,
|
view_context: &'c mut ViewContext<'a, 'b, V>,
|
||||||
new_parents: &'c mut HashMap<usize, usize>,
|
new_parents: &'c mut HashMap<usize, usize>,
|
||||||
@ -3500,26 +3531,39 @@ impl<'a, 'b, 'c, V: View> LayoutContext<'a, 'b, 'c, V> {
|
|||||||
.push(self_view_id);
|
.push(self_view_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn text_style(&self) -> Arc<TextStyle> {
|
pub fn with_text_style<F, T>(&mut self, style: TextStyle, f: F) -> T
|
||||||
self.text_style_stack
|
|
||||||
.last()
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or(Default::default())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_text_style<S, F, T>(&mut self, style: S, f: F) -> T
|
|
||||||
where
|
where
|
||||||
S: Into<Arc<TextStyle>>,
|
|
||||||
F: FnOnce(&mut Self) -> T,
|
F: FnOnce(&mut Self) -> T,
|
||||||
{
|
{
|
||||||
self.text_style_stack.push(style.into());
|
self.push_text_style(style);
|
||||||
let result = f(self);
|
let result = f(self);
|
||||||
self.text_style_stack.pop();
|
self.pop_text_style();
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'b, 'c, V: View> Deref for LayoutContext<'a, 'b, 'c, V> {
|
impl<'a, 'b, 'c, V> RenderContext<'a, 'b, V> for LayoutContext<'a, 'b, 'c, V> {
|
||||||
|
fn text_style(&self) -> TextStyle {
|
||||||
|
self.text_style_stack
|
||||||
|
.last()
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or(TextStyle::default(&self.font_cache))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_text_style(&mut self, style: TextStyle) {
|
||||||
|
self.text_style_stack.push(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pop_text_style(&mut self) {
|
||||||
|
self.text_style_stack.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_view_context(&mut self) -> &mut ViewContext<'a, 'b, V> {
|
||||||
|
&mut self.view_context
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b, 'c, V> Deref for LayoutContext<'a, 'b, 'c, V> {
|
||||||
type Target = ViewContext<'a, 'b, V>;
|
type Target = ViewContext<'a, 'b, V>;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
@ -3527,13 +3571,13 @@ impl<'a, 'b, 'c, V: View> Deref for LayoutContext<'a, 'b, 'c, V> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> DerefMut for LayoutContext<'_, '_, '_, V> {
|
impl<V> DerefMut for LayoutContext<'_, '_, '_, V> {
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
&mut self.view_context
|
&mut self.view_context
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> BorrowAppContext for LayoutContext<'_, '_, '_, V> {
|
impl<V> BorrowAppContext for LayoutContext<'_, '_, '_, V> {
|
||||||
fn read_with<T, F: FnOnce(&AppContext) -> T>(&self, f: F) -> T {
|
fn read_with<T, F: FnOnce(&AppContext) -> T>(&self, f: F) -> T {
|
||||||
BorrowAppContext::read_with(&*self.view_context, f)
|
BorrowAppContext::read_with(&*self.view_context, f)
|
||||||
}
|
}
|
||||||
@ -3543,7 +3587,7 @@ impl<V: View> BorrowAppContext for LayoutContext<'_, '_, '_, V> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> BorrowWindowContext for LayoutContext<'_, '_, '_, V> {
|
impl<V> BorrowWindowContext for LayoutContext<'_, '_, '_, V> {
|
||||||
type Result<T> = T;
|
type Result<T> = T;
|
||||||
|
|
||||||
fn read_window<T, F: FnOnce(&WindowContext) -> T>(&self, window: AnyWindowHandle, f: F) -> T {
|
fn read_window<T, F: FnOnce(&WindowContext) -> T>(&self, window: AnyWindowHandle, f: F) -> T {
|
||||||
@ -3573,39 +3617,42 @@ impl<V: View> BorrowWindowContext for LayoutContext<'_, '_, '_, V> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PaintContext<'a, 'b, 'c, V: View> {
|
pub struct PaintContext<'a, 'b, 'c, V> {
|
||||||
view_context: &'c mut ViewContext<'a, 'b, V>,
|
pub view_context: &'c mut ViewContext<'a, 'b, V>,
|
||||||
text_style_stack: Vec<Arc<TextStyle>>,
|
text_style_stack: Vec<TextStyle>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'b, 'c, V: View> PaintContext<'a, 'b, 'c, V> {
|
impl<'a, 'b, 'c, V> PaintContext<'a, 'b, 'c, V> {
|
||||||
pub fn new(view_context: &'c mut ViewContext<'a, 'b, V>) -> Self {
|
pub fn new(view_context: &'c mut ViewContext<'a, 'b, V>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
view_context,
|
view_context,
|
||||||
text_style_stack: Vec::new(),
|
text_style_stack: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn text_style(&self) -> Arc<TextStyle> {
|
impl<'a, 'b, 'c, V> RenderContext<'a, 'b, V> for PaintContext<'a, 'b, 'c, V> {
|
||||||
|
fn text_style(&self) -> TextStyle {
|
||||||
self.text_style_stack
|
self.text_style_stack
|
||||||
.last()
|
.last()
|
||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or(Default::default())
|
.unwrap_or(TextStyle::default(&self.font_cache))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_text_style<S, F, T>(&mut self, style: S, f: F) -> T
|
fn push_text_style(&mut self, style: TextStyle) {
|
||||||
where
|
self.text_style_stack.push(style);
|
||||||
S: Into<Arc<TextStyle>>,
|
}
|
||||||
F: FnOnce(&mut Self) -> T,
|
|
||||||
{
|
fn pop_text_style(&mut self) {
|
||||||
self.text_style_stack.push(style.into());
|
|
||||||
let result = f(self);
|
|
||||||
self.text_style_stack.pop();
|
self.text_style_stack.pop();
|
||||||
result
|
}
|
||||||
|
|
||||||
|
fn as_view_context(&mut self) -> &mut ViewContext<'a, 'b, V> {
|
||||||
|
&mut self.view_context
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'b, 'c, V: View> Deref for PaintContext<'a, 'b, 'c, V> {
|
impl<'a, 'b, 'c, V> Deref for PaintContext<'a, 'b, 'c, V> {
|
||||||
type Target = ViewContext<'a, 'b, V>;
|
type Target = ViewContext<'a, 'b, V>;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
@ -3613,13 +3660,13 @@ impl<'a, 'b, 'c, V: View> Deref for PaintContext<'a, 'b, 'c, V> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> DerefMut for PaintContext<'_, '_, '_, V> {
|
impl<V> DerefMut for PaintContext<'_, '_, '_, V> {
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
&mut self.view_context
|
&mut self.view_context
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> BorrowAppContext for PaintContext<'_, '_, '_, V> {
|
impl<V> BorrowAppContext for PaintContext<'_, '_, '_, V> {
|
||||||
fn read_with<T, F: FnOnce(&AppContext) -> T>(&self, f: F) -> T {
|
fn read_with<T, F: FnOnce(&AppContext) -> T>(&self, f: F) -> T {
|
||||||
BorrowAppContext::read_with(&*self.view_context, f)
|
BorrowAppContext::read_with(&*self.view_context, f)
|
||||||
}
|
}
|
||||||
@ -3629,7 +3676,7 @@ impl<V: View> BorrowAppContext for PaintContext<'_, '_, '_, V> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> BorrowWindowContext for PaintContext<'_, '_, '_, V> {
|
impl<V> BorrowWindowContext for PaintContext<'_, '_, '_, V> {
|
||||||
type Result<T> = T;
|
type Result<T> = T;
|
||||||
|
|
||||||
fn read_window<T, F>(&self, window: AnyWindowHandle, f: F) -> Self::Result<T>
|
fn read_window<T, F>(&self, window: AnyWindowHandle, f: F) -> Self::Result<T>
|
||||||
@ -3661,25 +3708,37 @@ impl<V: View> BorrowWindowContext for PaintContext<'_, '_, '_, V> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct EventContext<'a, 'b, 'c, V: View> {
|
pub struct EventContext<'a, 'b, 'c, V> {
|
||||||
view_context: &'c mut ViewContext<'a, 'b, V>,
|
view_context: &'c mut ViewContext<'a, 'b, V>,
|
||||||
pub(crate) handled: bool,
|
pub(crate) handled: bool,
|
||||||
|
// I would like to replace handled with this.
|
||||||
|
// Being additive for now.
|
||||||
|
pub bubble: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'b, 'c, V: View> EventContext<'a, 'b, 'c, V> {
|
impl<'a, 'b, 'c, V: 'static> EventContext<'a, 'b, 'c, V> {
|
||||||
pub(crate) fn new(view_context: &'c mut ViewContext<'a, 'b, V>) -> Self {
|
pub fn new(view_context: &'c mut ViewContext<'a, 'b, V>) -> Self {
|
||||||
EventContext {
|
EventContext {
|
||||||
view_context,
|
view_context,
|
||||||
handled: true,
|
handled: true,
|
||||||
|
bubble: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn propagate_event(&mut self) {
|
pub fn propagate_event(&mut self) {
|
||||||
self.handled = false;
|
self.handled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn bubble_event(&mut self) {
|
||||||
|
self.bubble = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'b, 'c, V: View> Deref for EventContext<'a, 'b, 'c, V> {
|
pub fn event_bubbled(&self) -> bool {
|
||||||
|
self.bubble
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b, 'c, V> Deref for EventContext<'a, 'b, 'c, V> {
|
||||||
type Target = ViewContext<'a, 'b, V>;
|
type Target = ViewContext<'a, 'b, V>;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
@ -3687,13 +3746,13 @@ impl<'a, 'b, 'c, V: View> Deref for EventContext<'a, 'b, 'c, V> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> DerefMut for EventContext<'_, '_, '_, V> {
|
impl<V> DerefMut for EventContext<'_, '_, '_, V> {
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
&mut self.view_context
|
&mut self.view_context
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> BorrowAppContext for EventContext<'_, '_, '_, V> {
|
impl<V> BorrowAppContext for EventContext<'_, '_, '_, V> {
|
||||||
fn read_with<T, F: FnOnce(&AppContext) -> T>(&self, f: F) -> T {
|
fn read_with<T, F: FnOnce(&AppContext) -> T>(&self, f: F) -> T {
|
||||||
BorrowAppContext::read_with(&*self.view_context, f)
|
BorrowAppContext::read_with(&*self.view_context, f)
|
||||||
}
|
}
|
||||||
@ -3703,7 +3762,7 @@ impl<V: View> BorrowAppContext for EventContext<'_, '_, '_, V> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> BorrowWindowContext for EventContext<'_, '_, '_, V> {
|
impl<V> BorrowWindowContext for EventContext<'_, '_, '_, V> {
|
||||||
type Result<T> = T;
|
type Result<T> = T;
|
||||||
|
|
||||||
fn read_window<T, F: FnOnce(&WindowContext) -> T>(&self, window: AnyWindowHandle, f: F) -> T {
|
fn read_window<T, F: FnOnce(&WindowContext) -> T>(&self, window: AnyWindowHandle, f: F) -> T {
|
||||||
@ -3764,14 +3823,20 @@ impl<'a, T> DerefMut for Reference<'a, T> {
|
|||||||
pub struct MouseState {
|
pub struct MouseState {
|
||||||
pub(crate) hovered: bool,
|
pub(crate) hovered: bool,
|
||||||
pub(crate) clicked: Option<MouseButton>,
|
pub(crate) clicked: Option<MouseButton>,
|
||||||
|
pub(crate) mouse_down: bool,
|
||||||
pub(crate) accessed_hovered: bool,
|
pub(crate) accessed_hovered: bool,
|
||||||
pub(crate) accessed_clicked: bool,
|
pub(crate) accessed_clicked: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MouseState {
|
impl MouseState {
|
||||||
|
pub fn dragging(&mut self) -> bool {
|
||||||
|
self.accessed_hovered = true;
|
||||||
|
self.hovered && self.mouse_down
|
||||||
|
}
|
||||||
|
|
||||||
pub fn hovered(&mut self) -> bool {
|
pub fn hovered(&mut self) -> bool {
|
||||||
self.accessed_hovered = true;
|
self.accessed_hovered = true;
|
||||||
self.hovered
|
self.hovered && (!self.mouse_down || self.clicked.is_some())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clicked(&mut self) -> Option<MouseButton> {
|
pub fn clicked(&mut self) -> Option<MouseButton> {
|
||||||
@ -4031,7 +4096,7 @@ impl<V> Clone for WindowHandle<V> {
|
|||||||
|
|
||||||
impl<V> Copy for WindowHandle<V> {}
|
impl<V> Copy for WindowHandle<V> {}
|
||||||
|
|
||||||
impl<V: View> WindowHandle<V> {
|
impl<V: 'static> WindowHandle<V> {
|
||||||
fn new(window_id: usize) -> Self {
|
fn new(window_id: usize) -> Self {
|
||||||
WindowHandle {
|
WindowHandle {
|
||||||
any_handle: AnyWindowHandle::new(window_id, TypeId::of::<V>()),
|
any_handle: AnyWindowHandle::new(window_id, TypeId::of::<V>()),
|
||||||
@ -4069,7 +4134,9 @@ impl<V: View> WindowHandle<V> {
|
|||||||
.update(cx, update)
|
.update(cx, update)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V: View> WindowHandle<V> {
|
||||||
pub fn replace_root<C, F>(&self, cx: &mut C, build_root: F) -> C::Result<ViewHandle<V>>
|
pub fn replace_root<C, F>(&self, cx: &mut C, build_root: F) -> C::Result<ViewHandle<V>>
|
||||||
where
|
where
|
||||||
C: BorrowWindowContext,
|
C: BorrowWindowContext,
|
||||||
@ -4149,7 +4216,7 @@ impl AnyWindowHandle {
|
|||||||
self.update(cx, |cx| cx.add_view(build_view))
|
self.update(cx, |cx| cx.add_view(build_view))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn downcast<V: View>(self) -> Option<WindowHandle<V>> {
|
pub fn downcast<V: 'static>(self) -> Option<WindowHandle<V>> {
|
||||||
if self.root_view_type == TypeId::of::<V>() {
|
if self.root_view_type == TypeId::of::<V>() {
|
||||||
Some(WindowHandle {
|
Some(WindowHandle {
|
||||||
any_handle: self,
|
any_handle: self,
|
||||||
@ -4160,7 +4227,7 @@ impl AnyWindowHandle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn root_is<V: View>(&self) -> bool {
|
pub fn root_is<V: 'static>(&self) -> bool {
|
||||||
self.root_view_type == TypeId::of::<V>()
|
self.root_view_type == TypeId::of::<V>()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4238,9 +4305,9 @@ impl AnyWindowHandle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[repr(transparent)]
|
#[repr(transparent)]
|
||||||
pub struct ViewHandle<T> {
|
pub struct ViewHandle<V> {
|
||||||
any_handle: AnyViewHandle,
|
any_handle: AnyViewHandle,
|
||||||
view_type: PhantomData<T>,
|
view_type: PhantomData<V>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Deref for ViewHandle<T> {
|
impl<T> Deref for ViewHandle<T> {
|
||||||
@ -4251,15 +4318,15 @@ impl<T> Deref for ViewHandle<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: View> ViewHandle<T> {
|
impl<V: 'static> ViewHandle<V> {
|
||||||
fn new(window: AnyWindowHandle, view_id: usize, ref_counts: &Arc<Mutex<RefCounts>>) -> Self {
|
fn new(window: AnyWindowHandle, view_id: usize, ref_counts: &Arc<Mutex<RefCounts>>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
any_handle: AnyViewHandle::new(window, view_id, TypeId::of::<T>(), ref_counts.clone()),
|
any_handle: AnyViewHandle::new(window, view_id, TypeId::of::<V>(), ref_counts.clone()),
|
||||||
view_type: PhantomData,
|
view_type: PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn downgrade(&self) -> WeakViewHandle<T> {
|
pub fn downgrade(&self) -> WeakViewHandle<V> {
|
||||||
WeakViewHandle::new(self.window, self.view_id)
|
WeakViewHandle::new(self.window, self.view_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4275,14 +4342,14 @@ impl<T: View> ViewHandle<T> {
|
|||||||
self.view_id
|
self.view_id
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read<'a>(&self, cx: &'a AppContext) -> &'a T {
|
pub fn read<'a>(&self, cx: &'a AppContext) -> &'a V {
|
||||||
cx.read_view(self)
|
cx.read_view(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_with<C, F, S>(&self, cx: &C, read: F) -> C::Result<S>
|
pub fn read_with<C, F, S>(&self, cx: &C, read: F) -> C::Result<S>
|
||||||
where
|
where
|
||||||
C: BorrowWindowContext,
|
C: BorrowWindowContext,
|
||||||
F: FnOnce(&T, &ViewContext<T>) -> S,
|
F: FnOnce(&V, &ViewContext<V>) -> S,
|
||||||
{
|
{
|
||||||
cx.read_window(self.window, |cx| {
|
cx.read_window(self.window, |cx| {
|
||||||
let cx = ViewContext::immutable(cx, self.view_id);
|
let cx = ViewContext::immutable(cx, self.view_id);
|
||||||
@ -4293,7 +4360,7 @@ impl<T: View> ViewHandle<T> {
|
|||||||
pub fn update<C, F, S>(&self, cx: &mut C, update: F) -> C::Result<S>
|
pub fn update<C, F, S>(&self, cx: &mut C, update: F) -> C::Result<S>
|
||||||
where
|
where
|
||||||
C: BorrowWindowContext,
|
C: BorrowWindowContext,
|
||||||
F: FnOnce(&mut T, &mut ViewContext<T>) -> S,
|
F: FnOnce(&mut V, &mut ViewContext<V>) -> S,
|
||||||
{
|
{
|
||||||
let mut update = Some(update);
|
let mut update = Some(update);
|
||||||
|
|
||||||
@ -4429,8 +4496,8 @@ impl AnyViewHandle {
|
|||||||
TypeId::of::<T>() == self.view_type
|
TypeId::of::<T>() == self.view_type
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn downcast<T: View>(self) -> Option<ViewHandle<T>> {
|
pub fn downcast<V: 'static>(self) -> Option<ViewHandle<V>> {
|
||||||
if self.is::<T>() {
|
if self.is::<V>() {
|
||||||
Some(ViewHandle {
|
Some(ViewHandle {
|
||||||
any_handle: self,
|
any_handle: self,
|
||||||
view_type: PhantomData,
|
view_type: PhantomData,
|
||||||
@ -4440,8 +4507,8 @@ impl AnyViewHandle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn downcast_ref<T: View>(&self) -> Option<&ViewHandle<T>> {
|
pub fn downcast_ref<V: 'static>(&self) -> Option<&ViewHandle<V>> {
|
||||||
if self.is::<T>() {
|
if self.is::<V>() {
|
||||||
Some(unsafe { mem::transmute(self) })
|
Some(unsafe { mem::transmute(self) })
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@ -4620,12 +4687,13 @@ impl AnyWeakModelHandle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy)]
|
|
||||||
pub struct WeakViewHandle<T> {
|
pub struct WeakViewHandle<T> {
|
||||||
any_handle: AnyWeakViewHandle,
|
any_handle: AnyWeakViewHandle,
|
||||||
view_type: PhantomData<T>,
|
view_type: PhantomData<T>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T> Copy for WeakViewHandle<T> {}
|
||||||
|
|
||||||
impl<T> Debug for WeakViewHandle<T> {
|
impl<T> Debug for WeakViewHandle<T> {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
f.debug_struct(&format!("WeakViewHandle<{}>", type_name::<T>()))
|
f.debug_struct(&format!("WeakViewHandle<{}>", type_name::<T>()))
|
||||||
@ -4640,7 +4708,7 @@ impl<T> WeakHandle for WeakViewHandle<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> WeakViewHandle<V> {
|
impl<V: 'static> WeakViewHandle<V> {
|
||||||
fn new(window: AnyWindowHandle, view_id: usize) -> Self {
|
fn new(window: AnyWindowHandle, view_id: usize) -> Self {
|
||||||
Self {
|
Self {
|
||||||
any_handle: AnyWeakViewHandle {
|
any_handle: AnyWeakViewHandle {
|
||||||
@ -4680,28 +4748,47 @@ impl<V: View> WeakViewHandle<V> {
|
|||||||
cx.read(|cx| {
|
cx.read(|cx| {
|
||||||
let handle = cx
|
let handle = cx
|
||||||
.upgrade_view_handle(self)
|
.upgrade_view_handle(self)
|
||||||
.ok_or_else(|| anyhow!("view {} was dropped", V::ui_name()))?;
|
.ok_or_else(|| anyhow!("view was dropped"))?;
|
||||||
cx.read_window(self.window, |cx| handle.read_with(cx, read))
|
cx.read_window(self.window, |cx| handle.read_with(cx, read))
|
||||||
.ok_or_else(|| anyhow!("window was removed"))
|
.ok_or_else(|| anyhow!("window was removed"))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update<T>(
|
pub fn update<T, B>(
|
||||||
&self,
|
&self,
|
||||||
cx: &mut AsyncAppContext,
|
cx: &mut B,
|
||||||
update: impl FnOnce(&mut V, &mut ViewContext<V>) -> T,
|
update: impl FnOnce(&mut V, &mut ViewContext<V>) -> T,
|
||||||
) -> Result<T> {
|
) -> Result<T>
|
||||||
cx.update(|cx| {
|
where
|
||||||
let handle = cx
|
B: BorrowWindowContext,
|
||||||
.upgrade_view_handle(self)
|
B::Result<Option<T>>: Flatten<T>,
|
||||||
.ok_or_else(|| anyhow!("view {} was dropped", V::ui_name()))?;
|
{
|
||||||
cx.update_window(self.window, |cx| handle.update(cx, update))
|
cx.update_window(self.window(), |cx| {
|
||||||
.ok_or_else(|| anyhow!("window was removed"))
|
cx.upgrade_view_handle(self)
|
||||||
|
.map(|handle| handle.update(cx, update))
|
||||||
})
|
})
|
||||||
|
.flatten()
|
||||||
|
.ok_or_else(|| anyhow!("window was removed"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Deref for WeakViewHandle<T> {
|
pub trait Flatten<T> {
|
||||||
|
fn flatten(self) -> Option<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Flatten<T> for Option<Option<T>> {
|
||||||
|
fn flatten(self) -> Option<T> {
|
||||||
|
self.flatten()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Flatten<T> for Option<T> {
|
||||||
|
fn flatten(self) -> Option<T> {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V> Deref for WeakViewHandle<V> {
|
||||||
type Target = AnyWeakViewHandle;
|
type Target = AnyWeakViewHandle;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
@ -4709,7 +4796,7 @@ impl<T> Deref for WeakViewHandle<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Clone for WeakViewHandle<T> {
|
impl<V> Clone for WeakViewHandle<V> {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
any_handle: self.any_handle.clone(),
|
any_handle: self.any_handle.clone(),
|
||||||
@ -5263,6 +5350,7 @@ mod tests {
|
|||||||
button: MouseButton::Left,
|
button: MouseButton::Left,
|
||||||
modifiers: Default::default(),
|
modifiers: Default::default(),
|
||||||
click_count: 1,
|
click_count: 1,
|
||||||
|
is_down: true,
|
||||||
}),
|
}),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
elements::AnyRootElement,
|
elements::AnyRootElement,
|
||||||
geometry::rect::RectF,
|
geometry::{rect::RectF, Size},
|
||||||
json::ToJson,
|
json::ToJson,
|
||||||
keymap_matcher::{Binding, KeymapContext, Keystroke, MatchResult},
|
keymap_matcher::{Binding, KeymapContext, Keystroke, MatchResult},
|
||||||
platform::{
|
platform::{
|
||||||
@ -8,8 +8,9 @@ use crate::{
|
|||||||
MouseButton, MouseMovedEvent, PromptLevel, WindowBounds,
|
MouseButton, MouseMovedEvent, PromptLevel, WindowBounds,
|
||||||
},
|
},
|
||||||
scene::{
|
scene::{
|
||||||
CursorRegion, MouseClick, MouseClickOut, MouseDown, MouseDownOut, MouseDrag, MouseEvent,
|
CursorRegion, EventHandler, MouseClick, MouseClickOut, MouseDown, MouseDownOut, MouseDrag,
|
||||||
MouseHover, MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut, Scene,
|
MouseEvent, MouseHover, MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut,
|
||||||
|
Scene,
|
||||||
},
|
},
|
||||||
text_layout::TextLayoutCache,
|
text_layout::TextLayoutCache,
|
||||||
util::post_inc,
|
util::post_inc,
|
||||||
@ -31,7 +32,11 @@ use sqlez::{
|
|||||||
use std::{
|
use std::{
|
||||||
any::TypeId,
|
any::TypeId,
|
||||||
mem,
|
mem,
|
||||||
ops::{Deref, DerefMut, Range},
|
ops::{Deref, DerefMut, Range, Sub},
|
||||||
|
};
|
||||||
|
use taffy::{
|
||||||
|
tree::{Measurable, MeasureFunc},
|
||||||
|
Taffy,
|
||||||
};
|
};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@ -39,6 +44,7 @@ use uuid::Uuid;
|
|||||||
use super::{Reference, ViewMetadata};
|
use super::{Reference, ViewMetadata};
|
||||||
|
|
||||||
pub struct Window {
|
pub struct Window {
|
||||||
|
layout_engines: Vec<LayoutEngine>,
|
||||||
pub(crate) root_view: Option<AnyViewHandle>,
|
pub(crate) root_view: Option<AnyViewHandle>,
|
||||||
pub(crate) focused_view_id: Option<usize>,
|
pub(crate) focused_view_id: Option<usize>,
|
||||||
pub(crate) parents: HashMap<usize, usize>,
|
pub(crate) parents: HashMap<usize, usize>,
|
||||||
@ -51,6 +57,7 @@ pub struct Window {
|
|||||||
appearance: Appearance,
|
appearance: Appearance,
|
||||||
cursor_regions: Vec<CursorRegion>,
|
cursor_regions: Vec<CursorRegion>,
|
||||||
mouse_regions: Vec<(MouseRegion, usize)>,
|
mouse_regions: Vec<(MouseRegion, usize)>,
|
||||||
|
event_handlers: Vec<EventHandler>,
|
||||||
last_mouse_moved_event: Option<Event>,
|
last_mouse_moved_event: Option<Event>,
|
||||||
pub(crate) hovered_region_ids: Vec<MouseRegionId>,
|
pub(crate) hovered_region_ids: Vec<MouseRegionId>,
|
||||||
pub(crate) clicked_region_ids: Vec<MouseRegionId>,
|
pub(crate) clicked_region_ids: Vec<MouseRegionId>,
|
||||||
@ -67,12 +74,13 @@ impl Window {
|
|||||||
build_view: F,
|
build_view: F,
|
||||||
) -> Self
|
) -> Self
|
||||||
where
|
where
|
||||||
F: FnOnce(&mut ViewContext<V>) -> V,
|
|
||||||
V: View,
|
V: View,
|
||||||
|
F: FnOnce(&mut ViewContext<V>) -> V,
|
||||||
{
|
{
|
||||||
let titlebar_height = platform_window.titlebar_height();
|
let titlebar_height = platform_window.titlebar_height();
|
||||||
let appearance = platform_window.appearance();
|
let appearance = platform_window.appearance();
|
||||||
let mut window = Self {
|
let mut window = Self {
|
||||||
|
layout_engines: Vec::new(),
|
||||||
root_view: None,
|
root_view: None,
|
||||||
focused_view_id: None,
|
focused_view_id: None,
|
||||||
parents: Default::default(),
|
parents: Default::default(),
|
||||||
@ -83,6 +91,7 @@ impl Window {
|
|||||||
rendered_views: Default::default(),
|
rendered_views: Default::default(),
|
||||||
cursor_regions: Default::default(),
|
cursor_regions: Default::default(),
|
||||||
mouse_regions: Default::default(),
|
mouse_regions: Default::default(),
|
||||||
|
event_handlers: Default::default(),
|
||||||
text_layout_cache: TextLayoutCache::new(cx.font_system.clone()),
|
text_layout_cache: TextLayoutCache::new(cx.font_system.clone()),
|
||||||
last_mouse_moved_event: None,
|
last_mouse_moved_event: None,
|
||||||
hovered_region_ids: Default::default(),
|
hovered_region_ids: Default::default(),
|
||||||
@ -109,6 +118,10 @@ impl Window {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.expect("root_view called during window construction")
|
.expect("root_view called during window construction")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn take_event_handlers(&mut self) -> Vec<EventHandler> {
|
||||||
|
mem::take(&mut self.event_handlers)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct WindowContext<'a> {
|
pub struct WindowContext<'a> {
|
||||||
@ -207,6 +220,24 @@ impl<'a> WindowContext<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn repaint(&mut self) {
|
||||||
|
let window = self.window();
|
||||||
|
self.pending_effects
|
||||||
|
.push_back(Effect::RepaintWindow { window });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn layout_engine(&mut self) -> Option<&mut LayoutEngine> {
|
||||||
|
self.window.layout_engines.last_mut()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push_layout_engine(&mut self, engine: LayoutEngine) {
|
||||||
|
self.window.layout_engines.push(engine);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pop_layout_engine(&mut self) -> Option<LayoutEngine> {
|
||||||
|
self.window.layout_engines.pop()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn remove_window(&mut self) {
|
pub fn remove_window(&mut self) {
|
||||||
self.removed = true;
|
self.removed = true;
|
||||||
}
|
}
|
||||||
@ -227,6 +258,10 @@ impl<'a> WindowContext<'a> {
|
|||||||
self.window.platform_window.content_size()
|
self.window.platform_window.content_size()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn mouse_position(&self) -> Vector2F {
|
||||||
|
self.window.mouse_position
|
||||||
|
}
|
||||||
|
|
||||||
pub fn text_layout_cache(&self) -> &TextLayoutCache {
|
pub fn text_layout_cache(&self) -> &TextLayoutCache {
|
||||||
&self.window.text_layout_cache
|
&self.window.text_layout_cache
|
||||||
}
|
}
|
||||||
@ -242,14 +277,11 @@ impl<'a> WindowContext<'a> {
|
|||||||
Some(result)
|
Some(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn update_view<T, S>(
|
pub(crate) fn update_view<V: 'static, S>(
|
||||||
&mut self,
|
&mut self,
|
||||||
handle: &ViewHandle<T>,
|
handle: &ViewHandle<V>,
|
||||||
update: &mut dyn FnMut(&mut T, &mut ViewContext<T>) -> S,
|
update: &mut dyn FnMut(&mut V, &mut ViewContext<V>) -> S,
|
||||||
) -> S
|
) -> S {
|
||||||
where
|
|
||||||
T: View,
|
|
||||||
{
|
|
||||||
self.update_any_view(handle.view_id, |view, cx| {
|
self.update_any_view(handle.view_id, |view, cx| {
|
||||||
let mut cx = ViewContext::mutable(cx, handle.view_id);
|
let mut cx = ViewContext::mutable(cx, handle.view_id);
|
||||||
update(
|
update(
|
||||||
@ -475,6 +507,8 @@ impl<'a> WindowContext<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn dispatch_event(&mut self, event: Event, event_reused: bool) -> bool {
|
pub(crate) fn dispatch_event(&mut self, event: Event, event_reused: bool) -> bool {
|
||||||
|
self.dispatch_to_new_event_handlers(&event);
|
||||||
|
|
||||||
let mut mouse_events = SmallVec::<[_; 2]>::new();
|
let mut mouse_events = SmallVec::<[_; 2]>::new();
|
||||||
let mut notified_views: HashSet<usize> = Default::default();
|
let mut notified_views: HashSet<usize> = Default::default();
|
||||||
let handle = self.window_handle;
|
let handle = self.window_handle;
|
||||||
@ -583,7 +617,8 @@ impl<'a> WindowContext<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if self
|
if pressed_button.is_none()
|
||||||
|
&& self
|
||||||
.window
|
.window
|
||||||
.platform_window
|
.platform_window
|
||||||
.is_topmost_for_position(*position)
|
.is_topmost_for_position(*position)
|
||||||
@ -753,6 +788,11 @@ impl<'a> WindowContext<'a> {
|
|||||||
.contains_point(self.window.mouse_position)
|
.contains_point(self.window.mouse_position)
|
||||||
{
|
{
|
||||||
valid_regions.push(mouse_region.clone());
|
valid_regions.push(mouse_region.clone());
|
||||||
|
} else {
|
||||||
|
// Let the view know that it hasn't been clicked anymore
|
||||||
|
if mouse_region.notify_on_click {
|
||||||
|
notified_views.insert(mouse_region.id().view_id());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -852,6 +892,18 @@ impl<'a> WindowContext<'a> {
|
|||||||
any_event_handled
|
any_event_handled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn dispatch_to_new_event_handlers(&mut self, event: &Event) {
|
||||||
|
if let Some(mouse_event) = event.mouse_event() {
|
||||||
|
let event_handlers = self.window.take_event_handlers();
|
||||||
|
for event_handler in event_handlers.iter().rev() {
|
||||||
|
if event_handler.event_type == mouse_event.type_id() {
|
||||||
|
(event_handler.handler)(mouse_event, self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.window.event_handlers = event_handlers;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn dispatch_key_down(&mut self, event: &KeyDownEvent) -> bool {
|
pub(crate) fn dispatch_key_down(&mut self, event: &KeyDownEvent) -> bool {
|
||||||
let handle = self.window_handle;
|
let handle = self.window_handle;
|
||||||
if let Some(focused_view_id) = self.window.focused_view_id {
|
if let Some(focused_view_id) = self.window.focused_view_id {
|
||||||
@ -942,14 +994,16 @@ impl<'a> WindowContext<'a> {
|
|||||||
Ok(element)
|
Ok(element)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn layout(&mut self, refreshing: bool) -> Result<HashMap<usize, usize>> {
|
pub fn layout(&mut self, refreshing: bool) -> Result<HashMap<usize, usize>> {
|
||||||
let window_size = self.window.platform_window.content_size();
|
let window_size = self.window.platform_window.content_size();
|
||||||
let root_view_id = self.window.root_view().id();
|
let root_view_id = self.window.root_view().id();
|
||||||
|
|
||||||
let mut rendered_root = self.window.rendered_views.remove(&root_view_id).unwrap();
|
let mut rendered_root = self.window.rendered_views.remove(&root_view_id).unwrap();
|
||||||
|
|
||||||
let mut new_parents = HashMap::default();
|
let mut new_parents = HashMap::default();
|
||||||
let mut views_to_notify_if_ancestors_change = HashMap::default();
|
let mut views_to_notify_if_ancestors_change = HashMap::default();
|
||||||
rendered_root.layout(
|
rendered_root.layout(
|
||||||
SizeConstraint::strict(window_size),
|
SizeConstraint::new(window_size, window_size),
|
||||||
&mut new_parents,
|
&mut new_parents,
|
||||||
&mut views_to_notify_if_ancestors_change,
|
&mut views_to_notify_if_ancestors_change,
|
||||||
refreshing,
|
refreshing,
|
||||||
@ -982,7 +1036,7 @@ impl<'a> WindowContext<'a> {
|
|||||||
Ok(old_parents)
|
Ok(old_parents)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn paint(&mut self) -> Result<Scene> {
|
pub fn paint(&mut self) -> Result<Scene> {
|
||||||
let window_size = self.window.platform_window.content_size();
|
let window_size = self.window.platform_window.content_size();
|
||||||
let scale_factor = self.window.platform_window.scale_factor();
|
let scale_factor = self.window.platform_window.scale_factor();
|
||||||
|
|
||||||
@ -1001,9 +1055,10 @@ impl<'a> WindowContext<'a> {
|
|||||||
.insert(root_view_id, rendered_root);
|
.insert(root_view_id, rendered_root);
|
||||||
|
|
||||||
self.window.text_layout_cache.finish_frame();
|
self.window.text_layout_cache.finish_frame();
|
||||||
let scene = scene_builder.build();
|
let mut scene = scene_builder.build();
|
||||||
self.window.cursor_regions = scene.cursor_regions();
|
self.window.cursor_regions = scene.cursor_regions();
|
||||||
self.window.mouse_regions = scene.mouse_regions();
|
self.window.mouse_regions = scene.mouse_regions();
|
||||||
|
self.window.event_handlers = scene.take_event_handlers();
|
||||||
|
|
||||||
if self.window_is_active() {
|
if self.window_is_active() {
|
||||||
if let Some(event) = self.window.last_mouse_moved_event.clone() {
|
if let Some(event) = self.window.last_mouse_moved_event.clone() {
|
||||||
@ -1014,6 +1069,11 @@ impl<'a> WindowContext<'a> {
|
|||||||
Ok(scene)
|
Ok(scene)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn root_element(&self) -> &Box<dyn AnyRootElement> {
|
||||||
|
let view_id = self.window.root_view().id();
|
||||||
|
self.window.rendered_views.get(&view_id).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn rect_for_text_range(&self, range_utf16: Range<usize>) -> Option<RectF> {
|
pub fn rect_for_text_range(&self, range_utf16: Range<usize>) -> Option<RectF> {
|
||||||
let focused_view_id = self.window.focused_view_id?;
|
let focused_view_id = self.window.focused_view_id?;
|
||||||
self.window
|
self.window
|
||||||
@ -1216,6 +1276,119 @@ impl<'a> WindowContext<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct LayoutEngine(Taffy);
|
||||||
|
pub use taffy::style::Style as LayoutStyle;
|
||||||
|
|
||||||
|
impl LayoutEngine {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Default::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_node<C>(&mut self, style: LayoutStyle, children: C) -> Result<LayoutId>
|
||||||
|
where
|
||||||
|
C: IntoIterator<Item = LayoutId>,
|
||||||
|
{
|
||||||
|
Ok(self
|
||||||
|
.0
|
||||||
|
.new_with_children(style, &children.into_iter().collect::<Vec<_>>())?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_measured_node<F>(&mut self, style: LayoutStyle, measure: F) -> Result<LayoutId>
|
||||||
|
where
|
||||||
|
F: Fn(MeasureParams) -> Size<f32> + Sync + Send + 'static,
|
||||||
|
{
|
||||||
|
Ok(self
|
||||||
|
.0
|
||||||
|
.new_leaf_with_measure(style, MeasureFunc::Boxed(Box::new(MeasureFn(measure))))?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compute_layout(&mut self, root: LayoutId, available_space: Vector2F) -> Result<()> {
|
||||||
|
self.0.compute_layout(
|
||||||
|
root,
|
||||||
|
taffy::geometry::Size {
|
||||||
|
width: available_space.x().into(),
|
||||||
|
height: available_space.y().into(),
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn computed_layout(&mut self, node: LayoutId) -> Result<EngineLayout> {
|
||||||
|
Ok(self.0.layout(node)?.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MeasureFn<F>(F);
|
||||||
|
|
||||||
|
impl<F: Send + Sync> Measurable for MeasureFn<F>
|
||||||
|
where
|
||||||
|
F: Fn(MeasureParams) -> Size<f32>,
|
||||||
|
{
|
||||||
|
fn measure(
|
||||||
|
&self,
|
||||||
|
known_dimensions: taffy::prelude::Size<Option<f32>>,
|
||||||
|
available_space: taffy::prelude::Size<taffy::style::AvailableSpace>,
|
||||||
|
) -> taffy::prelude::Size<f32> {
|
||||||
|
(self.0)(MeasureParams {
|
||||||
|
known_dimensions: known_dimensions.into(),
|
||||||
|
available_space: available_space.into(),
|
||||||
|
})
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct EngineLayout {
|
||||||
|
pub bounds: RectF,
|
||||||
|
pub order: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MeasureParams {
|
||||||
|
pub known_dimensions: Size<Option<f32>>,
|
||||||
|
pub available_space: Size<AvailableSpace>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum AvailableSpace {
|
||||||
|
/// The amount of space available is the specified number of pixels
|
||||||
|
Pixels(f32),
|
||||||
|
/// The amount of space available is indefinite and the node should be laid out under a min-content constraint
|
||||||
|
MinContent,
|
||||||
|
/// The amount of space available is indefinite and the node should be laid out under a max-content constraint
|
||||||
|
MaxContent,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AvailableSpace {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Pixels(0.)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<taffy::prelude::AvailableSpace> for AvailableSpace {
|
||||||
|
fn from(value: taffy::prelude::AvailableSpace) -> Self {
|
||||||
|
match value {
|
||||||
|
taffy::prelude::AvailableSpace::Definite(pixels) => Self::Pixels(pixels),
|
||||||
|
taffy::prelude::AvailableSpace::MinContent => Self::MinContent,
|
||||||
|
taffy::prelude::AvailableSpace::MaxContent => Self::MaxContent,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&taffy::tree::Layout> for EngineLayout {
|
||||||
|
fn from(value: &taffy::tree::Layout) -> Self {
|
||||||
|
Self {
|
||||||
|
bounds: RectF::new(
|
||||||
|
vec2f(value.location.x, value.location.y),
|
||||||
|
vec2f(value.size.width, value.size.height),
|
||||||
|
),
|
||||||
|
order: value.order,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type LayoutId = taffy::prelude::NodeId;
|
||||||
|
|
||||||
pub struct RenderParams {
|
pub struct RenderParams {
|
||||||
pub view_id: usize,
|
pub view_id: usize,
|
||||||
pub titlebar_height: f32,
|
pub titlebar_height: f32,
|
||||||
@ -1324,6 +1497,12 @@ impl SizeConstraint {
|
|||||||
max: size,
|
max: size,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pub fn loose(max: Vector2F) -> Self {
|
||||||
|
Self {
|
||||||
|
min: Vector2F::zero(),
|
||||||
|
max,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn strict_along(axis: Axis, max: f32) -> Self {
|
pub fn strict_along(axis: Axis, max: f32) -> Self {
|
||||||
match axis {
|
match axis {
|
||||||
@ -1360,6 +1539,17 @@ impl SizeConstraint {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Sub<Vector2F> for SizeConstraint {
|
||||||
|
type Output = SizeConstraint;
|
||||||
|
|
||||||
|
fn sub(self, rhs: Vector2F) -> SizeConstraint {
|
||||||
|
SizeConstraint {
|
||||||
|
min: self.min - rhs,
|
||||||
|
max: self.max - rhs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for SizeConstraint {
|
impl Default for SizeConstraint {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
SizeConstraint {
|
SizeConstraint {
|
||||||
@ -1378,6 +1568,7 @@ impl ToJson for SizeConstraint {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct ChildView {
|
pub struct ChildView {
|
||||||
view_id: usize,
|
view_id: usize,
|
||||||
view_name: &'static str,
|
view_name: &'static str,
|
||||||
@ -1393,7 +1584,7 @@ impl ChildView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> Element<V> for ChildView {
|
impl<V: 'static> Element<V> for ChildView {
|
||||||
type LayoutState = ();
|
type LayoutState = ();
|
||||||
type PaintState = ();
|
type PaintState = ();
|
||||||
|
|
||||||
|
@ -15,35 +15,75 @@ use serde_json::json;
|
|||||||
|
|
||||||
#[derive(Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord, JsonSchema)]
|
#[derive(Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord, JsonSchema)]
|
||||||
#[repr(transparent)]
|
#[repr(transparent)]
|
||||||
pub struct Color(#[schemars(with = "String")] ColorU);
|
pub struct Color(#[schemars(with = "String")] pub ColorU);
|
||||||
|
|
||||||
|
pub fn color(rgba: u32) -> Color {
|
||||||
|
Color::from_u32(rgba)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rgb(r: f32, g: f32, b: f32) -> Color {
|
||||||
|
Color(ColorF::new(r, g, b, 1.).to_u8())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rgba(r: f32, g: f32, b: f32, a: f32) -> Color {
|
||||||
|
Color(ColorF::new(r, g, b, a).to_u8())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn transparent_black() -> Color {
|
||||||
|
Color(ColorU::transparent_black())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn black() -> Color {
|
||||||
|
Color(ColorU::black())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn white() -> Color {
|
||||||
|
Color(ColorU::white())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn red() -> Color {
|
||||||
|
color(0xff0000ff)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn green() -> Color {
|
||||||
|
color(0x00ff00ff)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn blue() -> Color {
|
||||||
|
color(0x0000ffff)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn yellow() -> Color {
|
||||||
|
color(0xffff00ff)
|
||||||
|
}
|
||||||
|
|
||||||
impl Color {
|
impl Color {
|
||||||
pub fn transparent_black() -> Self {
|
pub fn transparent_black() -> Self {
|
||||||
Self(ColorU::transparent_black())
|
transparent_black()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn black() -> Self {
|
pub fn black() -> Self {
|
||||||
Self(ColorU::black())
|
black()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn white() -> Self {
|
pub fn white() -> Self {
|
||||||
Self(ColorU::white())
|
white()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn red() -> Self {
|
pub fn red() -> Self {
|
||||||
Self(ColorU::from_u32(0xff0000ff))
|
Color::from_u32(0xff0000ff)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn green() -> Self {
|
pub fn green() -> Self {
|
||||||
Self(ColorU::from_u32(0x00ff00ff))
|
Color::from_u32(0x00ff00ff)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn blue() -> Self {
|
pub fn blue() -> Self {
|
||||||
Self(ColorU::from_u32(0x0000ffff))
|
Color::from_u32(0x0000ffff)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn yellow() -> Self {
|
pub fn yellow() -> Self {
|
||||||
Self(ColorU::from_u32(0xffff00ff))
|
Color::from_u32(0xffff00ff)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
|
pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||||
@ -101,6 +141,12 @@ impl<'de> Deserialize<'de> for Color {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<u32> for Color {
|
||||||
|
fn from(value: u32) -> Self {
|
||||||
|
Self(ColorU::from_u32(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ToJson for Color {
|
impl ToJson for Color {
|
||||||
fn to_json(&self) -> serde_json::Value {
|
fn to_json(&self) -> serde_json::Value {
|
||||||
json!(format!(
|
json!(format!(
|
||||||
|
@ -34,7 +34,7 @@ use crate::{
|
|||||||
rect::RectF,
|
rect::RectF,
|
||||||
vector::{vec2f, Vector2F},
|
vector::{vec2f, Vector2F},
|
||||||
},
|
},
|
||||||
json, Action, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, TypeTag, View,
|
json, Action, Entity, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, TypeTag, View,
|
||||||
ViewContext, WeakViewHandle, WindowContext,
|
ViewContext, WeakViewHandle, WindowContext,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
@ -42,14 +42,19 @@ use collections::HashMap;
|
|||||||
use core::panic;
|
use core::panic;
|
||||||
use json::ToJson;
|
use json::ToJson;
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use std::{any::Any, borrow::Cow, mem, ops::Range};
|
use std::{
|
||||||
|
any::{type_name, Any},
|
||||||
|
borrow::Cow,
|
||||||
|
mem,
|
||||||
|
ops::Range,
|
||||||
|
};
|
||||||
|
|
||||||
pub trait Element<V: View>: 'static {
|
pub trait Element<V: 'static>: 'static {
|
||||||
type LayoutState;
|
type LayoutState;
|
||||||
type PaintState;
|
type PaintState;
|
||||||
|
|
||||||
fn view_name(&self) -> &'static str {
|
fn view_name(&self) -> &'static str {
|
||||||
V::ui_name()
|
type_name::<V>()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn layout(
|
fn layout(
|
||||||
@ -229,13 +234,30 @@ pub trait Element<V: View>: 'static {
|
|||||||
{
|
{
|
||||||
MouseEventHandler::for_child::<Tag>(self.into_any(), region_id)
|
MouseEventHandler::for_child::<Tag>(self.into_any(), region_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn component(self) -> StatelessElementAdapter
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
StatelessElementAdapter::new(self.into_any())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait RenderElement {
|
fn stateful_component(self) -> StatefulElementAdapter<V>
|
||||||
fn render<V: View>(&mut self, view: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V>;
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
StatefulElementAdapter::new(self.into_any())
|
||||||
}
|
}
|
||||||
|
|
||||||
trait AnyElementState<V: View> {
|
fn styleable_component(self) -> StylableAdapter<StatelessElementAdapter>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
StatelessElementAdapter::new(self.into_any()).stylable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trait AnyElementState<V> {
|
||||||
fn layout(
|
fn layout(
|
||||||
&mut self,
|
&mut self,
|
||||||
constraint: SizeConstraint,
|
constraint: SizeConstraint,
|
||||||
@ -249,7 +271,7 @@ trait AnyElementState<V: View> {
|
|||||||
origin: Vector2F,
|
origin: Vector2F,
|
||||||
visible_bounds: RectF,
|
visible_bounds: RectF,
|
||||||
view: &mut V,
|
view: &mut V,
|
||||||
cx: &mut ViewContext<V>,
|
cx: &mut PaintContext<V>,
|
||||||
);
|
);
|
||||||
|
|
||||||
fn rect_for_text_range(
|
fn rect_for_text_range(
|
||||||
@ -266,7 +288,7 @@ trait AnyElementState<V: View> {
|
|||||||
fn metadata(&self) -> Option<&dyn Any>;
|
fn metadata(&self) -> Option<&dyn Any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ElementState<V: View, E: Element<V>> {
|
enum ElementState<V: 'static, E: Element<V>> {
|
||||||
Empty,
|
Empty,
|
||||||
Init {
|
Init {
|
||||||
element: E,
|
element: E,
|
||||||
@ -287,7 +309,7 @@ enum ElementState<V: View, E: Element<V>> {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View, E: Element<V>> AnyElementState<V> for ElementState<V, E> {
|
impl<V, E: Element<V>> AnyElementState<V> for ElementState<V, E> {
|
||||||
fn layout(
|
fn layout(
|
||||||
&mut self,
|
&mut self,
|
||||||
constraint: SizeConstraint,
|
constraint: SizeConstraint,
|
||||||
@ -330,7 +352,7 @@ impl<V: View, E: Element<V>> AnyElementState<V> for ElementState<V, E> {
|
|||||||
origin: Vector2F,
|
origin: Vector2F,
|
||||||
visible_bounds: RectF,
|
visible_bounds: RectF,
|
||||||
view: &mut V,
|
view: &mut V,
|
||||||
cx: &mut ViewContext<V>,
|
cx: &mut PaintContext<V>,
|
||||||
) {
|
) {
|
||||||
*self = match mem::take(self) {
|
*self = match mem::take(self) {
|
||||||
ElementState::PostLayout {
|
ElementState::PostLayout {
|
||||||
@ -469,18 +491,18 @@ impl<V: View, E: Element<V>> AnyElementState<V> for ElementState<V, E> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View, E: Element<V>> Default for ElementState<V, E> {
|
impl<V, E: Element<V>> Default for ElementState<V, E> {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::Empty
|
Self::Empty
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AnyElement<V: View> {
|
pub struct AnyElement<V> {
|
||||||
state: Box<dyn AnyElementState<V>>,
|
state: Box<dyn AnyElementState<V>>,
|
||||||
name: Option<Cow<'static, str>>,
|
name: Option<Cow<'static, str>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> AnyElement<V> {
|
impl<V> AnyElement<V> {
|
||||||
pub fn name(&self) -> Option<&str> {
|
pub fn name(&self) -> Option<&str> {
|
||||||
self.name.as_deref()
|
self.name.as_deref()
|
||||||
}
|
}
|
||||||
@ -506,7 +528,7 @@ impl<V: View> AnyElement<V> {
|
|||||||
origin: Vector2F,
|
origin: Vector2F,
|
||||||
visible_bounds: RectF,
|
visible_bounds: RectF,
|
||||||
view: &mut V,
|
view: &mut V,
|
||||||
cx: &mut ViewContext<V>,
|
cx: &mut PaintContext<V>,
|
||||||
) {
|
) {
|
||||||
self.state.paint(scene, origin, visible_bounds, view, cx);
|
self.state.paint(scene, origin, visible_bounds, view, cx);
|
||||||
}
|
}
|
||||||
@ -548,7 +570,7 @@ impl<V: View> AnyElement<V> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> Element<V> for AnyElement<V> {
|
impl<V: 'static> Element<V> for AnyElement<V> {
|
||||||
type LayoutState = ();
|
type LayoutState = ();
|
||||||
type PaintState = ();
|
type PaintState = ();
|
||||||
|
|
||||||
@ -606,12 +628,18 @@ impl<V: View> Element<V> for AnyElement<V> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct RootElement<V: View> {
|
impl Entity for AnyElement<()> {
|
||||||
|
type Event = ();
|
||||||
|
}
|
||||||
|
|
||||||
|
// impl View for AnyElement<()> {}
|
||||||
|
|
||||||
|
pub struct RootElement<V> {
|
||||||
element: AnyElement<V>,
|
element: AnyElement<V>,
|
||||||
view: WeakViewHandle<V>,
|
view: WeakViewHandle<V>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> RootElement<V> {
|
impl<V> RootElement<V> {
|
||||||
pub fn new(element: AnyElement<V>, view: WeakViewHandle<V>) -> Self {
|
pub fn new(element: AnyElement<V>, view: WeakViewHandle<V>) -> Self {
|
||||||
Self { element, view }
|
Self { element, view }
|
||||||
}
|
}
|
||||||
@ -679,7 +707,9 @@ impl<V: View> AnyRootElement for RootElement<V> {
|
|||||||
.ok_or_else(|| anyhow!("paint called on a root element for a dropped view"))?;
|
.ok_or_else(|| anyhow!("paint called on a root element for a dropped view"))?;
|
||||||
|
|
||||||
view.update(cx, |view, cx| {
|
view.update(cx, |view, cx| {
|
||||||
self.element.paint(scene, origin, visible_bounds, view, cx);
|
let mut cx = PaintContext::new(cx);
|
||||||
|
self.element
|
||||||
|
.paint(scene, origin, visible_bounds, view, &mut cx);
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -719,7 +749,7 @@ impl<V: View> AnyRootElement for RootElement<V> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait ParentElement<'a, V: View>: Extend<AnyElement<V>> + Sized {
|
pub trait ParentElement<'a, V: 'static>: Extend<AnyElement<V>> + Sized {
|
||||||
fn add_children<E: Element<V>>(&mut self, children: impl IntoIterator<Item = E>) {
|
fn add_children<E: Element<V>>(&mut self, children: impl IntoIterator<Item = E>) {
|
||||||
self.extend(children.into_iter().map(|child| child.into_any()));
|
self.extend(children.into_iter().map(|child| child.into_any()));
|
||||||
}
|
}
|
||||||
@ -739,7 +769,12 @@ pub trait ParentElement<'a, V: View>: Extend<AnyElement<V>> + Sized {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, V: View, T> ParentElement<'a, V> for T where T: Extend<AnyElement<V>> {}
|
impl<'a, V, T> ParentElement<'a, V> for T
|
||||||
|
where
|
||||||
|
V: 'static,
|
||||||
|
T: Extend<AnyElement<V>>,
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
pub fn constrain_size_preserving_aspect_ratio(max_size: Vector2F, size: Vector2F) -> Vector2F {
|
pub fn constrain_size_preserving_aspect_ratio(max_size: Vector2F, size: Vector2F) -> Vector2F {
|
||||||
if max_size.x().is_infinite() && max_size.y().is_infinite() {
|
if max_size.x().is_infinite() && max_size.y().is_infinite() {
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
geometry::{rect::RectF, vector::Vector2F},
|
geometry::{rect::RectF, vector::Vector2F},
|
||||||
json, AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View,
|
json, AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint,
|
||||||
ViewContext,
|
ViewContext,
|
||||||
};
|
};
|
||||||
use json::ToJson;
|
use json::ToJson;
|
||||||
|
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
pub struct Align<V: View> {
|
pub struct Align<V> {
|
||||||
child: AnyElement<V>,
|
child: AnyElement<V>,
|
||||||
alignment: Vector2F,
|
alignment: Vector2F,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> Align<V> {
|
impl<V> Align<V> {
|
||||||
pub fn new(child: AnyElement<V>) -> Self {
|
pub fn new(child: AnyElement<V>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
child,
|
child,
|
||||||
@ -41,7 +41,7 @@ impl<V: View> Align<V> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> Element<V> for Align<V> {
|
impl<V: 'static> Element<V> for Align<V> {
|
||||||
type LayoutState = ();
|
type LayoutState = ();
|
||||||
type PaintState = ();
|
type PaintState = ();
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ use std::marker::PhantomData;
|
|||||||
use super::Element;
|
use super::Element;
|
||||||
use crate::{
|
use crate::{
|
||||||
json::{self, json},
|
json::{self, json},
|
||||||
PaintContext, SceneBuilder, View, ViewContext,
|
PaintContext, SceneBuilder, ViewContext,
|
||||||
};
|
};
|
||||||
use json::ToJson;
|
use json::ToJson;
|
||||||
use pathfinder_geometry::{
|
use pathfinder_geometry::{
|
||||||
@ -15,7 +15,6 @@ pub struct Canvas<V, F>(F, PhantomData<V>);
|
|||||||
|
|
||||||
impl<V, F> Canvas<V, F>
|
impl<V, F> Canvas<V, F>
|
||||||
where
|
where
|
||||||
V: View,
|
|
||||||
F: FnMut(&mut SceneBuilder, RectF, RectF, &mut V, &mut ViewContext<V>),
|
F: FnMut(&mut SceneBuilder, RectF, RectF, &mut V, &mut ViewContext<V>),
|
||||||
{
|
{
|
||||||
pub fn new(f: F) -> Self {
|
pub fn new(f: F) -> Self {
|
||||||
@ -23,7 +22,7 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View, F> Element<V> for Canvas<V, F>
|
impl<V: 'static, F> Element<V> for Canvas<V, F>
|
||||||
where
|
where
|
||||||
F: 'static + FnMut(&mut SceneBuilder, RectF, RectF, &mut V, &mut ViewContext<V>),
|
F: 'static + FnMut(&mut SceneBuilder, RectF, RectF, &mut V, &mut ViewContext<V>),
|
||||||
{
|
{
|
||||||
|
@ -4,21 +4,21 @@ use pathfinder_geometry::{rect::RectF, vector::Vector2F};
|
|||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
json, AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View,
|
json, AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint,
|
||||||
ViewContext,
|
ViewContext,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Clipped<V: View> {
|
pub struct Clipped<V> {
|
||||||
child: AnyElement<V>,
|
child: AnyElement<V>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> Clipped<V> {
|
impl<V> Clipped<V> {
|
||||||
pub fn new(child: AnyElement<V>) -> Self {
|
pub fn new(child: AnyElement<V>) -> Self {
|
||||||
Self { child }
|
Self { child }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> Element<V> for Clipped<V> {
|
impl<V: 'static> Element<V> for Clipped<V> {
|
||||||
type LayoutState = ();
|
type LayoutState = ();
|
||||||
type PaintState = ();
|
type PaintState = ();
|
||||||
|
|
||||||
|
@ -1,79 +1,81 @@
|
|||||||
use std::marker::PhantomData;
|
use std::{any::Any, marker::PhantomData};
|
||||||
|
|
||||||
use pathfinder_geometry::{rect::RectF, vector::Vector2F};
|
use pathfinder_geometry::{rect::RectF, vector::Vector2F};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View,
|
AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, ViewContext,
|
||||||
ViewContext,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::Empty;
|
use super::Empty;
|
||||||
|
|
||||||
pub trait GeneralComponent {
|
/// The core stateless component trait, simply rendering an element tree
|
||||||
fn render<V: View>(self, v: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V>;
|
pub trait Component {
|
||||||
fn element<V: View>(self) -> ComponentAdapter<V, Self>
|
fn render<V: 'static>(self, cx: &mut ViewContext<V>) -> AnyElement<V>;
|
||||||
|
|
||||||
|
fn element<V: 'static>(self) -> ComponentAdapter<V, Self>
|
||||||
where
|
where
|
||||||
Self: Sized,
|
Self: Sized,
|
||||||
{
|
{
|
||||||
ComponentAdapter::new(self)
|
ComponentAdapter::new(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn stylable(self) -> StylableAdapter<Self>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
StylableAdapter::new(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait StyleableComponent {
|
fn stateful<V: 'static>(self) -> StatefulAdapter<Self, V>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
StatefulAdapter::new(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Allows a a component's styles to be rebound in a simple way.
|
||||||
|
pub trait Stylable: Component {
|
||||||
type Style: Clone;
|
type Style: Clone;
|
||||||
type Output: GeneralComponent;
|
|
||||||
|
fn with_style(self, style: Self::Style) -> Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This trait models the typestate pattern for a component's style,
|
||||||
|
/// enforcing at compile time that a component is only usable after
|
||||||
|
/// it has been styled while still allowing for late binding of the
|
||||||
|
/// styling information
|
||||||
|
pub trait SafeStylable {
|
||||||
|
type Style: Clone;
|
||||||
|
type Output: Component;
|
||||||
|
|
||||||
fn with_style(self, style: Self::Style) -> Self::Output;
|
fn with_style(self, style: Self::Style) -> Self::Output;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GeneralComponent for () {
|
/// All stylable components can trivially implement SafeStylable
|
||||||
fn render<V: View>(self, _: &mut V, _: &mut ViewContext<V>) -> AnyElement<V> {
|
impl<C: Stylable> SafeStylable for C {
|
||||||
Empty::new().into_any()
|
type Style = C::Style;
|
||||||
|
|
||||||
|
type Output = C;
|
||||||
|
|
||||||
|
fn with_style(self, style: Self::Style) -> Self::Output {
|
||||||
|
self.with_style(style)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StyleableComponent for () {
|
/// Allows converting an unstylable component into a stylable one
|
||||||
type Style = ();
|
/// by using `()` as the style type
|
||||||
type Output = ();
|
pub struct StylableAdapter<C: Component> {
|
||||||
|
|
||||||
fn with_style(self, _: Self::Style) -> Self::Output {
|
|
||||||
()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait Component<V: View> {
|
|
||||||
fn render(self, v: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V>;
|
|
||||||
|
|
||||||
fn element(self) -> ComponentAdapter<V, Self>
|
|
||||||
where
|
|
||||||
Self: Sized,
|
|
||||||
{
|
|
||||||
ComponentAdapter::new(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V: View, C: GeneralComponent> Component<V> for C {
|
|
||||||
fn render(self, v: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V> {
|
|
||||||
self.render(v, cx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// StylableComponent -> GeneralComponent
|
|
||||||
pub struct StylableComponentAdapter<C: Component<V>, V: View> {
|
|
||||||
component: C,
|
component: C,
|
||||||
phantom: std::marker::PhantomData<V>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<C: Component<V>, V: View> StylableComponentAdapter<C, V> {
|
impl<C: Component> StylableAdapter<C> {
|
||||||
pub fn new(component: C) -> Self {
|
pub fn new(component: C) -> Self {
|
||||||
Self {
|
Self { component }
|
||||||
component,
|
|
||||||
phantom: std::marker::PhantomData,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<C: GeneralComponent, V: View> StyleableComponent for StylableComponentAdapter<C, V> {
|
impl<C: Component> SafeStylable for StylableAdapter<C> {
|
||||||
type Style = ();
|
type Style = ();
|
||||||
|
|
||||||
type Output = C;
|
type Output = C;
|
||||||
@ -83,13 +85,150 @@ impl<C: GeneralComponent, V: View> StyleableComponent for StylableComponentAdapt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Element -> Component
|
/// This is a secondary trait for components that can be styled
|
||||||
pub struct ElementAdapter<V: View> {
|
/// which rely on their view's state. This is useful for components that, for example,
|
||||||
|
/// want to take click handler callbacks Unfortunately, the generic bound on the
|
||||||
|
/// Component trait makes it incompatible with the stateless components above.
|
||||||
|
// So let's just replicate them for now
|
||||||
|
pub trait StatefulComponent<V: 'static> {
|
||||||
|
fn render(self, v: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V>;
|
||||||
|
|
||||||
|
fn element(self) -> ComponentAdapter<V, Self>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
ComponentAdapter::new(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn styleable(self) -> StatefulStylableAdapter<Self, V>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
StatefulStylableAdapter::new(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stateless(self) -> StatelessElementAdapter
|
||||||
|
where
|
||||||
|
Self: Sized + 'static,
|
||||||
|
{
|
||||||
|
StatelessElementAdapter::new(self.element().into_any())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// It is trivial to convert stateless components to stateful components, so lets
|
||||||
|
/// do so en masse. Note that the reverse is impossible without a helper.
|
||||||
|
impl<V: 'static, C: Component> StatefulComponent<V> for C {
|
||||||
|
fn render(self, _: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V> {
|
||||||
|
self.render(cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Same as stylable, but generic over a view type
|
||||||
|
pub trait StatefulStylable<V: 'static>: StatefulComponent<V> {
|
||||||
|
type Style: Clone;
|
||||||
|
|
||||||
|
fn with_style(self, style: Self::Style) -> Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Same as SafeStylable, but generic over a view type
|
||||||
|
pub trait StatefulSafeStylable<V: 'static> {
|
||||||
|
type Style: Clone;
|
||||||
|
type Output: StatefulComponent<V>;
|
||||||
|
|
||||||
|
fn with_style(self, style: Self::Style) -> Self::Output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converting from stateless to stateful
|
||||||
|
impl<V: 'static, C: SafeStylable> StatefulSafeStylable<V> for C {
|
||||||
|
type Style = C::Style;
|
||||||
|
|
||||||
|
type Output = C::Output;
|
||||||
|
|
||||||
|
fn with_style(self, style: Self::Style) -> Self::Output {
|
||||||
|
self.with_style(style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A helper for converting stateless components into stateful ones
|
||||||
|
pub struct StatefulAdapter<C, V> {
|
||||||
|
component: C,
|
||||||
|
phantom: std::marker::PhantomData<V>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C: Component, V: 'static> StatefulAdapter<C, V> {
|
||||||
|
pub fn new(component: C) -> Self {
|
||||||
|
Self {
|
||||||
|
component,
|
||||||
|
phantom: std::marker::PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C: Component, V: 'static> StatefulComponent<V> for StatefulAdapter<C, V> {
|
||||||
|
fn render(self, _: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V> {
|
||||||
|
self.component.render(cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A helper for converting stateful but style-less components into stylable ones
|
||||||
|
// by using `()` as the style type
|
||||||
|
pub struct StatefulStylableAdapter<C: StatefulComponent<V>, V: 'static> {
|
||||||
|
component: C,
|
||||||
|
phantom: std::marker::PhantomData<V>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C: StatefulComponent<V>, V: 'static> StatefulStylableAdapter<C, V> {
|
||||||
|
pub fn new(component: C) -> Self {
|
||||||
|
Self {
|
||||||
|
component,
|
||||||
|
phantom: std::marker::PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C: StatefulComponent<V>, V: 'static> StatefulSafeStylable<V>
|
||||||
|
for StatefulStylableAdapter<C, V>
|
||||||
|
{
|
||||||
|
type Style = ();
|
||||||
|
|
||||||
|
type Output = C;
|
||||||
|
|
||||||
|
fn with_style(self, _: Self::Style) -> Self::Output {
|
||||||
|
self.component
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A way of erasing the view generic from an element, useful
|
||||||
|
/// for wrapping up an explicit element tree into stateless
|
||||||
|
/// components
|
||||||
|
pub struct StatelessElementAdapter {
|
||||||
|
element: Box<dyn Any>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StatelessElementAdapter {
|
||||||
|
pub fn new<V: 'static>(element: AnyElement<V>) -> Self {
|
||||||
|
StatelessElementAdapter {
|
||||||
|
element: Box::new(element) as Box<dyn Any>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for StatelessElementAdapter {
|
||||||
|
fn render<V: 'static>(self, _: &mut ViewContext<V>) -> AnyElement<V> {
|
||||||
|
*self
|
||||||
|
.element
|
||||||
|
.downcast::<AnyElement<V>>()
|
||||||
|
.expect("Don't move elements out of their view :(")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For converting elements into stateful components
|
||||||
|
pub struct StatefulElementAdapter<V: 'static> {
|
||||||
element: AnyElement<V>,
|
element: AnyElement<V>,
|
||||||
_phantom: std::marker::PhantomData<V>,
|
_phantom: std::marker::PhantomData<V>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> ElementAdapter<V> {
|
impl<V: 'static> StatefulElementAdapter<V> {
|
||||||
pub fn new(element: AnyElement<V>) -> Self {
|
pub fn new(element: AnyElement<V>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
element,
|
element,
|
||||||
@ -98,20 +237,35 @@ impl<V: View> ElementAdapter<V> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> Component<V> for ElementAdapter<V> {
|
impl<V: 'static> StatefulComponent<V> for StatefulElementAdapter<V> {
|
||||||
fn render(self, _: &mut V, _: &mut ViewContext<V>) -> AnyElement<V> {
|
fn render(self, _: &mut V, _: &mut ViewContext<V>) -> AnyElement<V> {
|
||||||
self.element
|
self.element
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Component -> Element
|
/// A convenient shorthand for creating an empty component.
|
||||||
pub struct ComponentAdapter<V: View, E> {
|
impl Component for () {
|
||||||
|
fn render<V: 'static>(self, _: &mut ViewContext<V>) -> AnyElement<V> {
|
||||||
|
Empty::new().into_any()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Stylable for () {
|
||||||
|
type Style = ();
|
||||||
|
|
||||||
|
fn with_style(self, _: Self::Style) -> Self {
|
||||||
|
()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For converting components back into Elements
|
||||||
|
pub struct ComponentAdapter<V: 'static, E> {
|
||||||
component: Option<E>,
|
component: Option<E>,
|
||||||
element: Option<AnyElement<V>>,
|
element: Option<AnyElement<V>>,
|
||||||
phantom: PhantomData<V>,
|
phantom: PhantomData<V>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<E, V: View> ComponentAdapter<V, E> {
|
impl<E, V: 'static> ComponentAdapter<V, E> {
|
||||||
pub fn new(e: E) -> Self {
|
pub fn new(e: E) -> Self {
|
||||||
Self {
|
Self {
|
||||||
component: Some(e),
|
component: Some(e),
|
||||||
@ -121,7 +275,7 @@ impl<E, V: View> ComponentAdapter<V, E> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View, C: Component<V> + 'static> Element<V> for ComponentAdapter<V, C> {
|
impl<V: 'static, C: StatefulComponent<V> + 'static> Element<V> for ComponentAdapter<V, C> {
|
||||||
type LayoutState = ();
|
type LayoutState = ();
|
||||||
|
|
||||||
type PaintState = ();
|
type PaintState = ();
|
||||||
@ -184,6 +338,7 @@ impl<V: View, C: Component<V> + 'static> Element<V> for ComponentAdapter<V, C> {
|
|||||||
) -> serde_json::Value {
|
) -> serde_json::Value {
|
||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"type": "ComponentAdapter",
|
"type": "ComponentAdapter",
|
||||||
|
"component": std::any::type_name::<C>(),
|
||||||
"child": self.element.as_ref().map(|el| el.debug(view, cx)),
|
"child": self.element.as_ref().map(|el| el.debug(view, cx)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -5,21 +5,21 @@ use serde_json::json;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
geometry::{rect::RectF, vector::Vector2F},
|
geometry::{rect::RectF, vector::Vector2F},
|
||||||
json, AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View,
|
json, AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint,
|
||||||
ViewContext,
|
ViewContext,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct ConstrainedBox<V: View> {
|
pub struct ConstrainedBox<V> {
|
||||||
child: AnyElement<V>,
|
child: AnyElement<V>,
|
||||||
constraint: Constraint<V>,
|
constraint: Constraint<V>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum Constraint<V: View> {
|
pub enum Constraint<V> {
|
||||||
Static(SizeConstraint),
|
Static(SizeConstraint),
|
||||||
Dynamic(Box<dyn FnMut(SizeConstraint, &mut V, &mut LayoutContext<V>) -> SizeConstraint>),
|
Dynamic(Box<dyn FnMut(SizeConstraint, &mut V, &mut LayoutContext<V>) -> SizeConstraint>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> ToJson for Constraint<V> {
|
impl<V> ToJson for Constraint<V> {
|
||||||
fn to_json(&self) -> serde_json::Value {
|
fn to_json(&self) -> serde_json::Value {
|
||||||
match self {
|
match self {
|
||||||
Constraint::Static(constraint) => constraint.to_json(),
|
Constraint::Static(constraint) => constraint.to_json(),
|
||||||
@ -28,7 +28,7 @@ impl<V: View> ToJson for Constraint<V> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> ConstrainedBox<V> {
|
impl<V: 'static> ConstrainedBox<V> {
|
||||||
pub fn new(child: impl Element<V>) -> Self {
|
pub fn new(child: impl Element<V>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
child: child.into_any(),
|
child: child.into_any(),
|
||||||
@ -132,7 +132,7 @@ impl<V: View> ConstrainedBox<V> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> Element<V> for ConstrainedBox<V> {
|
impl<V: 'static> Element<V> for ConstrainedBox<V> {
|
||||||
type LayoutState = ();
|
type LayoutState = ();
|
||||||
type PaintState = ();
|
type PaintState = ();
|
||||||
|
|
||||||
|
@ -10,8 +10,7 @@ use crate::{
|
|||||||
json::ToJson,
|
json::ToJson,
|
||||||
platform::CursorStyle,
|
platform::CursorStyle,
|
||||||
scene::{self, Border, CornerRadii, CursorRegion, Quad},
|
scene::{self, Border, CornerRadii, CursorRegion, Quad},
|
||||||
AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View,
|
AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, ViewContext,
|
||||||
ViewContext,
|
|
||||||
};
|
};
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@ -45,14 +44,22 @@ impl ContainerStyle {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn additional_length(&self) -> f32 {
|
||||||
|
self.padding.left
|
||||||
|
+ self.padding.right
|
||||||
|
+ self.border.width * 2.
|
||||||
|
+ self.margin.left
|
||||||
|
+ self.margin.right
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Container<V: View> {
|
pub struct Container<V> {
|
||||||
child: AnyElement<V>,
|
child: AnyElement<V>,
|
||||||
style: ContainerStyle,
|
style: ContainerStyle,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> Container<V> {
|
impl<V> Container<V> {
|
||||||
pub fn new(child: AnyElement<V>) -> Self {
|
pub fn new(child: AnyElement<V>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
child,
|
child,
|
||||||
@ -199,7 +206,7 @@ impl<V: View> Container<V> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> Element<V> for Container<V> {
|
impl<V: 'static> Element<V> for Container<V> {
|
||||||
type LayoutState = ();
|
type LayoutState = ();
|
||||||
type PaintState = ();
|
type PaintState = ();
|
||||||
|
|
||||||
@ -350,8 +357,8 @@ impl ToJson for ContainerStyle {
|
|||||||
#[derive(Clone, Copy, Debug, Default, JsonSchema)]
|
#[derive(Clone, Copy, Debug, Default, JsonSchema)]
|
||||||
pub struct Margin {
|
pub struct Margin {
|
||||||
pub top: f32,
|
pub top: f32,
|
||||||
pub left: f32,
|
|
||||||
pub bottom: f32,
|
pub bottom: f32,
|
||||||
|
pub left: f32,
|
||||||
pub right: f32,
|
pub right: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ use crate::{
|
|||||||
vector::{vec2f, Vector2F},
|
vector::{vec2f, Vector2F},
|
||||||
},
|
},
|
||||||
json::{json, ToJson},
|
json::{json, ToJson},
|
||||||
LayoutContext, PaintContext, SceneBuilder, View, ViewContext,
|
LayoutContext, PaintContext, SceneBuilder, ViewContext,
|
||||||
};
|
};
|
||||||
use crate::{Element, SizeConstraint};
|
use crate::{Element, SizeConstraint};
|
||||||
|
|
||||||
@ -26,7 +26,7 @@ impl Empty {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> Element<V> for Empty {
|
impl<V: 'static> Element<V> for Empty {
|
||||||
type LayoutState = ();
|
type LayoutState = ();
|
||||||
type PaintState = ();
|
type PaintState = ();
|
||||||
|
|
||||||
|
@ -2,18 +2,18 @@ use std::ops::Range;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
geometry::{rect::RectF, vector::Vector2F},
|
geometry::{rect::RectF, vector::Vector2F},
|
||||||
json, AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View,
|
json, AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint,
|
||||||
ViewContext,
|
ViewContext,
|
||||||
};
|
};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
pub struct Expanded<V: View> {
|
pub struct Expanded<V> {
|
||||||
child: AnyElement<V>,
|
child: AnyElement<V>,
|
||||||
full_width: bool,
|
full_width: bool,
|
||||||
full_height: bool,
|
full_height: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> Expanded<V> {
|
impl<V: 'static> Expanded<V> {
|
||||||
pub fn new(child: impl Element<V>) -> Self {
|
pub fn new(child: impl Element<V>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
child: child.into_any(),
|
child: child.into_any(),
|
||||||
@ -35,7 +35,7 @@ impl<V: View> Expanded<V> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> Element<V> for Expanded<V> {
|
impl<V: 'static> Element<V> for Expanded<V> {
|
||||||
type LayoutState = ();
|
type LayoutState = ();
|
||||||
type PaintState = ();
|
type PaintState = ();
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ use std::{any::Any, cell::Cell, f32::INFINITY, ops::Range, rc::Rc};
|
|||||||
use crate::{
|
use crate::{
|
||||||
json::{self, ToJson, Value},
|
json::{self, ToJson, Value},
|
||||||
AnyElement, Axis, Element, ElementStateHandle, LayoutContext, PaintContext, SceneBuilder,
|
AnyElement, Axis, Element, ElementStateHandle, LayoutContext, PaintContext, SceneBuilder,
|
||||||
SizeConstraint, Vector2FExt, View, ViewContext,
|
SizeConstraint, Vector2FExt, ViewContext,
|
||||||
};
|
};
|
||||||
use pathfinder_geometry::{
|
use pathfinder_geometry::{
|
||||||
rect::RectF,
|
rect::RectF,
|
||||||
@ -17,20 +17,22 @@ struct ScrollState {
|
|||||||
scroll_position: Cell<f32>,
|
scroll_position: Cell<f32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Flex<V: View> {
|
pub struct Flex<V> {
|
||||||
axis: Axis,
|
axis: Axis,
|
||||||
children: Vec<AnyElement<V>>,
|
children: Vec<AnyElement<V>>,
|
||||||
scroll_state: Option<(ElementStateHandle<Rc<ScrollState>>, usize)>,
|
scroll_state: Option<(ElementStateHandle<Rc<ScrollState>>, usize)>,
|
||||||
child_alignment: f32,
|
child_alignment: f32,
|
||||||
|
spacing: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> Flex<V> {
|
impl<V: 'static> Flex<V> {
|
||||||
pub fn new(axis: Axis) -> Self {
|
pub fn new(axis: Axis) -> Self {
|
||||||
Self {
|
Self {
|
||||||
axis,
|
axis,
|
||||||
children: Default::default(),
|
children: Default::default(),
|
||||||
scroll_state: None,
|
scroll_state: None,
|
||||||
child_alignment: -1.,
|
child_alignment: -1.,
|
||||||
|
spacing: 0.,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,6 +53,11 @@ impl<V: View> Flex<V> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_spacing(mut self, spacing: f32) -> Self {
|
||||||
|
self.spacing = spacing;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn scrollable<Tag>(
|
pub fn scrollable<Tag>(
|
||||||
mut self,
|
mut self,
|
||||||
element_id: usize,
|
element_id: usize,
|
||||||
@ -81,7 +88,8 @@ impl<V: View> Flex<V> {
|
|||||||
cx: &mut LayoutContext<V>,
|
cx: &mut LayoutContext<V>,
|
||||||
) {
|
) {
|
||||||
let cross_axis = self.axis.invert();
|
let cross_axis = self.axis.invert();
|
||||||
for child in &mut self.children {
|
let last = self.children.len() - 1;
|
||||||
|
for (ix, child) in &mut self.children.iter_mut().enumerate() {
|
||||||
if let Some(metadata) = child.metadata::<FlexParentData>() {
|
if let Some(metadata) = child.metadata::<FlexParentData>() {
|
||||||
if let Some((flex, expanded)) = metadata.flex {
|
if let Some((flex, expanded)) = metadata.flex {
|
||||||
if expanded != layout_expanded {
|
if expanded != layout_expanded {
|
||||||
@ -93,6 +101,10 @@ impl<V: View> Flex<V> {
|
|||||||
} else {
|
} else {
|
||||||
let space_per_flex = *remaining_space / *remaining_flex;
|
let space_per_flex = *remaining_space / *remaining_flex;
|
||||||
space_per_flex * flex
|
space_per_flex * flex
|
||||||
|
} - if ix == 0 || ix == last {
|
||||||
|
self.spacing / 2.
|
||||||
|
} else {
|
||||||
|
self.spacing
|
||||||
};
|
};
|
||||||
let child_min = if expanded { child_max } else { 0. };
|
let child_min = if expanded { child_max } else { 0. };
|
||||||
let child_constraint = match self.axis {
|
let child_constraint = match self.axis {
|
||||||
@ -115,13 +127,13 @@ impl<V: View> Flex<V> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> Extend<AnyElement<V>> for Flex<V> {
|
impl<V> Extend<AnyElement<V>> for Flex<V> {
|
||||||
fn extend<T: IntoIterator<Item = AnyElement<V>>>(&mut self, children: T) {
|
fn extend<T: IntoIterator<Item = AnyElement<V>>>(&mut self, children: T) {
|
||||||
self.children.extend(children);
|
self.children.extend(children);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> Element<V> for Flex<V> {
|
impl<V: 'static> Element<V> for Flex<V> {
|
||||||
type LayoutState = f32;
|
type LayoutState = f32;
|
||||||
type PaintState = ();
|
type PaintState = ();
|
||||||
|
|
||||||
@ -137,7 +149,8 @@ impl<V: View> Element<V> for Flex<V> {
|
|||||||
|
|
||||||
let cross_axis = self.axis.invert();
|
let cross_axis = self.axis.invert();
|
||||||
let mut cross_axis_max: f32 = 0.0;
|
let mut cross_axis_max: f32 = 0.0;
|
||||||
for child in &mut self.children {
|
let last = self.children.len().saturating_sub(1);
|
||||||
|
for (ix, child) in &mut self.children.iter_mut().enumerate() {
|
||||||
let metadata = child.metadata::<FlexParentData>();
|
let metadata = child.metadata::<FlexParentData>();
|
||||||
contains_float |= metadata.map_or(false, |metadata| metadata.float);
|
contains_float |= metadata.map_or(false, |metadata| metadata.float);
|
||||||
|
|
||||||
@ -155,7 +168,12 @@ impl<V: View> Element<V> for Flex<V> {
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
let size = child.layout(child_constraint, view, cx);
|
let size = child.layout(child_constraint, view, cx);
|
||||||
fixed_space += size.along(self.axis);
|
fixed_space += size.along(self.axis)
|
||||||
|
+ if ix == 0 || ix == last {
|
||||||
|
self.spacing / 2.
|
||||||
|
} else {
|
||||||
|
self.spacing
|
||||||
|
};
|
||||||
cross_axis_max = cross_axis_max.max(size.along(cross_axis));
|
cross_axis_max = cross_axis_max.max(size.along(cross_axis));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -315,7 +333,8 @@ impl<V: View> Element<V> for Flex<V> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for child in &mut self.children {
|
let last = self.children.len().saturating_sub(1);
|
||||||
|
for (ix, child) in &mut self.children.iter_mut().enumerate() {
|
||||||
if remaining_space > 0. {
|
if remaining_space > 0. {
|
||||||
if let Some(metadata) = child.metadata::<FlexParentData>() {
|
if let Some(metadata) = child.metadata::<FlexParentData>() {
|
||||||
if metadata.float {
|
if metadata.float {
|
||||||
@ -353,9 +372,11 @@ impl<V: View> Element<V> for Flex<V> {
|
|||||||
|
|
||||||
child.paint(scene, aligned_child_origin, visible_bounds, view, cx);
|
child.paint(scene, aligned_child_origin, visible_bounds, view, cx);
|
||||||
|
|
||||||
|
let spacing = if ix == last { 0. } else { self.spacing };
|
||||||
|
|
||||||
match self.axis {
|
match self.axis {
|
||||||
Axis::Horizontal => child_origin += vec2f(child.size().x(), 0.0),
|
Axis::Horizontal => child_origin += vec2f(child.size().x() + spacing, 0.0),
|
||||||
Axis::Vertical => child_origin += vec2f(0.0, child.size().y()),
|
Axis::Vertical => child_origin += vec2f(0.0, child.size().y() + spacing),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -401,12 +422,12 @@ struct FlexParentData {
|
|||||||
float: bool,
|
float: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct FlexItem<V: View> {
|
pub struct FlexItem<V> {
|
||||||
metadata: FlexParentData,
|
metadata: FlexParentData,
|
||||||
child: AnyElement<V>,
|
child: AnyElement<V>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> FlexItem<V> {
|
impl<V: 'static> FlexItem<V> {
|
||||||
pub fn new(child: impl Element<V>) -> Self {
|
pub fn new(child: impl Element<V>) -> Self {
|
||||||
FlexItem {
|
FlexItem {
|
||||||
metadata: FlexParentData {
|
metadata: FlexParentData {
|
||||||
@ -428,7 +449,7 @@ impl<V: View> FlexItem<V> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> Element<V> for FlexItem<V> {
|
impl<V: 'static> Element<V> for FlexItem<V> {
|
||||||
type LayoutState = ();
|
type LayoutState = ();
|
||||||
type PaintState = ();
|
type PaintState = ();
|
||||||
|
|
||||||
|
@ -3,16 +3,15 @@ use std::ops::Range;
|
|||||||
use crate::{
|
use crate::{
|
||||||
geometry::{rect::RectF, vector::Vector2F},
|
geometry::{rect::RectF, vector::Vector2F},
|
||||||
json::json,
|
json::json,
|
||||||
AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View,
|
AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, ViewContext,
|
||||||
ViewContext,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Hook<V: View> {
|
pub struct Hook<V> {
|
||||||
child: AnyElement<V>,
|
child: AnyElement<V>,
|
||||||
after_layout: Option<Box<dyn FnMut(Vector2F, &mut ViewContext<V>)>>,
|
after_layout: Option<Box<dyn FnMut(Vector2F, &mut ViewContext<V>)>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> Hook<V> {
|
impl<V: 'static> Hook<V> {
|
||||||
pub fn new(child: impl Element<V>) -> Self {
|
pub fn new(child: impl Element<V>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
child: child.into_any(),
|
child: child.into_any(),
|
||||||
@ -29,7 +28,7 @@ impl<V: View> Hook<V> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> Element<V> for Hook<V> {
|
impl<V: 'static> Element<V> for Hook<V> {
|
||||||
type LayoutState = ();
|
type LayoutState = ();
|
||||||
type PaintState = ();
|
type PaintState = ();
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ use crate::{
|
|||||||
},
|
},
|
||||||
json::{json, ToJson},
|
json::{json, ToJson},
|
||||||
scene, Border, Element, ImageData, LayoutContext, PaintContext, SceneBuilder, SizeConstraint,
|
scene, Border, Element, ImageData, LayoutContext, PaintContext, SceneBuilder, SizeConstraint,
|
||||||
View, ViewContext,
|
ViewContext,
|
||||||
};
|
};
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@ -57,7 +57,7 @@ impl Image {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> Element<V> for Image {
|
impl<V: 'static> Element<V> for Image {
|
||||||
type LayoutState = Option<Arc<ImageData>>;
|
type LayoutState = Option<Arc<ImageData>>;
|
||||||
type PaintState = ();
|
type PaintState = ();
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ impl KeystrokeLabel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> Element<V> for KeystrokeLabel {
|
impl<V: 'static> Element<V> for KeystrokeLabel {
|
||||||
type LayoutState = AnyElement<V>;
|
type LayoutState = AnyElement<V>;
|
||||||
type PaintState = ();
|
type PaintState = ();
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ use crate::{
|
|||||||
},
|
},
|
||||||
json::{ToJson, Value},
|
json::{ToJson, Value},
|
||||||
text_layout::{Line, RunStyle},
|
text_layout::{Line, RunStyle},
|
||||||
Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View, ViewContext,
|
Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, ViewContext,
|
||||||
};
|
};
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@ -128,7 +128,7 @@ impl Label {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> Element<V> for Label {
|
impl<V: 'static> Element<V> for Label {
|
||||||
type LayoutState = Line;
|
type LayoutState = Line;
|
||||||
type PaintState = ();
|
type PaintState = ();
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user