mirror of
https://github.com/zed-industries/zed.git
synced 2024-09-18 18:08:07 +03:00
Remove 2 suffix from gpui
Co-authored-by: Mikayla <mikayla@zed.dev>
This commit is contained in:
parent
3c81dda8e2
commit
f5ba22659b
207
Cargo.lock
generated
207
Cargo.lock
generated
@ -10,7 +10,7 @@ dependencies = [
|
|||||||
"auto_update",
|
"auto_update",
|
||||||
"editor",
|
"editor",
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"language",
|
"language",
|
||||||
"project",
|
"project",
|
||||||
"settings",
|
"settings",
|
||||||
@ -82,7 +82,7 @@ dependencies = [
|
|||||||
"async-trait",
|
"async-trait",
|
||||||
"bincode",
|
"bincode",
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"isahc",
|
"isahc",
|
||||||
"language",
|
"language",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
@ -312,7 +312,7 @@ dependencies = [
|
|||||||
"env_logger",
|
"env_logger",
|
||||||
"fs",
|
"fs",
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"indoc",
|
"indoc",
|
||||||
"isahc",
|
"isahc",
|
||||||
"language",
|
"language",
|
||||||
@ -662,7 +662,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"collections",
|
"collections",
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"log",
|
"log",
|
||||||
"parking_lot 0.11.2",
|
"parking_lot 0.11.2",
|
||||||
"rodio",
|
"rodio",
|
||||||
@ -676,7 +676,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"client",
|
"client",
|
||||||
"db",
|
"db",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"isahc",
|
"isahc",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
@ -1010,7 +1010,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"collections",
|
"collections",
|
||||||
"editor",
|
"editor",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"itertools 0.10.5",
|
"itertools 0.10.5",
|
||||||
"language",
|
"language",
|
||||||
"outline",
|
"outline",
|
||||||
@ -1111,7 +1111,7 @@ dependencies = [
|
|||||||
"collections",
|
"collections",
|
||||||
"fs",
|
"fs",
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"image",
|
"image",
|
||||||
"language",
|
"language",
|
||||||
"live_kit_client",
|
"live_kit_client",
|
||||||
@ -1200,7 +1200,7 @@ dependencies = [
|
|||||||
"db",
|
"db",
|
||||||
"feature_flags",
|
"feature_flags",
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"image",
|
"image",
|
||||||
"language",
|
"language",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
@ -1374,7 +1374,7 @@ dependencies = [
|
|||||||
"db",
|
"db",
|
||||||
"feature_flags",
|
"feature_flags",
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"image",
|
"image",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
@ -1470,7 +1470,7 @@ dependencies = [
|
|||||||
"fs",
|
"fs",
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"git",
|
"git",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"hyper",
|
"hyper",
|
||||||
"indoc",
|
"indoc",
|
||||||
"language",
|
"language",
|
||||||
@ -1534,7 +1534,7 @@ dependencies = [
|
|||||||
"feedback",
|
"feedback",
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"language",
|
"language",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
@ -1603,7 +1603,7 @@ dependencies = [
|
|||||||
"env_logger",
|
"env_logger",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
"go_to_line",
|
"go_to_line",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"language",
|
"language",
|
||||||
"menu",
|
"menu",
|
||||||
"picker",
|
"picker",
|
||||||
@ -1702,7 +1702,7 @@ dependencies = [
|
|||||||
"collections",
|
"collections",
|
||||||
"fs",
|
"fs",
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"language",
|
"language",
|
||||||
"log",
|
"log",
|
||||||
"lsp",
|
"lsp",
|
||||||
@ -1727,7 +1727,7 @@ dependencies = [
|
|||||||
"editor",
|
"editor",
|
||||||
"fs",
|
"fs",
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"language",
|
"language",
|
||||||
"settings",
|
"settings",
|
||||||
"smol",
|
"smol",
|
||||||
@ -2127,7 +2127,7 @@ dependencies = [
|
|||||||
"async-trait",
|
"async-trait",
|
||||||
"collections",
|
"collections",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"indoc",
|
"indoc",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
@ -2229,7 +2229,7 @@ dependencies = [
|
|||||||
"collections",
|
"collections",
|
||||||
"editor",
|
"editor",
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"language",
|
"language",
|
||||||
"log",
|
"log",
|
||||||
"lsp",
|
"lsp",
|
||||||
@ -2397,7 +2397,7 @@ dependencies = [
|
|||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
"git",
|
"git",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"indoc",
|
"indoc",
|
||||||
"itertools 0.10.5",
|
"itertools 0.10.5",
|
||||||
"language",
|
"language",
|
||||||
@ -2606,7 +2606,7 @@ name = "feature_flags"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"gpui2",
|
"gpui",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2619,7 +2619,7 @@ dependencies = [
|
|||||||
"db",
|
"db",
|
||||||
"editor",
|
"editor",
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"human_bytes",
|
"human_bytes",
|
||||||
"isahc",
|
"isahc",
|
||||||
"language",
|
"language",
|
||||||
@ -2653,7 +2653,7 @@ dependencies = [
|
|||||||
"editor",
|
"editor",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"language",
|
"language",
|
||||||
"menu",
|
"menu",
|
||||||
"picker",
|
"picker",
|
||||||
@ -2822,7 +2822,7 @@ dependencies = [
|
|||||||
"fsevent",
|
"fsevent",
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"git2",
|
"git2",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
@ -3014,7 +3014,7 @@ dependencies = [
|
|||||||
name = "fuzzy"
|
name = "fuzzy"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gpui2",
|
"gpui",
|
||||||
"util",
|
"util",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -3149,7 +3149,7 @@ name = "go_to_line"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"editor",
|
"editor",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"menu",
|
"menu",
|
||||||
"postage",
|
"postage",
|
||||||
"serde",
|
"serde",
|
||||||
@ -3164,68 +3164,6 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui"
|
name = "gpui"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"async-task",
|
|
||||||
"backtrace",
|
|
||||||
"bindgen 0.65.1",
|
|
||||||
"block",
|
|
||||||
"cc",
|
|
||||||
"cocoa",
|
|
||||||
"collections",
|
|
||||||
"core-foundation",
|
|
||||||
"core-graphics",
|
|
||||||
"core-text",
|
|
||||||
"ctor",
|
|
||||||
"derive_more",
|
|
||||||
"dhat",
|
|
||||||
"env_logger",
|
|
||||||
"etagere",
|
|
||||||
"font-kit",
|
|
||||||
"foreign-types",
|
|
||||||
"futures 0.3.28",
|
|
||||||
"gpui_macros",
|
|
||||||
"image",
|
|
||||||
"itertools 0.10.5",
|
|
||||||
"lazy_static",
|
|
||||||
"log",
|
|
||||||
"media",
|
|
||||||
"metal",
|
|
||||||
"num_cpus",
|
|
||||||
"objc",
|
|
||||||
"ordered-float 2.10.0",
|
|
||||||
"parking",
|
|
||||||
"parking_lot 0.11.2",
|
|
||||||
"pathfinder_color",
|
|
||||||
"pathfinder_geometry",
|
|
||||||
"png",
|
|
||||||
"postage",
|
|
||||||
"rand 0.8.5",
|
|
||||||
"refineable",
|
|
||||||
"resvg",
|
|
||||||
"schemars",
|
|
||||||
"seahash",
|
|
||||||
"serde",
|
|
||||||
"serde_derive",
|
|
||||||
"serde_json",
|
|
||||||
"simplelog",
|
|
||||||
"smallvec",
|
|
||||||
"smol",
|
|
||||||
"sqlez",
|
|
||||||
"sum_tree",
|
|
||||||
"taffy 0.3.11 (git+https://github.com/DioxusLabs/taffy?rev=4fb530bdd71609bb1d3f76c6a8bde1ba82805d5e)",
|
|
||||||
"thiserror",
|
|
||||||
"time",
|
|
||||||
"tiny-skia",
|
|
||||||
"usvg",
|
|
||||||
"util",
|
|
||||||
"uuid 1.4.1",
|
|
||||||
"waker-fn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "gpui2"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-task",
|
"async-task",
|
||||||
@ -3277,7 +3215,7 @@ dependencies = [
|
|||||||
"smol",
|
"smol",
|
||||||
"sqlez",
|
"sqlez",
|
||||||
"sum_tree",
|
"sum_tree",
|
||||||
"taffy 0.3.11 (git+https://github.com/DioxusLabs/taffy?rev=1876f72bee5e376023eaa518aa7b8a34c769bd1b)",
|
"taffy",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"time",
|
"time",
|
||||||
"tiny-skia",
|
"tiny-skia",
|
||||||
@ -3306,12 +3244,6 @@ dependencies = [
|
|||||||
"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 = "grid"
|
name = "grid"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
@ -3694,7 +3626,7 @@ name = "install_cli"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"log",
|
"log",
|
||||||
"smol",
|
"smol",
|
||||||
"util",
|
"util",
|
||||||
@ -3847,7 +3779,7 @@ dependencies = [
|
|||||||
"chrono",
|
"chrono",
|
||||||
"dirs 4.0.0",
|
"dirs 4.0.0",
|
||||||
"editor",
|
"editor",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"log",
|
"log",
|
||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
@ -3940,7 +3872,7 @@ dependencies = [
|
|||||||
"fuzzy",
|
"fuzzy",
|
||||||
"git",
|
"git",
|
||||||
"globset",
|
"globset",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"indoc",
|
"indoc",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
@ -3985,7 +3917,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"editor",
|
"editor",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"language",
|
"language",
|
||||||
"picker",
|
"picker",
|
||||||
"project",
|
"project",
|
||||||
@ -4006,7 +3938,7 @@ dependencies = [
|
|||||||
"editor",
|
"editor",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"language",
|
"language",
|
||||||
"lsp",
|
"lsp",
|
||||||
"project",
|
"project",
|
||||||
@ -4181,7 +4113,7 @@ dependencies = [
|
|||||||
"core-graphics",
|
"core-graphics",
|
||||||
"foreign-types",
|
"foreign-types",
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"hmac 0.12.1",
|
"hmac 0.12.1",
|
||||||
"jwt",
|
"jwt",
|
||||||
"live_kit_server",
|
"live_kit_server",
|
||||||
@ -4247,7 +4179,7 @@ dependencies = [
|
|||||||
"ctor",
|
"ctor",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"log",
|
"log",
|
||||||
"lsp-types",
|
"lsp-types",
|
||||||
"parking_lot 0.11.2",
|
"parking_lot 0.11.2",
|
||||||
@ -4399,7 +4331,7 @@ dependencies = [
|
|||||||
name = "menu"
|
name = "menu"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gpui2",
|
"gpui",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -4569,7 +4501,7 @@ dependencies = [
|
|||||||
"env_logger",
|
"env_logger",
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"git",
|
"git",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"indoc",
|
"indoc",
|
||||||
"itertools 0.10.5",
|
"itertools 0.10.5",
|
||||||
"language",
|
"language",
|
||||||
@ -4761,7 +4693,7 @@ dependencies = [
|
|||||||
"collections",
|
"collections",
|
||||||
"db",
|
"db",
|
||||||
"feature_flags",
|
"feature_flags",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"rpc",
|
"rpc",
|
||||||
"settings",
|
"settings",
|
||||||
"sum_tree",
|
"sum_tree",
|
||||||
@ -5162,7 +5094,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"editor",
|
"editor",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"language",
|
"language",
|
||||||
"ordered-float 2.10.0",
|
"ordered-float 2.10.0",
|
||||||
"picker",
|
"picker",
|
||||||
@ -5386,7 +5318,7 @@ dependencies = [
|
|||||||
"ctor",
|
"ctor",
|
||||||
"editor",
|
"editor",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"menu",
|
"menu",
|
||||||
"parking_lot 0.11.2",
|
"parking_lot 0.11.2",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@ -5570,7 +5502,7 @@ dependencies = [
|
|||||||
"collections",
|
"collections",
|
||||||
"fs",
|
"fs",
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"language",
|
"language",
|
||||||
"log",
|
"log",
|
||||||
"lsp",
|
"lsp",
|
||||||
@ -5687,7 +5619,7 @@ dependencies = [
|
|||||||
"git",
|
"git",
|
||||||
"git2",
|
"git2",
|
||||||
"globset",
|
"globset",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"ignore",
|
"ignore",
|
||||||
"itertools 0.10.5",
|
"itertools 0.10.5",
|
||||||
"language",
|
"language",
|
||||||
@ -5730,7 +5662,7 @@ dependencies = [
|
|||||||
"db",
|
"db",
|
||||||
"editor",
|
"editor",
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"language",
|
"language",
|
||||||
"menu",
|
"menu",
|
||||||
"postage",
|
"postage",
|
||||||
@ -5758,7 +5690,7 @@ dependencies = [
|
|||||||
"editor",
|
"editor",
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"language",
|
"language",
|
||||||
"lsp",
|
"lsp",
|
||||||
"ordered-float 2.10.0",
|
"ordered-float 2.10.0",
|
||||||
@ -5935,7 +5867,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"assistant",
|
"assistant",
|
||||||
"editor",
|
"editor",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"search",
|
"search",
|
||||||
"ui",
|
"ui",
|
||||||
"workspace",
|
"workspace",
|
||||||
@ -6109,7 +6041,7 @@ dependencies = [
|
|||||||
"editor",
|
"editor",
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"language",
|
"language",
|
||||||
"ordered-float 2.10.0",
|
"ordered-float 2.10.0",
|
||||||
"picker",
|
"picker",
|
||||||
@ -6306,7 +6238,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"collections",
|
"collections",
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"language",
|
"language",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"pulldown-cmark",
|
"pulldown-cmark",
|
||||||
@ -6397,7 +6329,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayvec 0.7.4",
|
"arrayvec 0.7.4",
|
||||||
"bromberg_sl2",
|
"bromberg_sl2",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"log",
|
"log",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
@ -6427,7 +6359,7 @@ dependencies = [
|
|||||||
"ctor",
|
"ctor",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"parking_lot 0.11.2",
|
"parking_lot 0.11.2",
|
||||||
"prost 0.8.0",
|
"prost 0.8.0",
|
||||||
"prost-build",
|
"prost-build",
|
||||||
@ -6887,7 +6819,7 @@ dependencies = [
|
|||||||
"collections",
|
"collections",
|
||||||
"editor",
|
"editor",
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"language",
|
"language",
|
||||||
"log",
|
"log",
|
||||||
"menu",
|
"menu",
|
||||||
@ -6943,7 +6875,7 @@ dependencies = [
|
|||||||
"env_logger",
|
"env_logger",
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"globset",
|
"globset",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"language",
|
"language",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
@ -7110,7 +7042,7 @@ dependencies = [
|
|||||||
"feature_flags",
|
"feature_flags",
|
||||||
"fs",
|
"fs",
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"indoc",
|
"indoc",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"postage",
|
"postage",
|
||||||
@ -7714,7 +7646,7 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
|||||||
name = "story"
|
name = "story"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gpui2",
|
"gpui",
|
||||||
"itertools 0.10.5",
|
"itertools 0.10.5",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
]
|
]
|
||||||
@ -7730,7 +7662,7 @@ dependencies = [
|
|||||||
"dialoguer",
|
"dialoguer",
|
||||||
"editor",
|
"editor",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"indoc",
|
"indoc",
|
||||||
"itertools 0.11.0",
|
"itertools 0.11.0",
|
||||||
"language",
|
"language",
|
||||||
@ -7957,18 +7889,7 @@ version = "0.3.11"
|
|||||||
source = "git+https://github.com/DioxusLabs/taffy?rev=1876f72bee5e376023eaa518aa7b8a34c769bd1b#1876f72bee5e376023eaa518aa7b8a34c769bd1b"
|
source = "git+https://github.com/DioxusLabs/taffy?rev=1876f72bee5e376023eaa518aa7b8a34c769bd1b#1876f72bee5e376023eaa518aa7b8a34c769bd1b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayvec 0.7.4",
|
"arrayvec 0.7.4",
|
||||||
"grid 0.11.0",
|
"grid",
|
||||||
"num-traits",
|
|
||||||
"slotmap",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "taffy"
|
|
||||||
version = "0.3.11"
|
|
||||||
source = "git+https://github.com/DioxusLabs/taffy?rev=4fb530bdd71609bb1d3f76c6a8bde1ba82805d5e#4fb530bdd71609bb1d3f76c6a8bde1ba82805d5e"
|
|
||||||
dependencies = [
|
|
||||||
"arrayvec 0.7.4",
|
|
||||||
"grid 0.10.0",
|
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"slotmap",
|
"slotmap",
|
||||||
]
|
]
|
||||||
@ -8032,7 +7953,7 @@ dependencies = [
|
|||||||
"db",
|
"db",
|
||||||
"dirs 4.0.0",
|
"dirs 4.0.0",
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"itertools 0.10.5",
|
"itertools 0.10.5",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"libc",
|
"libc",
|
||||||
@ -8062,7 +7983,7 @@ dependencies = [
|
|||||||
"dirs 4.0.0",
|
"dirs 4.0.0",
|
||||||
"editor",
|
"editor",
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"itertools 0.10.5",
|
"itertools 0.10.5",
|
||||||
"language",
|
"language",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
@ -8096,7 +8017,7 @@ dependencies = [
|
|||||||
"ctor",
|
"ctor",
|
||||||
"digest 0.9.0",
|
"digest 0.9.0",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
"parking_lot 0.11.2",
|
"parking_lot 0.11.2",
|
||||||
@ -8121,7 +8042,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"fs",
|
"fs",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"indexmap 1.9.3",
|
"indexmap 1.9.3",
|
||||||
"itertools 0.11.0",
|
"itertools 0.11.0",
|
||||||
"parking_lot 0.11.2",
|
"parking_lot 0.11.2",
|
||||||
@ -8145,7 +8066,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"clap 4.4.4",
|
"clap 4.4.4",
|
||||||
"convert_case 0.6.0",
|
"convert_case 0.6.0",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"indexmap 1.9.3",
|
"indexmap 1.9.3",
|
||||||
"json_comments",
|
"json_comments",
|
||||||
"log",
|
"log",
|
||||||
@ -8168,7 +8089,7 @@ dependencies = [
|
|||||||
"feature_flags",
|
"feature_flags",
|
||||||
"fs",
|
"fs",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"log",
|
"log",
|
||||||
"parking_lot 0.11.2",
|
"parking_lot 0.11.2",
|
||||||
"picker",
|
"picker",
|
||||||
@ -8985,7 +8906,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"itertools 0.11.0",
|
"itertools 0.11.0",
|
||||||
"menu",
|
"menu",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
@ -9238,7 +9159,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"fs",
|
"fs",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"picker",
|
"picker",
|
||||||
"ui",
|
"ui",
|
||||||
"util",
|
"util",
|
||||||
@ -9263,7 +9184,7 @@ dependencies = [
|
|||||||
"diagnostics",
|
"diagnostics",
|
||||||
"editor",
|
"editor",
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"indoc",
|
"indoc",
|
||||||
"itertools 0.10.5",
|
"itertools 0.10.5",
|
||||||
"language",
|
"language",
|
||||||
@ -9682,7 +9603,7 @@ dependencies = [
|
|||||||
"editor",
|
"editor",
|
||||||
"fs",
|
"fs",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"install_cli",
|
"install_cli",
|
||||||
"log",
|
"log",
|
||||||
"picker",
|
"picker",
|
||||||
@ -9951,7 +9872,7 @@ dependencies = [
|
|||||||
"env_logger",
|
"env_logger",
|
||||||
"fs",
|
"fs",
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"indoc",
|
"indoc",
|
||||||
"install_cli",
|
"install_cli",
|
||||||
"itertools 0.10.5",
|
"itertools 0.10.5",
|
||||||
@ -10091,7 +10012,7 @@ dependencies = [
|
|||||||
"fsevent",
|
"fsevent",
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"go_to_line",
|
"go_to_line",
|
||||||
"gpui2",
|
"gpui",
|
||||||
"ignore",
|
"ignore",
|
||||||
"image",
|
"image",
|
||||||
"indexmap 1.9.3",
|
"indexmap 1.9.3",
|
||||||
@ -10187,7 +10108,7 @@ dependencies = [
|
|||||||
name = "zed_actions"
|
name = "zed_actions"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gpui2",
|
"gpui",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ members = [
|
|||||||
"crates/go_to_line",
|
"crates/go_to_line",
|
||||||
"crates/gpui",
|
"crates/gpui",
|
||||||
"crates/gpui_macros",
|
"crates/gpui_macros",
|
||||||
"crates/gpui2",
|
"crates/gpui",
|
||||||
"crates/gpui2_macros",
|
"crates/gpui2_macros",
|
||||||
"crates/install_cli",
|
"crates/install_cli",
|
||||||
"crates/journal",
|
"crates/journal",
|
||||||
|
@ -12,7 +12,7 @@ doctest = false
|
|||||||
auto_update = { path = "../auto_update" }
|
auto_update = { path = "../auto_update" }
|
||||||
editor = { path = "../editor" }
|
editor = { path = "../editor" }
|
||||||
language = { path = "../language" }
|
language = { path = "../language" }
|
||||||
gpui = { path = "../gpui2", package = "gpui2" }
|
gpui = { path = "../gpui" }
|
||||||
project = { path = "../project" }
|
project = { path = "../project" }
|
||||||
settings = { path = "../settings" }
|
settings = { path = "../settings" }
|
||||||
ui = { path = "../ui" }
|
ui = { path = "../ui" }
|
||||||
|
@ -12,7 +12,7 @@ doctest = false
|
|||||||
test-support = []
|
test-support = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
gpui = { package = "gpui2", path = "../gpui2" }
|
gpui = { path = "../gpui" }
|
||||||
util = { path = "../util" }
|
util = { path = "../util" }
|
||||||
language = { path = "../language" }
|
language = { path = "../language" }
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
@ -35,4 +35,4 @@ rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
|
|||||||
bincode = "1.3.3"
|
bincode = "1.3.3"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
|
@ -14,7 +14,7 @@ client = { path = "../client" }
|
|||||||
collections = { path = "../collections"}
|
collections = { path = "../collections"}
|
||||||
editor = { path = "../editor" }
|
editor = { path = "../editor" }
|
||||||
fs = { path = "../fs" }
|
fs = { path = "../fs" }
|
||||||
gpui = { package = "gpui2", path = "../gpui2" }
|
gpui = { path = "../gpui" }
|
||||||
language = { path = "../language" }
|
language = { path = "../language" }
|
||||||
menu = { path = "../menu" }
|
menu = { path = "../menu" }
|
||||||
multi_buffer = { path = "../multi_buffer" }
|
multi_buffer = { path = "../multi_buffer" }
|
||||||
|
@ -9,7 +9,7 @@ path = "src/audio.rs"
|
|||||||
doctest = false
|
doctest = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
gpui = { package = "gpui2", path = "../gpui2" }
|
gpui = { path = "../gpui" }
|
||||||
collections = { path = "../collections" }
|
collections = { path = "../collections" }
|
||||||
util = { path = "../util" }
|
util = { path = "../util" }
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ doctest = false
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
db = { path = "../db" }
|
db = { path = "../db" }
|
||||||
client = { path = "../client" }
|
client = { path = "../client" }
|
||||||
gpui = { package = "gpui2", path = "../gpui2" }
|
gpui = { path = "../gpui" }
|
||||||
menu = { path = "../menu" }
|
menu = { path = "../menu" }
|
||||||
project = { path = "../project" }
|
project = { path = "../project" }
|
||||||
settings = { path = "../settings" }
|
settings = { path = "../settings" }
|
||||||
|
@ -11,7 +11,7 @@ doctest = false
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
collections = { path = "../collections" }
|
collections = { path = "../collections" }
|
||||||
editor = { path = "../editor" }
|
editor = { path = "../editor" }
|
||||||
gpui = { package = "gpui2", path = "../gpui2" }
|
gpui = { path = "../gpui" }
|
||||||
ui = { path = "../ui" }
|
ui = { path = "../ui" }
|
||||||
language = { path = "../language" }
|
language = { path = "../language" }
|
||||||
project = { path = "../project" }
|
project = { path = "../project" }
|
||||||
@ -24,5 +24,5 @@ itertools = "0.10"
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
editor = { path = "../editor", features = ["test-support"] }
|
editor = { path = "../editor", features = ["test-support"] }
|
||||||
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
workspace = { path = "../workspace", features = ["test-support"] }
|
workspace = { path = "../workspace", features = ["test-support"] }
|
||||||
|
@ -22,7 +22,7 @@ test-support = [
|
|||||||
audio = { path = "../audio" }
|
audio = { path = "../audio" }
|
||||||
client = { path = "../client" }
|
client = { path = "../client" }
|
||||||
collections = { path = "../collections" }
|
collections = { path = "../collections" }
|
||||||
gpui = { package = "gpui2", path = "../gpui2" }
|
gpui = { path = "../gpui" }
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
live_kit_client = { path = "../live_kit_client" }
|
live_kit_client = { path = "../live_kit_client" }
|
||||||
fs = { path = "../fs" }
|
fs = { path = "../fs" }
|
||||||
@ -48,7 +48,7 @@ client = { path = "../client", features = ["test-support"] }
|
|||||||
fs = { path = "../fs", features = ["test-support"] }
|
fs = { path = "../fs", features = ["test-support"] }
|
||||||
language = { path = "../language", features = ["test-support"] }
|
language = { path = "../language", features = ["test-support"] }
|
||||||
collections = { path = "../collections", features = ["test-support"] }
|
collections = { path = "../collections", features = ["test-support"] }
|
||||||
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
live_kit_client = { path = "../live_kit_client", features = ["test-support"] }
|
live_kit_client = { path = "../live_kit_client", features = ["test-support"] }
|
||||||
project = { path = "../project", features = ["test-support"] }
|
project = { path = "../project", features = ["test-support"] }
|
||||||
util = { path = "../util", features = ["test-support"] }
|
util = { path = "../util", features = ["test-support"] }
|
||||||
|
@ -15,7 +15,7 @@ test-support = ["collections/test-support", "gpui/test-support", "rpc/test-suppo
|
|||||||
client = { path = "../client" }
|
client = { path = "../client" }
|
||||||
collections = { path = "../collections" }
|
collections = { path = "../collections" }
|
||||||
db = { path = "../db" }
|
db = { path = "../db" }
|
||||||
gpui = { package = "gpui2", path = "../gpui2" }
|
gpui = { path = "../gpui" }
|
||||||
util = { path = "../util" }
|
util = { path = "../util" }
|
||||||
rpc = { path = "../rpc" }
|
rpc = { path = "../rpc" }
|
||||||
text = { path = "../text" }
|
text = { path = "../text" }
|
||||||
@ -47,7 +47,7 @@ tempfile = "3"
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
collections = { path = "../collections", features = ["test-support"] }
|
collections = { path = "../collections", features = ["test-support"] }
|
||||||
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
rpc = { path = "../rpc", features = ["test-support"] }
|
rpc = { path = "../rpc", features = ["test-support"] }
|
||||||
client = { path = "../client", features = ["test-support"] }
|
client = { path = "../client", features = ["test-support"] }
|
||||||
settings = { path = "../settings", features = ["test-support"] }
|
settings = { path = "../settings", features = ["test-support"] }
|
||||||
|
@ -15,7 +15,7 @@ test-support = ["collections/test-support", "gpui/test-support", "rpc/test-suppo
|
|||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
collections = { path = "../collections" }
|
collections = { path = "../collections" }
|
||||||
db = { path = "../db" }
|
db = { path = "../db" }
|
||||||
gpui = { package = "gpui2", path = "../gpui2" }
|
gpui = { path = "../gpui" }
|
||||||
util = { path = "../util" }
|
util = { path = "../util" }
|
||||||
rpc = { path = "../rpc" }
|
rpc = { path = "../rpc" }
|
||||||
text = { path = "../text" }
|
text = { path = "../text" }
|
||||||
@ -47,7 +47,7 @@ url = "2.2"
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
collections = { path = "../collections", features = ["test-support"] }
|
collections = { path = "../collections", features = ["test-support"] }
|
||||||
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
rpc = { path = "../rpc", features = ["test-support"] }
|
rpc = { path = "../rpc", features = ["test-support"] }
|
||||||
settings = { path = "../settings", features = ["test-support"] }
|
settings = { path = "../settings", features = ["test-support"] }
|
||||||
util = { path = "../util", features = ["test-support"] }
|
util = { path = "../util", features = ["test-support"] }
|
||||||
|
@ -62,7 +62,7 @@ uuid.workspace = true
|
|||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
audio = { path = "../audio" }
|
audio = { path = "../audio" }
|
||||||
collections = { path = "../collections", features = ["test-support"] }
|
collections = { path = "../collections", features = ["test-support"] }
|
||||||
gpui = { package = "gpui2", path = "../gpui2", 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" }
|
channel = { path = "../channel" }
|
||||||
|
99
crates/collab2/Cargo.toml
Normal file
99
crates/collab2/Cargo.toml
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
[package]
|
||||||
|
authors = ["Nathan Sobo <nathan@zed.dev>"]
|
||||||
|
default-run = "collab"
|
||||||
|
edition = "2021"
|
||||||
|
name = "collab"
|
||||||
|
version = "0.28.0"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "collab"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "seed"
|
||||||
|
required-features = ["seed-support"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clock = { path = "../clock" }
|
||||||
|
collections = { path = "../collections" }
|
||||||
|
live_kit_server = { path = "../live_kit_server" }
|
||||||
|
text = { path = "../text" }
|
||||||
|
rpc = { path = "../rpc" }
|
||||||
|
util = { path = "../util" }
|
||||||
|
|
||||||
|
anyhow.workspace = true
|
||||||
|
async-tungstenite = "0.16"
|
||||||
|
axum = { version = "0.5", features = ["json", "headers", "ws"] }
|
||||||
|
axum-extra = { version = "0.3", features = ["erased-json"] }
|
||||||
|
base64 = "0.13"
|
||||||
|
clap = { version = "3.1", features = ["derive"], optional = true }
|
||||||
|
dashmap = "5.4"
|
||||||
|
envy = "0.4.2"
|
||||||
|
futures.workspace = true
|
||||||
|
hyper = "0.14"
|
||||||
|
lazy_static.workspace = true
|
||||||
|
lipsum = { version = "0.8", optional = true }
|
||||||
|
log.workspace = true
|
||||||
|
nanoid = "0.4"
|
||||||
|
parking_lot.workspace = true
|
||||||
|
prometheus = "0.13"
|
||||||
|
prost.workspace = true
|
||||||
|
rand.workspace = true
|
||||||
|
reqwest = { version = "0.11", features = ["json"], optional = true }
|
||||||
|
scrypt = "0.7"
|
||||||
|
smallvec.workspace = true
|
||||||
|
sea-orm = { version = "0.12.x", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls", "with-uuid"] }
|
||||||
|
serde.workspace = true
|
||||||
|
serde_derive.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
sha-1 = "0.9"
|
||||||
|
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "json", "time", "uuid", "any"] }
|
||||||
|
time.workspace = true
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
tokio-tungstenite = "0.17"
|
||||||
|
tonic = "0.6"
|
||||||
|
tower = "0.4"
|
||||||
|
toml.workspace = true
|
||||||
|
tracing = "0.1.34"
|
||||||
|
tracing-log = "0.1.3"
|
||||||
|
tracing-subscriber = { version = "0.3.11", features = ["env-filter", "json"] }
|
||||||
|
uuid.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
audio = { path = "../audio" }
|
||||||
|
collections = { path = "../collections", features = ["test-support"] }
|
||||||
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
|
call = { path = "../call", features = ["test-support"] }
|
||||||
|
client = { path = "../client", features = ["test-support"] }
|
||||||
|
channel = { path = "../channel" }
|
||||||
|
editor = { path = "../editor", features = ["test-support"] }
|
||||||
|
language = { path = "../language", features = ["test-support"] }
|
||||||
|
fs = { path = "../fs", features = ["test-support"] }
|
||||||
|
git = { path = "../git", features = ["test-support"] }
|
||||||
|
live_kit_client = { path = "../live_kit_client", features = ["test-support"] }
|
||||||
|
lsp = { path = "../lsp", features = ["test-support"] }
|
||||||
|
node_runtime = { path = "../node_runtime" }
|
||||||
|
notifications = { path = "../notifications", features = ["test-support"] }
|
||||||
|
|
||||||
|
project = { path = "../project", features = ["test-support"] }
|
||||||
|
rpc = { path = "../rpc", features = ["test-support"] }
|
||||||
|
settings = { path = "../settings", features = ["test-support"] }
|
||||||
|
theme = { path = "../theme" }
|
||||||
|
workspace = { path = "../workspace", features = ["test-support"] }
|
||||||
|
|
||||||
|
collab_ui = { path = "../collab_ui", features = ["test-support"] }
|
||||||
|
|
||||||
|
async-trait.workspace = true
|
||||||
|
pretty_assertions.workspace = true
|
||||||
|
ctor.workspace = true
|
||||||
|
env_logger.workspace = true
|
||||||
|
indoc.workspace = true
|
||||||
|
util = { path = "../util" }
|
||||||
|
lazy_static.workspace = true
|
||||||
|
sea-orm = { version = "0.12.x", features = ["sqlx-sqlite"] }
|
||||||
|
serde_json.workspace = true
|
||||||
|
sqlx = { version = "0.7", features = ["sqlite"] }
|
||||||
|
unindent.workspace = true
|
||||||
|
|
||||||
|
[features]
|
||||||
|
seed-support = ["clap", "lipsum", "reqwest"]
|
@ -34,7 +34,7 @@ collections = { path = "../collections" }
|
|||||||
editor = { path = "../editor" }
|
editor = { path = "../editor" }
|
||||||
feedback = { path = "../feedback" }
|
feedback = { path = "../feedback" }
|
||||||
fuzzy = { path = "../fuzzy" }
|
fuzzy = { path = "../fuzzy" }
|
||||||
gpui = { package = "gpui2", path = "../gpui2" }
|
gpui = { path = "../gpui" }
|
||||||
language = { path = "../language" }
|
language = { path = "../language" }
|
||||||
menu = { path = "../menu" }
|
menu = { path = "../menu" }
|
||||||
notifications = { path = "../notifications" }
|
notifications = { path = "../notifications" }
|
||||||
@ -69,7 +69,7 @@ call = { path = "../call", features = ["test-support"] }
|
|||||||
client = { path = "../client", features = ["test-support"] }
|
client = { path = "../client", features = ["test-support"] }
|
||||||
collections = { path = "../collections", features = ["test-support"] }
|
collections = { path = "../collections", features = ["test-support"] }
|
||||||
editor = { path = "../editor", features = ["test-support"] }
|
editor = { path = "../editor", features = ["test-support"] }
|
||||||
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
notifications = { path = "../notifications", features = ["test-support"] }
|
notifications = { path = "../notifications", features = ["test-support"] }
|
||||||
project = { path = "../project", features = ["test-support"] }
|
project = { path = "../project", features = ["test-support"] }
|
||||||
rpc = { path = "../rpc", features = ["test-support"] }
|
rpc = { path = "../rpc", features = ["test-support"] }
|
||||||
|
@ -12,7 +12,7 @@ doctest = false
|
|||||||
collections = { path = "../collections" }
|
collections = { path = "../collections" }
|
||||||
editor = { path = "../editor" }
|
editor = { path = "../editor" }
|
||||||
fuzzy = { path = "../fuzzy" }
|
fuzzy = { path = "../fuzzy" }
|
||||||
gpui = { package = "gpui2", path = "../gpui2" }
|
gpui = { path = "../gpui" }
|
||||||
picker = { path = "../picker" }
|
picker = { path = "../picker" }
|
||||||
project = { path = "../project" }
|
project = { path = "../project" }
|
||||||
settings = { path = "../settings" }
|
settings = { path = "../settings" }
|
||||||
@ -25,7 +25,7 @@ anyhow.workspace = true
|
|||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
editor = { path = "../editor", features = ["test-support"] }
|
editor = { path = "../editor", features = ["test-support"] }
|
||||||
language = { path = "../language", features = ["test-support"] }
|
language = { path = "../language", features = ["test-support"] }
|
||||||
project = { path = "../project", features = ["test-support"] }
|
project = { path = "../project", features = ["test-support"] }
|
||||||
|
@ -21,7 +21,7 @@ test-support = [
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
collections = { path = "../collections" }
|
collections = { path = "../collections" }
|
||||||
# context_menu = { path = "../context_menu" }
|
# context_menu = { path = "../context_menu" }
|
||||||
gpui = { package = "gpui2", path = "../gpui2" }
|
gpui = { path = "../gpui" }
|
||||||
language = { path = "../language" }
|
language = { path = "../language" }
|
||||||
settings = { path = "../settings" }
|
settings = { path = "../settings" }
|
||||||
theme = { path = "../theme" }
|
theme = { path = "../theme" }
|
||||||
@ -43,7 +43,7 @@ parking_lot.workspace = true
|
|||||||
clock = { path = "../clock" }
|
clock = { path = "../clock" }
|
||||||
collections = { path = "../collections", features = ["test-support"] }
|
collections = { path = "../collections", features = ["test-support"] }
|
||||||
fs = { path = "../fs", features = ["test-support"] }
|
fs = { path = "../fs", features = ["test-support"] }
|
||||||
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
language = { path = "../language", features = ["test-support"] }
|
language = { path = "../language", features = ["test-support"] }
|
||||||
lsp = { path = "../lsp", features = ["test-support"] }
|
lsp = { path = "../lsp", features = ["test-support"] }
|
||||||
rpc = { path = "../rpc", features = ["test-support"] }
|
rpc = { path = "../rpc", features = ["test-support"] }
|
||||||
|
@ -13,7 +13,7 @@ copilot = { path = "../copilot" }
|
|||||||
editor = { path = "../editor" }
|
editor = { path = "../editor" }
|
||||||
fs = { path = "../fs" }
|
fs = { path = "../fs" }
|
||||||
zed_actions = { path = "../zed_actions"}
|
zed_actions = { path = "../zed_actions"}
|
||||||
gpui = { package = "gpui2", path = "../gpui2" }
|
gpui = { path = "../gpui" }
|
||||||
language = { path = "../language" }
|
language = { path = "../language" }
|
||||||
settings = { path = "../settings" }
|
settings = { path = "../settings" }
|
||||||
theme = { path = "../theme" }
|
theme = { path = "../theme" }
|
||||||
|
@ -13,7 +13,7 @@ test-support = []
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
collections = { path = "../collections" }
|
collections = { path = "../collections" }
|
||||||
gpui = { package = "gpui2", path = "../gpui2" }
|
gpui = { path = "../gpui" }
|
||||||
sqlez = { path = "../sqlez" }
|
sqlez = { path = "../sqlez" }
|
||||||
sqlez_macros = { path = "../sqlez_macros" }
|
sqlez_macros = { path = "../sqlez_macros" }
|
||||||
util = { path = "../util" }
|
util = { path = "../util" }
|
||||||
@ -28,6 +28,6 @@ serde_derive.workspace = true
|
|||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
env_logger.workspace = true
|
env_logger.workspace = true
|
||||||
tempdir.workspace = true
|
tempdir.workspace = true
|
||||||
|
@ -11,7 +11,7 @@ doctest = false
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
collections = { path = "../collections" }
|
collections = { path = "../collections" }
|
||||||
editor = { path = "../editor" }
|
editor = { path = "../editor" }
|
||||||
gpui = { package = "gpui2", path = "../gpui2" }
|
gpui = { path = "../gpui" }
|
||||||
ui = { path = "../ui" }
|
ui = { path = "../ui" }
|
||||||
language = { path = "../language" }
|
language = { path = "../language" }
|
||||||
lsp = { path = "../lsp" }
|
lsp = { path = "../lsp" }
|
||||||
@ -35,7 +35,7 @@ client = { path = "../client", features = ["test-support"] }
|
|||||||
editor = { path = "../editor", features = ["test-support"] }
|
editor = { path = "../editor", features = ["test-support"] }
|
||||||
language = { path = "../language", features = ["test-support"] }
|
language = { path = "../language", features = ["test-support"] }
|
||||||
lsp = { path = "../lsp", features = ["test-support"] }
|
lsp = { path = "../lsp", features = ["test-support"] }
|
||||||
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
workspace = {path = "../workspace", features = ["test-support"] }
|
workspace = {path = "../workspace", features = ["test-support"] }
|
||||||
theme = { path = "../theme", features = ["test-support"] }
|
theme = { path = "../theme", features = ["test-support"] }
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ collections = { path = "../collections" }
|
|||||||
# context_menu = { path = "../context_menu" }
|
# context_menu = { path = "../context_menu" }
|
||||||
fuzzy = { path = "../fuzzy" }
|
fuzzy = { path = "../fuzzy" }
|
||||||
git = { path = "../git" }
|
git = { path = "../git" }
|
||||||
gpui = { package = "gpui2", path = "../gpui2" }
|
gpui = { path = "../gpui" }
|
||||||
language = { path = "../language" }
|
language = { path = "../language" }
|
||||||
lsp = { path = "../lsp" }
|
lsp = { path = "../lsp" }
|
||||||
multi_buffer = { path = "../multi_buffer" }
|
multi_buffer = { path = "../multi_buffer" }
|
||||||
@ -76,7 +76,7 @@ copilot = { path = "../copilot", features = ["test-support"] }
|
|||||||
text = { path = "../text", features = ["test-support"] }
|
text = { path = "../text", features = ["test-support"] }
|
||||||
language = { path = "../language", features = ["test-support"] }
|
language = { path = "../language", features = ["test-support"] }
|
||||||
lsp = { path = "../lsp", features = ["test-support"] }
|
lsp = { path = "../lsp", features = ["test-support"] }
|
||||||
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
util = { path = "../util", features = ["test-support"] }
|
util = { path = "../util", features = ["test-support"] }
|
||||||
project = { path = "../project", features = ["test-support"] }
|
project = { path = "../project", features = ["test-support"] }
|
||||||
settings = { path = "../settings", features = ["test-support"] }
|
settings = { path = "../settings", features = ["test-support"] }
|
||||||
|
@ -8,5 +8,5 @@ publish = false
|
|||||||
path = "src/feature_flags.rs"
|
path = "src/feature_flags.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
gpui = { package = "gpui2", path = "../gpui2" }
|
gpui = { path = "../gpui" }
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
@ -14,7 +14,7 @@ test-support = []
|
|||||||
client = { path = "../client" }
|
client = { path = "../client" }
|
||||||
db = { path = "../db" }
|
db = { path = "../db" }
|
||||||
editor = { path = "../editor" }
|
editor = { path = "../editor" }
|
||||||
gpui = { package = "gpui2", path = "../gpui2" }
|
gpui = { path = "../gpui" }
|
||||||
language = { path = "../language" }
|
language = { path = "../language" }
|
||||||
menu = { path = "../menu" }
|
menu = { path = "../menu" }
|
||||||
project = { path = "../project" }
|
project = { path = "../project" }
|
||||||
|
@ -12,7 +12,7 @@ doctest = false
|
|||||||
editor = { path = "../editor" }
|
editor = { path = "../editor" }
|
||||||
collections = { path = "../collections" }
|
collections = { path = "../collections" }
|
||||||
fuzzy = { path = "../fuzzy" }
|
fuzzy = { path = "../fuzzy" }
|
||||||
gpui = { package = "gpui2", path = "../gpui2" }
|
gpui = { path = "../gpui" }
|
||||||
menu = { path = "../menu" }
|
menu = { path = "../menu" }
|
||||||
picker = { path = "../picker" }
|
picker = { path = "../picker" }
|
||||||
project = { path = "../project" }
|
project = { path = "../project" }
|
||||||
@ -27,7 +27,7 @@ serde.workspace = true
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
editor = { path = "../editor", features = ["test-support"] }
|
editor = { path = "../editor", features = ["test-support"] }
|
||||||
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
language = { path = "../language", features = ["test-support"] }
|
language = { path = "../language", features = ["test-support"] }
|
||||||
workspace = { path = "../workspace", features = ["test-support"] }
|
workspace = { path = "../workspace", features = ["test-support"] }
|
||||||
theme = { path = "../theme", features = ["test-support"] }
|
theme = { path = "../theme", features = ["test-support"] }
|
||||||
|
@ -31,10 +31,10 @@ log.workspace = true
|
|||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
time.workspace = true
|
time.workspace = true
|
||||||
|
|
||||||
gpui = { package = "gpui2", path = "../gpui2", optional = true}
|
gpui = { path = "../gpui", optional = true}
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
test-support = ["gpui/test-support"]
|
test-support = ["gpui/test-support"]
|
||||||
|
@ -9,5 +9,5 @@ path = "src/fuzzy.rs"
|
|||||||
doctest = false
|
doctest = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
gpui = { package = "gpui2", path = "../gpui2" }
|
gpui = { path = "../gpui" }
|
||||||
util = { path = "../util" }
|
util = { path = "../util" }
|
||||||
|
@ -10,7 +10,7 @@ doctest = false
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
editor = { path = "../editor" }
|
editor = { path = "../editor" }
|
||||||
gpui = { package = "gpui2", path = "../gpui2" }
|
gpui = { path = "../gpui" }
|
||||||
menu = { path = "../menu" }
|
menu = { path = "../menu" }
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
settings = { path = "../settings" }
|
settings = { path = "../settings" }
|
||||||
|
@ -1,27 +1,28 @@
|
|||||||
[package]
|
[package]
|
||||||
authors = ["Nathan Sobo <nathansobo@gmail.com>"]
|
|
||||||
edition = "2021"
|
|
||||||
name = "gpui"
|
name = "gpui"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "A GPU-accelerated UI framework"
|
edition = "2021"
|
||||||
|
authors = ["Nathan Sobo <nathan@zed.dev>"]
|
||||||
|
description = "The next version of Zed's GPU-accelerated UI framework"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
|
[features]
|
||||||
|
test-support = ["backtrace", "dhat", "env_logger", "collections/test-support", "util/test-support"]
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
path = "src/gpui.rs"
|
path = "src/gpui.rs"
|
||||||
doctest = false
|
doctest = false
|
||||||
|
|
||||||
[features]
|
|
||||||
test-support = ["backtrace", "dhat", "env_logger", "collections/test-support", "util/test-support"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
collections = { path = "../collections" }
|
collections = { path = "../collections" }
|
||||||
gpui_macros = { path = "../gpui_macros" }
|
gpui2_macros = { path = "../gpui2_macros" }
|
||||||
util = { path = "../util" }
|
util = { path = "../util" }
|
||||||
sum_tree = { path = "../sum_tree" }
|
sum_tree = { path = "../sum_tree" }
|
||||||
sqlez = { path = "../sqlez" }
|
sqlez = { path = "../sqlez" }
|
||||||
async-task = "4.0.3"
|
async-task = "4.0.3"
|
||||||
backtrace = { version = "0.3", optional = true }
|
backtrace = { version = "0.3", optional = true }
|
||||||
ctor.workspace = true
|
ctor.workspace = true
|
||||||
|
linkme = "0.3"
|
||||||
derive_more.workspace = true
|
derive_more.workspace = true
|
||||||
dhat = { version = "0.3", optional = true }
|
dhat = { version = "0.3", optional = true }
|
||||||
env_logger = { version = "0.9", optional = true }
|
env_logger = { version = "0.9", optional = true }
|
||||||
@ -35,30 +36,27 @@ num_cpus = "1.13"
|
|||||||
ordered-float.workspace = true
|
ordered-float.workspace = true
|
||||||
parking = "2.0.0"
|
parking = "2.0.0"
|
||||||
parking_lot.workspace = true
|
parking_lot.workspace = true
|
||||||
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
|
refineable.workspace = true
|
||||||
resvg = "0.14"
|
resvg = "0.14"
|
||||||
schemars = "0.8"
|
|
||||||
seahash = "4.1"
|
seahash = "4.1"
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_derive.workspace = true
|
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 = "4fb530bdd71609bb1d3f76c6a8bde1ba82805d5e" }
|
taffy = { git = "https://github.com/DioxusLabs/taffy", rev = "1876f72bee5e376023eaa518aa7b8a34c769bd1b" }
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
time.workspace = true
|
time.workspace = true
|
||||||
tiny-skia = "0.5"
|
tiny-skia = "0.5"
|
||||||
usvg = { version = "0.14", features = [] }
|
usvg = { version = "0.14", features = [] }
|
||||||
uuid.workspace = true
|
uuid = { version = "1.1.2", features = ["v4"] }
|
||||||
waker-fn = "1.1.0"
|
waker-fn = "1.1.0"
|
||||||
|
slotmap = "1.0.6"
|
||||||
[build-dependencies]
|
schemars.workspace = true
|
||||||
bindgen = "0.65.1"
|
bitflags = "2.4.0"
|
||||||
cc = "1.0.67"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
backtrace = "0.3"
|
backtrace = "0.3"
|
||||||
@ -69,6 +67,10 @@ png = "0.16"
|
|||||||
simplelog = "0.9"
|
simplelog = "0.9"
|
||||||
util = { path = "../util", features = ["test-support"] }
|
util = { path = "../util", features = ["test-support"] }
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
bindgen = "0.65.1"
|
||||||
|
cbindgen = "0.26.0"
|
||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
media = { path = "../media" }
|
media = { path = "../media" }
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
use std::{
|
use std::{
|
||||||
env,
|
env,
|
||||||
path::PathBuf,
|
path::{Path, PathBuf},
|
||||||
process::{self, Command},
|
process::{self, Command},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use cbindgen::Config;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
generate_dispatch_bindings();
|
generate_dispatch_bindings();
|
||||||
compile_metal_shaders();
|
let header_path = generate_shader_bindings();
|
||||||
generate_shader_bindings();
|
compile_metal_shaders(&header_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_dispatch_bindings() {
|
fn generate_dispatch_bindings() {
|
||||||
@ -17,7 +19,12 @@ fn generate_dispatch_bindings() {
|
|||||||
let bindings = bindgen::Builder::default()
|
let bindings = bindgen::Builder::default()
|
||||||
.header("src/platform/mac/dispatch.h")
|
.header("src/platform/mac/dispatch.h")
|
||||||
.allowlist_var("_dispatch_main_q")
|
.allowlist_var("_dispatch_main_q")
|
||||||
|
.allowlist_var("DISPATCH_QUEUE_PRIORITY_DEFAULT")
|
||||||
|
.allowlist_var("DISPATCH_TIME_NOW")
|
||||||
|
.allowlist_function("dispatch_get_global_queue")
|
||||||
.allowlist_function("dispatch_async_f")
|
.allowlist_function("dispatch_async_f")
|
||||||
|
.allowlist_function("dispatch_after_f")
|
||||||
|
.allowlist_function("dispatch_time")
|
||||||
.parse_callbacks(Box::new(bindgen::CargoCallbacks))
|
.parse_callbacks(Box::new(bindgen::CargoCallbacks))
|
||||||
.layout_tests(false)
|
.layout_tests(false)
|
||||||
.generate()
|
.generate()
|
||||||
@ -29,14 +36,61 @@ fn generate_dispatch_bindings() {
|
|||||||
.expect("couldn't write dispatch bindings");
|
.expect("couldn't write dispatch bindings");
|
||||||
}
|
}
|
||||||
|
|
||||||
const SHADER_HEADER_PATH: &str = "./src/platform/mac/shaders/shaders.h";
|
fn generate_shader_bindings() -> PathBuf {
|
||||||
|
let output_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("scene.h");
|
||||||
|
let crate_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||||
|
let mut config = Config::default();
|
||||||
|
config.include_guard = Some("SCENE_H".into());
|
||||||
|
config.language = cbindgen::Language::C;
|
||||||
|
config.export.include.extend([
|
||||||
|
"Bounds".into(),
|
||||||
|
"Corners".into(),
|
||||||
|
"Edges".into(),
|
||||||
|
"Size".into(),
|
||||||
|
"Pixels".into(),
|
||||||
|
"PointF".into(),
|
||||||
|
"Hsla".into(),
|
||||||
|
"ContentMask".into(),
|
||||||
|
"Uniforms".into(),
|
||||||
|
"AtlasTile".into(),
|
||||||
|
"PathRasterizationInputIndex".into(),
|
||||||
|
"PathVertex_ScaledPixels".into(),
|
||||||
|
"ShadowInputIndex".into(),
|
||||||
|
"Shadow".into(),
|
||||||
|
"QuadInputIndex".into(),
|
||||||
|
"Underline".into(),
|
||||||
|
"UnderlineInputIndex".into(),
|
||||||
|
"Quad".into(),
|
||||||
|
"SpriteInputIndex".into(),
|
||||||
|
"MonochromeSprite".into(),
|
||||||
|
"PolychromeSprite".into(),
|
||||||
|
"PathSprite".into(),
|
||||||
|
"SurfaceInputIndex".into(),
|
||||||
|
"SurfaceBounds".into(),
|
||||||
|
]);
|
||||||
|
config.no_includes = true;
|
||||||
|
config.enumeration.prefix_with_name = true;
|
||||||
|
cbindgen::Builder::new()
|
||||||
|
.with_src(crate_dir.join("src/scene.rs"))
|
||||||
|
.with_src(crate_dir.join("src/geometry.rs"))
|
||||||
|
.with_src(crate_dir.join("src/color.rs"))
|
||||||
|
.with_src(crate_dir.join("src/window.rs"))
|
||||||
|
.with_src(crate_dir.join("src/platform.rs"))
|
||||||
|
.with_src(crate_dir.join("src/platform/mac/metal_renderer.rs"))
|
||||||
|
.with_config(config)
|
||||||
|
.generate()
|
||||||
|
.expect("Unable to generate bindings")
|
||||||
|
.write_to_file(&output_path);
|
||||||
|
|
||||||
fn compile_metal_shaders() {
|
output_path
|
||||||
let shader_path = "./src/platform/mac/shaders/shaders.metal";
|
}
|
||||||
|
|
||||||
|
fn compile_metal_shaders(header_path: &Path) {
|
||||||
|
let shader_path = "./src/platform/mac/shaders.metal";
|
||||||
let air_output_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("shaders.air");
|
let air_output_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("shaders.air");
|
||||||
let metallib_output_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("shaders.metallib");
|
let metallib_output_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("shaders.metallib");
|
||||||
|
|
||||||
println!("cargo:rerun-if-changed={}", SHADER_HEADER_PATH);
|
println!("cargo:rerun-if-changed={}", header_path.display());
|
||||||
println!("cargo:rerun-if-changed={}", shader_path);
|
println!("cargo:rerun-if-changed={}", shader_path);
|
||||||
|
|
||||||
let output = Command::new("xcrun")
|
let output = Command::new("xcrun")
|
||||||
@ -49,6 +103,8 @@ fn compile_metal_shaders() {
|
|||||||
"-MO",
|
"-MO",
|
||||||
"-c",
|
"-c",
|
||||||
shader_path,
|
shader_path,
|
||||||
|
"-include",
|
||||||
|
&header_path.to_str().unwrap(),
|
||||||
"-o",
|
"-o",
|
||||||
])
|
])
|
||||||
.arg(&air_output_path)
|
.arg(&air_output_path)
|
||||||
@ -79,18 +135,3 @@ fn compile_metal_shaders() {
|
|||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_shader_bindings() {
|
|
||||||
let bindings = bindgen::Builder::default()
|
|
||||||
.header(SHADER_HEADER_PATH)
|
|
||||||
.allowlist_type("GPUI.*")
|
|
||||||
.parse_callbacks(Box::new(bindgen::CargoCallbacks))
|
|
||||||
.layout_tests(false)
|
|
||||||
.generate()
|
|
||||||
.expect("unable to generate bindings");
|
|
||||||
|
|
||||||
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
|
|
||||||
bindings
|
|
||||||
.write_to_file(out_path.join("shaders.rs"))
|
|
||||||
.expect("couldn't write shader bindings");
|
|
||||||
}
|
|
||||||
|
@ -1,237 +0,0 @@
|
|||||||
use button_component::Button;
|
|
||||||
|
|
||||||
use gpui::{
|
|
||||||
color::Color,
|
|
||||||
elements::{ContainerStyle, Flex, Label, ParentElement, StatefulComponent},
|
|
||||||
fonts::{self, TextStyle},
|
|
||||||
platform::WindowOptions,
|
|
||||||
AnyElement, App, Element, Entity, View, ViewContext,
|
|
||||||
};
|
|
||||||
use log::LevelFilter;
|
|
||||||
use pathfinder_geometry::vector::vec2f;
|
|
||||||
use simplelog::SimpleLogger;
|
|
||||||
use theme::Toggleable;
|
|
||||||
use toggleable_button::ToggleableButton;
|
|
||||||
|
|
||||||
// cargo run -p gpui --example components
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
|
|
||||||
|
|
||||||
App::new(()).unwrap().run(|cx| {
|
|
||||||
cx.platform().activate(true);
|
|
||||||
cx.add_window(WindowOptions::with_bounds(vec2f(300., 200.)), |_| {
|
|
||||||
TestView {
|
|
||||||
count: 0,
|
|
||||||
is_doubling: false,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct TestView {
|
|
||||||
count: usize,
|
|
||||||
is_doubling: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TestView {
|
|
||||||
fn increase_count(&mut self) {
|
|
||||||
if self.is_doubling {
|
|
||||||
self.count *= 2;
|
|
||||||
} else {
|
|
||||||
self.count += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Entity for TestView {
|
|
||||||
type Event = ();
|
|
||||||
}
|
|
||||||
|
|
||||||
type ButtonStyle = ContainerStyle;
|
|
||||||
|
|
||||||
impl View for TestView {
|
|
||||||
fn ui_name() -> &'static str {
|
|
||||||
"TestView"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> AnyElement<Self> {
|
|
||||||
fonts::with_font_cache(cx.font_cache.to_owned(), || {
|
|
||||||
Flex::column()
|
|
||||||
.with_child(Label::new(
|
|
||||||
format!("Count: {}", self.count),
|
|
||||||
TextStyle::for_color(Color::red()),
|
|
||||||
))
|
|
||||||
.with_child(
|
|
||||||
Button::new(move |_, v: &mut Self, cx| {
|
|
||||||
v.increase_count();
|
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
.with_text(
|
|
||||||
"Hello from a counting BUTTON",
|
|
||||||
TextStyle::for_color(Color::blue()),
|
|
||||||
)
|
|
||||||
.with_style(ButtonStyle::fill(Color::yellow()))
|
|
||||||
.element(),
|
|
||||||
)
|
|
||||||
.with_child(
|
|
||||||
ToggleableButton::new(self.is_doubling, move |_, v: &mut Self, cx| {
|
|
||||||
v.is_doubling = !v.is_doubling;
|
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
.with_text("Double the count?", TextStyle::for_color(Color::black()))
|
|
||||||
.with_style(Toggleable {
|
|
||||||
inactive: ButtonStyle::fill(Color::red()),
|
|
||||||
active: ButtonStyle::fill(Color::green()),
|
|
||||||
})
|
|
||||||
.element(),
|
|
||||||
)
|
|
||||||
.expanded()
|
|
||||||
.contained()
|
|
||||||
.with_background_color(Color::white())
|
|
||||||
.into_any()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod theme {
|
|
||||||
pub struct Toggleable<T> {
|
|
||||||
pub inactive: T,
|
|
||||||
pub active: T,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Toggleable<T> {
|
|
||||||
pub fn style_for(&self, active: bool) -> &T {
|
|
||||||
if active {
|
|
||||||
&self.active
|
|
||||||
} else {
|
|
||||||
&self.inactive
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Component creation:
|
|
||||||
mod toggleable_button {
|
|
||||||
use gpui::{
|
|
||||||
elements::{ContainerStyle, LabelStyle, StatefulComponent},
|
|
||||||
scene::MouseClick,
|
|
||||||
EventContext, View,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{button_component::Button, theme::Toggleable};
|
|
||||||
|
|
||||||
pub struct ToggleableButton<V: View> {
|
|
||||||
active: bool,
|
|
||||||
style: Option<Toggleable<ContainerStyle>>,
|
|
||||||
button: Button<V>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V: View> ToggleableButton<V> {
|
|
||||||
pub fn new<F>(active: bool, on_click: F) -> Self
|
|
||||||
where
|
|
||||||
F: Fn(MouseClick, &mut V, &mut EventContext<V>) + 'static,
|
|
||||||
{
|
|
||||||
Self {
|
|
||||||
active,
|
|
||||||
button: Button::new(on_click),
|
|
||||||
style: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_text(self, text: &str, style: impl Into<LabelStyle>) -> ToggleableButton<V> {
|
|
||||||
ToggleableButton {
|
|
||||||
active: self.active,
|
|
||||||
style: self.style,
|
|
||||||
button: self.button.with_text(text, style),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_style(self, style: Toggleable<ContainerStyle>) -> ToggleableButton<V> {
|
|
||||||
ToggleableButton {
|
|
||||||
active: self.active,
|
|
||||||
style: Some(style),
|
|
||||||
button: self.button,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V: View> StatefulComponent<V> for ToggleableButton<V> {
|
|
||||||
fn render(self, v: &mut V, cx: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
|
|
||||||
let button = if let Some(style) = self.style {
|
|
||||||
self.button.with_style(*style.style_for(self.active))
|
|
||||||
} else {
|
|
||||||
self.button
|
|
||||||
};
|
|
||||||
button.render(v, cx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod button_component {
|
|
||||||
|
|
||||||
use gpui::{
|
|
||||||
elements::{ContainerStyle, Label, LabelStyle, MouseEventHandler, StatefulComponent},
|
|
||||||
platform::MouseButton,
|
|
||||||
scene::MouseClick,
|
|
||||||
AnyElement, Element, EventContext, TypeTag, View, ViewContext,
|
|
||||||
};
|
|
||||||
|
|
||||||
type ClickHandler<V> = Box<dyn Fn(MouseClick, &mut V, &mut EventContext<V>)>;
|
|
||||||
|
|
||||||
pub struct Button<V: View> {
|
|
||||||
click_handler: ClickHandler<V>,
|
|
||||||
tag: TypeTag,
|
|
||||||
contents: Option<AnyElement<V>>,
|
|
||||||
style: Option<ContainerStyle>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V: View> Button<V> {
|
|
||||||
pub fn new<F: Fn(MouseClick, &mut V, &mut EventContext<V>) + 'static>(handler: F) -> Self {
|
|
||||||
Self {
|
|
||||||
click_handler: Box::new(handler),
|
|
||||||
tag: TypeTag::new::<F>(),
|
|
||||||
style: None,
|
|
||||||
contents: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_text(mut self, text: &str, style: impl Into<LabelStyle>) -> Self {
|
|
||||||
self.contents = Some(Label::new(text.to_string(), style).into_any());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _with_contents<E: Element<V>>(mut self, contents: E) -> Self {
|
|
||||||
self.contents = Some(contents.into_any());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_style(mut self, style: ContainerStyle) -> Self {
|
|
||||||
self.style = Some(style);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V: View> StatefulComponent<V> for Button<V> {
|
|
||||||
fn render(self, _: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V> {
|
|
||||||
let click_handler = self.click_handler;
|
|
||||||
|
|
||||||
let result = MouseEventHandler::new_dynamic(self.tag, 0, cx, |_, _| {
|
|
||||||
self.contents
|
|
||||||
.unwrap_or_else(|| gpui::elements::Empty::new().into_any())
|
|
||||||
})
|
|
||||||
.on_click(MouseButton::Left, move |click, v, cx| {
|
|
||||||
click_handler(click, v, cx);
|
|
||||||
})
|
|
||||||
.contained();
|
|
||||||
|
|
||||||
let result = if let Some(style) = self.style {
|
|
||||||
result.with_style(style)
|
|
||||||
} else {
|
|
||||||
result
|
|
||||||
};
|
|
||||||
|
|
||||||
result.into_any()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,154 +0,0 @@
|
|||||||
use gpui::{
|
|
||||||
color::Color, geometry::rect::RectF, scene::Shadow, AnyElement, App, Element, Entity, Quad,
|
|
||||||
View,
|
|
||||||
};
|
|
||||||
use log::LevelFilter;
|
|
||||||
use pathfinder_geometry::vector::vec2f;
|
|
||||||
use simplelog::SimpleLogger;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
|
|
||||||
|
|
||||||
App::new(()).unwrap().run(|cx| {
|
|
||||||
cx.platform().activate(true);
|
|
||||||
cx.add_window(Default::default(), |_| CornersView);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
struct CornersView;
|
|
||||||
|
|
||||||
impl Entity for CornersView {
|
|
||||||
type Event = ();
|
|
||||||
}
|
|
||||||
|
|
||||||
impl View for CornersView {
|
|
||||||
fn ui_name() -> &'static str {
|
|
||||||
"CornersView"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render(&mut self, _: &mut gpui::ViewContext<Self>) -> AnyElement<CornersView> {
|
|
||||||
CornersElement.into_any()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct CornersElement;
|
|
||||||
|
|
||||||
impl<V: View> gpui::Element<V> for CornersElement {
|
|
||||||
type LayoutState = ();
|
|
||||||
|
|
||||||
type PaintState = ();
|
|
||||||
|
|
||||||
fn layout(
|
|
||||||
&mut self,
|
|
||||||
constraint: gpui::SizeConstraint,
|
|
||||||
_: &mut V,
|
|
||||||
_: &mut gpui::ViewContext<V>,
|
|
||||||
) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
|
|
||||||
(constraint.max, ())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn paint(
|
|
||||||
&mut self,
|
|
||||||
bounds: pathfinder_geometry::rect::RectF,
|
|
||||||
_: pathfinder_geometry::rect::RectF,
|
|
||||||
_: &mut Self::LayoutState,
|
|
||||||
_: &mut V,
|
|
||||||
cx: &mut gpui::ViewContext<V>,
|
|
||||||
) -> Self::PaintState {
|
|
||||||
cx.scene().push_quad(Quad {
|
|
||||||
bounds,
|
|
||||||
background: Some(Color::white()),
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.scene().push_layer(None);
|
|
||||||
|
|
||||||
cx.scene().push_quad(Quad {
|
|
||||||
bounds: RectF::new(vec2f(100., 100.), vec2f(100., 100.)),
|
|
||||||
background: Some(Color::red()),
|
|
||||||
border: Default::default(),
|
|
||||||
corner_radii: gpui::scene::CornerRadii {
|
|
||||||
top_left: 20.,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.scene().push_quad(Quad {
|
|
||||||
bounds: RectF::new(vec2f(200., 100.), vec2f(100., 100.)),
|
|
||||||
background: Some(Color::green()),
|
|
||||||
border: Default::default(),
|
|
||||||
corner_radii: gpui::scene::CornerRadii {
|
|
||||||
top_right: 20.,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.scene().push_quad(Quad {
|
|
||||||
bounds: RectF::new(vec2f(100., 200.), vec2f(100., 100.)),
|
|
||||||
background: Some(Color::blue()),
|
|
||||||
border: Default::default(),
|
|
||||||
corner_radii: gpui::scene::CornerRadii {
|
|
||||||
bottom_left: 20.,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.scene().push_quad(Quad {
|
|
||||||
bounds: RectF::new(vec2f(200., 200.), vec2f(100., 100.)),
|
|
||||||
background: Some(Color::yellow()),
|
|
||||||
border: Default::default(),
|
|
||||||
corner_radii: gpui::scene::CornerRadii {
|
|
||||||
bottom_right: 20.,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.scene().push_shadow(Shadow {
|
|
||||||
bounds: RectF::new(vec2f(400., 100.), vec2f(100., 100.)),
|
|
||||||
corner_radii: gpui::scene::CornerRadii {
|
|
||||||
bottom_right: 20.,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
sigma: 20.0,
|
|
||||||
color: Color::black(),
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.scene().push_layer(None);
|
|
||||||
cx.scene().push_quad(Quad {
|
|
||||||
bounds: RectF::new(vec2f(400., 100.), vec2f(100., 100.)),
|
|
||||||
background: Some(Color::red()),
|
|
||||||
border: Default::default(),
|
|
||||||
corner_radii: gpui::scene::CornerRadii {
|
|
||||||
bottom_right: 20.,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.scene().pop_layer();
|
|
||||||
cx.scene().pop_layer();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
|
||||||
&self,
|
|
||||||
_: std::ops::Range<usize>,
|
|
||||||
_: pathfinder_geometry::rect::RectF,
|
|
||||||
_: pathfinder_geometry::rect::RectF,
|
|
||||||
_: &Self::LayoutState,
|
|
||||||
_: &Self::PaintState,
|
|
||||||
_: &V,
|
|
||||||
_: &gpui::ViewContext<V>,
|
|
||||||
) -> Option<pathfinder_geometry::rect::RectF> {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn debug(
|
|
||||||
&self,
|
|
||||||
_: pathfinder_geometry::rect::RectF,
|
|
||||||
_: &Self::LayoutState,
|
|
||||||
_: &Self::PaintState,
|
|
||||||
_: &V,
|
|
||||||
_: &gpui::ViewContext<V>,
|
|
||||||
) -> serde_json::Value {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,81 +0,0 @@
|
|||||||
use gpui::{
|
|
||||||
color::Color,
|
|
||||||
elements::Text,
|
|
||||||
fonts::{HighlightStyle, TextStyle},
|
|
||||||
platform::{CursorStyle, MouseButton},
|
|
||||||
AnyElement, CursorRegion, Element, MouseRegion,
|
|
||||||
};
|
|
||||||
use log::LevelFilter;
|
|
||||||
use simplelog::SimpleLogger;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
|
|
||||||
|
|
||||||
gpui::App::new(()).unwrap().run(|cx| {
|
|
||||||
cx.platform().activate(true);
|
|
||||||
cx.add_window(Default::default(), |_| TextView);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
struct TextView;
|
|
||||||
|
|
||||||
impl gpui::Entity for TextView {
|
|
||||||
type Event = ();
|
|
||||||
}
|
|
||||||
|
|
||||||
impl gpui::View for TextView {
|
|
||||||
fn ui_name() -> &'static str {
|
|
||||||
"View"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> AnyElement<TextView> {
|
|
||||||
let font_size = 12.;
|
|
||||||
let family = cx
|
|
||||||
.font_cache
|
|
||||||
.load_family(&["Monaco"], &Default::default())
|
|
||||||
.unwrap();
|
|
||||||
let font_id = cx
|
|
||||||
.font_cache
|
|
||||||
.select_font(family, &Default::default())
|
|
||||||
.unwrap();
|
|
||||||
let view_id = cx.view_id();
|
|
||||||
|
|
||||||
let underline = HighlightStyle {
|
|
||||||
underline: Some(gpui::fonts::Underline {
|
|
||||||
thickness: 1.0.into(),
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
Text::new(
|
|
||||||
"The text:\nHello, beautiful world, hello!",
|
|
||||||
TextStyle {
|
|
||||||
font_id,
|
|
||||||
font_size,
|
|
||||||
color: Color::red(),
|
|
||||||
font_family_name: "".into(),
|
|
||||||
font_family_id: family,
|
|
||||||
underline: Default::default(),
|
|
||||||
font_properties: Default::default(),
|
|
||||||
soft_wrap: false,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.with_highlights(vec![(17..26, underline), (34..40, underline)])
|
|
||||||
.with_custom_runs(vec![(17..26), (34..40)], move |ix, bounds, cx| {
|
|
||||||
cx.scene().push_cursor_region(CursorRegion {
|
|
||||||
bounds,
|
|
||||||
style: CursorStyle::PointingHand,
|
|
||||||
});
|
|
||||||
cx.scene().push_mouse_region(
|
|
||||||
MouseRegion::new::<Self>(view_id, ix, bounds).on_click::<Self, _>(
|
|
||||||
MouseButton::Left,
|
|
||||||
move |_, _, _| {
|
|
||||||
eprintln!("clicked link {ix}");
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.into_any()
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
@ -1,120 +0,0 @@
|
|||||||
use std::any::{Any, TypeId};
|
|
||||||
|
|
||||||
use crate::TypeTag;
|
|
||||||
|
|
||||||
pub trait Action: 'static {
|
|
||||||
fn id(&self) -> TypeId;
|
|
||||||
fn namespace(&self) -> &'static str;
|
|
||||||
fn name(&self) -> &'static str;
|
|
||||||
fn as_any(&self) -> &dyn Any;
|
|
||||||
fn type_tag(&self) -> TypeTag;
|
|
||||||
fn boxed_clone(&self) -> Box<dyn Action>;
|
|
||||||
fn eq(&self, other: &dyn Action) -> bool;
|
|
||||||
|
|
||||||
fn qualified_name() -> &'static str
|
|
||||||
where
|
|
||||||
Self: Sized;
|
|
||||||
fn from_json_str(json: serde_json::Value) -> anyhow::Result<Box<dyn Action>>
|
|
||||||
where
|
|
||||||
Self: Sized;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Debug for dyn Action {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.debug_struct("dyn Action")
|
|
||||||
.field("namespace", &self.namespace())
|
|
||||||
.field("name", &self.name())
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Define a set of unit struct types that all implement the `Action` trait.
|
|
||||||
///
|
|
||||||
/// The first argument is a namespace that will be associated with each of
|
|
||||||
/// the given action types, to ensure that they have globally unique
|
|
||||||
/// qualified names for use in keymap files.
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! actions {
|
|
||||||
($namespace:path, [ $($name:ident),* $(,)? ]) => {
|
|
||||||
$(
|
|
||||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
|
||||||
pub struct $name;
|
|
||||||
$crate::__impl_action! {
|
|
||||||
$namespace,
|
|
||||||
$name,
|
|
||||||
fn from_json_str(_: $crate::serde_json::Value) -> $crate::anyhow::Result<Box<dyn $crate::Action>> {
|
|
||||||
Ok(Box::new(Self))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)*
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Implement the `Action` trait for a set of existing types.
|
|
||||||
///
|
|
||||||
/// The first argument is a namespace that will be associated with each of
|
|
||||||
/// the given action types, to ensure that they have globally unique
|
|
||||||
/// qualified names for use in keymap files.
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! impl_actions {
|
|
||||||
($namespace:path, [ $($name:ident),* $(,)? ]) => {
|
|
||||||
$(
|
|
||||||
$crate::__impl_action! {
|
|
||||||
$namespace,
|
|
||||||
$name,
|
|
||||||
fn from_json_str(json: $crate::serde_json::Value) -> $crate::anyhow::Result<Box<dyn $crate::Action>> {
|
|
||||||
Ok(Box::new($crate::serde_json::from_value::<Self>(json)?))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)*
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[doc(hidden)]
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! __impl_action {
|
|
||||||
($namespace:path, $name:ident, $from_json_fn:item) => {
|
|
||||||
impl $crate::action::Action for $name {
|
|
||||||
fn namespace(&self) -> &'static str {
|
|
||||||
stringify!($namespace)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn name(&self) -> &'static str {
|
|
||||||
stringify!($name)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn qualified_name() -> &'static str {
|
|
||||||
concat!(
|
|
||||||
stringify!($namespace),
|
|
||||||
"::",
|
|
||||||
stringify!($name),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn id(&self) -> std::any::TypeId {
|
|
||||||
std::any::TypeId::of::<$name>()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn as_any(&self) -> &dyn std::any::Any {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn boxed_clone(&self) -> Box<dyn $crate::Action> {
|
|
||||||
Box::new(self.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn eq(&self, other: &dyn $crate::Action) -> bool {
|
|
||||||
if let Some(other) = other.as_any().downcast_ref::<Self>() {
|
|
||||||
self == other
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn type_tag(&self) -> $crate::TypeTag {
|
|
||||||
$crate::TypeTag::new::<Self>()
|
|
||||||
}
|
|
||||||
|
|
||||||
$from_json_fn
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,164 +0,0 @@
|
|||||||
use collections::{BTreeMap, HashMap, HashSet};
|
|
||||||
use parking_lot::Mutex;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::{hash::Hash, sync::Weak};
|
|
||||||
|
|
||||||
pub struct CallbackCollection<K: Clone + Hash + Eq, F> {
|
|
||||||
internal: Arc<Mutex<Mapping<K, F>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Subscription<K: Clone + Hash + Eq, F> {
|
|
||||||
key: K,
|
|
||||||
id: usize,
|
|
||||||
mapping: Option<Weak<Mutex<Mapping<K, F>>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Mapping<K, F> {
|
|
||||||
callbacks: HashMap<K, BTreeMap<usize, F>>,
|
|
||||||
dropped_subscriptions: HashMap<K, HashSet<usize>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<K: Hash + Eq, F> Mapping<K, F> {
|
|
||||||
fn clear_dropped_state(&mut self, key: &K, subscription_id: usize) -> bool {
|
|
||||||
if let Some(subscriptions) = self.dropped_subscriptions.get_mut(&key) {
|
|
||||||
subscriptions.remove(&subscription_id)
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<K, F> Default for Mapping<K, F> {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
callbacks: Default::default(),
|
|
||||||
dropped_subscriptions: Default::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<K: Clone + Hash + Eq, F> Clone for CallbackCollection<K, F> {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
Self {
|
|
||||||
internal: self.internal.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<K: Clone + Hash + Eq + Copy, F> Default for CallbackCollection<K, F> {
|
|
||||||
fn default() -> Self {
|
|
||||||
CallbackCollection {
|
|
||||||
internal: Arc::new(Mutex::new(Default::default())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<K: Clone + Hash + Eq + Copy, F> CallbackCollection<K, F> {
|
|
||||||
#[cfg(test)]
|
|
||||||
pub fn is_empty(&self) -> bool {
|
|
||||||
self.internal.lock().callbacks.is_empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn subscribe(&mut self, key: K, subscription_id: usize) -> Subscription<K, F> {
|
|
||||||
Subscription {
|
|
||||||
key,
|
|
||||||
id: subscription_id,
|
|
||||||
mapping: Some(Arc::downgrade(&self.internal)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_callback(&mut self, key: K, subscription_id: usize, callback: F) {
|
|
||||||
let mut this = self.internal.lock();
|
|
||||||
|
|
||||||
// If this callback's subscription was dropped before the callback was
|
|
||||||
// added, then just drop the callback.
|
|
||||||
if this.clear_dropped_state(&key, subscription_id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.callbacks
|
|
||||||
.entry(key)
|
|
||||||
.or_default()
|
|
||||||
.insert(subscription_id, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn remove(&mut self, key: K) {
|
|
||||||
// Drop these callbacks after releasing the lock, in case one of them
|
|
||||||
// owns a subscription to this callback collection.
|
|
||||||
let mut this = self.internal.lock();
|
|
||||||
let callbacks = this.callbacks.remove(&key);
|
|
||||||
this.dropped_subscriptions.remove(&key);
|
|
||||||
drop(this);
|
|
||||||
drop(callbacks);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn emit<C>(&mut self, key: K, mut call_callback: C)
|
|
||||||
where
|
|
||||||
C: FnMut(&mut F) -> bool,
|
|
||||||
{
|
|
||||||
let callbacks = self.internal.lock().callbacks.remove(&key);
|
|
||||||
if let Some(callbacks) = callbacks {
|
|
||||||
for (subscription_id, mut callback) in callbacks {
|
|
||||||
// If this callback's subscription was dropped while invoking an
|
|
||||||
// earlier callback, then just drop the callback.
|
|
||||||
let mut this = self.internal.lock();
|
|
||||||
if this.clear_dropped_state(&key, subscription_id) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
drop(this);
|
|
||||||
let alive = call_callback(&mut callback);
|
|
||||||
|
|
||||||
// If this callback's subscription was dropped while invoking the callback
|
|
||||||
// itself, or if the callback returns false, then just drop the callback.
|
|
||||||
let mut this = self.internal.lock();
|
|
||||||
if this.clear_dropped_state(&key, subscription_id) || !alive {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.callbacks
|
|
||||||
.entry(key)
|
|
||||||
.or_default()
|
|
||||||
.insert(subscription_id, callback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<K: Clone + Hash + Eq, F> Subscription<K, F> {
|
|
||||||
pub fn id(&self) -> usize {
|
|
||||||
self.id
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn detach(&mut self) {
|
|
||||||
self.mapping.take();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<K: Clone + Hash + Eq, F> Drop for Subscription<K, F> {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
if let Some(mapping) = self.mapping.as_ref().and_then(|mapping| mapping.upgrade()) {
|
|
||||||
let mut mapping = mapping.lock();
|
|
||||||
|
|
||||||
// If the callback is present in the mapping, then just remove it.
|
|
||||||
if let Some(callbacks) = mapping.callbacks.get_mut(&self.key) {
|
|
||||||
let callback = callbacks.remove(&self.id);
|
|
||||||
if callback.is_some() {
|
|
||||||
drop(mapping);
|
|
||||||
drop(callback);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If this subscription's callback is not present, then either it has been
|
|
||||||
// temporarily removed during emit, or it has not yet been added. Record
|
|
||||||
// that this subscription has been dropped so that the callback can be
|
|
||||||
// removed later.
|
|
||||||
mapping
|
|
||||||
.dropped_subscriptions
|
|
||||||
.entry(self.key.clone())
|
|
||||||
.or_default()
|
|
||||||
.insert(self.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,99 +0,0 @@
|
|||||||
use crate::{platform::ForegroundPlatform, Action, App, AppContext};
|
|
||||||
|
|
||||||
pub struct Menu<'a> {
|
|
||||||
pub name: &'a str,
|
|
||||||
pub items: Vec<MenuItem<'a>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum MenuItem<'a> {
|
|
||||||
Separator,
|
|
||||||
Submenu(Menu<'a>),
|
|
||||||
Action {
|
|
||||||
name: &'a str,
|
|
||||||
action: Box<dyn Action>,
|
|
||||||
os_action: Option<OsAction>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> MenuItem<'a> {
|
|
||||||
pub fn separator() -> Self {
|
|
||||||
Self::Separator
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn submenu(menu: Menu<'a>) -> Self {
|
|
||||||
Self::Submenu(menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn action(name: &'a str, action: impl Action) -> Self {
|
|
||||||
Self::Action {
|
|
||||||
name,
|
|
||||||
action: Box::new(action),
|
|
||||||
os_action: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn os_action(name: &'a str, action: impl Action, os_action: OsAction) -> Self {
|
|
||||||
Self::Action {
|
|
||||||
name,
|
|
||||||
action: Box::new(action),
|
|
||||||
os_action: Some(os_action),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
|
||||||
pub enum OsAction {
|
|
||||||
Cut,
|
|
||||||
Copy,
|
|
||||||
Paste,
|
|
||||||
SelectAll,
|
|
||||||
Undo,
|
|
||||||
Redo,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AppContext {
|
|
||||||
pub fn set_menus(&mut self, menus: Vec<Menu>) {
|
|
||||||
self.foreground_platform
|
|
||||||
.set_menus(menus, &self.keystroke_matcher);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn setup_menu_handlers(foreground_platform: &dyn ForegroundPlatform, app: &App) {
|
|
||||||
foreground_platform.on_will_open_menu(Box::new({
|
|
||||||
let cx = app.0.clone();
|
|
||||||
move || {
|
|
||||||
let mut cx = cx.borrow_mut();
|
|
||||||
cx.keystroke_matcher.clear_pending();
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
foreground_platform.on_validate_menu_command(Box::new({
|
|
||||||
let cx = app.0.clone();
|
|
||||||
move |action| {
|
|
||||||
let cx = cx.borrow_mut();
|
|
||||||
!cx.keystroke_matcher.has_pending_keystrokes() && cx.is_action_available(action)
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
foreground_platform.on_menu_command(Box::new({
|
|
||||||
let cx = app.0.clone();
|
|
||||||
move |action| {
|
|
||||||
let mut cx = cx.borrow_mut();
|
|
||||||
if let Some(main_window) = cx.active_window() {
|
|
||||||
let dispatched = main_window
|
|
||||||
.update(&mut *cx, |cx| {
|
|
||||||
if let Some(view_id) = cx.focused_view_id() {
|
|
||||||
cx.dispatch_action(Some(view_id), action);
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.unwrap_or(false);
|
|
||||||
|
|
||||||
if dispatched {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cx.dispatch_global_action_any(action);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
@ -1,220 +0,0 @@
|
|||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use lazy_static::lazy_static;
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
use parking_lot::Mutex;
|
|
||||||
|
|
||||||
use collections::{hash_map::Entry, HashMap, HashSet};
|
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
use crate::util::post_inc;
|
|
||||||
use crate::{AnyWindowHandle, ElementStateId};
|
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
static ref LEAK_BACKTRACE: bool =
|
|
||||||
std::env::var("LEAK_BACKTRACE").map_or(false, |b| !b.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ElementStateRefCount {
|
|
||||||
ref_count: usize,
|
|
||||||
frame_id: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct RefCounts {
|
|
||||||
entity_counts: HashMap<usize, usize>,
|
|
||||||
element_state_counts: HashMap<ElementStateId, ElementStateRefCount>,
|
|
||||||
dropped_models: HashSet<usize>,
|
|
||||||
dropped_views: HashSet<(AnyWindowHandle, usize)>,
|
|
||||||
dropped_element_states: HashSet<ElementStateId>,
|
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
pub leak_detector: Arc<Mutex<LeakDetector>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RefCounts {
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
pub fn new(leak_detector: Arc<Mutex<LeakDetector>>) -> Self {
|
|
||||||
Self {
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
leak_detector,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn inc_model(&mut self, model_id: usize) {
|
|
||||||
match self.entity_counts.entry(model_id) {
|
|
||||||
Entry::Occupied(mut entry) => {
|
|
||||||
*entry.get_mut() += 1;
|
|
||||||
}
|
|
||||||
Entry::Vacant(entry) => {
|
|
||||||
entry.insert(1);
|
|
||||||
self.dropped_models.remove(&model_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn inc_view(&mut self, window: AnyWindowHandle, view_id: usize) {
|
|
||||||
match self.entity_counts.entry(view_id) {
|
|
||||||
Entry::Occupied(mut entry) => *entry.get_mut() += 1,
|
|
||||||
Entry::Vacant(entry) => {
|
|
||||||
entry.insert(1);
|
|
||||||
self.dropped_views.remove(&(window, view_id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn inc_element_state(&mut self, id: ElementStateId, frame_id: usize) {
|
|
||||||
match self.element_state_counts.entry(id) {
|
|
||||||
Entry::Occupied(mut entry) => {
|
|
||||||
let entry = entry.get_mut();
|
|
||||||
if entry.frame_id == frame_id || entry.ref_count >= 2 {
|
|
||||||
panic!("used the same element state more than once in the same frame");
|
|
||||||
}
|
|
||||||
entry.ref_count += 1;
|
|
||||||
entry.frame_id = frame_id;
|
|
||||||
}
|
|
||||||
Entry::Vacant(entry) => {
|
|
||||||
entry.insert(ElementStateRefCount {
|
|
||||||
ref_count: 1,
|
|
||||||
frame_id,
|
|
||||||
});
|
|
||||||
self.dropped_element_states.remove(&id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn dec_model(&mut self, model_id: usize) {
|
|
||||||
let count = self.entity_counts.get_mut(&model_id).unwrap();
|
|
||||||
*count -= 1;
|
|
||||||
if *count == 0 {
|
|
||||||
self.entity_counts.remove(&model_id);
|
|
||||||
self.dropped_models.insert(model_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn dec_view(&mut self, window: AnyWindowHandle, view_id: usize) {
|
|
||||||
let count = self.entity_counts.get_mut(&view_id).unwrap();
|
|
||||||
*count -= 1;
|
|
||||||
if *count == 0 {
|
|
||||||
self.entity_counts.remove(&view_id);
|
|
||||||
self.dropped_views.insert((window, view_id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn dec_element_state(&mut self, id: ElementStateId) {
|
|
||||||
let entry = self.element_state_counts.get_mut(&id).unwrap();
|
|
||||||
entry.ref_count -= 1;
|
|
||||||
if entry.ref_count == 0 {
|
|
||||||
self.element_state_counts.remove(&id);
|
|
||||||
self.dropped_element_states.insert(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_entity_alive(&self, entity_id: usize) -> bool {
|
|
||||||
self.entity_counts.contains_key(&entity_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn take_dropped(
|
|
||||||
&mut self,
|
|
||||||
) -> (
|
|
||||||
HashSet<usize>,
|
|
||||||
HashSet<(AnyWindowHandle, usize)>,
|
|
||||||
HashSet<ElementStateId>,
|
|
||||||
) {
|
|
||||||
(
|
|
||||||
std::mem::take(&mut self.dropped_models),
|
|
||||||
std::mem::take(&mut self.dropped_views),
|
|
||||||
std::mem::take(&mut self.dropped_element_states),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct LeakDetector {
|
|
||||||
next_handle_id: usize,
|
|
||||||
#[allow(clippy::type_complexity)]
|
|
||||||
handle_backtraces: HashMap<
|
|
||||||
usize,
|
|
||||||
(
|
|
||||||
Option<&'static str>,
|
|
||||||
HashMap<usize, Option<backtrace::Backtrace>>,
|
|
||||||
),
|
|
||||||
>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
impl LeakDetector {
|
|
||||||
pub fn handle_created(&mut self, type_name: Option<&'static str>, entity_id: usize) -> usize {
|
|
||||||
let handle_id = post_inc(&mut self.next_handle_id);
|
|
||||||
let entry = self.handle_backtraces.entry(entity_id).or_default();
|
|
||||||
let backtrace = if *LEAK_BACKTRACE {
|
|
||||||
Some(backtrace::Backtrace::new_unresolved())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
if let Some(type_name) = type_name {
|
|
||||||
entry.0.get_or_insert(type_name);
|
|
||||||
}
|
|
||||||
entry.1.insert(handle_id, backtrace);
|
|
||||||
handle_id
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle_dropped(&mut self, entity_id: usize, handle_id: usize) {
|
|
||||||
if let Some((_, backtraces)) = self.handle_backtraces.get_mut(&entity_id) {
|
|
||||||
assert!(backtraces.remove(&handle_id).is_some());
|
|
||||||
if backtraces.is_empty() {
|
|
||||||
self.handle_backtraces.remove(&entity_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn assert_dropped(&mut self, entity_id: usize) {
|
|
||||||
if let Some((type_name, backtraces)) = self.handle_backtraces.get_mut(&entity_id) {
|
|
||||||
for trace in backtraces.values_mut().flatten() {
|
|
||||||
trace.resolve();
|
|
||||||
eprintln!("{:?}", crate::util::CwdBacktrace(trace));
|
|
||||||
}
|
|
||||||
|
|
||||||
let hint = if *LEAK_BACKTRACE {
|
|
||||||
""
|
|
||||||
} else {
|
|
||||||
" – set LEAK_BACKTRACE=1 for more information"
|
|
||||||
};
|
|
||||||
|
|
||||||
panic!(
|
|
||||||
"{} handles to {} {} still exist{}",
|
|
||||||
backtraces.len(),
|
|
||||||
type_name.unwrap_or("entity"),
|
|
||||||
entity_id,
|
|
||||||
hint
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn detect(&mut self) {
|
|
||||||
let mut found_leaks = false;
|
|
||||||
for (id, (type_name, backtraces)) in self.handle_backtraces.iter_mut() {
|
|
||||||
eprintln!(
|
|
||||||
"leaked {} handles to {} {}",
|
|
||||||
backtraces.len(),
|
|
||||||
type_name.unwrap_or("entity"),
|
|
||||||
id
|
|
||||||
);
|
|
||||||
for trace in backtraces.values_mut().flatten() {
|
|
||||||
trace.resolve();
|
|
||||||
eprintln!("{:?}", crate::util::CwdBacktrace(trace));
|
|
||||||
}
|
|
||||||
found_leaks = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
let hint = if *LEAK_BACKTRACE {
|
|
||||||
""
|
|
||||||
} else {
|
|
||||||
" – set LEAK_BACKTRACE=1 for more information"
|
|
||||||
};
|
|
||||||
assert!(!found_leaks, "detected leaked handles{}", hint);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,661 +0,0 @@
|
|||||||
use crate::{
|
|
||||||
executor,
|
|
||||||
geometry::vector::Vector2F,
|
|
||||||
keymap_matcher::{Binding, Keystroke},
|
|
||||||
platform,
|
|
||||||
platform::{Event, InputHandler, KeyDownEvent, Platform},
|
|
||||||
Action, AnyWindowHandle, AppContext, BorrowAppContext, BorrowWindowContext, Entity, FontCache,
|
|
||||||
Handle, ModelContext, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle,
|
|
||||||
WeakHandle, WindowContext, WindowHandle,
|
|
||||||
};
|
|
||||||
use collections::BTreeMap;
|
|
||||||
use futures::Future;
|
|
||||||
use itertools::Itertools;
|
|
||||||
use parking_lot::{Mutex, RwLock};
|
|
||||||
use smallvec::SmallVec;
|
|
||||||
use smol::stream::StreamExt;
|
|
||||||
use std::{
|
|
||||||
any::Any,
|
|
||||||
cell::RefCell,
|
|
||||||
mem,
|
|
||||||
path::PathBuf,
|
|
||||||
rc::Rc,
|
|
||||||
sync::{
|
|
||||||
atomic::{AtomicUsize, Ordering},
|
|
||||||
Arc,
|
|
||||||
},
|
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{
|
|
||||||
ref_counts::LeakDetector, window_input_handler::WindowInputHandler, AsyncAppContext, RefCounts,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct TestAppContext {
|
|
||||||
cx: Rc<RefCell<AppContext>>,
|
|
||||||
foreground_platform: Rc<platform::test::ForegroundPlatform>,
|
|
||||||
condition_duration: Option<Duration>,
|
|
||||||
pub function_name: String,
|
|
||||||
assertion_context: AssertionContextManager,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TestAppContext {
|
|
||||||
pub fn new(
|
|
||||||
foreground_platform: Rc<platform::test::ForegroundPlatform>,
|
|
||||||
platform: Arc<dyn Platform>,
|
|
||||||
foreground: Rc<executor::Foreground>,
|
|
||||||
background: Arc<executor::Background>,
|
|
||||||
font_cache: Arc<FontCache>,
|
|
||||||
leak_detector: Arc<Mutex<LeakDetector>>,
|
|
||||||
first_entity_id: usize,
|
|
||||||
function_name: String,
|
|
||||||
) -> Self {
|
|
||||||
let mut cx = AppContext::new(
|
|
||||||
foreground,
|
|
||||||
background,
|
|
||||||
platform,
|
|
||||||
foreground_platform.clone(),
|
|
||||||
font_cache,
|
|
||||||
util::http::FakeHttpClient::with_404_response(),
|
|
||||||
RefCounts::new(leak_detector),
|
|
||||||
(),
|
|
||||||
);
|
|
||||||
cx.next_id = first_entity_id;
|
|
||||||
let cx = TestAppContext {
|
|
||||||
cx: Rc::new(RefCell::new(cx)),
|
|
||||||
foreground_platform,
|
|
||||||
condition_duration: None,
|
|
||||||
function_name,
|
|
||||||
assertion_context: AssertionContextManager::new(),
|
|
||||||
};
|
|
||||||
cx.cx.borrow_mut().weak_self = Some(Rc::downgrade(&cx.cx));
|
|
||||||
cx
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn dispatch_action<A: Action>(&mut self, window: AnyWindowHandle, action: A) {
|
|
||||||
self.update_window(window, |window| {
|
|
||||||
window.dispatch_action(window.focused_view_id(), &action);
|
|
||||||
})
|
|
||||||
.expect("window not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn available_actions(
|
|
||||||
&self,
|
|
||||||
window: AnyWindowHandle,
|
|
||||||
view_id: usize,
|
|
||||||
) -> Vec<(&'static str, Box<dyn Action>, SmallVec<[Binding; 1]>)> {
|
|
||||||
self.read_window(window, |cx| cx.available_actions(view_id))
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn dispatch_global_action<A: Action>(&mut self, action: A) {
|
|
||||||
self.update(|cx| cx.dispatch_global_action_any(&action));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn dispatch_keystroke(
|
|
||||||
&mut self,
|
|
||||||
window: AnyWindowHandle,
|
|
||||||
keystroke: Keystroke,
|
|
||||||
is_held: bool,
|
|
||||||
) {
|
|
||||||
let handled = window.update(self, |cx| {
|
|
||||||
if cx.dispatch_keystroke(&keystroke) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if cx.dispatch_event(
|
|
||||||
Event::KeyDown(KeyDownEvent {
|
|
||||||
keystroke: keystroke.clone(),
|
|
||||||
is_held,
|
|
||||||
}),
|
|
||||||
false,
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
false
|
|
||||||
});
|
|
||||||
|
|
||||||
if !handled && !keystroke.cmd && !keystroke.ctrl {
|
|
||||||
WindowInputHandler {
|
|
||||||
app: self.cx.clone(),
|
|
||||||
window,
|
|
||||||
}
|
|
||||||
.replace_text_in_range(None, &keystroke.key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn read_window<T, F: FnOnce(&WindowContext) -> T>(
|
|
||||||
&self,
|
|
||||||
window: AnyWindowHandle,
|
|
||||||
callback: F,
|
|
||||||
) -> Option<T> {
|
|
||||||
self.cx.borrow().read_window(window, callback)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_window<T, F: FnOnce(&mut WindowContext) -> T>(
|
|
||||||
&mut self,
|
|
||||||
window: AnyWindowHandle,
|
|
||||||
callback: F,
|
|
||||||
) -> Option<T> {
|
|
||||||
self.cx.borrow_mut().update_window(window, callback)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_model<T, F>(&mut self, build_model: F) -> ModelHandle<T>
|
|
||||||
where
|
|
||||||
T: Entity,
|
|
||||||
F: FnOnce(&mut ModelContext<T>) -> T,
|
|
||||||
{
|
|
||||||
self.cx.borrow_mut().add_model(build_model)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_window<V, F>(&mut self, build_root_view: F) -> WindowHandle<V>
|
|
||||||
where
|
|
||||||
V: View,
|
|
||||||
F: FnOnce(&mut ViewContext<V>) -> V,
|
|
||||||
{
|
|
||||||
let window = self
|
|
||||||
.cx
|
|
||||||
.borrow_mut()
|
|
||||||
.add_window(Default::default(), build_root_view);
|
|
||||||
window.simulate_activation(self);
|
|
||||||
window
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn observe_global<E, F>(&mut self, callback: F) -> Subscription
|
|
||||||
where
|
|
||||||
E: Any,
|
|
||||||
F: 'static + FnMut(&mut AppContext),
|
|
||||||
{
|
|
||||||
self.cx.borrow_mut().observe_global::<E, F>(callback)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_global<T: 'static>(&mut self, state: T) {
|
|
||||||
self.cx.borrow_mut().set_global(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn subscribe_global<E, F>(&mut self, callback: F) -> Subscription
|
|
||||||
where
|
|
||||||
E: Any,
|
|
||||||
F: 'static + FnMut(&E, &mut AppContext),
|
|
||||||
{
|
|
||||||
self.cx.borrow_mut().subscribe_global(callback)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn windows(&self) -> Vec<AnyWindowHandle> {
|
|
||||||
self.cx.borrow().windows().collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn remove_all_windows(&mut self) {
|
|
||||||
self.update(|cx| cx.windows.clear());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn read<T, F: FnOnce(&AppContext) -> T>(&self, callback: F) -> T {
|
|
||||||
callback(&*self.cx.borrow())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update<T, F: FnOnce(&mut AppContext) -> T>(&mut self, callback: F) -> T {
|
|
||||||
let mut state = self.cx.borrow_mut();
|
|
||||||
// Don't increment pending flushes in order for effects to be flushed before the callback
|
|
||||||
// completes, which is helpful in tests.
|
|
||||||
let result = callback(&mut *state);
|
|
||||||
// Flush effects after the callback just in case there are any. This can happen in edge
|
|
||||||
// cases such as the closure dropping handles.
|
|
||||||
state.flush_effects();
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_async(&self) -> AsyncAppContext {
|
|
||||||
AsyncAppContext(self.cx.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn font_cache(&self) -> Arc<FontCache> {
|
|
||||||
self.cx.borrow().font_cache.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn foreground_platform(&self) -> Rc<platform::test::ForegroundPlatform> {
|
|
||||||
self.foreground_platform.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn platform(&self) -> Arc<dyn platform::Platform> {
|
|
||||||
self.cx.borrow().platform.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn foreground(&self) -> Rc<executor::Foreground> {
|
|
||||||
self.cx.borrow().foreground().clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn background(&self) -> Arc<executor::Background> {
|
|
||||||
self.cx.borrow().background().clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn spawn<F, Fut, T>(&self, f: F) -> Task<T>
|
|
||||||
where
|
|
||||||
F: FnOnce(AsyncAppContext) -> Fut,
|
|
||||||
Fut: 'static + Future<Output = T>,
|
|
||||||
T: 'static,
|
|
||||||
{
|
|
||||||
let foreground = self.foreground();
|
|
||||||
let future = f(self.to_async());
|
|
||||||
let cx = self.to_async();
|
|
||||||
foreground.spawn(async move {
|
|
||||||
let result = future.await;
|
|
||||||
cx.0.borrow_mut().flush_effects();
|
|
||||||
result
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn simulate_new_path_selection(&self, result: impl FnOnce(PathBuf) -> Option<PathBuf>) {
|
|
||||||
self.foreground_platform.simulate_new_path_selection(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn did_prompt_for_new_path(&self) -> bool {
|
|
||||||
self.foreground_platform.as_ref().did_prompt_for_new_path()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn leak_detector(&self) -> Arc<Mutex<LeakDetector>> {
|
|
||||||
self.cx.borrow().leak_detector()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn assert_dropped(&self, handle: impl WeakHandle) {
|
|
||||||
self.cx
|
|
||||||
.borrow()
|
|
||||||
.leak_detector()
|
|
||||||
.lock()
|
|
||||||
.assert_dropped(handle.id())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Drop a handle, assuming it is the last. If it is not the last, panic with debug information about
|
|
||||||
/// where the stray handles were created.
|
|
||||||
pub fn drop_last<T, W: WeakHandle, H: Handle<T, Weak = W>>(&mut self, handle: H) {
|
|
||||||
let weak = handle.downgrade();
|
|
||||||
self.update(|_| drop(handle));
|
|
||||||
self.assert_dropped(weak);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_condition_duration(&mut self, duration: Option<Duration>) {
|
|
||||||
self.condition_duration = duration;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn condition_duration(&self) -> Duration {
|
|
||||||
self.condition_duration.unwrap_or_else(|| {
|
|
||||||
if std::env::var("CI").is_ok() {
|
|
||||||
Duration::from_secs(2)
|
|
||||||
} else {
|
|
||||||
Duration::from_millis(500)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn assert_clipboard_content(&mut self, expected_content: Option<&str>) {
|
|
||||||
self.update(|cx| {
|
|
||||||
let actual_content = cx.read_from_clipboard().map(|item| item.text().to_owned());
|
|
||||||
let expected_content = expected_content.map(|content| content.to_owned());
|
|
||||||
assert_eq!(actual_content, expected_content);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_assertion_context(&self, context: String) -> ContextHandle {
|
|
||||||
self.assertion_context.add_context(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn assertion_context(&self) -> String {
|
|
||||||
self.assertion_context.context()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BorrowAppContext for TestAppContext {
|
|
||||||
fn read_with<T, F: FnOnce(&AppContext) -> T>(&self, f: F) -> T {
|
|
||||||
self.cx.borrow().read_with(f)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update<T, F: FnOnce(&mut AppContext) -> T>(&mut self, f: F) -> T {
|
|
||||||
self.cx.borrow_mut().update(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BorrowWindowContext for TestAppContext {
|
|
||||||
type Result<T> = T;
|
|
||||||
|
|
||||||
fn read_window<T, F: FnOnce(&WindowContext) -> T>(&self, window: AnyWindowHandle, f: F) -> T {
|
|
||||||
self.cx
|
|
||||||
.borrow()
|
|
||||||
.read_window(window, f)
|
|
||||||
.expect("window was closed")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_window_optional<T, F>(&self, window: AnyWindowHandle, f: F) -> Option<T>
|
|
||||||
where
|
|
||||||
F: FnOnce(&WindowContext) -> Option<T>,
|
|
||||||
{
|
|
||||||
BorrowWindowContext::read_window(self, window, f)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_window<T, F: FnOnce(&mut WindowContext) -> T>(
|
|
||||||
&mut self,
|
|
||||||
window: AnyWindowHandle,
|
|
||||||
f: F,
|
|
||||||
) -> T {
|
|
||||||
self.cx
|
|
||||||
.borrow_mut()
|
|
||||||
.update_window(window, f)
|
|
||||||
.expect("window was closed")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_window_optional<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Option<T>
|
|
||||||
where
|
|
||||||
F: FnOnce(&mut WindowContext) -> Option<T>,
|
|
||||||
{
|
|
||||||
BorrowWindowContext::update_window(self, window, f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Entity> ModelHandle<T> {
|
|
||||||
pub fn next_notification(&self, cx: &TestAppContext) -> impl Future<Output = ()> {
|
|
||||||
let (tx, mut rx) = futures::channel::mpsc::unbounded();
|
|
||||||
let mut cx = cx.cx.borrow_mut();
|
|
||||||
let subscription = cx.observe(self, move |_, _| {
|
|
||||||
tx.unbounded_send(()).ok();
|
|
||||||
});
|
|
||||||
|
|
||||||
let duration = if std::env::var("CI").is_ok() {
|
|
||||||
Duration::from_secs(5)
|
|
||||||
} else {
|
|
||||||
Duration::from_secs(1)
|
|
||||||
};
|
|
||||||
|
|
||||||
let executor = cx.background().clone();
|
|
||||||
async move {
|
|
||||||
executor.start_waiting();
|
|
||||||
let notification = crate::util::timeout(duration, rx.next())
|
|
||||||
.await
|
|
||||||
.expect("next notification timed out");
|
|
||||||
drop(subscription);
|
|
||||||
notification.expect("model dropped while test was waiting for its next notification")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn next_event(&self, cx: &TestAppContext) -> impl Future<Output = T::Event>
|
|
||||||
where
|
|
||||||
T::Event: Clone,
|
|
||||||
{
|
|
||||||
let (tx, mut rx) = futures::channel::mpsc::unbounded();
|
|
||||||
let mut cx = cx.cx.borrow_mut();
|
|
||||||
let subscription = cx.subscribe(self, move |_, event, _| {
|
|
||||||
tx.unbounded_send(event.clone()).ok();
|
|
||||||
});
|
|
||||||
|
|
||||||
let duration = if std::env::var("CI").is_ok() {
|
|
||||||
Duration::from_secs(5)
|
|
||||||
} else {
|
|
||||||
Duration::from_secs(1)
|
|
||||||
};
|
|
||||||
|
|
||||||
cx.foreground.start_waiting();
|
|
||||||
async move {
|
|
||||||
let event = crate::util::timeout(duration, rx.next())
|
|
||||||
.await
|
|
||||||
.expect("next event timed out");
|
|
||||||
drop(subscription);
|
|
||||||
event.expect("model dropped while test was waiting for its next event")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn condition(
|
|
||||||
&self,
|
|
||||||
cx: &TestAppContext,
|
|
||||||
mut predicate: impl FnMut(&T, &AppContext) -> bool,
|
|
||||||
) -> impl Future<Output = ()> {
|
|
||||||
let (tx, mut rx) = futures::channel::mpsc::unbounded();
|
|
||||||
|
|
||||||
let mut cx = cx.cx.borrow_mut();
|
|
||||||
let subscriptions = (
|
|
||||||
cx.observe(self, {
|
|
||||||
let tx = tx.clone();
|
|
||||||
move |_, _| {
|
|
||||||
tx.unbounded_send(()).ok();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
cx.subscribe(self, {
|
|
||||||
move |_, _, _| {
|
|
||||||
tx.unbounded_send(()).ok();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
let cx = cx.weak_self.as_ref().unwrap().upgrade().unwrap();
|
|
||||||
let handle = self.downgrade();
|
|
||||||
let duration = if std::env::var("CI").is_ok() {
|
|
||||||
Duration::from_secs(5)
|
|
||||||
} else {
|
|
||||||
Duration::from_secs(1)
|
|
||||||
};
|
|
||||||
|
|
||||||
async move {
|
|
||||||
crate::util::timeout(duration, async move {
|
|
||||||
loop {
|
|
||||||
{
|
|
||||||
let cx = cx.borrow();
|
|
||||||
let cx = &*cx;
|
|
||||||
if predicate(
|
|
||||||
handle
|
|
||||||
.upgrade(cx)
|
|
||||||
.expect("model dropped with pending condition")
|
|
||||||
.read(cx),
|
|
||||||
cx,
|
|
||||||
) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cx.borrow().foreground().start_waiting();
|
|
||||||
rx.next()
|
|
||||||
.await
|
|
||||||
.expect("model dropped with pending condition");
|
|
||||||
cx.borrow().foreground().finish_waiting();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.expect("condition timed out");
|
|
||||||
drop(subscriptions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AnyWindowHandle {
|
|
||||||
pub fn has_pending_prompt(&self, cx: &mut TestAppContext) -> bool {
|
|
||||||
let window = self.platform_window_mut(cx);
|
|
||||||
let prompts = window.pending_prompts.borrow_mut();
|
|
||||||
!prompts.is_empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn current_title(&self, cx: &mut TestAppContext) -> Option<String> {
|
|
||||||
self.platform_window_mut(cx).title.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn simulate_close(&self, cx: &mut TestAppContext) -> bool {
|
|
||||||
let handler = self.platform_window_mut(cx).should_close_handler.take();
|
|
||||||
if let Some(mut handler) = handler {
|
|
||||||
let should_close = handler();
|
|
||||||
self.platform_window_mut(cx).should_close_handler = Some(handler);
|
|
||||||
should_close
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn simulate_resize(&self, size: Vector2F, cx: &mut TestAppContext) {
|
|
||||||
let mut window = self.platform_window_mut(cx);
|
|
||||||
window.size = size;
|
|
||||||
let mut handlers = mem::take(&mut window.resize_handlers);
|
|
||||||
drop(window);
|
|
||||||
for handler in &mut handlers {
|
|
||||||
handler();
|
|
||||||
}
|
|
||||||
self.platform_window_mut(cx).resize_handlers = handlers;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_edited(&self, cx: &mut TestAppContext) -> bool {
|
|
||||||
self.platform_window_mut(cx).edited
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn simulate_prompt_answer(&self, answer: usize, cx: &mut TestAppContext) {
|
|
||||||
use postage::prelude::Sink as _;
|
|
||||||
|
|
||||||
let mut done_tx = self
|
|
||||||
.platform_window_mut(cx)
|
|
||||||
.pending_prompts
|
|
||||||
.borrow_mut()
|
|
||||||
.pop_front()
|
|
||||||
.expect("prompt was not called");
|
|
||||||
done_tx.try_send(answer).ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn platform_window_mut<'a>(
|
|
||||||
&self,
|
|
||||||
cx: &'a mut TestAppContext,
|
|
||||||
) -> std::cell::RefMut<'a, platform::test::Window> {
|
|
||||||
std::cell::RefMut::map(cx.cx.borrow_mut(), |state| {
|
|
||||||
let window = state.windows.get_mut(&self).unwrap();
|
|
||||||
let test_window = window
|
|
||||||
.platform_window
|
|
||||||
.as_any_mut()
|
|
||||||
.downcast_mut::<platform::test::Window>()
|
|
||||||
.unwrap();
|
|
||||||
test_window
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: View> ViewHandle<T> {
|
|
||||||
pub fn next_notification(&self, cx: &TestAppContext) -> impl Future<Output = ()> {
|
|
||||||
use postage::prelude::{Sink as _, Stream as _};
|
|
||||||
|
|
||||||
let (mut tx, mut rx) = postage::mpsc::channel(1);
|
|
||||||
let mut cx = cx.cx.borrow_mut();
|
|
||||||
let subscription = cx.observe(self, move |_, _| {
|
|
||||||
tx.try_send(()).ok();
|
|
||||||
});
|
|
||||||
|
|
||||||
let duration = if std::env::var("CI").is_ok() {
|
|
||||||
Duration::from_secs(5)
|
|
||||||
} else {
|
|
||||||
Duration::from_secs(1)
|
|
||||||
};
|
|
||||||
|
|
||||||
async move {
|
|
||||||
let notification = crate::util::timeout(duration, rx.recv())
|
|
||||||
.await
|
|
||||||
.expect("next notification timed out");
|
|
||||||
drop(subscription);
|
|
||||||
notification.expect("model dropped while test was waiting for its next notification")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn condition(
|
|
||||||
&self,
|
|
||||||
cx: &TestAppContext,
|
|
||||||
mut predicate: impl FnMut(&T, &AppContext) -> bool,
|
|
||||||
) -> impl Future<Output = ()> {
|
|
||||||
use postage::prelude::{Sink as _, Stream as _};
|
|
||||||
|
|
||||||
let (tx, mut rx) = postage::mpsc::channel(1024);
|
|
||||||
let timeout_duration = cx.condition_duration();
|
|
||||||
|
|
||||||
let mut cx = cx.cx.borrow_mut();
|
|
||||||
let subscriptions = (
|
|
||||||
cx.observe(self, {
|
|
||||||
let mut tx = tx.clone();
|
|
||||||
move |_, _| {
|
|
||||||
tx.blocking_send(()).ok();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
cx.subscribe(self, {
|
|
||||||
let mut tx = tx.clone();
|
|
||||||
move |_, _, _| {
|
|
||||||
tx.blocking_send(()).ok();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
let cx = cx.weak_self.as_ref().unwrap().upgrade().unwrap();
|
|
||||||
let handle = self.downgrade();
|
|
||||||
|
|
||||||
async move {
|
|
||||||
crate::util::timeout(timeout_duration, async move {
|
|
||||||
loop {
|
|
||||||
{
|
|
||||||
let cx = cx.borrow();
|
|
||||||
let cx = &*cx;
|
|
||||||
if predicate(
|
|
||||||
handle
|
|
||||||
.upgrade(cx)
|
|
||||||
.expect("view dropped with pending condition")
|
|
||||||
.read(cx),
|
|
||||||
cx,
|
|
||||||
) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cx.borrow().foreground().start_waiting();
|
|
||||||
rx.recv()
|
|
||||||
.await
|
|
||||||
.expect("view dropped with pending condition");
|
|
||||||
cx.borrow().foreground().finish_waiting();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.expect("condition timed out");
|
|
||||||
drop(subscriptions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Tracks string context to be printed when assertions fail.
|
|
||||||
/// Often this is done by storing a context string in the manager and returning the handle.
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct AssertionContextManager {
|
|
||||||
id: Arc<AtomicUsize>,
|
|
||||||
contexts: Arc<RwLock<BTreeMap<usize, String>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AssertionContextManager {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
id: Arc::new(AtomicUsize::new(0)),
|
|
||||||
contexts: Arc::new(RwLock::new(BTreeMap::new())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_context(&self, context: String) -> ContextHandle {
|
|
||||||
let id = self.id.fetch_add(1, Ordering::Relaxed);
|
|
||||||
let mut contexts = self.contexts.write();
|
|
||||||
contexts.insert(id, context);
|
|
||||||
ContextHandle {
|
|
||||||
id,
|
|
||||||
manager: self.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn context(&self) -> String {
|
|
||||||
let contexts = self.contexts.read();
|
|
||||||
format!("\n{}\n", contexts.values().join("\n"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Used to track the lifetime of a piece of context so that it can be provided when an assertion fails.
|
|
||||||
/// For example, in the EditorTestContext, `set_state` returns a context handle so that if an assertion fails,
|
|
||||||
/// the state that was set initially for the failure can be printed in the error message
|
|
||||||
pub struct ContextHandle {
|
|
||||||
id: usize,
|
|
||||||
manager: AssertionContextManager,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for ContextHandle {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
let mut contexts = self.manager.contexts.write();
|
|
||||||
contexts.remove(&self.id);
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
@ -1,90 +0,0 @@
|
|||||||
use std::{cell::RefCell, ops::Range, rc::Rc};
|
|
||||||
|
|
||||||
use pathfinder_geometry::rect::RectF;
|
|
||||||
|
|
||||||
use crate::{platform::InputHandler, window::WindowContext, AnyView, AnyWindowHandle, AppContext};
|
|
||||||
|
|
||||||
pub struct WindowInputHandler {
|
|
||||||
pub app: Rc<RefCell<AppContext>>,
|
|
||||||
pub window: AnyWindowHandle,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WindowInputHandler {
|
|
||||||
fn read_focused_view<T, F>(&self, f: F) -> Option<T>
|
|
||||||
where
|
|
||||||
F: FnOnce(&dyn AnyView, &WindowContext) -> T,
|
|
||||||
{
|
|
||||||
// Input-related application hooks are sometimes called by the OS during
|
|
||||||
// a call to a window-manipulation API, like prompting the user for file
|
|
||||||
// paths. In that case, the AppContext will already be borrowed, so any
|
|
||||||
// InputHandler methods need to fail gracefully.
|
|
||||||
//
|
|
||||||
// See https://github.com/zed-industries/community/issues/444
|
|
||||||
let mut app = self.app.try_borrow_mut().ok()?;
|
|
||||||
self.window.update_optional(&mut *app, |cx| {
|
|
||||||
let view_id = cx.window.focused_view_id?;
|
|
||||||
let view = cx.views.get(&(self.window, view_id))?;
|
|
||||||
let result = f(view.as_ref(), &cx);
|
|
||||||
Some(result)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_focused_view<T, F>(&mut self, f: F) -> Option<T>
|
|
||||||
where
|
|
||||||
F: FnOnce(&mut dyn AnyView, &mut WindowContext, usize) -> T,
|
|
||||||
{
|
|
||||||
let mut app = self.app.try_borrow_mut().ok()?;
|
|
||||||
self.window
|
|
||||||
.update(&mut *app, |cx| {
|
|
||||||
let view_id = cx.window.focused_view_id?;
|
|
||||||
cx.update_any_view(view_id, |view, cx| f(view, cx, view_id))
|
|
||||||
})
|
|
||||||
.flatten()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl InputHandler for WindowInputHandler {
|
|
||||||
fn text_for_range(&self, range: Range<usize>) -> Option<String> {
|
|
||||||
self.read_focused_view(|view, cx| view.text_for_range(range.clone(), cx))
|
|
||||||
.flatten()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn selected_text_range(&self) -> Option<Range<usize>> {
|
|
||||||
self.read_focused_view(|view, cx| view.selected_text_range(cx))
|
|
||||||
.flatten()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn replace_text_in_range(&mut self, range: Option<Range<usize>>, text: &str) {
|
|
||||||
self.update_focused_view(|view, cx, view_id| {
|
|
||||||
view.replace_text_in_range(range, text, cx, view_id);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn marked_text_range(&self) -> Option<Range<usize>> {
|
|
||||||
self.read_focused_view(|view, cx| view.marked_text_range(cx))
|
|
||||||
.flatten()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn unmark_text(&mut self) {
|
|
||||||
self.update_focused_view(|view, cx, view_id| {
|
|
||||||
view.unmark_text(cx, view_id);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn replace_and_mark_text_in_range(
|
|
||||||
&mut self,
|
|
||||||
range: Option<Range<usize>>,
|
|
||||||
new_text: &str,
|
|
||||||
new_selected_range: Option<Range<usize>>,
|
|
||||||
) {
|
|
||||||
self.update_focused_view(|view, cx, view_id| {
|
|
||||||
view.replace_and_mark_text_in_range(range, new_text, new_selected_range, cx, view_id);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_range(&self, range_utf16: Range<usize>) -> Option<RectF> {
|
|
||||||
self.window.read_optional_with(&*self.app.borrow(), |cx| {
|
|
||||||
cx.rect_for_text_range(range_utf16)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,12 +1,16 @@
|
|||||||
use anyhow::{anyhow, Result};
|
use crate::{size, DevicePixels, Result, SharedString, Size};
|
||||||
use image::ImageFormat;
|
use anyhow::anyhow;
|
||||||
use std::{borrow::Cow, cell::RefCell, collections::HashMap, sync::Arc};
|
use image::{Bgra, ImageBuffer};
|
||||||
|
use std::{
|
||||||
use crate::ImageData;
|
borrow::Cow,
|
||||||
|
fmt,
|
||||||
|
hash::Hash,
|
||||||
|
sync::atomic::{AtomicUsize, Ordering::SeqCst},
|
||||||
|
};
|
||||||
|
|
||||||
pub trait AssetSource: 'static + Send + Sync {
|
pub trait AssetSource: 'static + Send + Sync {
|
||||||
fn load(&self, path: &str) -> Result<Cow<[u8]>>;
|
fn load(&self, path: &str) -> Result<Cow<[u8]>>;
|
||||||
fn list(&self, path: &str) -> Vec<Cow<'static, str>>;
|
fn list(&self, path: &str) -> Result<Vec<SharedString>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AssetSource for () {
|
impl AssetSource for () {
|
||||||
@ -17,49 +21,44 @@ impl AssetSource for () {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn list(&self, _: &str) -> Vec<Cow<'static, str>> {
|
fn list(&self, _path: &str) -> Result<Vec<SharedString>> {
|
||||||
vec![]
|
Ok(vec![])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AssetCache {
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||||
source: Box<dyn AssetSource>,
|
pub struct ImageId(usize);
|
||||||
svgs: RefCell<HashMap<String, usvg::Tree>>,
|
|
||||||
pngs: RefCell<HashMap<String, Arc<ImageData>>>,
|
pub struct ImageData {
|
||||||
|
pub id: ImageId,
|
||||||
|
data: ImageBuffer<Bgra<u8>, Vec<u8>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AssetCache {
|
impl ImageData {
|
||||||
pub fn new(source: impl AssetSource) -> Self {
|
pub fn new(data: ImageBuffer<Bgra<u8>, Vec<u8>>) -> Self {
|
||||||
|
static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
source: Box::new(source),
|
id: ImageId(NEXT_ID.fetch_add(1, SeqCst)),
|
||||||
svgs: RefCell::new(HashMap::new()),
|
data,
|
||||||
pngs: RefCell::new(HashMap::new()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn svg(&self, path: &str) -> Result<usvg::Tree> {
|
pub fn as_bytes(&self) -> &[u8] {
|
||||||
let mut svgs = self.svgs.borrow_mut();
|
&self.data
|
||||||
if let Some(svg) = svgs.get(path) {
|
|
||||||
Ok(svg.clone())
|
|
||||||
} else {
|
|
||||||
let bytes = self.source.load(path)?;
|
|
||||||
let svg = usvg::Tree::from_data(&bytes, &usvg::Options::default())?;
|
|
||||||
svgs.insert(path.to_string(), svg.clone());
|
|
||||||
Ok(svg)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn png(&self, path: &str) -> Result<Arc<ImageData>> {
|
pub fn size(&self) -> Size<DevicePixels> {
|
||||||
let mut pngs = self.pngs.borrow_mut();
|
let (width, height) = self.data.dimensions();
|
||||||
if let Some(png) = pngs.get(path) {
|
size(width.into(), height.into())
|
||||||
Ok(png.clone())
|
}
|
||||||
} else {
|
}
|
||||||
let bytes = self.source.load(path)?;
|
|
||||||
let image = ImageData::new(
|
impl fmt::Debug for ImageData {
|
||||||
image::load_from_memory_with_format(&bytes, ImageFormat::Png)?.into_bgra8(),
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
);
|
f.debug_struct("ImageData")
|
||||||
pngs.insert(path.to_string(), image.clone());
|
.field("id", &self.id)
|
||||||
Ok(image)
|
.field("size", &self.data.dimensions())
|
||||||
}
|
.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,42 +0,0 @@
|
|||||||
use seahash::SeaHasher;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::hash::{Hash, Hasher};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
||||||
pub struct ClipboardItem {
|
|
||||||
pub(crate) text: String,
|
|
||||||
pub(crate) metadata: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ClipboardItem {
|
|
||||||
pub fn new(text: String) -> Self {
|
|
||||||
Self {
|
|
||||||
text,
|
|
||||||
metadata: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_metadata<T: Serialize>(mut self, metadata: T) -> Self {
|
|
||||||
self.metadata = Some(serde_json::to_string(&metadata).unwrap());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn text(&self) -> &String {
|
|
||||||
&self.text
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn metadata<T>(&self) -> Option<T>
|
|
||||||
where
|
|
||||||
T: for<'a> Deserialize<'a>,
|
|
||||||
{
|
|
||||||
self.metadata
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|m| serde_json::from_str(m).ok())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn text_hash(text: &str) -> u64 {
|
|
||||||
let mut hasher = SeaHasher::new();
|
|
||||||
text.hash(&mut hasher);
|
|
||||||
hasher.finish()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,65 +1,223 @@
|
|||||||
use std::{
|
use anyhow::bail;
|
||||||
borrow::Cow,
|
use serde::de::{self, Deserialize, Deserializer, Visitor};
|
||||||
fmt,
|
use std::fmt;
|
||||||
ops::{Deref, DerefMut},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::json::ToJson;
|
pub fn rgb<C: From<Rgba>>(hex: u32) -> C {
|
||||||
use pathfinder_color::{ColorF, ColorU};
|
let r = ((hex >> 16) & 0xFF) as f32 / 255.0;
|
||||||
use schemars::JsonSchema;
|
let g = ((hex >> 8) & 0xFF) as f32 / 255.0;
|
||||||
use serde::{
|
let b = (hex & 0xFF) as f32 / 255.0;
|
||||||
de::{self, Unexpected},
|
Rgba { r, g, b, a: 1.0 }.into()
|
||||||
Deserialize, Deserializer,
|
|
||||||
};
|
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord, JsonSchema)]
|
|
||||||
#[repr(transparent)]
|
|
||||||
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 {
|
pub fn rgba(hex: u32) -> Rgba {
|
||||||
Color(ColorF::new(r, g, b, 1.).to_u8())
|
let r = ((hex >> 24) & 0xFF) as f32 / 255.0;
|
||||||
|
let g = ((hex >> 16) & 0xFF) as f32 / 255.0;
|
||||||
|
let b = ((hex >> 8) & 0xFF) as f32 / 255.0;
|
||||||
|
let a = (hex & 0xFF) as f32 / 255.0;
|
||||||
|
Rgba { r, g, b, a }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn rgba(r: f32, g: f32, b: f32, a: f32) -> Color {
|
#[derive(PartialEq, Clone, Copy, Default)]
|
||||||
Color(ColorF::new(r, g, b, a).to_u8())
|
pub struct Rgba {
|
||||||
|
pub r: f32,
|
||||||
|
pub g: f32,
|
||||||
|
pub b: f32,
|
||||||
|
pub a: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn transparent_black() -> Color {
|
impl fmt::Debug for Rgba {
|
||||||
Color(ColorU::transparent_black())
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "rgba({:#010x})", u32::from(*self))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn black() -> Color {
|
impl Rgba {
|
||||||
Color(ColorU::black())
|
pub fn blend(&self, other: Rgba) -> Self {
|
||||||
|
if other.a >= 1.0 {
|
||||||
|
other
|
||||||
|
} else if other.a <= 0.0 {
|
||||||
|
return *self;
|
||||||
|
} else {
|
||||||
|
return Rgba {
|
||||||
|
r: (self.r * (1.0 - other.a)) + (other.r * other.a),
|
||||||
|
g: (self.g * (1.0 - other.a)) + (other.g * other.a),
|
||||||
|
b: (self.b * (1.0 - other.a)) + (other.b * other.a),
|
||||||
|
a: self.a,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn white() -> Color {
|
impl From<Rgba> for u32 {
|
||||||
Color(ColorU::white())
|
fn from(rgba: Rgba) -> Self {
|
||||||
|
let r = (rgba.r * 255.0) as u32;
|
||||||
|
let g = (rgba.g * 255.0) as u32;
|
||||||
|
let b = (rgba.b * 255.0) as u32;
|
||||||
|
let a = (rgba.a * 255.0) as u32;
|
||||||
|
(r << 24) | (g << 16) | (b << 8) | a
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn red() -> Color {
|
struct RgbaVisitor;
|
||||||
color(0xff0000ff)
|
|
||||||
|
impl<'de> Visitor<'de> for RgbaVisitor {
|
||||||
|
type Value = Rgba;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
formatter.write_str("a string in the format #rrggbb or #rrggbbaa")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_str<E: de::Error>(self, value: &str) -> Result<Rgba, E> {
|
||||||
|
Rgba::try_from(value).map_err(E::custom)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn green() -> Color {
|
impl<'de> Deserialize<'de> for Rgba {
|
||||||
color(0x00ff00ff)
|
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||||
|
deserializer.deserialize_str(RgbaVisitor)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn blue() -> Color {
|
impl From<Hsla> for Rgba {
|
||||||
color(0x0000ffff)
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn yellow() -> Color {
|
impl TryFrom<&'_ str> for Rgba {
|
||||||
color(0xffff00ff)
|
type Error = anyhow::Error;
|
||||||
|
|
||||||
|
fn try_from(value: &'_ str) -> Result<Self, Self::Error> {
|
||||||
|
const RGB: usize = "rgb".len();
|
||||||
|
const RGBA: usize = "rgba".len();
|
||||||
|
const RRGGBB: usize = "rrggbb".len();
|
||||||
|
const RRGGBBAA: usize = "rrggbbaa".len();
|
||||||
|
|
||||||
|
const EXPECTED_FORMATS: &str = "Expected #rgb, #rgba, #rrggbb, or #rrggbbaa";
|
||||||
|
|
||||||
|
let Some(("", hex)) = value.trim().split_once('#') else {
|
||||||
|
bail!("invalid RGBA hex color: '{value}'. {EXPECTED_FORMATS}");
|
||||||
|
};
|
||||||
|
|
||||||
|
let (r, g, b, a) = match hex.len() {
|
||||||
|
RGB | RGBA => {
|
||||||
|
let r = u8::from_str_radix(&hex[0..1], 16)?;
|
||||||
|
let g = u8::from_str_radix(&hex[1..2], 16)?;
|
||||||
|
let b = u8::from_str_radix(&hex[2..3], 16)?;
|
||||||
|
let a = if hex.len() == RGBA {
|
||||||
|
u8::from_str_radix(&hex[3..4], 16)?
|
||||||
|
} else {
|
||||||
|
0xf
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Duplicates a given hex digit.
|
||||||
|
/// E.g., `0xf` -> `0xff`.
|
||||||
|
const fn duplicate(value: u8) -> u8 {
|
||||||
|
value << 4 | value
|
||||||
|
}
|
||||||
|
|
||||||
|
(duplicate(r), duplicate(g), duplicate(b), duplicate(a))
|
||||||
|
}
|
||||||
|
RRGGBB | RRGGBBAA => {
|
||||||
|
let r = u8::from_str_radix(&hex[0..2], 16)?;
|
||||||
|
let g = u8::from_str_radix(&hex[2..4], 16)?;
|
||||||
|
let b = u8::from_str_radix(&hex[4..6], 16)?;
|
||||||
|
let a = if hex.len() == RRGGBBAA {
|
||||||
|
u8::from_str_radix(&hex[6..8], 16)?
|
||||||
|
} else {
|
||||||
|
0xff
|
||||||
|
};
|
||||||
|
(r, g, b, a)
|
||||||
|
}
|
||||||
|
_ => bail!("invalid RGBA hex color: '{value}'. {EXPECTED_FORMATS}"),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Rgba {
|
||||||
|
r: r as f32 / 255.,
|
||||||
|
g: g as f32 / 255.,
|
||||||
|
b: b as f32 / 255.,
|
||||||
|
a: a as f32 / 255.,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Color {
|
#[derive(Default, Copy, Clone, Debug)]
|
||||||
pub fn transparent_black() -> Self {
|
#[repr(C)]
|
||||||
transparent_black()
|
pub struct Hsla {
|
||||||
|
pub h: f32,
|
||||||
|
pub s: f32,
|
||||||
|
pub l: f32,
|
||||||
|
pub a: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for Hsla {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.h
|
||||||
|
.total_cmp(&other.h)
|
||||||
|
.then(self.s.total_cmp(&other.s))
|
||||||
|
.then(self.l.total_cmp(&other.l).then(self.a.total_cmp(&other.a)))
|
||||||
|
.is_eq()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for Hsla {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||||
|
// SAFETY: The total ordering relies on this always being Some()
|
||||||
|
Some(
|
||||||
|
self.h
|
||||||
|
.total_cmp(&other.h)
|
||||||
|
.then(self.s.total_cmp(&other.s))
|
||||||
|
.then(self.l.total_cmp(&other.l).then(self.a.total_cmp(&other.a))),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for Hsla {
|
||||||
|
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||||
|
// SAFETY: The partial comparison is a total comparison
|
||||||
|
unsafe { self.partial_cmp(other).unwrap_unchecked() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hsla {
|
||||||
|
pub fn to_rgb(self) -> Rgba {
|
||||||
|
self.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn red() -> Self {
|
||||||
|
red()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn green() -> Self {
|
||||||
|
green()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn blue() -> Self {
|
||||||
|
blue()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn black() -> Self {
|
pub fn black() -> Self {
|
||||||
@ -70,107 +228,230 @@ impl Color {
|
|||||||
white()
|
white()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn red() -> Self {
|
pub fn transparent_black() -> Self {
|
||||||
Color::from_u32(0xff0000ff)
|
transparent_black()
|
||||||
}
|
|
||||||
|
|
||||||
pub fn green() -> Self {
|
|
||||||
Color::from_u32(0x00ff00ff)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn blue() -> Self {
|
|
||||||
Color::from_u32(0x0000ffff)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn yellow() -> Self {
|
|
||||||
Color::from_u32(0xffff00ff)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
|
|
||||||
Self(ColorU::new(r, g, b, a))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_u32(rgba: u32) -> Self {
|
|
||||||
Self(ColorU::from_u32(rgba))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn blend(source: Color, dest: Color) -> Color {
|
|
||||||
// Skip blending if we don't need it.
|
|
||||||
if source.a == 255 {
|
|
||||||
return source;
|
|
||||||
} else if source.a == 0 {
|
|
||||||
return dest;
|
|
||||||
}
|
|
||||||
|
|
||||||
let source = source.0.to_f32();
|
|
||||||
let dest = dest.0.to_f32();
|
|
||||||
|
|
||||||
let a = source.a() + (dest.a() * (1. - source.a()));
|
|
||||||
let r = ((source.r() * source.a()) + (dest.r() * dest.a() * (1. - source.a()))) / a;
|
|
||||||
let g = ((source.g() * source.a()) + (dest.g() * dest.a() * (1. - source.a()))) / a;
|
|
||||||
let b = ((source.b() * source.a()) + (dest.b() * dest.a() * (1. - source.a()))) / a;
|
|
||||||
|
|
||||||
Self(ColorF::new(r, g, b, a).to_u8())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fade_out(&mut self, fade: f32) {
|
|
||||||
let fade = fade.clamp(0., 1.);
|
|
||||||
self.0.a = (self.0.a as f32 * (1. - fade)) as u8;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for Color {
|
impl Eq for Hsla {}
|
||||||
|
|
||||||
|
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.,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn transparent_black() -> Hsla {
|
||||||
|
Hsla {
|
||||||
|
h: 0.,
|
||||||
|
s: 0.,
|
||||||
|
l: 0.,
|
||||||
|
a: 0.,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn white() -> Hsla {
|
||||||
|
Hsla {
|
||||||
|
h: 0.,
|
||||||
|
s: 0.,
|
||||||
|
l: 1.,
|
||||||
|
a: 1.,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn red() -> Hsla {
|
||||||
|
Hsla {
|
||||||
|
h: 0.,
|
||||||
|
s: 1.,
|
||||||
|
l: 0.5,
|
||||||
|
a: 1.,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn blue() -> Hsla {
|
||||||
|
Hsla {
|
||||||
|
h: 0.6,
|
||||||
|
s: 1.,
|
||||||
|
l: 0.5,
|
||||||
|
a: 1.,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn green() -> Hsla {
|
||||||
|
Hsla {
|
||||||
|
h: 0.33,
|
||||||
|
s: 1.,
|
||||||
|
l: 0.5,
|
||||||
|
a: 1.,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn yellow() -> Hsla {
|
||||||
|
Hsla {
|
||||||
|
h: 0.16,
|
||||||
|
s: 1.,
|
||||||
|
l: 0.5,
|
||||||
|
a: 1.,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hsla {
|
||||||
|
/// Returns true if the HSLA color is fully transparent, false otherwise.
|
||||||
|
pub fn is_transparent(&self) -> bool {
|
||||||
|
self.a == 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Blends `other` on top of `self` based on `other`'s alpha value. The resulting color is a combination of `self`'s and `other`'s colors.
|
||||||
|
///
|
||||||
|
/// If `other`'s alpha value is 1.0 or greater, `other` color is fully opaque, thus `other` is returned as the output color.
|
||||||
|
/// If `other`'s alpha value is 0.0 or less, `other` color is fully transparent, thus `self` is returned as the output color.
|
||||||
|
/// Else, the output color is calculated as a blend of `self` and `other` based on their weighted alpha values.
|
||||||
|
///
|
||||||
|
/// Assumptions:
|
||||||
|
/// - Alpha values are contained in the range [0, 1], with 1 as fully opaque and 0 as fully transparent.
|
||||||
|
/// - The relative contributions of `self` and `other` is based on `self`'s alpha value (`self.a`) and `other`'s alpha value (`other.a`), `self` contributing `self.a * (1.0 - other.a)` and `other` contributing it's own alpha value.
|
||||||
|
/// - RGB color components are contained in the range [0, 1].
|
||||||
|
/// - If `self` and `other` colors are out of the valid range, the blend operation's output and behavior is undefined.
|
||||||
|
pub fn blend(self, other: Hsla) -> Hsla {
|
||||||
|
let alpha = other.a;
|
||||||
|
|
||||||
|
if alpha >= 1.0 {
|
||||||
|
other
|
||||||
|
} else if alpha <= 0.0 {
|
||||||
|
return self;
|
||||||
|
} else {
|
||||||
|
let converted_self = Rgba::from(self);
|
||||||
|
let converted_other = Rgba::from(other);
|
||||||
|
let blended_rgb = converted_self.blend(converted_other);
|
||||||
|
return Hsla::from(blended_rgb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fade out the color by a given factor. This factor should be between 0.0 and 1.0.
|
||||||
|
/// Where 0.0 will leave the color unchanged, and 1.0 will completely fade out the color.
|
||||||
|
pub fn fade_out(&mut self, factor: f32) {
|
||||||
|
self.a *= 1.0 - factor.clamp(0., 1.);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// impl From<Hsla> for Rgba {
|
||||||
|
// fn from(value: Hsla) -> Self {
|
||||||
|
// let h = value.h;
|
||||||
|
// let s = value.s;
|
||||||
|
// let l = value.l;
|
||||||
|
|
||||||
|
// let c = (1 - |2L - 1|) X s
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
impl From<Rgba> for Hsla {
|
||||||
|
fn from(color: Rgba) -> Self {
|
||||||
|
let r = color.r;
|
||||||
|
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<'de> Deserialize<'de> for Hsla {
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
where
|
where
|
||||||
D: Deserializer<'de>,
|
D: Deserializer<'de>,
|
||||||
{
|
{
|
||||||
let literal: Cow<str> = Deserialize::deserialize(deserializer)?;
|
// First, deserialize it into Rgba
|
||||||
if let Some(digits) = literal.strip_prefix('#') {
|
let rgba = Rgba::deserialize(deserializer)?;
|
||||||
if let Ok(value) = u32::from_str_radix(digits, 16) {
|
|
||||||
if digits.len() == 6 {
|
// Then, use the From<Rgba> for Hsla implementation to convert it
|
||||||
return Ok(Color::from_u32((value << 8) | 0xFF));
|
Ok(Hsla::from(rgba))
|
||||||
} else if digits.len() == 8 {
|
|
||||||
return Ok(Color::from_u32(value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(de::Error::invalid_value(
|
|
||||||
Unexpected::Str(literal.as_ref()),
|
|
||||||
&"#RRGGBB[AA]",
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<u32> for Color {
|
#[cfg(test)]
|
||||||
fn from(value: u32) -> Self {
|
mod tests {
|
||||||
Self(ColorU::from_u32(value))
|
use serde_json::json;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ToJson for Color {
|
use super::*;
|
||||||
fn to_json(&self) -> serde_json::Value {
|
|
||||||
json!(format!(
|
|
||||||
"0x{:x}{:x}{:x}{:x}",
|
|
||||||
self.0.r, self.0.g, self.0.b, self.0.a
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for Color {
|
#[test]
|
||||||
type Target = ColorU;
|
fn test_deserialize_three_value_hex_to_rgba() {
|
||||||
fn deref(&self) -> &Self::Target {
|
let actual: Rgba = serde_json::from_value(json!("#f09")).unwrap();
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DerefMut for Color {
|
assert_eq!(actual, rgba(0xff0099ff))
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
||||||
&mut self.0
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Debug for Color {
|
#[test]
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn test_deserialize_four_value_hex_to_rgba() {
|
||||||
self.0.fmt(f)
|
let actual: Rgba = serde_json::from_value(json!("#f09f")).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(actual, rgba(0xff0099ff))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deserialize_six_value_hex_to_rgba() {
|
||||||
|
let actual: Rgba = serde_json::from_value(json!("#ff0099")).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(actual, rgba(0xff0099ff))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deserialize_eight_value_hex_to_rgba() {
|
||||||
|
let actual: Rgba = serde_json::from_value(json!("#ff0099ff")).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(actual, rgba(0xff0099ff))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deserialize_eight_value_hex_with_padding_to_rgba() {
|
||||||
|
let actual: Rgba = serde_json::from_value(json!(" #f5f5f5ff ")).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(actual, rgba(0xf5f5f5ff))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deserialize_eight_value_hex_with_mixed_case_to_rgba() {
|
||||||
|
let actual: Rgba = serde_json::from_value(json!("#DeAdbEeF")).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(actual, rgba(0xdeadbeef))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1 +0,0 @@
|
|||||||
|
|
@ -1,740 +0,0 @@
|
|||||||
mod align;
|
|
||||||
mod canvas;
|
|
||||||
mod clipped;
|
|
||||||
mod component;
|
|
||||||
mod constrained_box;
|
|
||||||
mod container;
|
|
||||||
mod empty;
|
|
||||||
mod expanded;
|
|
||||||
mod flex;
|
|
||||||
mod hook;
|
|
||||||
mod image;
|
|
||||||
mod keystroke_label;
|
|
||||||
mod label;
|
|
||||||
mod list;
|
|
||||||
mod mouse_event_handler;
|
|
||||||
mod overlay;
|
|
||||||
mod resizable;
|
|
||||||
mod stack;
|
|
||||||
mod svg;
|
|
||||||
mod text;
|
|
||||||
mod tooltip;
|
|
||||||
mod uniform_list;
|
|
||||||
|
|
||||||
pub use self::{
|
|
||||||
align::*, canvas::*, component::*, constrained_box::*, container::*, empty::*, flex::*,
|
|
||||||
hook::*, image::*, keystroke_label::*, label::*, list::*, mouse_event_handler::*, overlay::*,
|
|
||||||
resizable::*, stack::*, svg::*, text::*, tooltip::*, uniform_list::*,
|
|
||||||
};
|
|
||||||
pub use crate::window::ChildView;
|
|
||||||
|
|
||||||
use self::{clipped::Clipped, expanded::Expanded};
|
|
||||||
use crate::{
|
|
||||||
geometry::{
|
|
||||||
rect::RectF,
|
|
||||||
vector::{vec2f, Vector2F},
|
|
||||||
},
|
|
||||||
json, Action, Entity, SizeConstraint, TypeTag, View, ViewContext, WeakViewHandle,
|
|
||||||
WindowContext,
|
|
||||||
};
|
|
||||||
use anyhow::{anyhow, Result};
|
|
||||||
use core::panic;
|
|
||||||
use json::ToJson;
|
|
||||||
use std::{
|
|
||||||
any::{type_name, Any},
|
|
||||||
borrow::Cow,
|
|
||||||
mem,
|
|
||||||
ops::Range,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub trait Element<V: 'static>: 'static {
|
|
||||||
type LayoutState;
|
|
||||||
type PaintState;
|
|
||||||
|
|
||||||
fn view_name(&self) -> &'static str {
|
|
||||||
type_name::<V>()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn layout(
|
|
||||||
&mut self,
|
|
||||||
constraint: SizeConstraint,
|
|
||||||
view: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> (Vector2F, Self::LayoutState);
|
|
||||||
|
|
||||||
fn paint(
|
|
||||||
&mut self,
|
|
||||||
bounds: RectF,
|
|
||||||
visible_bounds: RectF,
|
|
||||||
layout: &mut Self::LayoutState,
|
|
||||||
view: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> Self::PaintState;
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
|
||||||
&self,
|
|
||||||
range_utf16: Range<usize>,
|
|
||||||
bounds: RectF,
|
|
||||||
visible_bounds: RectF,
|
|
||||||
layout: &Self::LayoutState,
|
|
||||||
paint: &Self::PaintState,
|
|
||||||
view: &V,
|
|
||||||
cx: &ViewContext<V>,
|
|
||||||
) -> Option<RectF>;
|
|
||||||
|
|
||||||
fn metadata(&self) -> Option<&dyn Any> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn debug(
|
|
||||||
&self,
|
|
||||||
bounds: RectF,
|
|
||||||
layout: &Self::LayoutState,
|
|
||||||
paint: &Self::PaintState,
|
|
||||||
view: &V,
|
|
||||||
cx: &ViewContext<V>,
|
|
||||||
) -> serde_json::Value;
|
|
||||||
|
|
||||||
fn into_any(self) -> AnyElement<V>
|
|
||||||
where
|
|
||||||
Self: 'static + Sized,
|
|
||||||
{
|
|
||||||
AnyElement {
|
|
||||||
state: Box::new(ElementState::Init { element: self }),
|
|
||||||
name: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn into_any_named(self, name: impl Into<Cow<'static, str>>) -> AnyElement<V>
|
|
||||||
where
|
|
||||||
Self: 'static + Sized,
|
|
||||||
{
|
|
||||||
AnyElement {
|
|
||||||
state: Box::new(ElementState::Init { element: self }),
|
|
||||||
name: Some(name.into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn into_root_element(self, cx: &ViewContext<V>) -> RootElement<V>
|
|
||||||
where
|
|
||||||
Self: 'static + Sized,
|
|
||||||
{
|
|
||||||
RootElement {
|
|
||||||
element: self.into_any(),
|
|
||||||
view: cx.handle().downgrade(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn constrained(self) -> ConstrainedBox<V>
|
|
||||||
where
|
|
||||||
Self: 'static + Sized,
|
|
||||||
{
|
|
||||||
ConstrainedBox::new(self.into_any())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn aligned(self) -> Align<V>
|
|
||||||
where
|
|
||||||
Self: 'static + Sized,
|
|
||||||
{
|
|
||||||
Align::new(self.into_any())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn clipped(self) -> Clipped<V>
|
|
||||||
where
|
|
||||||
Self: 'static + Sized,
|
|
||||||
{
|
|
||||||
Clipped::new(self.into_any())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn contained(self) -> Container<V>
|
|
||||||
where
|
|
||||||
Self: 'static + Sized,
|
|
||||||
{
|
|
||||||
Container::new(self.into_any())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn expanded(self) -> Expanded<V>
|
|
||||||
where
|
|
||||||
Self: 'static + Sized,
|
|
||||||
{
|
|
||||||
Expanded::new(self.into_any())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn flex(self, flex: f32, expanded: bool) -> FlexItem<V>
|
|
||||||
where
|
|
||||||
Self: 'static + Sized,
|
|
||||||
{
|
|
||||||
FlexItem::new(self.into_any()).flex(flex, expanded)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn flex_float(self) -> FlexItem<V>
|
|
||||||
where
|
|
||||||
Self: 'static + Sized,
|
|
||||||
{
|
|
||||||
FlexItem::new(self.into_any()).float()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn with_dynamic_tooltip(
|
|
||||||
self,
|
|
||||||
tag: TypeTag,
|
|
||||||
id: usize,
|
|
||||||
text: impl Into<Cow<'static, str>>,
|
|
||||||
action: Option<Box<dyn Action>>,
|
|
||||||
style: TooltipStyle,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> Tooltip<V>
|
|
||||||
where
|
|
||||||
Self: 'static + Sized,
|
|
||||||
{
|
|
||||||
Tooltip::new_dynamic(tag, id, text, action, style, self.into_any(), cx)
|
|
||||||
}
|
|
||||||
fn with_tooltip<Tag: 'static>(
|
|
||||||
self,
|
|
||||||
id: usize,
|
|
||||||
text: impl Into<Cow<'static, str>>,
|
|
||||||
action: Option<Box<dyn Action>>,
|
|
||||||
style: TooltipStyle,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> Tooltip<V>
|
|
||||||
where
|
|
||||||
Self: 'static + Sized,
|
|
||||||
{
|
|
||||||
Tooltip::new::<Tag>(id, text, action, style, self.into_any(), cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Uses the the given element to calculate resizes for the given tag
|
|
||||||
fn provide_resize_bounds<Tag: 'static>(self) -> BoundsProvider<V, Tag>
|
|
||||||
where
|
|
||||||
Self: 'static + Sized,
|
|
||||||
{
|
|
||||||
BoundsProvider::<_, Tag>::new(self.into_any())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calls the given closure with the new size of the element whenever the
|
|
||||||
/// handle is dragged. This will be calculated in relation to the bounds
|
|
||||||
/// provided by the given tag
|
|
||||||
fn resizable<Tag: 'static>(
|
|
||||||
self,
|
|
||||||
side: HandleSide,
|
|
||||||
size: f32,
|
|
||||||
on_resize: impl 'static + FnMut(&mut V, Option<f32>, &mut ViewContext<V>),
|
|
||||||
) -> Resizable<V>
|
|
||||||
where
|
|
||||||
Self: 'static + Sized,
|
|
||||||
{
|
|
||||||
Resizable::new::<Tag>(self.into_any(), side, size, on_resize)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mouse<Tag: 'static>(self, region_id: usize) -> MouseEventHandler<V>
|
|
||||||
where
|
|
||||||
Self: Sized,
|
|
||||||
{
|
|
||||||
MouseEventHandler::for_child::<Tag>(self.into_any(), region_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn component(self) -> StatelessElementAdapter
|
|
||||||
where
|
|
||||||
Self: Sized,
|
|
||||||
{
|
|
||||||
StatelessElementAdapter::new(self.into_any())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn stateful_component(self) -> StatefulElementAdapter<V>
|
|
||||||
where
|
|
||||||
Self: Sized,
|
|
||||||
{
|
|
||||||
StatefulElementAdapter::new(self.into_any())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn styleable_component(self) -> StylableAdapter<StatelessElementAdapter>
|
|
||||||
where
|
|
||||||
Self: Sized,
|
|
||||||
{
|
|
||||||
StatelessElementAdapter::new(self.into_any()).stylable()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
trait AnyElementState<V> {
|
|
||||||
fn layout(
|
|
||||||
&mut self,
|
|
||||||
constraint: SizeConstraint,
|
|
||||||
view: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> Vector2F;
|
|
||||||
|
|
||||||
fn paint(
|
|
||||||
&mut self,
|
|
||||||
origin: Vector2F,
|
|
||||||
visible_bounds: RectF,
|
|
||||||
view: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
);
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
|
||||||
&self,
|
|
||||||
range_utf16: Range<usize>,
|
|
||||||
view: &V,
|
|
||||||
cx: &ViewContext<V>,
|
|
||||||
) -> Option<RectF>;
|
|
||||||
|
|
||||||
fn debug(&self, view: &V, cx: &ViewContext<V>) -> serde_json::Value;
|
|
||||||
|
|
||||||
fn size(&self) -> Vector2F;
|
|
||||||
|
|
||||||
fn metadata(&self) -> Option<&dyn Any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ElementState<V: 'static, E: Element<V>> {
|
|
||||||
Empty,
|
|
||||||
Init {
|
|
||||||
element: E,
|
|
||||||
},
|
|
||||||
PostLayout {
|
|
||||||
element: E,
|
|
||||||
constraint: SizeConstraint,
|
|
||||||
size: Vector2F,
|
|
||||||
layout: E::LayoutState,
|
|
||||||
},
|
|
||||||
PostPaint {
|
|
||||||
element: E,
|
|
||||||
constraint: SizeConstraint,
|
|
||||||
bounds: RectF,
|
|
||||||
visible_bounds: RectF,
|
|
||||||
layout: E::LayoutState,
|
|
||||||
paint: E::PaintState,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V, E: Element<V>> AnyElementState<V> for ElementState<V, E> {
|
|
||||||
fn layout(
|
|
||||||
&mut self,
|
|
||||||
constraint: SizeConstraint,
|
|
||||||
view: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> Vector2F {
|
|
||||||
let result;
|
|
||||||
*self = match mem::take(self) {
|
|
||||||
ElementState::Empty => unreachable!(),
|
|
||||||
ElementState::Init { mut element }
|
|
||||||
| ElementState::PostLayout { mut element, .. }
|
|
||||||
| ElementState::PostPaint { mut element, .. } => {
|
|
||||||
let (size, layout) = element.layout(constraint, view, cx);
|
|
||||||
debug_assert!(
|
|
||||||
size.x().is_finite(),
|
|
||||||
"Element for {:?} had infinite x size after layout",
|
|
||||||
element.view_name()
|
|
||||||
);
|
|
||||||
debug_assert!(
|
|
||||||
size.y().is_finite(),
|
|
||||||
"Element for {:?} had infinite y size after layout",
|
|
||||||
element.view_name()
|
|
||||||
);
|
|
||||||
|
|
||||||
result = size;
|
|
||||||
ElementState::PostLayout {
|
|
||||||
element,
|
|
||||||
constraint,
|
|
||||||
size,
|
|
||||||
layout,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
fn paint(
|
|
||||||
&mut self,
|
|
||||||
origin: Vector2F,
|
|
||||||
visible_bounds: RectF,
|
|
||||||
view: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) {
|
|
||||||
*self = match mem::take(self) {
|
|
||||||
ElementState::PostLayout {
|
|
||||||
mut element,
|
|
||||||
constraint,
|
|
||||||
size,
|
|
||||||
mut layout,
|
|
||||||
} => {
|
|
||||||
let bounds = RectF::new(origin, size);
|
|
||||||
let paint = element.paint(bounds, visible_bounds, &mut layout, view, cx);
|
|
||||||
ElementState::PostPaint {
|
|
||||||
element,
|
|
||||||
constraint,
|
|
||||||
bounds,
|
|
||||||
visible_bounds,
|
|
||||||
layout,
|
|
||||||
paint,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ElementState::PostPaint {
|
|
||||||
mut element,
|
|
||||||
constraint,
|
|
||||||
bounds,
|
|
||||||
mut layout,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
let bounds = RectF::new(origin, bounds.size());
|
|
||||||
let paint = element.paint(bounds, visible_bounds, &mut layout, view, cx);
|
|
||||||
ElementState::PostPaint {
|
|
||||||
element,
|
|
||||||
constraint,
|
|
||||||
bounds,
|
|
||||||
visible_bounds,
|
|
||||||
layout,
|
|
||||||
paint,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ElementState::Empty => panic!("invalid element lifecycle state"),
|
|
||||||
ElementState::Init { .. } => {
|
|
||||||
panic!("invalid element lifecycle state, paint called before layout")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
|
||||||
&self,
|
|
||||||
range_utf16: Range<usize>,
|
|
||||||
view: &V,
|
|
||||||
cx: &ViewContext<V>,
|
|
||||||
) -> Option<RectF> {
|
|
||||||
if let ElementState::PostPaint {
|
|
||||||
element,
|
|
||||||
bounds,
|
|
||||||
visible_bounds,
|
|
||||||
layout,
|
|
||||||
paint,
|
|
||||||
..
|
|
||||||
} = self
|
|
||||||
{
|
|
||||||
element.rect_for_text_range(
|
|
||||||
range_utf16,
|
|
||||||
*bounds,
|
|
||||||
*visible_bounds,
|
|
||||||
layout,
|
|
||||||
paint,
|
|
||||||
view,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn size(&self) -> Vector2F {
|
|
||||||
match self {
|
|
||||||
ElementState::Empty | ElementState::Init { .. } => {
|
|
||||||
panic!("invalid element lifecycle state")
|
|
||||||
}
|
|
||||||
ElementState::PostLayout { size, .. } => *size,
|
|
||||||
ElementState::PostPaint { bounds, .. } => bounds.size(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn metadata(&self) -> Option<&dyn Any> {
|
|
||||||
match self {
|
|
||||||
ElementState::Empty => unreachable!(),
|
|
||||||
ElementState::Init { element }
|
|
||||||
| ElementState::PostLayout { element, .. }
|
|
||||||
| ElementState::PostPaint { element, .. } => element.metadata(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn debug(&self, view: &V, cx: &ViewContext<V>) -> serde_json::Value {
|
|
||||||
match self {
|
|
||||||
ElementState::PostPaint {
|
|
||||||
element,
|
|
||||||
constraint,
|
|
||||||
bounds,
|
|
||||||
visible_bounds,
|
|
||||||
layout,
|
|
||||||
paint,
|
|
||||||
} => {
|
|
||||||
let mut value = element.debug(*bounds, layout, paint, view, cx);
|
|
||||||
if let json::Value::Object(map) = &mut value {
|
|
||||||
let mut new_map: crate::json::Map<String, serde_json::Value> =
|
|
||||||
Default::default();
|
|
||||||
if let Some(typ) = map.remove("type") {
|
|
||||||
new_map.insert("type".into(), typ);
|
|
||||||
}
|
|
||||||
new_map.insert("constraint".into(), constraint.to_json());
|
|
||||||
new_map.insert("bounds".into(), bounds.to_json());
|
|
||||||
new_map.insert("visible_bounds".into(), visible_bounds.to_json());
|
|
||||||
new_map.append(map);
|
|
||||||
json::Value::Object(new_map)
|
|
||||||
} else {
|
|
||||||
value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_ => panic!("invalid element lifecycle state"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V, E: Element<V>> Default for ElementState<V, E> {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Empty
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct AnyElement<V> {
|
|
||||||
state: Box<dyn AnyElementState<V>>,
|
|
||||||
name: Option<Cow<'static, str>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V> AnyElement<V> {
|
|
||||||
pub fn name(&self) -> Option<&str> {
|
|
||||||
self.name.as_deref()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn metadata<T: 'static>(&self) -> Option<&T> {
|
|
||||||
self.state
|
|
||||||
.metadata()
|
|
||||||
.and_then(|data| data.downcast_ref::<T>())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn layout(
|
|
||||||
&mut self,
|
|
||||||
constraint: SizeConstraint,
|
|
||||||
view: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> Vector2F {
|
|
||||||
self.state.layout(constraint, view, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn paint(
|
|
||||||
&mut self,
|
|
||||||
origin: Vector2F,
|
|
||||||
visible_bounds: RectF,
|
|
||||||
view: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) {
|
|
||||||
self.state.paint(origin, visible_bounds, view, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn rect_for_text_range(
|
|
||||||
&self,
|
|
||||||
range_utf16: Range<usize>,
|
|
||||||
view: &V,
|
|
||||||
cx: &ViewContext<V>,
|
|
||||||
) -> Option<RectF> {
|
|
||||||
self.state.rect_for_text_range(range_utf16, view, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn size(&self) -> Vector2F {
|
|
||||||
self.state.size()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn debug(&self, view: &V, cx: &ViewContext<V>) -> json::Value {
|
|
||||||
let mut value = self.state.debug(view, cx);
|
|
||||||
|
|
||||||
if let Some(name) = &self.name {
|
|
||||||
if let json::Value::Object(map) = &mut value {
|
|
||||||
let mut new_map: crate::json::Map<String, serde_json::Value> = Default::default();
|
|
||||||
new_map.insert("name".into(), json::Value::String(name.to_string()));
|
|
||||||
new_map.append(map);
|
|
||||||
return json::Value::Object(new_map);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
value
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_metadata<T, F, R>(&self, f: F) -> R
|
|
||||||
where
|
|
||||||
T: 'static,
|
|
||||||
F: FnOnce(Option<&T>) -> R,
|
|
||||||
{
|
|
||||||
f(self.state.metadata().and_then(|m| m.downcast_ref()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V: 'static> Element<V> for AnyElement<V> {
|
|
||||||
type LayoutState = ();
|
|
||||||
type PaintState = ();
|
|
||||||
|
|
||||||
fn layout(
|
|
||||||
&mut self,
|
|
||||||
constraint: SizeConstraint,
|
|
||||||
view: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> (Vector2F, Self::LayoutState) {
|
|
||||||
let size = self.layout(constraint, view, cx);
|
|
||||||
(size, ())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn paint(
|
|
||||||
&mut self,
|
|
||||||
bounds: RectF,
|
|
||||||
visible_bounds: RectF,
|
|
||||||
_: &mut Self::LayoutState,
|
|
||||||
view: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> Self::PaintState {
|
|
||||||
self.paint(bounds.origin(), visible_bounds, view, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
|
||||||
&self,
|
|
||||||
range_utf16: Range<usize>,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
_: &Self::LayoutState,
|
|
||||||
_: &Self::PaintState,
|
|
||||||
view: &V,
|
|
||||||
cx: &ViewContext<V>,
|
|
||||||
) -> Option<RectF> {
|
|
||||||
self.rect_for_text_range(range_utf16, view, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn debug(
|
|
||||||
&self,
|
|
||||||
_: RectF,
|
|
||||||
_: &Self::LayoutState,
|
|
||||||
_: &Self::PaintState,
|
|
||||||
view: &V,
|
|
||||||
cx: &ViewContext<V>,
|
|
||||||
) -> serde_json::Value {
|
|
||||||
self.debug(view, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn into_any(self) -> AnyElement<V>
|
|
||||||
where
|
|
||||||
Self: Sized,
|
|
||||||
{
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Entity for AnyElement<()> {
|
|
||||||
type Event = ();
|
|
||||||
}
|
|
||||||
|
|
||||||
// impl View for AnyElement<()> {}
|
|
||||||
|
|
||||||
pub struct RootElement<V> {
|
|
||||||
element: AnyElement<V>,
|
|
||||||
view: WeakViewHandle<V>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V> RootElement<V> {
|
|
||||||
pub fn new(element: AnyElement<V>, view: WeakViewHandle<V>) -> Self {
|
|
||||||
Self { element, view }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait AnyRootElement {
|
|
||||||
fn layout(&mut self, constraint: SizeConstraint, cx: &mut WindowContext) -> Result<Vector2F>;
|
|
||||||
fn paint(
|
|
||||||
&mut self,
|
|
||||||
origin: Vector2F,
|
|
||||||
visible_bounds: RectF,
|
|
||||||
cx: &mut WindowContext,
|
|
||||||
) -> Result<()>;
|
|
||||||
fn rect_for_text_range(
|
|
||||||
&self,
|
|
||||||
range_utf16: Range<usize>,
|
|
||||||
cx: &WindowContext,
|
|
||||||
) -> Result<Option<RectF>>;
|
|
||||||
fn debug(&self, cx: &WindowContext) -> Result<serde_json::Value>;
|
|
||||||
fn name(&self) -> Option<&str>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V: View> AnyRootElement for RootElement<V> {
|
|
||||||
fn layout(&mut self, constraint: SizeConstraint, cx: &mut WindowContext) -> Result<Vector2F> {
|
|
||||||
let view = self
|
|
||||||
.view
|
|
||||||
.upgrade(cx)
|
|
||||||
.ok_or_else(|| anyhow!("layout called on a root element for a dropped view"))?;
|
|
||||||
view.update(cx, |view, cx| Ok(self.element.layout(constraint, view, cx)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn paint(
|
|
||||||
&mut self,
|
|
||||||
origin: Vector2F,
|
|
||||||
visible_bounds: RectF,
|
|
||||||
cx: &mut WindowContext,
|
|
||||||
) -> Result<()> {
|
|
||||||
let view = self
|
|
||||||
.view
|
|
||||||
.upgrade(cx)
|
|
||||||
.ok_or_else(|| anyhow!("paint called on a root element for a dropped view"))?;
|
|
||||||
|
|
||||||
view.update(cx, |view, cx| {
|
|
||||||
self.element.paint(origin, visible_bounds, view, cx);
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
|
||||||
&self,
|
|
||||||
range_utf16: Range<usize>,
|
|
||||||
cx: &WindowContext,
|
|
||||||
) -> Result<Option<RectF>> {
|
|
||||||
let view = self.view.upgrade(cx).ok_or_else(|| {
|
|
||||||
anyhow!("rect_for_text_range called on a root element for a dropped view")
|
|
||||||
})?;
|
|
||||||
let view = view.read(cx);
|
|
||||||
let view_context = ViewContext::immutable(cx, self.view.id());
|
|
||||||
Ok(self
|
|
||||||
.element
|
|
||||||
.rect_for_text_range(range_utf16, view, &view_context))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn debug(&self, cx: &WindowContext) -> Result<serde_json::Value> {
|
|
||||||
let view = self
|
|
||||||
.view
|
|
||||||
.upgrade(cx)
|
|
||||||
.ok_or_else(|| anyhow!("debug called on a root element for a dropped view"))?;
|
|
||||||
let view = view.read(cx);
|
|
||||||
let view_context = ViewContext::immutable(cx, self.view.id());
|
|
||||||
Ok(serde_json::json!({
|
|
||||||
"view_id": self.view.id(),
|
|
||||||
"view_name": V::ui_name(),
|
|
||||||
"view": view.debug_json(cx),
|
|
||||||
"element": self.element.debug(view, &view_context)
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn name(&self) -> Option<&str> {
|
|
||||||
self.element.name()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait ParentElement<'a, V: 'static>: Extend<AnyElement<V>> + Sized {
|
|
||||||
fn add_children<E: Element<V>>(&mut self, children: impl IntoIterator<Item = E>) {
|
|
||||||
self.extend(children.into_iter().map(|child| child.into_any()));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_child<D: Element<V>>(&mut self, child: D) {
|
|
||||||
self.extend(Some(child.into_any()));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn with_children<D: Element<V>>(mut self, children: impl IntoIterator<Item = D>) -> Self {
|
|
||||||
self.extend(children.into_iter().map(|child| child.into_any()));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn with_child<D: Element<V>>(mut self, child: D) -> Self {
|
|
||||||
self.extend(Some(child.into_any()));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
if max_size.x().is_infinite() && max_size.y().is_infinite() {
|
|
||||||
size
|
|
||||||
} else if max_size.x().is_infinite() || max_size.x() / max_size.y() > size.x() / size.y() {
|
|
||||||
vec2f(size.x() * max_size.y() / size.y(), max_size.y())
|
|
||||||
} else {
|
|
||||||
vec2f(max_size.x(), size.y() * max_size.x() / size.x())
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,115 +0,0 @@
|
|||||||
use crate::{
|
|
||||||
geometry::{rect::RectF, vector::Vector2F},
|
|
||||||
json, AnyElement, Element, SizeConstraint, ViewContext,
|
|
||||||
};
|
|
||||||
use json::ToJson;
|
|
||||||
|
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
pub struct Align<V> {
|
|
||||||
child: AnyElement<V>,
|
|
||||||
alignment: Vector2F,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V> Align<V> {
|
|
||||||
pub fn new(child: AnyElement<V>) -> Self {
|
|
||||||
Self {
|
|
||||||
child,
|
|
||||||
alignment: Vector2F::zero(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn top(mut self) -> Self {
|
|
||||||
self.alignment.set_y(-1.0);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn bottom(mut self) -> Self {
|
|
||||||
self.alignment.set_y(1.0);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn left(mut self) -> Self {
|
|
||||||
self.alignment.set_x(-1.0);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn right(mut self) -> Self {
|
|
||||||
self.alignment.set_x(1.0);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V: 'static> Element<V> for Align<V> {
|
|
||||||
type LayoutState = ();
|
|
||||||
type PaintState = ();
|
|
||||||
|
|
||||||
fn layout(
|
|
||||||
&mut self,
|
|
||||||
mut constraint: SizeConstraint,
|
|
||||||
view: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> (Vector2F, Self::LayoutState) {
|
|
||||||
let mut size = constraint.max;
|
|
||||||
constraint.min = Vector2F::zero();
|
|
||||||
let child_size = self.child.layout(constraint, view, cx);
|
|
||||||
if size.x().is_infinite() {
|
|
||||||
size.set_x(child_size.x());
|
|
||||||
}
|
|
||||||
if size.y().is_infinite() {
|
|
||||||
size.set_y(child_size.y());
|
|
||||||
}
|
|
||||||
(size, ())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn paint(
|
|
||||||
&mut self,
|
|
||||||
bounds: RectF,
|
|
||||||
visible_bounds: RectF,
|
|
||||||
_: &mut Self::LayoutState,
|
|
||||||
view: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> Self::PaintState {
|
|
||||||
let my_center = bounds.size() / 2.;
|
|
||||||
let my_target = my_center + my_center * self.alignment;
|
|
||||||
|
|
||||||
let child_center = self.child.size() / 2.;
|
|
||||||
let child_target = child_center + child_center * self.alignment;
|
|
||||||
|
|
||||||
self.child.paint(
|
|
||||||
bounds.origin() - (child_target - my_target),
|
|
||||||
visible_bounds,
|
|
||||||
view,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
|
||||||
&self,
|
|
||||||
range_utf16: std::ops::Range<usize>,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
_: &Self::LayoutState,
|
|
||||||
_: &Self::PaintState,
|
|
||||||
view: &V,
|
|
||||||
cx: &ViewContext<V>,
|
|
||||||
) -> Option<RectF> {
|
|
||||||
self.child.rect_for_text_range(range_utf16, view, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn debug(
|
|
||||||
&self,
|
|
||||||
bounds: pathfinder_geometry::rect::RectF,
|
|
||||||
_: &Self::LayoutState,
|
|
||||||
_: &Self::PaintState,
|
|
||||||
view: &V,
|
|
||||||
cx: &ViewContext<V>,
|
|
||||||
) -> json::Value {
|
|
||||||
json!({
|
|
||||||
"type": "Align",
|
|
||||||
"bounds": bounds.to_json(),
|
|
||||||
"alignment": self.alignment.to_json(),
|
|
||||||
"child": self.child.debug(view, cx),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,85 +1,54 @@
|
|||||||
use std::marker::PhantomData;
|
use refineable::Refineable as _;
|
||||||
|
|
||||||
use super::Element;
|
use crate::{Bounds, Element, IntoElement, Pixels, Style, StyleRefinement, Styled, WindowContext};
|
||||||
use crate::{
|
|
||||||
json::{self, json},
|
|
||||||
ViewContext,
|
|
||||||
};
|
|
||||||
use json::ToJson;
|
|
||||||
use pathfinder_geometry::{
|
|
||||||
rect::RectF,
|
|
||||||
vector::{vec2f, Vector2F},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct Canvas<V, F>(F, PhantomData<V>);
|
pub fn canvas(callback: impl 'static + FnOnce(&Bounds<Pixels>, &mut WindowContext)) -> Canvas {
|
||||||
|
Canvas {
|
||||||
impl<V, F> Canvas<V, F>
|
paint_callback: Some(Box::new(callback)),
|
||||||
where
|
style: StyleRefinement::default(),
|
||||||
F: FnMut(RectF, RectF, &mut V, &mut ViewContext<V>),
|
|
||||||
{
|
|
||||||
pub fn new(f: F) -> Self {
|
|
||||||
Self(f, PhantomData)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: 'static, F> Element<V> for Canvas<V, F>
|
pub struct Canvas {
|
||||||
where
|
paint_callback: Option<Box<dyn FnOnce(&Bounds<Pixels>, &mut WindowContext)>>,
|
||||||
F: 'static + FnMut(RectF, RectF, &mut V, &mut ViewContext<V>),
|
style: StyleRefinement,
|
||||||
{
|
}
|
||||||
type LayoutState = ();
|
|
||||||
type PaintState = ();
|
|
||||||
|
|
||||||
fn layout(
|
impl IntoElement for Canvas {
|
||||||
&mut self,
|
type Element = Self;
|
||||||
constraint: crate::SizeConstraint,
|
|
||||||
_: &mut V,
|
|
||||||
_: &mut crate::ViewContext<V>,
|
|
||||||
) -> (Vector2F, Self::LayoutState) {
|
|
||||||
let x = if constraint.max.x().is_finite() {
|
|
||||||
constraint.max.x()
|
|
||||||
} else {
|
|
||||||
constraint.min.x()
|
|
||||||
};
|
|
||||||
let y = if constraint.max.y().is_finite() {
|
|
||||||
constraint.max.y()
|
|
||||||
} else {
|
|
||||||
constraint.min.y()
|
|
||||||
};
|
|
||||||
(vec2f(x, y), ())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn paint(
|
fn element_id(&self) -> Option<crate::ElementId> {
|
||||||
&mut self,
|
|
||||||
bounds: RectF,
|
|
||||||
visible_bounds: RectF,
|
|
||||||
_: &mut Self::LayoutState,
|
|
||||||
view: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> Self::PaintState {
|
|
||||||
self.0(bounds, visible_bounds, view, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
|
||||||
&self,
|
|
||||||
_: std::ops::Range<usize>,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
_: &Self::LayoutState,
|
|
||||||
_: &Self::PaintState,
|
|
||||||
_: &V,
|
|
||||||
_: &ViewContext<V>,
|
|
||||||
) -> Option<RectF> {
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn debug(
|
fn into_element(self) -> Self::Element {
|
||||||
&self,
|
self
|
||||||
bounds: RectF,
|
}
|
||||||
_: &Self::LayoutState,
|
}
|
||||||
_: &Self::PaintState,
|
|
||||||
_: &V,
|
impl Element for Canvas {
|
||||||
_: &ViewContext<V>,
|
type State = Style;
|
||||||
) -> json::Value {
|
|
||||||
json!({"type": "Canvas", "bounds": bounds.to_json()})
|
fn request_layout(
|
||||||
|
&mut self,
|
||||||
|
_: Option<Self::State>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> (crate::LayoutId, Self::State) {
|
||||||
|
let mut style = Style::default();
|
||||||
|
style.refine(&self.style);
|
||||||
|
let layout_id = cx.request_layout(&style, []);
|
||||||
|
(layout_id, style)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(&mut self, bounds: Bounds<Pixels>, style: &mut Style, cx: &mut WindowContext) {
|
||||||
|
style.paint(bounds, cx, |cx| {
|
||||||
|
(self.paint_callback.take().unwrap())(&bounds, cx)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Styled for Canvas {
|
||||||
|
fn style(&mut self) -> &mut crate::StyleRefinement {
|
||||||
|
&mut self.style
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,71 +0,0 @@
|
|||||||
use std::ops::Range;
|
|
||||||
|
|
||||||
use pathfinder_geometry::{rect::RectF, vector::Vector2F};
|
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
use crate::{json, AnyElement, Element, SizeConstraint, ViewContext};
|
|
||||||
|
|
||||||
pub struct Clipped<V> {
|
|
||||||
child: AnyElement<V>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V> Clipped<V> {
|
|
||||||
pub fn new(child: AnyElement<V>) -> Self {
|
|
||||||
Self { child }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V: 'static> Element<V> for Clipped<V> {
|
|
||||||
type LayoutState = ();
|
|
||||||
type PaintState = ();
|
|
||||||
|
|
||||||
fn layout(
|
|
||||||
&mut self,
|
|
||||||
constraint: SizeConstraint,
|
|
||||||
view: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> (Vector2F, Self::LayoutState) {
|
|
||||||
(self.child.layout(constraint, view, cx), ())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn paint(
|
|
||||||
&mut self,
|
|
||||||
bounds: RectF,
|
|
||||||
visible_bounds: RectF,
|
|
||||||
_: &mut Self::LayoutState,
|
|
||||||
view: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> Self::PaintState {
|
|
||||||
cx.scene().push_layer(Some(bounds));
|
|
||||||
let state = self.child.paint(bounds.origin(), visible_bounds, view, cx);
|
|
||||||
cx.scene().pop_layer();
|
|
||||||
state
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
|
||||||
&self,
|
|
||||||
range_utf16: Range<usize>,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
_: &Self::LayoutState,
|
|
||||||
_: &Self::PaintState,
|
|
||||||
view: &V,
|
|
||||||
cx: &ViewContext<V>,
|
|
||||||
) -> Option<RectF> {
|
|
||||||
self.child.rect_for_text_range(range_utf16, view, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn debug(
|
|
||||||
&self,
|
|
||||||
_: RectF,
|
|
||||||
_: &Self::LayoutState,
|
|
||||||
_: &Self::PaintState,
|
|
||||||
view: &V,
|
|
||||||
cx: &ViewContext<V>,
|
|
||||||
) -> json::Value {
|
|
||||||
json!({
|
|
||||||
"type": "Clipped",
|
|
||||||
"child": self.child.debug(view, cx)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,342 +0,0 @@
|
|||||||
use std::{any::Any, marker::PhantomData};
|
|
||||||
|
|
||||||
use pathfinder_geometry::{rect::RectF, vector::Vector2F};
|
|
||||||
|
|
||||||
use crate::{AnyElement, Element, SizeConstraint, ViewContext};
|
|
||||||
|
|
||||||
use super::Empty;
|
|
||||||
|
|
||||||
/// The core stateless component trait, simply rendering an element tree
|
|
||||||
pub trait Component {
|
|
||||||
fn render<V: 'static>(self, cx: &mut ViewContext<V>) -> AnyElement<V>;
|
|
||||||
|
|
||||||
fn element<V: 'static>(self) -> ComponentAdapter<V, Self>
|
|
||||||
where
|
|
||||||
Self: Sized,
|
|
||||||
{
|
|
||||||
ComponentAdapter::new(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn stylable(self) -> StylableAdapter<Self>
|
|
||||||
where
|
|
||||||
Self: Sized,
|
|
||||||
{
|
|
||||||
StylableAdapter::new(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// All stylable components can trivially implement SafeStylable
|
|
||||||
impl<C: Stylable> SafeStylable for C {
|
|
||||||
type Style = C::Style;
|
|
||||||
|
|
||||||
type Output = C;
|
|
||||||
|
|
||||||
fn with_style(self, style: Self::Style) -> Self::Output {
|
|
||||||
self.with_style(style)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Allows converting an unstylable component into a stylable one
|
|
||||||
/// by using `()` as the style type
|
|
||||||
pub struct StylableAdapter<C: Component> {
|
|
||||||
component: C,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<C: Component> StylableAdapter<C> {
|
|
||||||
pub fn new(component: C) -> Self {
|
|
||||||
Self { component }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<C: Component> SafeStylable for StylableAdapter<C> {
|
|
||||||
type Style = ();
|
|
||||||
|
|
||||||
type Output = C;
|
|
||||||
|
|
||||||
fn with_style(self, _: Self::Style) -> Self::Output {
|
|
||||||
self.component
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This is a secondary trait for components that can be styled
|
|
||||||
/// 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>,
|
|
||||||
_phantom: std::marker::PhantomData<V>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V: 'static> StatefulElementAdapter<V> {
|
|
||||||
pub fn new(element: AnyElement<V>) -> Self {
|
|
||||||
Self {
|
|
||||||
element,
|
|
||||||
_phantom: std::marker::PhantomData,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V: 'static> StatefulComponent<V> for StatefulElementAdapter<V> {
|
|
||||||
fn render(self, _: &mut V, _: &mut ViewContext<V>) -> AnyElement<V> {
|
|
||||||
self.element
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A convenient shorthand for creating an empty component.
|
|
||||||
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>,
|
|
||||||
element: Option<AnyElement<V>>,
|
|
||||||
phantom: PhantomData<V>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<E, V: 'static> ComponentAdapter<V, E> {
|
|
||||||
pub fn new(e: E) -> Self {
|
|
||||||
Self {
|
|
||||||
component: Some(e),
|
|
||||||
element: None,
|
|
||||||
phantom: PhantomData,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V: 'static, C: StatefulComponent<V> + 'static> Element<V> for ComponentAdapter<V, C> {
|
|
||||||
type LayoutState = ();
|
|
||||||
|
|
||||||
type PaintState = ();
|
|
||||||
|
|
||||||
fn layout(
|
|
||||||
&mut self,
|
|
||||||
constraint: SizeConstraint,
|
|
||||||
view: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> (Vector2F, Self::LayoutState) {
|
|
||||||
if self.element.is_none() {
|
|
||||||
let element = self
|
|
||||||
.component
|
|
||||||
.take()
|
|
||||||
.expect("Component can only be rendered once")
|
|
||||||
.render(view, cx);
|
|
||||||
self.element = Some(element);
|
|
||||||
}
|
|
||||||
let constraint = self.element.as_mut().unwrap().layout(constraint, view, cx);
|
|
||||||
(constraint, ())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn paint(
|
|
||||||
&mut self,
|
|
||||||
bounds: RectF,
|
|
||||||
visible_bounds: RectF,
|
|
||||||
_: &mut Self::LayoutState,
|
|
||||||
view: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> Self::PaintState {
|
|
||||||
self.element
|
|
||||||
.as_mut()
|
|
||||||
.expect("Layout should always be called before paint")
|
|
||||||
.paint(bounds.origin(), visible_bounds, view, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
|
||||||
&self,
|
|
||||||
range_utf16: std::ops::Range<usize>,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
_: &Self::LayoutState,
|
|
||||||
_: &Self::PaintState,
|
|
||||||
view: &V,
|
|
||||||
cx: &ViewContext<V>,
|
|
||||||
) -> Option<RectF> {
|
|
||||||
self.element
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|el| el.rect_for_text_range(range_utf16, view, cx))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn debug(
|
|
||||||
&self,
|
|
||||||
_: RectF,
|
|
||||||
_: &Self::LayoutState,
|
|
||||||
_: &Self::PaintState,
|
|
||||||
view: &V,
|
|
||||||
cx: &ViewContext<V>,
|
|
||||||
) -> serde_json::Value {
|
|
||||||
serde_json::json!({
|
|
||||||
"type": "ComponentAdapter",
|
|
||||||
"component": std::any::type_name::<C>(),
|
|
||||||
"child": self.element.as_ref().map(|el| el.debug(view, cx)),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,187 +0,0 @@
|
|||||||
use std::ops::Range;
|
|
||||||
|
|
||||||
use json::ToJson;
|
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
geometry::{rect::RectF, vector::Vector2F},
|
|
||||||
json, AnyElement, Element, SizeConstraint, ViewContext,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct ConstrainedBox<V> {
|
|
||||||
child: AnyElement<V>,
|
|
||||||
constraint: Constraint<V>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum Constraint<V> {
|
|
||||||
Static(SizeConstraint),
|
|
||||||
Dynamic(Box<dyn FnMut(SizeConstraint, &mut V, &mut ViewContext<V>) -> SizeConstraint>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V> ToJson for Constraint<V> {
|
|
||||||
fn to_json(&self) -> serde_json::Value {
|
|
||||||
match self {
|
|
||||||
Constraint::Static(constraint) => constraint.to_json(),
|
|
||||||
Constraint::Dynamic(_) => "dynamic".into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V: 'static> ConstrainedBox<V> {
|
|
||||||
pub fn new(child: impl Element<V>) -> Self {
|
|
||||||
Self {
|
|
||||||
child: child.into_any(),
|
|
||||||
constraint: Constraint::Static(Default::default()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn dynamically(
|
|
||||||
mut self,
|
|
||||||
constraint: impl 'static + FnMut(SizeConstraint, &mut V, &mut ViewContext<V>) -> SizeConstraint,
|
|
||||||
) -> Self {
|
|
||||||
self.constraint = Constraint::Dynamic(Box::new(constraint));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_min_width(mut self, min_width: f32) -> Self {
|
|
||||||
if let Constraint::Dynamic(_) = self.constraint {
|
|
||||||
self.constraint = Constraint::Static(Default::default());
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Constraint::Static(constraint) = &mut self.constraint {
|
|
||||||
constraint.min.set_x(min_width);
|
|
||||||
} else {
|
|
||||||
unreachable!()
|
|
||||||
}
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_max_width(mut self, max_width: f32) -> Self {
|
|
||||||
if let Constraint::Dynamic(_) = self.constraint {
|
|
||||||
self.constraint = Constraint::Static(Default::default());
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Constraint::Static(constraint) = &mut self.constraint {
|
|
||||||
constraint.max.set_x(max_width);
|
|
||||||
} else {
|
|
||||||
unreachable!()
|
|
||||||
}
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_max_height(mut self, max_height: f32) -> Self {
|
|
||||||
if let Constraint::Dynamic(_) = self.constraint {
|
|
||||||
self.constraint = Constraint::Static(Default::default());
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Constraint::Static(constraint) = &mut self.constraint {
|
|
||||||
constraint.max.set_y(max_height);
|
|
||||||
} else {
|
|
||||||
unreachable!()
|
|
||||||
}
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_width(mut self, width: f32) -> Self {
|
|
||||||
if let Constraint::Dynamic(_) = self.constraint {
|
|
||||||
self.constraint = Constraint::Static(Default::default());
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Constraint::Static(constraint) = &mut self.constraint {
|
|
||||||
constraint.min.set_x(width);
|
|
||||||
constraint.max.set_x(width);
|
|
||||||
} else {
|
|
||||||
unreachable!()
|
|
||||||
}
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_height(mut self, height: f32) -> Self {
|
|
||||||
if let Constraint::Dynamic(_) = self.constraint {
|
|
||||||
self.constraint = Constraint::Static(Default::default());
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Constraint::Static(constraint) = &mut self.constraint {
|
|
||||||
constraint.min.set_y(height);
|
|
||||||
constraint.max.set_y(height);
|
|
||||||
} else {
|
|
||||||
unreachable!()
|
|
||||||
}
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn constraint(
|
|
||||||
&mut self,
|
|
||||||
input_constraint: SizeConstraint,
|
|
||||||
view: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> SizeConstraint {
|
|
||||||
match &mut self.constraint {
|
|
||||||
Constraint::Static(constraint) => *constraint,
|
|
||||||
Constraint::Dynamic(compute_constraint) => {
|
|
||||||
compute_constraint(input_constraint, view, cx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V: 'static> Element<V> for ConstrainedBox<V> {
|
|
||||||
type LayoutState = ();
|
|
||||||
type PaintState = ();
|
|
||||||
|
|
||||||
fn layout(
|
|
||||||
&mut self,
|
|
||||||
mut parent_constraint: SizeConstraint,
|
|
||||||
view: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> (Vector2F, Self::LayoutState) {
|
|
||||||
let constraint = self.constraint(parent_constraint, view, cx);
|
|
||||||
parent_constraint.min = parent_constraint.min.max(constraint.min);
|
|
||||||
parent_constraint.max = parent_constraint.max.min(constraint.max);
|
|
||||||
parent_constraint.max = parent_constraint.max.max(parent_constraint.min);
|
|
||||||
let size = self.child.layout(parent_constraint, view, cx);
|
|
||||||
(size, ())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn paint(
|
|
||||||
&mut self,
|
|
||||||
bounds: RectF,
|
|
||||||
visible_bounds: RectF,
|
|
||||||
_: &mut Self::LayoutState,
|
|
||||||
view: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> Self::PaintState {
|
|
||||||
cx.scene().push_layer(Some(visible_bounds));
|
|
||||||
self.child.paint(bounds.origin(), visible_bounds, view, cx);
|
|
||||||
cx.scene().pop_layer();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
|
||||||
&self,
|
|
||||||
range_utf16: Range<usize>,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
_: &Self::LayoutState,
|
|
||||||
_: &Self::PaintState,
|
|
||||||
view: &V,
|
|
||||||
cx: &ViewContext<V>,
|
|
||||||
) -> Option<RectF> {
|
|
||||||
self.child.rect_for_text_range(range_utf16, view, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn debug(
|
|
||||||
&self,
|
|
||||||
_: RectF,
|
|
||||||
_: &Self::LayoutState,
|
|
||||||
_: &Self::PaintState,
|
|
||||||
view: &V,
|
|
||||||
cx: &ViewContext<V>,
|
|
||||||
) -> json::Value {
|
|
||||||
json!({"type": "ConstrainedBox", "assigned_constraint": self.constraint.to_json(), "child": self.child.debug(view, cx)})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,684 +0,0 @@
|
|||||||
use std::ops::Range;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
color::Color,
|
|
||||||
geometry::{
|
|
||||||
deserialize_vec2f,
|
|
||||||
rect::RectF,
|
|
||||||
vector::{vec2f, Vector2F},
|
|
||||||
},
|
|
||||||
json::ToJson,
|
|
||||||
platform::CursorStyle,
|
|
||||||
scene::{self, CornerRadii, CursorRegion, Quad},
|
|
||||||
AnyElement, Element, SizeConstraint, ViewContext,
|
|
||||||
};
|
|
||||||
use schemars::JsonSchema;
|
|
||||||
use serde::Deserialize;
|
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema)]
|
|
||||||
pub struct ContainerStyle {
|
|
||||||
#[serde(default)]
|
|
||||||
pub margin: Margin,
|
|
||||||
#[serde(default)]
|
|
||||||
pub padding: Padding,
|
|
||||||
#[serde(rename = "background")]
|
|
||||||
pub background_color: Option<Color>,
|
|
||||||
#[serde(rename = "overlay")]
|
|
||||||
pub overlay_color: Option<Color>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub border: Border,
|
|
||||||
#[serde(default)]
|
|
||||||
#[serde(alias = "corner_radius")]
|
|
||||||
pub corner_radii: CornerRadii,
|
|
||||||
#[serde(default)]
|
|
||||||
pub shadow: Option<Shadow>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub cursor: Option<CursorStyle>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ContainerStyle {
|
|
||||||
pub fn fill(color: Color) -> Self {
|
|
||||||
Self {
|
|
||||||
background_color: Some(color),
|
|
||||||
..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> {
|
|
||||||
child: AnyElement<V>,
|
|
||||||
style: ContainerStyle,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V> Container<V> {
|
|
||||||
pub fn new(child: AnyElement<V>) -> Self {
|
|
||||||
Self {
|
|
||||||
child,
|
|
||||||
style: Default::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_style(mut self, style: ContainerStyle) -> Self {
|
|
||||||
self.style = style;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_margin_top(mut self, margin: f32) -> Self {
|
|
||||||
self.style.margin.top = margin;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_margin_bottom(mut self, margin: f32) -> Self {
|
|
||||||
self.style.margin.bottom = margin;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_margin_left(mut self, margin: f32) -> Self {
|
|
||||||
self.style.margin.left = margin;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_margin_right(mut self, margin: f32) -> Self {
|
|
||||||
self.style.margin.right = margin;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_horizontal_padding(mut self, padding: f32) -> Self {
|
|
||||||
self.style.padding.left = padding;
|
|
||||||
self.style.padding.right = padding;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_vertical_padding(mut self, padding: f32) -> Self {
|
|
||||||
self.style.padding.top = padding;
|
|
||||||
self.style.padding.bottom = padding;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_uniform_padding(mut self, padding: f32) -> Self {
|
|
||||||
self.style.padding = Padding {
|
|
||||||
top: padding,
|
|
||||||
left: padding,
|
|
||||||
bottom: padding,
|
|
||||||
right: padding,
|
|
||||||
};
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_padding_left(mut self, padding: f32) -> Self {
|
|
||||||
self.style.padding.left = padding;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_padding_right(mut self, padding: f32) -> Self {
|
|
||||||
self.style.padding.right = padding;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_padding_top(mut self, padding: f32) -> Self {
|
|
||||||
self.style.padding.top = padding;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_padding_bottom(mut self, padding: f32) -> Self {
|
|
||||||
self.style.padding.bottom = padding;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_background_color(mut self, color: Color) -> Self {
|
|
||||||
self.style.background_color = Some(color);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_overlay_color(mut self, color: Color) -> Self {
|
|
||||||
self.style.overlay_color = Some(color);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_border(mut self, border: Border) -> Self {
|
|
||||||
self.style.border = border;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_corner_radius(mut self, radius: f32) -> Self {
|
|
||||||
self.style.corner_radii.top_left = radius;
|
|
||||||
self.style.corner_radii.top_right = radius;
|
|
||||||
self.style.corner_radii.bottom_right = radius;
|
|
||||||
self.style.corner_radii.bottom_left = radius;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_shadow(mut self, offset: Vector2F, blur: f32, color: Color) -> Self {
|
|
||||||
self.style.shadow = Some(Shadow {
|
|
||||||
offset,
|
|
||||||
blur,
|
|
||||||
color,
|
|
||||||
});
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_cursor(mut self, style: CursorStyle) -> Self {
|
|
||||||
self.style.cursor = Some(style);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn margin_size(&self) -> Vector2F {
|
|
||||||
vec2f(
|
|
||||||
self.style.margin.left + self.style.margin.right,
|
|
||||||
self.style.margin.top + self.style.margin.bottom,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn padding_size(&self) -> Vector2F {
|
|
||||||
vec2f(
|
|
||||||
self.style.padding.left + self.style.padding.right,
|
|
||||||
self.style.padding.top + self.style.padding.bottom,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn border_size(&self) -> Vector2F {
|
|
||||||
let mut x = 0.0;
|
|
||||||
if self.style.border.left {
|
|
||||||
x += self.style.border.width;
|
|
||||||
}
|
|
||||||
if self.style.border.right {
|
|
||||||
x += self.style.border.width;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut y = 0.0;
|
|
||||||
if self.style.border.top {
|
|
||||||
y += self.style.border.width;
|
|
||||||
}
|
|
||||||
if self.style.border.bottom {
|
|
||||||
y += self.style.border.width;
|
|
||||||
}
|
|
||||||
|
|
||||||
vec2f(x, y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Default, JsonSchema)]
|
|
||||||
pub struct Border {
|
|
||||||
pub color: Color,
|
|
||||||
pub width: f32,
|
|
||||||
pub overlay: bool,
|
|
||||||
pub top: bool,
|
|
||||||
pub bottom: bool,
|
|
||||||
pub left: bool,
|
|
||||||
pub right: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Into<scene::Border> for Border {
|
|
||||||
fn into(self) -> scene::Border {
|
|
||||||
scene::Border {
|
|
||||||
color: self.color,
|
|
||||||
left: if self.left { self.width } else { 0.0 },
|
|
||||||
right: if self.right { self.width } else { 0.0 },
|
|
||||||
top: if self.top { self.width } else { 0.0 },
|
|
||||||
bottom: if self.bottom { self.width } else { 0.0 },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Border {
|
|
||||||
pub fn new(width: f32, color: Color) -> Self {
|
|
||||||
Self {
|
|
||||||
width,
|
|
||||||
color,
|
|
||||||
overlay: false,
|
|
||||||
top: false,
|
|
||||||
left: false,
|
|
||||||
bottom: false,
|
|
||||||
right: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn all(width: f32, color: Color) -> Self {
|
|
||||||
Self {
|
|
||||||
width,
|
|
||||||
color,
|
|
||||||
overlay: false,
|
|
||||||
top: true,
|
|
||||||
left: true,
|
|
||||||
bottom: true,
|
|
||||||
right: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn top(width: f32, color: Color) -> Self {
|
|
||||||
let mut border = Self::new(width, color);
|
|
||||||
border.top = true;
|
|
||||||
border
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn left(width: f32, color: Color) -> Self {
|
|
||||||
let mut border = Self::new(width, color);
|
|
||||||
border.left = true;
|
|
||||||
border
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn bottom(width: f32, color: Color) -> Self {
|
|
||||||
let mut border = Self::new(width, color);
|
|
||||||
border.bottom = true;
|
|
||||||
border
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn right(width: f32, color: Color) -> Self {
|
|
||||||
let mut border = Self::new(width, color);
|
|
||||||
border.right = true;
|
|
||||||
border
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_sides(mut self, top: bool, left: bool, bottom: bool, right: bool) -> Self {
|
|
||||||
self.top = top;
|
|
||||||
self.left = left;
|
|
||||||
self.bottom = bottom;
|
|
||||||
self.right = right;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn top_width(&self) -> f32 {
|
|
||||||
if self.top {
|
|
||||||
self.width
|
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn left_width(&self) -> f32 {
|
|
||||||
if self.left {
|
|
||||||
self.width
|
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for Border {
|
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
||||||
where
|
|
||||||
D: serde::Deserializer<'de>,
|
|
||||||
{
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct BorderData {
|
|
||||||
pub width: f32,
|
|
||||||
pub color: Color,
|
|
||||||
#[serde(default)]
|
|
||||||
pub overlay: bool,
|
|
||||||
#[serde(default)]
|
|
||||||
pub top: bool,
|
|
||||||
#[serde(default)]
|
|
||||||
pub right: bool,
|
|
||||||
#[serde(default)]
|
|
||||||
pub bottom: bool,
|
|
||||||
#[serde(default)]
|
|
||||||
pub left: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
let data = BorderData::deserialize(deserializer)?;
|
|
||||||
let mut border = Border {
|
|
||||||
width: data.width,
|
|
||||||
color: data.color,
|
|
||||||
overlay: data.overlay,
|
|
||||||
top: data.top,
|
|
||||||
bottom: data.bottom,
|
|
||||||
left: data.left,
|
|
||||||
right: data.right,
|
|
||||||
};
|
|
||||||
if !border.top && !border.bottom && !border.left && !border.right {
|
|
||||||
border.top = true;
|
|
||||||
border.bottom = true;
|
|
||||||
border.left = true;
|
|
||||||
border.right = true;
|
|
||||||
}
|
|
||||||
Ok(border)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ToJson for Border {
|
|
||||||
fn to_json(&self) -> serde_json::Value {
|
|
||||||
let mut value = json!({});
|
|
||||||
if self.top {
|
|
||||||
value["top"] = json!(self.width);
|
|
||||||
}
|
|
||||||
if self.right {
|
|
||||||
value["right"] = json!(self.width);
|
|
||||||
}
|
|
||||||
if self.bottom {
|
|
||||||
value["bottom"] = json!(self.width);
|
|
||||||
}
|
|
||||||
if self.left {
|
|
||||||
value["left"] = json!(self.width);
|
|
||||||
}
|
|
||||||
value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V: 'static> Element<V> for Container<V> {
|
|
||||||
type LayoutState = ();
|
|
||||||
type PaintState = ();
|
|
||||||
|
|
||||||
fn layout(
|
|
||||||
&mut self,
|
|
||||||
constraint: SizeConstraint,
|
|
||||||
view: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> (Vector2F, Self::LayoutState) {
|
|
||||||
let mut size_buffer = self.margin_size() + self.padding_size();
|
|
||||||
if !self.style.border.overlay {
|
|
||||||
size_buffer += self.border_size();
|
|
||||||
}
|
|
||||||
let child_constraint = SizeConstraint {
|
|
||||||
min: (constraint.min - size_buffer).max(Vector2F::zero()),
|
|
||||||
max: (constraint.max - size_buffer).max(Vector2F::zero()),
|
|
||||||
};
|
|
||||||
let child_size = self.child.layout(child_constraint, view, cx);
|
|
||||||
(child_size + size_buffer, ())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn paint(
|
|
||||||
&mut self,
|
|
||||||
bounds: RectF,
|
|
||||||
visible_bounds: RectF,
|
|
||||||
_: &mut Self::LayoutState,
|
|
||||||
view: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> Self::PaintState {
|
|
||||||
let quad_bounds = RectF::from_points(
|
|
||||||
bounds.origin() + vec2f(self.style.margin.left, self.style.margin.top),
|
|
||||||
bounds.lower_right() - vec2f(self.style.margin.right, self.style.margin.bottom),
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(shadow) = self.style.shadow.as_ref() {
|
|
||||||
cx.scene().push_shadow(scene::Shadow {
|
|
||||||
bounds: quad_bounds + shadow.offset,
|
|
||||||
corner_radii: self.style.corner_radii,
|
|
||||||
sigma: shadow.blur,
|
|
||||||
color: shadow.color,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(hit_bounds) = quad_bounds.intersection(visible_bounds) {
|
|
||||||
if let Some(style) = self.style.cursor {
|
|
||||||
cx.scene().push_cursor_region(CursorRegion {
|
|
||||||
bounds: hit_bounds,
|
|
||||||
style,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let child_origin =
|
|
||||||
quad_bounds.origin() + vec2f(self.style.padding.left, self.style.padding.top);
|
|
||||||
|
|
||||||
if self.style.border.overlay {
|
|
||||||
cx.scene().push_quad(Quad {
|
|
||||||
bounds: quad_bounds,
|
|
||||||
background: self.style.background_color,
|
|
||||||
border: Default::default(),
|
|
||||||
corner_radii: self.style.corner_radii.into(),
|
|
||||||
});
|
|
||||||
|
|
||||||
self.child.paint(child_origin, visible_bounds, view, cx);
|
|
||||||
|
|
||||||
cx.scene().push_layer(None);
|
|
||||||
cx.scene().push_quad(Quad {
|
|
||||||
bounds: quad_bounds,
|
|
||||||
background: self.style.overlay_color,
|
|
||||||
border: self.style.border.into(),
|
|
||||||
corner_radii: self.style.corner_radii.into(),
|
|
||||||
});
|
|
||||||
cx.scene().pop_layer();
|
|
||||||
} else {
|
|
||||||
cx.scene().push_quad(Quad {
|
|
||||||
bounds: quad_bounds,
|
|
||||||
background: self.style.background_color,
|
|
||||||
border: self.style.border.into(),
|
|
||||||
corner_radii: self.style.corner_radii.into(),
|
|
||||||
});
|
|
||||||
|
|
||||||
let child_origin = child_origin
|
|
||||||
+ vec2f(
|
|
||||||
self.style.border.left_width(),
|
|
||||||
self.style.border.top_width(),
|
|
||||||
);
|
|
||||||
self.child.paint(child_origin, visible_bounds, view, cx);
|
|
||||||
|
|
||||||
if self.style.overlay_color.is_some() {
|
|
||||||
cx.scene().push_layer(None);
|
|
||||||
cx.scene().push_quad(Quad {
|
|
||||||
bounds: quad_bounds,
|
|
||||||
background: self.style.overlay_color,
|
|
||||||
border: Default::default(),
|
|
||||||
corner_radii: self.style.corner_radii.into(),
|
|
||||||
});
|
|
||||||
cx.scene().pop_layer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
|
||||||
&self,
|
|
||||||
range_utf16: Range<usize>,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
_: &Self::LayoutState,
|
|
||||||
_: &Self::PaintState,
|
|
||||||
view: &V,
|
|
||||||
cx: &ViewContext<V>,
|
|
||||||
) -> Option<RectF> {
|
|
||||||
self.child.rect_for_text_range(range_utf16, view, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn debug(
|
|
||||||
&self,
|
|
||||||
bounds: RectF,
|
|
||||||
_: &Self::LayoutState,
|
|
||||||
_: &Self::PaintState,
|
|
||||||
view: &V,
|
|
||||||
cx: &ViewContext<V>,
|
|
||||||
) -> serde_json::Value {
|
|
||||||
json!({
|
|
||||||
"type": "Container",
|
|
||||||
"bounds": bounds.to_json(),
|
|
||||||
"details": self.style.to_json(),
|
|
||||||
"child": self.child.debug(view, cx),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ToJson for ContainerStyle {
|
|
||||||
fn to_json(&self) -> serde_json::Value {
|
|
||||||
json!({
|
|
||||||
"margin": self.margin.to_json(),
|
|
||||||
"padding": self.padding.to_json(),
|
|
||||||
"background_color": self.background_color.to_json(),
|
|
||||||
"border": self.border.to_json(),
|
|
||||||
"corner_radius": self.corner_radii,
|
|
||||||
"shadow": self.shadow.to_json(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default, JsonSchema)]
|
|
||||||
pub struct Margin {
|
|
||||||
pub top: f32,
|
|
||||||
pub bottom: f32,
|
|
||||||
pub left: f32,
|
|
||||||
pub right: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ToJson for Margin {
|
|
||||||
fn to_json(&self) -> serde_json::Value {
|
|
||||||
let mut value = json!({});
|
|
||||||
if self.top > 0. {
|
|
||||||
value["top"] = json!(self.top);
|
|
||||||
}
|
|
||||||
if self.right > 0. {
|
|
||||||
value["right"] = json!(self.right);
|
|
||||||
}
|
|
||||||
if self.bottom > 0. {
|
|
||||||
value["bottom"] = json!(self.bottom);
|
|
||||||
}
|
|
||||||
if self.left > 0. {
|
|
||||||
value["left"] = json!(self.left);
|
|
||||||
}
|
|
||||||
value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default, JsonSchema)]
|
|
||||||
pub struct Padding {
|
|
||||||
pub top: f32,
|
|
||||||
pub left: f32,
|
|
||||||
pub bottom: f32,
|
|
||||||
pub right: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Padding {
|
|
||||||
pub fn horizontal(padding: f32) -> Self {
|
|
||||||
Self {
|
|
||||||
left: padding,
|
|
||||||
right: padding,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn vertical(padding: f32) -> Self {
|
|
||||||
Self {
|
|
||||||
top: padding,
|
|
||||||
bottom: padding,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for Padding {
|
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
||||||
where
|
|
||||||
D: serde::Deserializer<'de>,
|
|
||||||
{
|
|
||||||
let spacing = Spacing::deserialize(deserializer)?;
|
|
||||||
Ok(match spacing {
|
|
||||||
Spacing::Uniform(size) => Padding {
|
|
||||||
top: size,
|
|
||||||
left: size,
|
|
||||||
bottom: size,
|
|
||||||
right: size,
|
|
||||||
},
|
|
||||||
Spacing::Specific {
|
|
||||||
top,
|
|
||||||
left,
|
|
||||||
bottom,
|
|
||||||
right,
|
|
||||||
} => Padding {
|
|
||||||
top,
|
|
||||||
left,
|
|
||||||
bottom,
|
|
||||||
right,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for Margin {
|
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
||||||
where
|
|
||||||
D: serde::Deserializer<'de>,
|
|
||||||
{
|
|
||||||
let spacing = Spacing::deserialize(deserializer)?;
|
|
||||||
Ok(match spacing {
|
|
||||||
Spacing::Uniform(size) => Margin {
|
|
||||||
top: size,
|
|
||||||
left: size,
|
|
||||||
bottom: size,
|
|
||||||
right: size,
|
|
||||||
},
|
|
||||||
Spacing::Specific {
|
|
||||||
top,
|
|
||||||
left,
|
|
||||||
bottom,
|
|
||||||
right,
|
|
||||||
} => Margin {
|
|
||||||
top,
|
|
||||||
left,
|
|
||||||
bottom,
|
|
||||||
right,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
#[serde(untagged)]
|
|
||||||
enum Spacing {
|
|
||||||
Uniform(f32),
|
|
||||||
Specific {
|
|
||||||
#[serde(default)]
|
|
||||||
top: f32,
|
|
||||||
#[serde(default)]
|
|
||||||
left: f32,
|
|
||||||
#[serde(default)]
|
|
||||||
bottom: f32,
|
|
||||||
#[serde(default)]
|
|
||||||
right: f32,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Padding {
|
|
||||||
pub fn uniform(padding: f32) -> Self {
|
|
||||||
Self {
|
|
||||||
top: padding,
|
|
||||||
left: padding,
|
|
||||||
bottom: padding,
|
|
||||||
right: padding,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ToJson for Padding {
|
|
||||||
fn to_json(&self) -> serde_json::Value {
|
|
||||||
let mut value = json!({});
|
|
||||||
if self.top > 0. {
|
|
||||||
value["top"] = json!(self.top);
|
|
||||||
}
|
|
||||||
if self.right > 0. {
|
|
||||||
value["right"] = json!(self.right);
|
|
||||||
}
|
|
||||||
if self.bottom > 0. {
|
|
||||||
value["bottom"] = json!(self.bottom);
|
|
||||||
}
|
|
||||||
if self.left > 0. {
|
|
||||||
value["left"] = json!(self.left);
|
|
||||||
}
|
|
||||||
value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema)]
|
|
||||||
pub struct Shadow {
|
|
||||||
#[serde(default, deserialize_with = "deserialize_vec2f")]
|
|
||||||
#[schemars(with = "Vec::<f32>")]
|
|
||||||
offset: Vector2F,
|
|
||||||
#[serde(default)]
|
|
||||||
blur: f32,
|
|
||||||
#[serde(default)]
|
|
||||||
color: Color,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ToJson for Shadow {
|
|
||||||
fn to_json(&self) -> serde_json::Value {
|
|
||||||
json!({
|
|
||||||
"offset": self.offset.to_json(),
|
|
||||||
"blur": self.blur,
|
|
||||||
"color": self.color.to_json()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,89 +0,0 @@
|
|||||||
use std::ops::Range;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
geometry::{
|
|
||||||
rect::RectF,
|
|
||||||
vector::{vec2f, Vector2F},
|
|
||||||
},
|
|
||||||
json::{json, ToJson},
|
|
||||||
ViewContext,
|
|
||||||
};
|
|
||||||
use crate::{Element, SizeConstraint};
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct Empty {
|
|
||||||
collapsed: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Empty {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn collapsed(mut self) -> Self {
|
|
||||||
self.collapsed = true;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V: 'static> Element<V> for Empty {
|
|
||||||
type LayoutState = ();
|
|
||||||
type PaintState = ();
|
|
||||||
|
|
||||||
fn layout(
|
|
||||||
&mut self,
|
|
||||||
constraint: SizeConstraint,
|
|
||||||
_: &mut V,
|
|
||||||
_: &mut ViewContext<V>,
|
|
||||||
) -> (Vector2F, Self::LayoutState) {
|
|
||||||
let x = if constraint.max.x().is_finite() && !self.collapsed {
|
|
||||||
constraint.max.x()
|
|
||||||
} else {
|
|
||||||
constraint.min.x()
|
|
||||||
};
|
|
||||||
let y = if constraint.max.y().is_finite() && !self.collapsed {
|
|
||||||
constraint.max.y()
|
|
||||||
} else {
|
|
||||||
constraint.min.y()
|
|
||||||
};
|
|
||||||
|
|
||||||
(vec2f(x, y), ())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn paint(
|
|
||||||
&mut self,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
_: &mut Self::LayoutState,
|
|
||||||
_: &mut V,
|
|
||||||
_: &mut ViewContext<V>,
|
|
||||||
) -> Self::PaintState {
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
|
||||||
&self,
|
|
||||||
_: Range<usize>,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
_: &Self::LayoutState,
|
|
||||||
_: &Self::PaintState,
|
|
||||||
_: &V,
|
|
||||||
_: &ViewContext<V>,
|
|
||||||
) -> Option<RectF> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn debug(
|
|
||||||
&self,
|
|
||||||
bounds: RectF,
|
|
||||||
_: &Self::LayoutState,
|
|
||||||
_: &Self::PaintState,
|
|
||||||
_: &V,
|
|
||||||
_: &ViewContext<V>,
|
|
||||||
) -> serde_json::Value {
|
|
||||||
json!({
|
|
||||||
"type": "Empty",
|
|
||||||
"bounds": bounds.to_json(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,96 +0,0 @@
|
|||||||
use std::ops::Range;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
geometry::{rect::RectF, vector::Vector2F},
|
|
||||||
json, AnyElement, Element, SizeConstraint, ViewContext,
|
|
||||||
};
|
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
pub struct Expanded<V> {
|
|
||||||
child: AnyElement<V>,
|
|
||||||
full_width: bool,
|
|
||||||
full_height: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V: 'static> Expanded<V> {
|
|
||||||
pub fn new(child: impl Element<V>) -> Self {
|
|
||||||
Self {
|
|
||||||
child: child.into_any(),
|
|
||||||
full_width: true,
|
|
||||||
full_height: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn full_width(mut self) -> Self {
|
|
||||||
self.full_width = true;
|
|
||||||
self.full_height = false;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn full_height(mut self) -> Self {
|
|
||||||
self.full_width = false;
|
|
||||||
self.full_height = true;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V: 'static> Element<V> for Expanded<V> {
|
|
||||||
type LayoutState = ();
|
|
||||||
type PaintState = ();
|
|
||||||
|
|
||||||
fn layout(
|
|
||||||
&mut self,
|
|
||||||
mut constraint: SizeConstraint,
|
|
||||||
view: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> (Vector2F, Self::LayoutState) {
|
|
||||||
if self.full_width {
|
|
||||||
constraint.min.set_x(constraint.max.x());
|
|
||||||
}
|
|
||||||
if self.full_height {
|
|
||||||
constraint.min.set_y(constraint.max.y());
|
|
||||||
}
|
|
||||||
let size = self.child.layout(constraint, view, cx);
|
|
||||||
(size, ())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn paint(
|
|
||||||
&mut self,
|
|
||||||
bounds: RectF,
|
|
||||||
visible_bounds: RectF,
|
|
||||||
_: &mut Self::LayoutState,
|
|
||||||
view: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> Self::PaintState {
|
|
||||||
self.child.paint(bounds.origin(), visible_bounds, view, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
|
||||||
&self,
|
|
||||||
range_utf16: Range<usize>,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
_: &Self::LayoutState,
|
|
||||||
_: &Self::PaintState,
|
|
||||||
view: &V,
|
|
||||||
cx: &ViewContext<V>,
|
|
||||||
) -> Option<RectF> {
|
|
||||||
self.child.rect_for_text_range(range_utf16, view, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn debug(
|
|
||||||
&self,
|
|
||||||
_: RectF,
|
|
||||||
_: &Self::LayoutState,
|
|
||||||
_: &Self::PaintState,
|
|
||||||
view: &V,
|
|
||||||
cx: &ViewContext<V>,
|
|
||||||
) -> json::Value {
|
|
||||||
json!({
|
|
||||||
"type": "Expanded",
|
|
||||||
"full_width": self.full_width,
|
|
||||||
"full_height": self.full_height,
|
|
||||||
"child": self.child.debug(view, cx)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,512 +0,0 @@
|
|||||||
use std::{any::Any, cell::Cell, f32::INFINITY, ops::Range, rc::Rc};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
json::{self, ToJson, Value},
|
|
||||||
AnyElement, Axis, Element, ElementStateHandle, SizeConstraint, TypeTag, Vector2FExt,
|
|
||||||
ViewContext,
|
|
||||||
};
|
|
||||||
use pathfinder_geometry::{
|
|
||||||
rect::RectF,
|
|
||||||
vector::{vec2f, Vector2F},
|
|
||||||
};
|
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
struct ScrollState {
|
|
||||||
scroll_to: Cell<Option<usize>>,
|
|
||||||
scroll_position: Cell<f32>,
|
|
||||||
type_tag: TypeTag,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Flex<V> {
|
|
||||||
axis: Axis,
|
|
||||||
children: Vec<AnyElement<V>>,
|
|
||||||
scroll_state: Option<(ElementStateHandle<Rc<ScrollState>>, usize)>,
|
|
||||||
child_alignment: f32,
|
|
||||||
spacing: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V: 'static> Flex<V> {
|
|
||||||
pub fn new(axis: Axis) -> Self {
|
|
||||||
Self {
|
|
||||||
axis,
|
|
||||||
children: Default::default(),
|
|
||||||
scroll_state: None,
|
|
||||||
child_alignment: -1.,
|
|
||||||
spacing: 0.,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn row() -> Self {
|
|
||||||
Self::new(Axis::Horizontal)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn column() -> Self {
|
|
||||||
Self::new(Axis::Vertical)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Render children centered relative to the cross-axis of the parent flex.
|
|
||||||
///
|
|
||||||
/// If this is a flex row, children will be centered vertically. If this is a
|
|
||||||
/// flex column, children will be centered horizontally.
|
|
||||||
pub fn align_children_center(mut self) -> Self {
|
|
||||||
self.child_alignment = 0.;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_spacing(mut self, spacing: f32) -> Self {
|
|
||||||
self.spacing = spacing;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn scrollable<Tag>(
|
|
||||||
mut self,
|
|
||||||
element_id: usize,
|
|
||||||
scroll_to: Option<usize>,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> Self
|
|
||||||
where
|
|
||||||
Tag: 'static,
|
|
||||||
{
|
|
||||||
// Don't assume that this initialization is what scroll_state really is in other panes:
|
|
||||||
// `element_state` is shared and there could be init races.
|
|
||||||
let scroll_state = cx.element_state::<Tag, Rc<ScrollState>>(
|
|
||||||
element_id,
|
|
||||||
Rc::new(ScrollState {
|
|
||||||
type_tag: TypeTag::new::<Tag>(),
|
|
||||||
scroll_to: Default::default(),
|
|
||||||
scroll_position: Default::default(),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
// Set scroll_to separately, because the default state is already picked as `None` by other panes
|
|
||||||
// by the time we start setting it here, hence update all others' state too.
|
|
||||||
scroll_state.update(cx, |this, _| {
|
|
||||||
this.scroll_to.set(scroll_to);
|
|
||||||
});
|
|
||||||
self.scroll_state = Some((scroll_state, cx.handle().id()));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_empty(&self) -> bool {
|
|
||||||
self.children.is_empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn layout_flex_children(
|
|
||||||
&mut self,
|
|
||||||
layout_expanded: bool,
|
|
||||||
constraint: SizeConstraint,
|
|
||||||
remaining_space: &mut f32,
|
|
||||||
remaining_flex: &mut f32,
|
|
||||||
cross_axis_max: &mut f32,
|
|
||||||
view: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) {
|
|
||||||
let cross_axis = self.axis.invert();
|
|
||||||
for child in self.children.iter_mut() {
|
|
||||||
if let Some(metadata) = child.metadata::<FlexParentData>() {
|
|
||||||
if let Some((flex, expanded)) = metadata.flex {
|
|
||||||
if expanded != layout_expanded {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let child_max = if *remaining_flex == 0.0 {
|
|
||||||
*remaining_space
|
|
||||||
} else {
|
|
||||||
let space_per_flex = *remaining_space / *remaining_flex;
|
|
||||||
space_per_flex * flex
|
|
||||||
};
|
|
||||||
let child_min = if expanded { child_max } else { 0. };
|
|
||||||
let child_constraint = match self.axis {
|
|
||||||
Axis::Horizontal => SizeConstraint::new(
|
|
||||||
vec2f(child_min, constraint.min.y()),
|
|
||||||
vec2f(child_max, constraint.max.y()),
|
|
||||||
),
|
|
||||||
Axis::Vertical => SizeConstraint::new(
|
|
||||||
vec2f(constraint.min.x(), child_min),
|
|
||||||
vec2f(constraint.max.x(), child_max),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
let child_size = child.layout(child_constraint, view, cx);
|
|
||||||
*remaining_space -= child_size.along(self.axis);
|
|
||||||
*remaining_flex -= flex;
|
|
||||||
*cross_axis_max = cross_axis_max.max(child_size.along(cross_axis));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V> Extend<AnyElement<V>> for Flex<V> {
|
|
||||||
fn extend<T: IntoIterator<Item = AnyElement<V>>>(&mut self, children: T) {
|
|
||||||
self.children.extend(children);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V: 'static> Element<V> for Flex<V> {
|
|
||||||
type LayoutState = f32;
|
|
||||||
type PaintState = ();
|
|
||||||
|
|
||||||
fn layout(
|
|
||||||
&mut self,
|
|
||||||
constraint: SizeConstraint,
|
|
||||||
view: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> (Vector2F, Self::LayoutState) {
|
|
||||||
let mut total_flex = None;
|
|
||||||
let mut fixed_space = self.children.len().saturating_sub(1) as f32 * self.spacing;
|
|
||||||
let mut contains_float = false;
|
|
||||||
|
|
||||||
let cross_axis = self.axis.invert();
|
|
||||||
let mut cross_axis_max: f32 = 0.0;
|
|
||||||
for child in self.children.iter_mut() {
|
|
||||||
let metadata = child.metadata::<FlexParentData>();
|
|
||||||
contains_float |= metadata.map_or(false, |metadata| metadata.float);
|
|
||||||
|
|
||||||
if let Some(flex) = metadata.and_then(|metadata| metadata.flex.map(|(flex, _)| flex)) {
|
|
||||||
*total_flex.get_or_insert(0.) += flex;
|
|
||||||
} else {
|
|
||||||
let child_constraint = match self.axis {
|
|
||||||
Axis::Horizontal => SizeConstraint::new(
|
|
||||||
vec2f(0.0, constraint.min.y()),
|
|
||||||
vec2f(INFINITY, constraint.max.y()),
|
|
||||||
),
|
|
||||||
Axis::Vertical => SizeConstraint::new(
|
|
||||||
vec2f(constraint.min.x(), 0.0),
|
|
||||||
vec2f(constraint.max.x(), INFINITY),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
let size = child.layout(child_constraint, view, cx);
|
|
||||||
fixed_space += size.along(self.axis);
|
|
||||||
cross_axis_max = cross_axis_max.max(size.along(cross_axis));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut remaining_space = constraint.max_along(self.axis) - fixed_space;
|
|
||||||
let mut size = if let Some(mut remaining_flex) = total_flex {
|
|
||||||
if remaining_space.is_infinite() {
|
|
||||||
panic!("flex contains flexible children but has an infinite constraint along the flex axis");
|
|
||||||
}
|
|
||||||
|
|
||||||
self.layout_flex_children(
|
|
||||||
false,
|
|
||||||
constraint,
|
|
||||||
&mut remaining_space,
|
|
||||||
&mut remaining_flex,
|
|
||||||
&mut cross_axis_max,
|
|
||||||
view,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
self.layout_flex_children(
|
|
||||||
true,
|
|
||||||
constraint,
|
|
||||||
&mut remaining_space,
|
|
||||||
&mut remaining_flex,
|
|
||||||
&mut cross_axis_max,
|
|
||||||
view,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
|
|
||||||
match self.axis {
|
|
||||||
Axis::Horizontal => vec2f(constraint.max.x() - remaining_space, cross_axis_max),
|
|
||||||
Axis::Vertical => vec2f(cross_axis_max, constraint.max.y() - remaining_space),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
match self.axis {
|
|
||||||
Axis::Horizontal => vec2f(fixed_space, cross_axis_max),
|
|
||||||
Axis::Vertical => vec2f(cross_axis_max, fixed_space),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if contains_float {
|
|
||||||
match self.axis {
|
|
||||||
Axis::Horizontal => size.set_x(size.x().max(constraint.max.x())),
|
|
||||||
Axis::Vertical => size.set_y(size.y().max(constraint.max.y())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if constraint.min.x().is_finite() {
|
|
||||||
size.set_x(size.x().max(constraint.min.x()));
|
|
||||||
}
|
|
||||||
if constraint.min.y().is_finite() {
|
|
||||||
size.set_y(size.y().max(constraint.min.y()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if size.x() > constraint.max.x() {
|
|
||||||
size.set_x(constraint.max.x());
|
|
||||||
}
|
|
||||||
if size.y() > constraint.max.y() {
|
|
||||||
size.set_y(constraint.max.y());
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(scroll_state) = self.scroll_state.as_ref() {
|
|
||||||
scroll_state.0.update(cx, |scroll_state, _| {
|
|
||||||
if let Some(scroll_to) = scroll_state.scroll_to.take() {
|
|
||||||
let visible_start = scroll_state.scroll_position.get();
|
|
||||||
let visible_end = visible_start + size.along(self.axis);
|
|
||||||
if let Some(child) = self.children.get(scroll_to) {
|
|
||||||
let child_start: f32 = self.children[..scroll_to]
|
|
||||||
.iter()
|
|
||||||
.map(|c| c.size().along(self.axis))
|
|
||||||
.sum();
|
|
||||||
let child_end = child_start + child.size().along(self.axis);
|
|
||||||
if child_start < visible_start {
|
|
||||||
scroll_state.scroll_position.set(child_start);
|
|
||||||
} else if child_end > visible_end {
|
|
||||||
scroll_state
|
|
||||||
.scroll_position
|
|
||||||
.set(child_end - size.along(self.axis));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
scroll_state.scroll_position.set(
|
|
||||||
scroll_state
|
|
||||||
.scroll_position
|
|
||||||
.get()
|
|
||||||
.min(-remaining_space)
|
|
||||||
.max(0.),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
(size, remaining_space)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn paint(
|
|
||||||
&mut self,
|
|
||||||
bounds: RectF,
|
|
||||||
visible_bounds: RectF,
|
|
||||||
remaining_space: &mut Self::LayoutState,
|
|
||||||
view: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> Self::PaintState {
|
|
||||||
let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
|
|
||||||
|
|
||||||
let mut remaining_space = *remaining_space;
|
|
||||||
let overflowing = remaining_space < 0.;
|
|
||||||
if overflowing {
|
|
||||||
cx.scene().push_layer(Some(visible_bounds));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some((scroll_state, id)) = &self.scroll_state {
|
|
||||||
let scroll_state = scroll_state.read(cx).clone();
|
|
||||||
cx.scene().push_mouse_region(
|
|
||||||
crate::MouseRegion::from_handlers(
|
|
||||||
scroll_state.type_tag,
|
|
||||||
*id,
|
|
||||||
0,
|
|
||||||
bounds,
|
|
||||||
Default::default(),
|
|
||||||
)
|
|
||||||
.on_scroll({
|
|
||||||
let axis = self.axis;
|
|
||||||
move |e, _: &mut V, cx| {
|
|
||||||
if remaining_space < 0. {
|
|
||||||
let scroll_delta = e.delta.raw();
|
|
||||||
|
|
||||||
let mut delta = match axis {
|
|
||||||
Axis::Horizontal => {
|
|
||||||
if scroll_delta.x().abs() >= scroll_delta.y().abs() {
|
|
||||||
scroll_delta.x()
|
|
||||||
} else {
|
|
||||||
scroll_delta.y()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Axis::Vertical => scroll_delta.y(),
|
|
||||||
};
|
|
||||||
if !e.delta.precise() {
|
|
||||||
delta *= 20.;
|
|
||||||
}
|
|
||||||
|
|
||||||
scroll_state
|
|
||||||
.scroll_position
|
|
||||||
.set(scroll_state.scroll_position.get() - delta);
|
|
||||||
|
|
||||||
cx.notify();
|
|
||||||
} else {
|
|
||||||
cx.propagate_event();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.on_move(|_, _: &mut V, _| { /* Capture move events */ }),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut child_origin = bounds.origin();
|
|
||||||
if let Some(scroll_state) = self.scroll_state.as_ref() {
|
|
||||||
let scroll_position = scroll_state.0.read(cx).scroll_position.get();
|
|
||||||
match self.axis {
|
|
||||||
Axis::Horizontal => child_origin.set_x(child_origin.x() - scroll_position),
|
|
||||||
Axis::Vertical => child_origin.set_y(child_origin.y() - scroll_position),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for child in self.children.iter_mut() {
|
|
||||||
if remaining_space > 0. {
|
|
||||||
if let Some(metadata) = child.metadata::<FlexParentData>() {
|
|
||||||
if metadata.float {
|
|
||||||
match self.axis {
|
|
||||||
Axis::Horizontal => child_origin += vec2f(remaining_space, 0.0),
|
|
||||||
Axis::Vertical => child_origin += vec2f(0.0, remaining_space),
|
|
||||||
}
|
|
||||||
remaining_space = 0.;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We use the child_alignment f32 to determine a point along the cross axis of the
|
|
||||||
// overall flex element and each child. We then align these points. So 0 would center
|
|
||||||
// each child relative to the overall height/width of the flex. -1 puts children at
|
|
||||||
// the start. 1 puts children at the end.
|
|
||||||
let aligned_child_origin = {
|
|
||||||
let cross_axis = self.axis.invert();
|
|
||||||
let my_center = bounds.size().along(cross_axis) / 2.;
|
|
||||||
let my_target = my_center + my_center * self.child_alignment;
|
|
||||||
|
|
||||||
let child_center = child.size().along(cross_axis) / 2.;
|
|
||||||
let child_target = child_center + child_center * self.child_alignment;
|
|
||||||
|
|
||||||
let mut aligned_child_origin = child_origin;
|
|
||||||
match self.axis {
|
|
||||||
Axis::Horizontal => aligned_child_origin
|
|
||||||
.set_y(aligned_child_origin.y() - (child_target - my_target)),
|
|
||||||
Axis::Vertical => aligned_child_origin
|
|
||||||
.set_x(aligned_child_origin.x() - (child_target - my_target)),
|
|
||||||
}
|
|
||||||
|
|
||||||
aligned_child_origin
|
|
||||||
};
|
|
||||||
|
|
||||||
child.paint(aligned_child_origin, visible_bounds, view, cx);
|
|
||||||
|
|
||||||
match self.axis {
|
|
||||||
Axis::Horizontal => child_origin += vec2f(child.size().x() + self.spacing, 0.0),
|
|
||||||
Axis::Vertical => child_origin += vec2f(0.0, child.size().y() + self.spacing),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if overflowing {
|
|
||||||
cx.scene().pop_layer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
|
||||||
&self,
|
|
||||||
range_utf16: Range<usize>,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
_: &Self::LayoutState,
|
|
||||||
_: &Self::PaintState,
|
|
||||||
view: &V,
|
|
||||||
cx: &ViewContext<V>,
|
|
||||||
) -> Option<RectF> {
|
|
||||||
self.children
|
|
||||||
.iter()
|
|
||||||
.find_map(|child| child.rect_for_text_range(range_utf16.clone(), view, cx))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn debug(
|
|
||||||
&self,
|
|
||||||
bounds: RectF,
|
|
||||||
_: &Self::LayoutState,
|
|
||||||
_: &Self::PaintState,
|
|
||||||
view: &V,
|
|
||||||
cx: &ViewContext<V>,
|
|
||||||
) -> json::Value {
|
|
||||||
json!({
|
|
||||||
"type": "Flex",
|
|
||||||
"bounds": bounds.to_json(),
|
|
||||||
"axis": self.axis.to_json(),
|
|
||||||
"children": self.children.iter().map(|child| child.debug(view, cx)).collect::<Vec<json::Value>>()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct FlexParentData {
|
|
||||||
flex: Option<(f32, bool)>,
|
|
||||||
float: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct FlexItem<V> {
|
|
||||||
metadata: FlexParentData,
|
|
||||||
child: AnyElement<V>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V: 'static> FlexItem<V> {
|
|
||||||
pub fn new(child: impl Element<V>) -> Self {
|
|
||||||
FlexItem {
|
|
||||||
metadata: FlexParentData {
|
|
||||||
flex: None,
|
|
||||||
float: false,
|
|
||||||
},
|
|
||||||
child: child.into_any(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn flex(mut self, flex: f32, expanded: bool) -> Self {
|
|
||||||
self.metadata.flex = Some((flex, expanded));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn float(mut self) -> Self {
|
|
||||||
self.metadata.float = true;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V: 'static> Element<V> for FlexItem<V> {
|
|
||||||
type LayoutState = ();
|
|
||||||
type PaintState = ();
|
|
||||||
|
|
||||||
fn layout(
|
|
||||||
&mut self,
|
|
||||||
constraint: SizeConstraint,
|
|
||||||
view: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> (Vector2F, Self::LayoutState) {
|
|
||||||
let size = self.child.layout(constraint, view, cx);
|
|
||||||
(size, ())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn paint(
|
|
||||||
&mut self,
|
|
||||||
bounds: RectF,
|
|
||||||
visible_bounds: RectF,
|
|
||||||
_: &mut Self::LayoutState,
|
|
||||||
view: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> Self::PaintState {
|
|
||||||
self.child.paint(bounds.origin(), visible_bounds, view, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
|
||||||
&self,
|
|
||||||
range_utf16: Range<usize>,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
_: &Self::LayoutState,
|
|
||||||
_: &Self::PaintState,
|
|
||||||
view: &V,
|
|
||||||
cx: &ViewContext<V>,
|
|
||||||
) -> Option<RectF> {
|
|
||||||
self.child.rect_for_text_range(range_utf16, view, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn metadata(&self) -> Option<&dyn Any> {
|
|
||||||
Some(&self.metadata)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn debug(
|
|
||||||
&self,
|
|
||||||
_: RectF,
|
|
||||||
_: &Self::LayoutState,
|
|
||||||
_: &Self::PaintState,
|
|
||||||
view: &V,
|
|
||||||
cx: &ViewContext<V>,
|
|
||||||
) -> Value {
|
|
||||||
json!({
|
|
||||||
"type": "Flexible",
|
|
||||||
"flex": self.metadata.flex,
|
|
||||||
"child": self.child.debug(view, cx)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,85 +0,0 @@
|
|||||||
use std::ops::Range;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
geometry::{rect::RectF, vector::Vector2F},
|
|
||||||
json::json,
|
|
||||||
AnyElement, Element, SizeConstraint, ViewContext,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct Hook<V> {
|
|
||||||
child: AnyElement<V>,
|
|
||||||
after_layout: Option<Box<dyn FnMut(Vector2F, &mut ViewContext<V>)>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V: 'static> Hook<V> {
|
|
||||||
pub fn new(child: impl Element<V>) -> Self {
|
|
||||||
Self {
|
|
||||||
child: child.into_any(),
|
|
||||||
after_layout: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_after_layout(
|
|
||||||
mut self,
|
|
||||||
f: impl 'static + FnMut(Vector2F, &mut ViewContext<V>),
|
|
||||||
) -> Self {
|
|
||||||
self.after_layout = Some(Box::new(f));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V: 'static> Element<V> for Hook<V> {
|
|
||||||
type LayoutState = ();
|
|
||||||
type PaintState = ();
|
|
||||||
|
|
||||||
fn layout(
|
|
||||||
&mut self,
|
|
||||||
constraint: SizeConstraint,
|
|
||||||
view: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> (Vector2F, Self::LayoutState) {
|
|
||||||
let size = self.child.layout(constraint, view, cx);
|
|
||||||
if let Some(handler) = self.after_layout.as_mut() {
|
|
||||||
handler(size, cx);
|
|
||||||
}
|
|
||||||
(size, ())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn paint(
|
|
||||||
&mut self,
|
|
||||||
bounds: RectF,
|
|
||||||
visible_bounds: RectF,
|
|
||||||
_: &mut Self::LayoutState,
|
|
||||||
view: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) {
|
|
||||||
self.child.paint(bounds.origin(), visible_bounds, view, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
|
||||||
&self,
|
|
||||||
range_utf16: Range<usize>,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
_: &Self::LayoutState,
|
|
||||||
_: &Self::PaintState,
|
|
||||||
view: &V,
|
|
||||||
cx: &ViewContext<V>,
|
|
||||||
) -> Option<RectF> {
|
|
||||||
self.child.rect_for_text_range(range_utf16, view, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn debug(
|
|
||||||
&self,
|
|
||||||
_: RectF,
|
|
||||||
_: &Self::LayoutState,
|
|
||||||
_: &Self::PaintState,
|
|
||||||
view: &V,
|
|
||||||
cx: &ViewContext<V>,
|
|
||||||
) -> serde_json::Value {
|
|
||||||
json!({
|
|
||||||
"type": "Hooks",
|
|
||||||
"child": self.child.debug(view, cx),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,137 +0,0 @@
|
|||||||
use super::{constrain_size_preserving_aspect_ratio, Border};
|
|
||||||
use crate::{
|
|
||||||
geometry::{
|
|
||||||
rect::RectF,
|
|
||||||
vector::{vec2f, Vector2F},
|
|
||||||
},
|
|
||||||
json::{json, ToJson},
|
|
||||||
scene, Element, ImageData, SizeConstraint, ViewContext,
|
|
||||||
};
|
|
||||||
use schemars::JsonSchema;
|
|
||||||
use serde::Deserialize;
|
|
||||||
use std::{ops::Range, sync::Arc};
|
|
||||||
|
|
||||||
enum ImageSource {
|
|
||||||
Path(&'static str),
|
|
||||||
Data(Arc<ImageData>),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Image {
|
|
||||||
source: ImageSource,
|
|
||||||
style: ImageStyle,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Default, Deserialize, JsonSchema)]
|
|
||||||
pub struct ImageStyle {
|
|
||||||
#[serde(default)]
|
|
||||||
pub border: Border,
|
|
||||||
#[serde(default)]
|
|
||||||
pub corner_radius: f32,
|
|
||||||
#[serde(default)]
|
|
||||||
pub height: Option<f32>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub width: Option<f32>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub grayscale: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Image {
|
|
||||||
pub fn new(asset_path: &'static str) -> Self {
|
|
||||||
Self {
|
|
||||||
source: ImageSource::Path(asset_path),
|
|
||||||
style: Default::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_data(data: Arc<ImageData>) -> Self {
|
|
||||||
Self {
|
|
||||||
source: ImageSource::Data(data),
|
|
||||||
style: Default::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_style(mut self, style: ImageStyle) -> Self {
|
|
||||||
self.style = style;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V: 'static> Element<V> for Image {
|
|
||||||
type LayoutState = Option<Arc<ImageData>>;
|
|
||||||
type PaintState = ();
|
|
||||||
|
|
||||||
fn layout(
|
|
||||||
&mut self,
|
|
||||||
constraint: SizeConstraint,
|
|
||||||
_: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> (Vector2F, Self::LayoutState) {
|
|
||||||
let data = match &self.source {
|
|
||||||
ImageSource::Path(path) => match cx.asset_cache.png(path) {
|
|
||||||
Ok(data) => data,
|
|
||||||
Err(error) => {
|
|
||||||
log::error!("could not load image: {}", error);
|
|
||||||
return (Vector2F::zero(), None);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
ImageSource::Data(data) => data.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let desired_size = vec2f(
|
|
||||||
self.style.width.unwrap_or_else(|| constraint.max.x()),
|
|
||||||
self.style.height.unwrap_or_else(|| constraint.max.y()),
|
|
||||||
);
|
|
||||||
let size = constrain_size_preserving_aspect_ratio(
|
|
||||||
constraint.constrain(desired_size),
|
|
||||||
data.size().to_f32(),
|
|
||||||
);
|
|
||||||
|
|
||||||
(size, Some(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn paint(
|
|
||||||
&mut self,
|
|
||||||
bounds: RectF,
|
|
||||||
_: RectF,
|
|
||||||
layout: &mut Self::LayoutState,
|
|
||||||
_: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> Self::PaintState {
|
|
||||||
if let Some(data) = layout {
|
|
||||||
cx.scene().push_image(scene::Image {
|
|
||||||
bounds,
|
|
||||||
border: self.style.border.into(),
|
|
||||||
corner_radii: self.style.corner_radius.into(),
|
|
||||||
grayscale: self.style.grayscale,
|
|
||||||
data: data.clone(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
|
||||||
&self,
|
|
||||||
_: Range<usize>,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
_: &Self::LayoutState,
|
|
||||||
_: &Self::PaintState,
|
|
||||||
_: &V,
|
|
||||||
_: &ViewContext<V>,
|
|
||||||
) -> Option<RectF> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn debug(
|
|
||||||
&self,
|
|
||||||
bounds: RectF,
|
|
||||||
_: &Self::LayoutState,
|
|
||||||
_: &Self::PaintState,
|
|
||||||
_: &V,
|
|
||||||
_: &ViewContext<V>,
|
|
||||||
) -> serde_json::Value {
|
|
||||||
json!({
|
|
||||||
"type": "Image",
|
|
||||||
"bounds": bounds.to_json(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,100 +0,0 @@
|
|||||||
use crate::{
|
|
||||||
elements::*,
|
|
||||||
fonts::TextStyle,
|
|
||||||
geometry::{rect::RectF, vector::Vector2F},
|
|
||||||
Action, AnyElement, SizeConstraint,
|
|
||||||
};
|
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
use super::ContainerStyle;
|
|
||||||
|
|
||||||
pub struct KeystrokeLabel {
|
|
||||||
action: Box<dyn Action>,
|
|
||||||
container_style: ContainerStyle,
|
|
||||||
text_style: TextStyle,
|
|
||||||
view_id: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl KeystrokeLabel {
|
|
||||||
pub fn new(
|
|
||||||
view_id: usize,
|
|
||||||
action: Box<dyn Action>,
|
|
||||||
container_style: ContainerStyle,
|
|
||||||
text_style: TextStyle,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
view_id,
|
|
||||||
action,
|
|
||||||
container_style,
|
|
||||||
text_style,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V: 'static> Element<V> for KeystrokeLabel {
|
|
||||||
type LayoutState = AnyElement<V>;
|
|
||||||
type PaintState = ();
|
|
||||||
|
|
||||||
fn layout(
|
|
||||||
&mut self,
|
|
||||||
constraint: SizeConstraint,
|
|
||||||
view: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> (Vector2F, AnyElement<V>) {
|
|
||||||
let mut element = if let Some(keystrokes) =
|
|
||||||
cx.keystrokes_for_action(self.view_id, self.action.as_ref())
|
|
||||||
{
|
|
||||||
Flex::row()
|
|
||||||
.with_children(keystrokes.iter().map(|keystroke| {
|
|
||||||
Label::new(keystroke.to_string(), self.text_style.clone())
|
|
||||||
.contained()
|
|
||||||
.with_style(self.container_style)
|
|
||||||
}))
|
|
||||||
.into_any()
|
|
||||||
} else {
|
|
||||||
Empty::new().collapsed().into_any()
|
|
||||||
};
|
|
||||||
|
|
||||||
let size = element.layout(constraint, view, cx);
|
|
||||||
(size, element)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn paint(
|
|
||||||
&mut self,
|
|
||||||
bounds: RectF,
|
|
||||||
visible_bounds: RectF,
|
|
||||||
element: &mut AnyElement<V>,
|
|
||||||
view: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) {
|
|
||||||
element.paint(bounds.origin(), visible_bounds, view, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
|
||||||
&self,
|
|
||||||
_: Range<usize>,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
_: &Self::LayoutState,
|
|
||||||
_: &Self::PaintState,
|
|
||||||
_: &V,
|
|
||||||
_: &ViewContext<V>,
|
|
||||||
) -> Option<RectF> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn debug(
|
|
||||||
&self,
|
|
||||||
_: RectF,
|
|
||||||
element: &AnyElement<V>,
|
|
||||||
_: &(),
|
|
||||||
view: &V,
|
|
||||||
cx: &ViewContext<V>,
|
|
||||||
) -> serde_json::Value {
|
|
||||||
json!({
|
|
||||||
"type": "KeystrokeLabel",
|
|
||||||
"action": self.action.name(),
|
|
||||||
"child": element.debug(view, cx)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,280 +0,0 @@
|
|||||||
use std::{borrow::Cow, ops::Range};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
fonts::TextStyle,
|
|
||||||
geometry::{
|
|
||||||
rect::RectF,
|
|
||||||
vector::{vec2f, Vector2F},
|
|
||||||
},
|
|
||||||
json::{ToJson, Value},
|
|
||||||
text_layout::{Line, RunStyle},
|
|
||||||
Element, SizeConstraint, ViewContext,
|
|
||||||
};
|
|
||||||
use schemars::JsonSchema;
|
|
||||||
use serde::Deserialize;
|
|
||||||
use serde_json::json;
|
|
||||||
use smallvec::{smallvec, SmallVec};
|
|
||||||
|
|
||||||
pub struct Label {
|
|
||||||
text: Cow<'static, str>,
|
|
||||||
style: LabelStyle,
|
|
||||||
highlight_indices: Vec<usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
|
|
||||||
pub struct LabelStyle {
|
|
||||||
pub text: TextStyle,
|
|
||||||
pub highlight_text: Option<TextStyle>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<TextStyle> for LabelStyle {
|
|
||||||
fn from(text: TextStyle) -> Self {
|
|
||||||
LabelStyle {
|
|
||||||
text,
|
|
||||||
highlight_text: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LabelStyle {
|
|
||||||
pub fn with_font_size(mut self, font_size: f32) -> Self {
|
|
||||||
self.text.font_size = font_size;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Label {
|
|
||||||
pub fn new<I: Into<Cow<'static, str>>>(text: I, style: impl Into<LabelStyle>) -> Self {
|
|
||||||
Self {
|
|
||||||
text: text.into(),
|
|
||||||
highlight_indices: Default::default(),
|
|
||||||
style: style.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_highlights(mut self, indices: Vec<usize>) -> Self {
|
|
||||||
self.highlight_indices = indices;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compute_runs(&self) -> SmallVec<[(usize, RunStyle); 8]> {
|
|
||||||
let font_id = self.style.text.font_id;
|
|
||||||
if self.highlight_indices.is_empty() {
|
|
||||||
return smallvec![(
|
|
||||||
self.text.len(),
|
|
||||||
RunStyle {
|
|
||||||
font_id,
|
|
||||||
color: self.style.text.color,
|
|
||||||
underline: self.style.text.underline,
|
|
||||||
}
|
|
||||||
)];
|
|
||||||
}
|
|
||||||
|
|
||||||
let highlight_font_id = self
|
|
||||||
.style
|
|
||||||
.highlight_text
|
|
||||||
.as_ref()
|
|
||||||
.map_or(font_id, |style| style.font_id);
|
|
||||||
|
|
||||||
let mut highlight_indices = self.highlight_indices.iter().copied().peekable();
|
|
||||||
let mut runs = SmallVec::new();
|
|
||||||
let highlight_style = self
|
|
||||||
.style
|
|
||||||
.highlight_text
|
|
||||||
.as_ref()
|
|
||||||
.unwrap_or(&self.style.text);
|
|
||||||
|
|
||||||
for (char_ix, c) in self.text.char_indices() {
|
|
||||||
let mut font_id = font_id;
|
|
||||||
let mut color = self.style.text.color;
|
|
||||||
let mut underline = self.style.text.underline;
|
|
||||||
if let Some(highlight_ix) = highlight_indices.peek() {
|
|
||||||
if char_ix == *highlight_ix {
|
|
||||||
font_id = highlight_font_id;
|
|
||||||
color = highlight_style.color;
|
|
||||||
underline = highlight_style.underline;
|
|
||||||
highlight_indices.next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let last_run: Option<&mut (usize, RunStyle)> = runs.last_mut();
|
|
||||||
let push_new_run = if let Some((last_len, last_style)) = last_run {
|
|
||||||
if font_id == last_style.font_id
|
|
||||||
&& color == last_style.color
|
|
||||||
&& underline == last_style.underline
|
|
||||||
{
|
|
||||||
*last_len += c.len_utf8();
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
};
|
|
||||||
|
|
||||||
if push_new_run {
|
|
||||||
runs.push((
|
|
||||||
c.len_utf8(),
|
|
||||||
RunStyle {
|
|
||||||
font_id,
|
|
||||||
color,
|
|
||||||
underline,
|
|
||||||
},
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
runs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V: 'static> Element<V> for Label {
|
|
||||||
type LayoutState = Line;
|
|
||||||
type PaintState = ();
|
|
||||||
|
|
||||||
fn layout(
|
|
||||||
&mut self,
|
|
||||||
constraint: SizeConstraint,
|
|
||||||
_: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> (Vector2F, Self::LayoutState) {
|
|
||||||
let runs = self.compute_runs();
|
|
||||||
let line = cx.text_layout_cache().layout_str(
|
|
||||||
&self.text,
|
|
||||||
self.style.text.font_size,
|
|
||||||
runs.as_slice(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let size = vec2f(
|
|
||||||
line.width()
|
|
||||||
.ceil()
|
|
||||||
.max(constraint.min.x())
|
|
||||||
.min(constraint.max.x()),
|
|
||||||
cx.font_cache.line_height(self.style.text.font_size),
|
|
||||||
);
|
|
||||||
|
|
||||||
(size, line)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn paint(
|
|
||||||
&mut self,
|
|
||||||
bounds: RectF,
|
|
||||||
visible_bounds: RectF,
|
|
||||||
line: &mut Self::LayoutState,
|
|
||||||
_: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> Self::PaintState {
|
|
||||||
let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
|
|
||||||
line.paint(bounds.origin(), visible_bounds, bounds.size().y(), cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
|
||||||
&self,
|
|
||||||
_: Range<usize>,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
_: &Self::LayoutState,
|
|
||||||
_: &Self::PaintState,
|
|
||||||
_: &V,
|
|
||||||
_: &ViewContext<V>,
|
|
||||||
) -> Option<RectF> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn debug(
|
|
||||||
&self,
|
|
||||||
bounds: RectF,
|
|
||||||
_: &Self::LayoutState,
|
|
||||||
_: &Self::PaintState,
|
|
||||||
_: &V,
|
|
||||||
_: &ViewContext<V>,
|
|
||||||
) -> Value {
|
|
||||||
json!({
|
|
||||||
"type": "Label",
|
|
||||||
"bounds": bounds.to_json(),
|
|
||||||
"text": &self.text,
|
|
||||||
"highlight_indices": self.highlight_indices,
|
|
||||||
"style": self.style.to_json(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ToJson for LabelStyle {
|
|
||||||
fn to_json(&self) -> Value {
|
|
||||||
json!({
|
|
||||||
"text": self.text.to_json(),
|
|
||||||
"highlight_text": self.highlight_text
|
|
||||||
.as_ref()
|
|
||||||
.map_or(serde_json::Value::Null, |style| style.to_json())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::color::Color;
|
|
||||||
use crate::fonts::{Properties as FontProperties, Weight};
|
|
||||||
|
|
||||||
#[crate::test(self)]
|
|
||||||
fn test_layout_label_with_highlights(cx: &mut crate::AppContext) {
|
|
||||||
let default_style = TextStyle::new(
|
|
||||||
"Menlo",
|
|
||||||
12.,
|
|
||||||
Default::default(),
|
|
||||||
Default::default(),
|
|
||||||
Default::default(),
|
|
||||||
Color::black(),
|
|
||||||
cx.font_cache(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let highlight_style = TextStyle::new(
|
|
||||||
"Menlo",
|
|
||||||
12.,
|
|
||||||
*FontProperties::new().weight(Weight::BOLD),
|
|
||||||
Default::default(),
|
|
||||||
Default::default(),
|
|
||||||
Color::new(255, 0, 0, 255),
|
|
||||||
cx.font_cache(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let label = Label::new(
|
|
||||||
".αβγδε.ⓐⓑⓒⓓⓔ.abcde.".to_string(),
|
|
||||||
LabelStyle {
|
|
||||||
text: default_style.clone(),
|
|
||||||
highlight_text: Some(highlight_style.clone()),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.with_highlights(vec![
|
|
||||||
".α".len(),
|
|
||||||
".αβ".len(),
|
|
||||||
".αβγδ".len(),
|
|
||||||
".αβγδε.ⓐ".len(),
|
|
||||||
".αβγδε.ⓐⓑ".len(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
let default_run_style = RunStyle {
|
|
||||||
font_id: default_style.font_id,
|
|
||||||
color: default_style.color,
|
|
||||||
underline: default_style.underline,
|
|
||||||
};
|
|
||||||
let highlight_run_style = RunStyle {
|
|
||||||
font_id: highlight_style.font_id,
|
|
||||||
color: highlight_style.color,
|
|
||||||
underline: highlight_style.underline,
|
|
||||||
};
|
|
||||||
let runs = label.compute_runs();
|
|
||||||
assert_eq!(
|
|
||||||
runs.as_slice(),
|
|
||||||
&[
|
|
||||||
(".α".len(), default_run_style),
|
|
||||||
("βγ".len(), highlight_run_style),
|
|
||||||
("δ".len(), default_run_style),
|
|
||||||
("ε".len(), highlight_run_style),
|
|
||||||
(".ⓐ".len(), default_run_style),
|
|
||||||
("ⓑⓒ".len(), highlight_run_style),
|
|
||||||
("ⓓⓔ.abcde.".len(), default_run_style),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
@ -1,323 +0,0 @@
|
|||||||
use super::Padding;
|
|
||||||
use crate::{
|
|
||||||
geometry::{
|
|
||||||
rect::RectF,
|
|
||||||
vector::{vec2f, Vector2F},
|
|
||||||
},
|
|
||||||
platform::CursorStyle,
|
|
||||||
platform::MouseButton,
|
|
||||||
scene::{
|
|
||||||
CursorRegion, HandlerSet, MouseClick, MouseClickOut, MouseDown, MouseDownOut, MouseDrag,
|
|
||||||
MouseHover, MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut,
|
|
||||||
},
|
|
||||||
AnyElement, Element, EventContext, MouseRegion, MouseState, SizeConstraint, TypeTag,
|
|
||||||
ViewContext,
|
|
||||||
};
|
|
||||||
use serde_json::json;
|
|
||||||
use std::ops::Range;
|
|
||||||
|
|
||||||
pub struct MouseEventHandler<V: 'static> {
|
|
||||||
child: AnyElement<V>,
|
|
||||||
region_id: usize,
|
|
||||||
cursor_style: Option<CursorStyle>,
|
|
||||||
handlers: HandlerSet,
|
|
||||||
hoverable: bool,
|
|
||||||
notify_on_hover: bool,
|
|
||||||
notify_on_click: bool,
|
|
||||||
above: bool,
|
|
||||||
padding: Padding,
|
|
||||||
tag: TypeTag,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Element which provides a render_child callback with a MouseState and paints a mouse
|
|
||||||
/// region under (or above) it for easy mouse event handling.
|
|
||||||
impl<V: 'static> MouseEventHandler<V> {
|
|
||||||
pub fn for_child<Tag: 'static>(child: impl Element<V>, region_id: usize) -> Self {
|
|
||||||
Self {
|
|
||||||
child: child.into_any(),
|
|
||||||
region_id,
|
|
||||||
cursor_style: None,
|
|
||||||
handlers: Default::default(),
|
|
||||||
notify_on_hover: false,
|
|
||||||
notify_on_click: false,
|
|
||||||
hoverable: false,
|
|
||||||
above: false,
|
|
||||||
padding: Default::default(),
|
|
||||||
tag: TypeTag::new::<Tag>(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new<Tag: 'static, E>(
|
|
||||||
region_id: usize,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
render_child: impl FnOnce(&mut MouseState, &mut ViewContext<V>) -> E,
|
|
||||||
) -> Self
|
|
||||||
where
|
|
||||||
E: Element<V>,
|
|
||||||
{
|
|
||||||
let mut mouse_state = cx.mouse_state_dynamic(TypeTag::new::<Tag>(), region_id);
|
|
||||||
let child = render_child(&mut mouse_state, cx).into_any();
|
|
||||||
let notify_on_hover = mouse_state.accessed_hovered();
|
|
||||||
let notify_on_click = mouse_state.accessed_clicked();
|
|
||||||
Self {
|
|
||||||
child,
|
|
||||||
region_id,
|
|
||||||
cursor_style: None,
|
|
||||||
handlers: Default::default(),
|
|
||||||
notify_on_hover,
|
|
||||||
notify_on_click,
|
|
||||||
hoverable: true,
|
|
||||||
above: false,
|
|
||||||
padding: Default::default(),
|
|
||||||
tag: TypeTag::new::<Tag>(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_dynamic(
|
|
||||||
tag: TypeTag,
|
|
||||||
region_id: usize,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
render_child: impl FnOnce(&mut MouseState, &mut ViewContext<V>) -> AnyElement<V>,
|
|
||||||
) -> Self {
|
|
||||||
let mut mouse_state = cx.mouse_state_dynamic(tag, region_id);
|
|
||||||
let child = render_child(&mut mouse_state, cx);
|
|
||||||
let notify_on_hover = mouse_state.accessed_hovered();
|
|
||||||
let notify_on_click = mouse_state.accessed_clicked();
|
|
||||||
Self {
|
|
||||||
child,
|
|
||||||
region_id,
|
|
||||||
cursor_style: None,
|
|
||||||
handlers: Default::default(),
|
|
||||||
notify_on_hover,
|
|
||||||
notify_on_click,
|
|
||||||
hoverable: true,
|
|
||||||
above: false,
|
|
||||||
padding: Default::default(),
|
|
||||||
tag,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Modifies the MouseEventHandler to render the MouseRegion above the child element. Useful
|
|
||||||
/// for drag and drop handling and similar events which should be captured before the child
|
|
||||||
/// gets the opportunity
|
|
||||||
pub fn above<Tag: 'static, D>(
|
|
||||||
region_id: usize,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
render_child: impl FnOnce(&mut MouseState, &mut ViewContext<V>) -> D,
|
|
||||||
) -> Self
|
|
||||||
where
|
|
||||||
D: Element<V>,
|
|
||||||
{
|
|
||||||
let mut handler = Self::new::<Tag, _>(region_id, cx, render_child);
|
|
||||||
handler.above = true;
|
|
||||||
handler
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_cursor_style(mut self, cursor: CursorStyle) -> Self {
|
|
||||||
self.cursor_style = Some(cursor);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn capture_all(mut self) -> Self {
|
|
||||||
self.handlers = HandlerSet::capture_all();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_move(
|
|
||||||
mut self,
|
|
||||||
handler: impl Fn(MouseMove, &mut V, &mut EventContext<V>) + 'static,
|
|
||||||
) -> Self {
|
|
||||||
self.handlers = self.handlers.on_move(handler);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_move_out(
|
|
||||||
mut self,
|
|
||||||
handler: impl Fn(MouseMoveOut, &mut V, &mut EventContext<V>) + 'static,
|
|
||||||
) -> Self {
|
|
||||||
self.handlers = self.handlers.on_move_out(handler);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_down(
|
|
||||||
mut self,
|
|
||||||
button: MouseButton,
|
|
||||||
handler: impl Fn(MouseDown, &mut V, &mut EventContext<V>) + 'static,
|
|
||||||
) -> Self {
|
|
||||||
self.handlers = self.handlers.on_down(button, handler);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_up(
|
|
||||||
mut self,
|
|
||||||
button: MouseButton,
|
|
||||||
handler: impl Fn(MouseUp, &mut V, &mut EventContext<V>) + 'static,
|
|
||||||
) -> Self {
|
|
||||||
self.handlers = self.handlers.on_up(button, handler);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_click(
|
|
||||||
mut self,
|
|
||||||
button: MouseButton,
|
|
||||||
handler: impl Fn(MouseClick, &mut V, &mut EventContext<V>) + 'static,
|
|
||||||
) -> Self {
|
|
||||||
self.handlers = self.handlers.on_click(button, handler);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_click_out(
|
|
||||||
mut self,
|
|
||||||
button: MouseButton,
|
|
||||||
handler: impl Fn(MouseClickOut, &mut V, &mut EventContext<V>) + 'static,
|
|
||||||
) -> Self {
|
|
||||||
self.handlers = self.handlers.on_click_out(button, handler);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_down_out(
|
|
||||||
mut self,
|
|
||||||
button: MouseButton,
|
|
||||||
handler: impl Fn(MouseDownOut, &mut V, &mut EventContext<V>) + 'static,
|
|
||||||
) -> Self {
|
|
||||||
self.handlers = self.handlers.on_down_out(button, handler);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_up_out(
|
|
||||||
mut self,
|
|
||||||
button: MouseButton,
|
|
||||||
handler: impl Fn(MouseUpOut, &mut V, &mut EventContext<V>) + 'static,
|
|
||||||
) -> Self {
|
|
||||||
self.handlers = self.handlers.on_up_out(button, handler);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_drag(
|
|
||||||
mut self,
|
|
||||||
button: MouseButton,
|
|
||||||
handler: impl Fn(MouseDrag, &mut V, &mut EventContext<V>) + 'static,
|
|
||||||
) -> Self {
|
|
||||||
self.handlers = self.handlers.on_drag(button, handler);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_hover(
|
|
||||||
mut self,
|
|
||||||
handler: impl Fn(MouseHover, &mut V, &mut EventContext<V>) + 'static,
|
|
||||||
) -> Self {
|
|
||||||
self.handlers = self.handlers.on_hover(handler);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_scroll(
|
|
||||||
mut self,
|
|
||||||
handler: impl Fn(MouseScrollWheel, &mut V, &mut EventContext<V>) + 'static,
|
|
||||||
) -> Self {
|
|
||||||
self.handlers = self.handlers.on_scroll(handler);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_hoverable(mut self, is_hoverable: bool) -> Self {
|
|
||||||
self.hoverable = is_hoverable;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_padding(mut self, padding: Padding) -> Self {
|
|
||||||
self.padding = padding;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn hit_bounds(&self, bounds: RectF) -> RectF {
|
|
||||||
RectF::from_points(
|
|
||||||
bounds.origin() - vec2f(self.padding.left, self.padding.top),
|
|
||||||
bounds.lower_right() + vec2f(self.padding.right, self.padding.bottom),
|
|
||||||
)
|
|
||||||
.round_out()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn paint_regions(&self, bounds: RectF, visible_bounds: RectF, cx: &mut ViewContext<V>) {
|
|
||||||
let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default();
|
|
||||||
let hit_bounds = self.hit_bounds(visible_bounds);
|
|
||||||
|
|
||||||
if let Some(style) = self.cursor_style {
|
|
||||||
cx.scene().push_cursor_region(CursorRegion {
|
|
||||||
bounds: hit_bounds,
|
|
||||||
style,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let view_id = cx.view_id();
|
|
||||||
cx.scene().push_mouse_region(
|
|
||||||
MouseRegion::from_handlers(
|
|
||||||
self.tag,
|
|
||||||
view_id,
|
|
||||||
self.region_id,
|
|
||||||
hit_bounds,
|
|
||||||
self.handlers.clone(),
|
|
||||||
)
|
|
||||||
.with_hoverable(self.hoverable)
|
|
||||||
.with_notify_on_hover(self.notify_on_hover)
|
|
||||||
.with_notify_on_click(self.notify_on_click),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V: 'static> Element<V> for MouseEventHandler<V> {
|
|
||||||
type LayoutState = ();
|
|
||||||
type PaintState = ();
|
|
||||||
|
|
||||||
fn layout(
|
|
||||||
&mut self,
|
|
||||||
constraint: SizeConstraint,
|
|
||||||
view: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> (Vector2F, Self::LayoutState) {
|
|
||||||
(self.child.layout(constraint, view, cx), ())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn paint(
|
|
||||||
&mut self,
|
|
||||||
bounds: RectF,
|
|
||||||
visible_bounds: RectF,
|
|
||||||
_: &mut Self::LayoutState,
|
|
||||||
view: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> Self::PaintState {
|
|
||||||
if self.above {
|
|
||||||
self.child.paint(bounds.origin(), visible_bounds, view, cx);
|
|
||||||
cx.paint_layer(None, |cx| {
|
|
||||||
self.paint_regions(bounds, visible_bounds, cx);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
self.paint_regions(bounds, visible_bounds, cx);
|
|
||||||
self.child.paint(bounds.origin(), visible_bounds, view, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
|
||||||
&self,
|
|
||||||
range_utf16: Range<usize>,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
_: &Self::LayoutState,
|
|
||||||
_: &Self::PaintState,
|
|
||||||
view: &V,
|
|
||||||
cx: &ViewContext<V>,
|
|
||||||
) -> Option<RectF> {
|
|
||||||
self.child.rect_for_text_range(range_utf16, view, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn debug(
|
|
||||||
&self,
|
|
||||||
_: RectF,
|
|
||||||
_: &Self::LayoutState,
|
|
||||||
_: &Self::PaintState,
|
|
||||||
view: &V,
|
|
||||||
cx: &ViewContext<V>,
|
|
||||||
) -> serde_json::Value {
|
|
||||||
json!({
|
|
||||||
"type": "MouseEventHandler",
|
|
||||||
"child": self.child.debug(view, cx),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,33 +1,187 @@
|
|||||||
use std::ops::Range;
|
use smallvec::SmallVec;
|
||||||
|
use taffy::style::{Display, Position};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
geometry::{rect::RectF, vector::Vector2F},
|
point, AnyElement, BorrowWindow, Bounds, Element, IntoElement, LayoutId, ParentElement, Pixels,
|
||||||
json::ToJson,
|
Point, Size, Style, WindowContext,
|
||||||
AnyElement, Axis, Element, MouseRegion, SizeConstraint, ViewContext,
|
|
||||||
};
|
};
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
pub struct Overlay<V> {
|
pub struct OverlayState {
|
||||||
child: AnyElement<V>,
|
child_layout_ids: SmallVec<[LayoutId; 4]>,
|
||||||
anchor_position: Option<Vector2F>,
|
|
||||||
anchor_corner: AnchorCorner,
|
|
||||||
fit_mode: OverlayFitMode,
|
|
||||||
position_mode: OverlayPositionMode,
|
|
||||||
hoverable: bool,
|
|
||||||
z_index: Option<usize>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone)]
|
pub struct Overlay {
|
||||||
|
children: SmallVec<[AnyElement; 2]>,
|
||||||
|
anchor_corner: AnchorCorner,
|
||||||
|
fit_mode: OverlayFitMode,
|
||||||
|
// todo!();
|
||||||
|
anchor_position: Option<Point<Pixels>>,
|
||||||
|
// position_mode: OverlayPositionMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// overlay gives you a floating element that will avoid overflowing the window bounds.
|
||||||
|
/// Its children should have no margin to avoid measurement issues.
|
||||||
|
pub fn overlay() -> Overlay {
|
||||||
|
Overlay {
|
||||||
|
children: SmallVec::new(),
|
||||||
|
anchor_corner: AnchorCorner::TopLeft,
|
||||||
|
fit_mode: OverlayFitMode::SwitchAnchor,
|
||||||
|
anchor_position: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Overlay {
|
||||||
|
/// Sets which corner of the overlay should be anchored to the current position.
|
||||||
|
pub fn anchor(mut self, anchor: AnchorCorner) -> Self {
|
||||||
|
self.anchor_corner = anchor;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the position in window co-ordinates
|
||||||
|
/// (otherwise the location the overlay is rendered is used)
|
||||||
|
pub fn position(mut self, anchor: Point<Pixels>) -> Self {
|
||||||
|
self.anchor_position = Some(anchor);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Snap to window edge instead of switching anchor corner when an overflow would occur.
|
||||||
|
pub fn snap_to_window(mut self) -> Self {
|
||||||
|
self.fit_mode = OverlayFitMode::SnapToWindow;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParentElement for Overlay {
|
||||||
|
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
|
||||||
|
&mut self.children
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element for Overlay {
|
||||||
|
type State = OverlayState;
|
||||||
|
|
||||||
|
fn request_layout(
|
||||||
|
&mut self,
|
||||||
|
_: Option<Self::State>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> (crate::LayoutId, Self::State) {
|
||||||
|
let child_layout_ids = self
|
||||||
|
.children
|
||||||
|
.iter_mut()
|
||||||
|
.map(|child| child.request_layout(cx))
|
||||||
|
.collect::<SmallVec<_>>();
|
||||||
|
|
||||||
|
let overlay_style = Style {
|
||||||
|
position: Position::Absolute,
|
||||||
|
display: Display::Flex,
|
||||||
|
..Style::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let layout_id = cx.request_layout(&overlay_style, child_layout_ids.iter().copied());
|
||||||
|
|
||||||
|
(layout_id, OverlayState { child_layout_ids })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(
|
||||||
|
&mut self,
|
||||||
|
bounds: crate::Bounds<crate::Pixels>,
|
||||||
|
element_state: &mut Self::State,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) {
|
||||||
|
if element_state.child_layout_ids.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut child_min = point(Pixels::MAX, Pixels::MAX);
|
||||||
|
let mut child_max = Point::default();
|
||||||
|
for child_layout_id in &element_state.child_layout_ids {
|
||||||
|
let child_bounds = cx.layout_bounds(*child_layout_id);
|
||||||
|
child_min = child_min.min(&child_bounds.origin);
|
||||||
|
child_max = child_max.max(&child_bounds.lower_right());
|
||||||
|
}
|
||||||
|
let size: Size<Pixels> = (child_max - child_min).into();
|
||||||
|
let origin = self.anchor_position.unwrap_or(bounds.origin);
|
||||||
|
|
||||||
|
let mut desired = self.anchor_corner.get_bounds(origin, size);
|
||||||
|
let limits = Bounds {
|
||||||
|
origin: Point::default(),
|
||||||
|
size: cx.viewport_size(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.fit_mode == OverlayFitMode::SwitchAnchor {
|
||||||
|
let mut anchor_corner = self.anchor_corner;
|
||||||
|
|
||||||
|
if desired.left() < limits.left() || desired.right() > limits.right() {
|
||||||
|
let switched = anchor_corner
|
||||||
|
.switch_axis(Axis::Horizontal)
|
||||||
|
.get_bounds(origin, size);
|
||||||
|
if !(switched.left() < limits.left() || switched.right() > limits.right()) {
|
||||||
|
anchor_corner = anchor_corner.switch_axis(Axis::Horizontal);
|
||||||
|
desired = switched
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if desired.top() < limits.top() || desired.bottom() > limits.bottom() {
|
||||||
|
let switched = anchor_corner
|
||||||
|
.switch_axis(Axis::Vertical)
|
||||||
|
.get_bounds(origin, size);
|
||||||
|
if !(switched.top() < limits.top() || switched.bottom() > limits.bottom()) {
|
||||||
|
desired = switched;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snap the horizontal edges of the overlay to the horizontal edges of the window if
|
||||||
|
// its horizontal bounds overflow, aligning to the left if it is wider than the limits.
|
||||||
|
if desired.right() > limits.right() {
|
||||||
|
desired.origin.x -= desired.right() - limits.right();
|
||||||
|
}
|
||||||
|
if desired.left() < limits.left() {
|
||||||
|
desired.origin.x = limits.origin.x;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snap the vertical edges of the overlay to the vertical edges of the window if
|
||||||
|
// its vertical bounds overflow, aligning to the top if it is taller than the limits.
|
||||||
|
if desired.bottom() > limits.bottom() {
|
||||||
|
desired.origin.y -= desired.bottom() - limits.bottom();
|
||||||
|
}
|
||||||
|
if desired.top() < limits.top() {
|
||||||
|
desired.origin.y = limits.origin.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut offset = cx.element_offset() + desired.origin - bounds.origin;
|
||||||
|
offset = point(offset.x.round(), offset.y.round());
|
||||||
|
cx.with_absolute_element_offset(offset, |cx| {
|
||||||
|
cx.break_content_mask(|cx| {
|
||||||
|
for child in &mut self.children {
|
||||||
|
child.paint(cx);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoElement for Overlay {
|
||||||
|
type Element = Self;
|
||||||
|
|
||||||
|
fn element_id(&self) -> Option<crate::ElementId> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_element(self) -> Self::Element {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Axis {
|
||||||
|
Horizontal,
|
||||||
|
Vertical,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, PartialEq)]
|
||||||
pub enum OverlayFitMode {
|
pub enum OverlayFitMode {
|
||||||
SnapToWindow,
|
SnapToWindow,
|
||||||
SwitchAnchor,
|
SwitchAnchor,
|
||||||
None,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
|
||||||
pub enum OverlayPositionMode {
|
|
||||||
Window,
|
|
||||||
Local,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
@ -39,18 +193,32 @@ pub enum AnchorCorner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AnchorCorner {
|
impl AnchorCorner {
|
||||||
fn get_bounds(&self, anchor_position: Vector2F, size: Vector2F) -> RectF {
|
fn get_bounds(&self, origin: Point<Pixels>, size: Size<Pixels>) -> Bounds<Pixels> {
|
||||||
|
let origin = match self {
|
||||||
|
Self::TopLeft => origin,
|
||||||
|
Self::TopRight => Point {
|
||||||
|
x: origin.x - size.width,
|
||||||
|
y: origin.y,
|
||||||
|
},
|
||||||
|
Self::BottomLeft => Point {
|
||||||
|
x: origin.x,
|
||||||
|
y: origin.y - size.height,
|
||||||
|
},
|
||||||
|
Self::BottomRight => Point {
|
||||||
|
x: origin.x - size.width,
|
||||||
|
y: origin.y - size.height,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Bounds { origin, size }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn corner(&self, bounds: Bounds<Pixels>) -> Point<Pixels> {
|
||||||
match self {
|
match self {
|
||||||
Self::TopLeft => RectF::from_points(anchor_position, anchor_position + size),
|
Self::TopLeft => bounds.origin,
|
||||||
Self::TopRight => RectF::from_points(
|
Self::TopRight => bounds.upper_right(),
|
||||||
anchor_position - Vector2F::new(size.x(), 0.),
|
Self::BottomLeft => bounds.lower_left(),
|
||||||
anchor_position + Vector2F::new(0., size.y()),
|
Self::BottomRight => bounds.lower_right(),
|
||||||
),
|
|
||||||
Self::BottomLeft => RectF::from_points(
|
|
||||||
anchor_position - Vector2F::new(0., size.y()),
|
|
||||||
anchor_position + Vector2F::new(size.x(), 0.),
|
|
||||||
),
|
|
||||||
Self::BottomRight => RectF::from_points(anchor_position - size, anchor_position),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,190 +239,3 @@ impl AnchorCorner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: 'static> Overlay<V> {
|
|
||||||
pub fn new(child: impl Element<V>) -> Self {
|
|
||||||
Self {
|
|
||||||
child: child.into_any(),
|
|
||||||
anchor_position: None,
|
|
||||||
anchor_corner: AnchorCorner::TopLeft,
|
|
||||||
fit_mode: OverlayFitMode::None,
|
|
||||||
position_mode: OverlayPositionMode::Window,
|
|
||||||
hoverable: false,
|
|
||||||
z_index: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_anchor_position(mut self, position: Vector2F) -> Self {
|
|
||||||
self.anchor_position = Some(position);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_anchor_corner(mut self, anchor_corner: AnchorCorner) -> Self {
|
|
||||||
self.anchor_corner = anchor_corner;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_fit_mode(mut self, fit_mode: OverlayFitMode) -> Self {
|
|
||||||
self.fit_mode = fit_mode;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_position_mode(mut self, position_mode: OverlayPositionMode) -> Self {
|
|
||||||
self.position_mode = position_mode;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_hoverable(mut self, hoverable: bool) -> Self {
|
|
||||||
self.hoverable = hoverable;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_z_index(mut self, z_index: usize) -> Self {
|
|
||||||
self.z_index = Some(z_index);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V: 'static> Element<V> for Overlay<V> {
|
|
||||||
type LayoutState = Vector2F;
|
|
||||||
type PaintState = ();
|
|
||||||
|
|
||||||
fn layout(
|
|
||||||
&mut self,
|
|
||||||
constraint: SizeConstraint,
|
|
||||||
view: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> (Vector2F, Self::LayoutState) {
|
|
||||||
let constraint = if self.anchor_position.is_some() {
|
|
||||||
SizeConstraint::new(Vector2F::zero(), cx.window_size())
|
|
||||||
} else {
|
|
||||||
constraint
|
|
||||||
};
|
|
||||||
let size = self.child.layout(constraint, view, cx);
|
|
||||||
(Vector2F::zero(), size)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn paint(
|
|
||||||
&mut self,
|
|
||||||
bounds: RectF,
|
|
||||||
_: RectF,
|
|
||||||
size: &mut Self::LayoutState,
|
|
||||||
view: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) {
|
|
||||||
let (anchor_position, mut bounds) = match self.position_mode {
|
|
||||||
OverlayPositionMode::Window => {
|
|
||||||
let anchor_position = self.anchor_position.unwrap_or_else(|| bounds.origin());
|
|
||||||
let bounds = self.anchor_corner.get_bounds(anchor_position, *size);
|
|
||||||
(anchor_position, bounds)
|
|
||||||
}
|
|
||||||
OverlayPositionMode::Local => {
|
|
||||||
let anchor_position = self.anchor_position.unwrap_or_default();
|
|
||||||
let bounds = self
|
|
||||||
.anchor_corner
|
|
||||||
.get_bounds(bounds.origin() + anchor_position, *size);
|
|
||||||
(anchor_position, bounds)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match self.fit_mode {
|
|
||||||
OverlayFitMode::SnapToWindow => {
|
|
||||||
// Snap the horizontal edges of the overlay to the horizontal edges of the window if
|
|
||||||
// its horizontal bounds overflow
|
|
||||||
if bounds.max_x() > cx.window_size().x() {
|
|
||||||
let mut lower_right = bounds.lower_right();
|
|
||||||
lower_right.set_x(cx.window_size().x());
|
|
||||||
bounds = RectF::from_points(lower_right - *size, lower_right);
|
|
||||||
} else if bounds.min_x() < 0. {
|
|
||||||
let mut upper_left = bounds.origin();
|
|
||||||
upper_left.set_x(0.);
|
|
||||||
bounds = RectF::from_points(upper_left, upper_left + *size);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Snap the vertical edges of the overlay to the vertical edges of the window if
|
|
||||||
// its vertical bounds overflow.
|
|
||||||
if bounds.max_y() > cx.window_size().y() {
|
|
||||||
let mut lower_right = bounds.lower_right();
|
|
||||||
lower_right.set_y(cx.window_size().y());
|
|
||||||
bounds = RectF::from_points(lower_right - *size, lower_right);
|
|
||||||
} else if bounds.min_y() < 0. {
|
|
||||||
let mut upper_left = bounds.origin();
|
|
||||||
upper_left.set_y(0.);
|
|
||||||
bounds = RectF::from_points(upper_left, upper_left + *size);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
OverlayFitMode::SwitchAnchor => {
|
|
||||||
let mut anchor_corner = self.anchor_corner;
|
|
||||||
|
|
||||||
if bounds.max_x() > cx.window_size().x() {
|
|
||||||
anchor_corner = anchor_corner.switch_axis(Axis::Horizontal);
|
|
||||||
}
|
|
||||||
|
|
||||||
if bounds.max_y() > cx.window_size().y() {
|
|
||||||
anchor_corner = anchor_corner.switch_axis(Axis::Vertical);
|
|
||||||
}
|
|
||||||
|
|
||||||
if bounds.min_x() < 0. {
|
|
||||||
anchor_corner = anchor_corner.switch_axis(Axis::Horizontal)
|
|
||||||
}
|
|
||||||
|
|
||||||
if bounds.min_y() < 0. {
|
|
||||||
anchor_corner = anchor_corner.switch_axis(Axis::Vertical)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update bounds if needed
|
|
||||||
if anchor_corner != self.anchor_corner {
|
|
||||||
bounds = anchor_corner.get_bounds(anchor_position, *size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
OverlayFitMode::None => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
cx.scene().push_stacking_context(None, self.z_index);
|
|
||||||
if self.hoverable {
|
|
||||||
enum OverlayHoverCapture {}
|
|
||||||
// Block hovers in lower stacking contexts
|
|
||||||
let view_id = cx.view_id();
|
|
||||||
cx.scene()
|
|
||||||
.push_mouse_region(MouseRegion::new::<OverlayHoverCapture>(
|
|
||||||
view_id, view_id, bounds,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
self.child.paint(
|
|
||||||
bounds.origin(),
|
|
||||||
RectF::new(Vector2F::zero(), cx.window_size()),
|
|
||||||
view,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
cx.scene().pop_stacking_context();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
|
||||||
&self,
|
|
||||||
range_utf16: Range<usize>,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
_: &Self::LayoutState,
|
|
||||||
_: &Self::PaintState,
|
|
||||||
view: &V,
|
|
||||||
cx: &ViewContext<V>,
|
|
||||||
) -> Option<RectF> {
|
|
||||||
self.child.rect_for_text_range(range_utf16, view, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn debug(
|
|
||||||
&self,
|
|
||||||
_: RectF,
|
|
||||||
_: &Self::LayoutState,
|
|
||||||
_: &Self::PaintState,
|
|
||||||
view: &V,
|
|
||||||
cx: &ViewContext<V>,
|
|
||||||
) -> serde_json::Value {
|
|
||||||
json!({
|
|
||||||
"type": "Overlay",
|
|
||||||
"abs_position": self.anchor_position.to_json(),
|
|
||||||
"child": self.child.debug(view, cx),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,290 +0,0 @@
|
|||||||
use std::{cell::RefCell, rc::Rc};
|
|
||||||
|
|
||||||
use collections::HashMap;
|
|
||||||
use pathfinder_geometry::vector::{vec2f, Vector2F};
|
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
geometry::rect::RectF,
|
|
||||||
platform::{CursorStyle, MouseButton},
|
|
||||||
AnyElement, AppContext, Axis, Element, MouseRegion, SizeConstraint, TypeTag, View, ViewContext,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
|
||||||
pub enum HandleSide {
|
|
||||||
Top,
|
|
||||||
Bottom,
|
|
||||||
Left,
|
|
||||||
Right,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HandleSide {
|
|
||||||
fn axis(&self) -> Axis {
|
|
||||||
match self {
|
|
||||||
HandleSide::Left | HandleSide::Right => Axis::Horizontal,
|
|
||||||
HandleSide::Top | HandleSide::Bottom => Axis::Vertical,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn relevant_component(&self, vector: Vector2F) -> f32 {
|
|
||||||
match self.axis() {
|
|
||||||
Axis::Horizontal => vector.x(),
|
|
||||||
Axis::Vertical => vector.y(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn of_rect(&self, bounds: RectF, handle_size: f32) -> RectF {
|
|
||||||
match self {
|
|
||||||
HandleSide::Top => RectF::new(bounds.origin(), vec2f(bounds.width(), handle_size)),
|
|
||||||
HandleSide::Left => RectF::new(bounds.origin(), vec2f(handle_size, bounds.height())),
|
|
||||||
HandleSide::Bottom => {
|
|
||||||
let mut origin = bounds.lower_left();
|
|
||||||
origin.set_y(origin.y() - handle_size);
|
|
||||||
RectF::new(origin, vec2f(bounds.width(), handle_size))
|
|
||||||
}
|
|
||||||
HandleSide::Right => {
|
|
||||||
let mut origin = bounds.upper_right();
|
|
||||||
origin.set_x(origin.x() - handle_size);
|
|
||||||
RectF::new(origin, vec2f(handle_size, bounds.height()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_bounds(tag: TypeTag, cx: &AppContext) -> Option<&(RectF, RectF)>
|
|
||||||
where
|
|
||||||
{
|
|
||||||
cx.optional_global::<ProviderMap>()
|
|
||||||
.and_then(|map| map.0.get(&tag))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Resizable<V: 'static> {
|
|
||||||
child: AnyElement<V>,
|
|
||||||
tag: TypeTag,
|
|
||||||
handle_side: HandleSide,
|
|
||||||
handle_size: f32,
|
|
||||||
on_resize: Rc<RefCell<dyn FnMut(&mut V, Option<f32>, &mut ViewContext<V>)>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULT_HANDLE_SIZE: f32 = 4.0;
|
|
||||||
|
|
||||||
impl<V: 'static> Resizable<V> {
|
|
||||||
pub fn new<Tag: 'static>(
|
|
||||||
child: AnyElement<V>,
|
|
||||||
handle_side: HandleSide,
|
|
||||||
size: f32,
|
|
||||||
on_resize: impl 'static + FnMut(&mut V, Option<f32>, &mut ViewContext<V>),
|
|
||||||
) -> Self {
|
|
||||||
let child = match handle_side.axis() {
|
|
||||||
Axis::Horizontal => child.constrained().with_max_width(size),
|
|
||||||
Axis::Vertical => child.constrained().with_max_height(size),
|
|
||||||
}
|
|
||||||
.into_any();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
child,
|
|
||||||
handle_side,
|
|
||||||
tag: TypeTag::new::<Tag>(),
|
|
||||||
handle_size: DEFAULT_HANDLE_SIZE,
|
|
||||||
on_resize: Rc::new(RefCell::new(on_resize)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_handle_size(mut self, handle_size: f32) -> Self {
|
|
||||||
self.handle_size = handle_size;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V: 'static> Element<V> for Resizable<V> {
|
|
||||||
type LayoutState = SizeConstraint;
|
|
||||||
type PaintState = ();
|
|
||||||
|
|
||||||
fn layout(
|
|
||||||
&mut self,
|
|
||||||
constraint: crate::SizeConstraint,
|
|
||||||
view: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> (Vector2F, Self::LayoutState) {
|
|
||||||
(self.child.layout(constraint, view, cx), constraint)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn paint(
|
|
||||||
&mut self,
|
|
||||||
bounds: pathfinder_geometry::rect::RectF,
|
|
||||||
visible_bounds: pathfinder_geometry::rect::RectF,
|
|
||||||
constraint: &mut SizeConstraint,
|
|
||||||
view: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> Self::PaintState {
|
|
||||||
cx.scene().push_stacking_context(None, None);
|
|
||||||
|
|
||||||
let handle_region = self.handle_side.of_rect(bounds, self.handle_size);
|
|
||||||
|
|
||||||
enum ResizeHandle {}
|
|
||||||
let view_id = cx.view_id();
|
|
||||||
cx.scene().push_mouse_region(
|
|
||||||
MouseRegion::new::<ResizeHandle>(view_id, self.handle_side as usize, handle_region)
|
|
||||||
.on_down(MouseButton::Left, |_, _: &mut V, _| {}) // This prevents the mouse down event from being propagated elsewhere
|
|
||||||
.on_click(MouseButton::Left, {
|
|
||||||
let on_resize = self.on_resize.clone();
|
|
||||||
move |click, v, cx| {
|
|
||||||
if click.click_count == 2 {
|
|
||||||
on_resize.borrow_mut()(v, None, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.on_drag(MouseButton::Left, {
|
|
||||||
let bounds = bounds.clone();
|
|
||||||
let side = self.handle_side;
|
|
||||||
let prev_size = side.relevant_component(bounds.size());
|
|
||||||
let min_size = side.relevant_component(constraint.min);
|
|
||||||
let max_size = side.relevant_component(constraint.max);
|
|
||||||
let on_resize = self.on_resize.clone();
|
|
||||||
let tag = self.tag;
|
|
||||||
move |event, view: &mut V, cx| {
|
|
||||||
if event.end {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some((bounds, _)) = get_bounds(tag, cx) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let new_size_raw = match side {
|
|
||||||
// Handle on top side of element => Element is on bottom
|
|
||||||
HandleSide::Top => {
|
|
||||||
bounds.height() + bounds.origin_y() - event.position.y()
|
|
||||||
}
|
|
||||||
// Handle on right side of element => Element is on left
|
|
||||||
HandleSide::Right => event.position.x() - bounds.lower_left().x(),
|
|
||||||
// Handle on left side of element => Element is on the right
|
|
||||||
HandleSide::Left => {
|
|
||||||
bounds.width() + bounds.origin_x() - event.position.x()
|
|
||||||
}
|
|
||||||
// Handle on bottom side of element => Element is on the top
|
|
||||||
HandleSide::Bottom => event.position.y() - bounds.lower_left().y(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let new_size = min_size.max(new_size_raw).min(max_size).round();
|
|
||||||
if new_size != prev_size {
|
|
||||||
on_resize.borrow_mut()(view, Some(new_size), cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
cx.scene().push_cursor_region(crate::CursorRegion {
|
|
||||||
bounds: handle_region,
|
|
||||||
style: match self.handle_side.axis() {
|
|
||||||
Axis::Horizontal => CursorStyle::ResizeLeftRight,
|
|
||||||
Axis::Vertical => CursorStyle::ResizeUpDown,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.scene().pop_stacking_context();
|
|
||||||
|
|
||||||
self.child.paint(bounds.origin(), visible_bounds, view, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
|
||||||
&self,
|
|
||||||
range_utf16: std::ops::Range<usize>,
|
|
||||||
_bounds: pathfinder_geometry::rect::RectF,
|
|
||||||
_visible_bounds: pathfinder_geometry::rect::RectF,
|
|
||||||
_layout: &Self::LayoutState,
|
|
||||||
_paint: &Self::PaintState,
|
|
||||||
view: &V,
|
|
||||||
cx: &ViewContext<V>,
|
|
||||||
) -> Option<pathfinder_geometry::rect::RectF> {
|
|
||||||
self.child.rect_for_text_range(range_utf16, view, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn debug(
|
|
||||||
&self,
|
|
||||||
_bounds: pathfinder_geometry::rect::RectF,
|
|
||||||
_layout: &Self::LayoutState,
|
|
||||||
_paint: &Self::PaintState,
|
|
||||||
view: &V,
|
|
||||||
cx: &ViewContext<V>,
|
|
||||||
) -> serde_json::Value {
|
|
||||||
json!({
|
|
||||||
"child": self.child.debug(view, cx),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
struct ProviderMap(HashMap<TypeTag, (RectF, RectF)>);
|
|
||||||
|
|
||||||
pub struct BoundsProvider<V: 'static, P> {
|
|
||||||
child: AnyElement<V>,
|
|
||||||
phantom: std::marker::PhantomData<P>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V: 'static, P: 'static> BoundsProvider<V, P> {
|
|
||||||
pub fn new(child: AnyElement<V>) -> Self {
|
|
||||||
Self {
|
|
||||||
child,
|
|
||||||
phantom: std::marker::PhantomData,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V: View, P: 'static> Element<V> for BoundsProvider<V, P> {
|
|
||||||
type LayoutState = ();
|
|
||||||
|
|
||||||
type PaintState = ();
|
|
||||||
|
|
||||||
fn layout(
|
|
||||||
&mut self,
|
|
||||||
constraint: crate::SizeConstraint,
|
|
||||||
view: &mut V,
|
|
||||||
cx: &mut crate::ViewContext<V>,
|
|
||||||
) -> (pathfinder_geometry::vector::Vector2F, Self::LayoutState) {
|
|
||||||
(self.child.layout(constraint, view, cx), ())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn paint(
|
|
||||||
&mut self,
|
|
||||||
bounds: pathfinder_geometry::rect::RectF,
|
|
||||||
visible_bounds: pathfinder_geometry::rect::RectF,
|
|
||||||
_: &mut Self::LayoutState,
|
|
||||||
view: &mut V,
|
|
||||||
cx: &mut crate::ViewContext<V>,
|
|
||||||
) -> Self::PaintState {
|
|
||||||
cx.update_default_global::<ProviderMap, _, _>(|map, _| {
|
|
||||||
map.0.insert(TypeTag::new::<P>(), (bounds, visible_bounds));
|
|
||||||
});
|
|
||||||
|
|
||||||
self.child.paint(bounds.origin(), visible_bounds, view, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
|
||||||
&self,
|
|
||||||
range_utf16: std::ops::Range<usize>,
|
|
||||||
_: pathfinder_geometry::rect::RectF,
|
|
||||||
_: pathfinder_geometry::rect::RectF,
|
|
||||||
_: &Self::LayoutState,
|
|
||||||
_: &Self::PaintState,
|
|
||||||
view: &V,
|
|
||||||
cx: &crate::ViewContext<V>,
|
|
||||||
) -> Option<pathfinder_geometry::rect::RectF> {
|
|
||||||
self.child.rect_for_text_range(range_utf16, view, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn debug(
|
|
||||||
&self,
|
|
||||||
_: pathfinder_geometry::rect::RectF,
|
|
||||||
_: &Self::LayoutState,
|
|
||||||
_: &Self::PaintState,
|
|
||||||
view: &V,
|
|
||||||
cx: &crate::ViewContext<V>,
|
|
||||||
) -> serde_json::Value {
|
|
||||||
serde_json::json!({
|
|
||||||
"type": "Provider",
|
|
||||||
"providing": format!("{:?}", TypeTag::new::<P>()),
|
|
||||||
"child": self.child.debug(view, cx),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,104 +0,0 @@
|
|||||||
use std::ops::Range;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
geometry::{rect::RectF, vector::Vector2F},
|
|
||||||
json::{self, json, ToJson},
|
|
||||||
AnyElement, Element, SizeConstraint, ViewContext,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Element which renders it's children in a stack on top of each other.
|
|
||||||
/// The first child determines the size of the others.
|
|
||||||
pub struct Stack<V> {
|
|
||||||
children: Vec<AnyElement<V>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V> Default for Stack<V> {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
children: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V> Stack<V> {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V: 'static> Element<V> for Stack<V> {
|
|
||||||
type LayoutState = ();
|
|
||||||
type PaintState = ();
|
|
||||||
|
|
||||||
fn layout(
|
|
||||||
&mut self,
|
|
||||||
mut constraint: SizeConstraint,
|
|
||||||
view: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> (Vector2F, Self::LayoutState) {
|
|
||||||
let mut size = constraint.min;
|
|
||||||
let mut children = self.children.iter_mut();
|
|
||||||
if let Some(bottom_child) = children.next() {
|
|
||||||
size = bottom_child.layout(constraint, view, cx);
|
|
||||||
constraint = SizeConstraint::strict(size);
|
|
||||||
}
|
|
||||||
|
|
||||||
for child in children {
|
|
||||||
child.layout(constraint, view, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
(size, ())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn paint(
|
|
||||||
&mut self,
|
|
||||||
bounds: RectF,
|
|
||||||
visible_bounds: RectF,
|
|
||||||
_: &mut Self::LayoutState,
|
|
||||||
view: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> Self::PaintState {
|
|
||||||
for child in &mut self.children {
|
|
||||||
cx.scene().push_layer(None);
|
|
||||||
child.paint(bounds.origin(), visible_bounds, view, cx);
|
|
||||||
cx.scene().pop_layer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
|
||||||
&self,
|
|
||||||
range_utf16: Range<usize>,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
_: &Self::LayoutState,
|
|
||||||
_: &Self::PaintState,
|
|
||||||
view: &V,
|
|
||||||
cx: &ViewContext<V>,
|
|
||||||
) -> Option<RectF> {
|
|
||||||
self.children
|
|
||||||
.iter()
|
|
||||||
.rev()
|
|
||||||
.find_map(|child| child.rect_for_text_range(range_utf16.clone(), view, cx))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn debug(
|
|
||||||
&self,
|
|
||||||
bounds: RectF,
|
|
||||||
_: &Self::LayoutState,
|
|
||||||
_: &Self::PaintState,
|
|
||||||
view: &V,
|
|
||||||
cx: &ViewContext<V>,
|
|
||||||
) -> json::Value {
|
|
||||||
json!({
|
|
||||||
"type": "Stack",
|
|
||||||
"bounds": bounds.to_json(),
|
|
||||||
"children": self.children.iter().map(|child| child.debug(view, cx)).collect::<Vec<json::Value>>()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V> Extend<AnyElement<V>> for Stack<V> {
|
|
||||||
fn extend<T: IntoIterator<Item = AnyElement<V>>>(&mut self, children: T) {
|
|
||||||
self.children.extend(children)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,141 +1,78 @@
|
|||||||
use super::constrain_size_preserving_aspect_ratio;
|
|
||||||
use crate::json::ToJson;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
color::Color,
|
Bounds, Element, ElementId, InteractiveElement, InteractiveElementState, Interactivity,
|
||||||
geometry::{
|
IntoElement, LayoutId, Pixels, SharedString, StyleRefinement, Styled, WindowContext,
|
||||||
rect::RectF,
|
|
||||||
vector::{vec2f, Vector2F},
|
|
||||||
},
|
|
||||||
scene, Element, SizeConstraint, ViewContext,
|
|
||||||
};
|
};
|
||||||
use schemars::JsonSchema;
|
use util::ResultExt;
|
||||||
use serde_derive::Deserialize;
|
|
||||||
use serde_json::json;
|
|
||||||
use std::{borrow::Cow, ops::Range};
|
|
||||||
|
|
||||||
pub struct Svg {
|
pub struct Svg {
|
||||||
path: Cow<'static, str>,
|
interactivity: Interactivity,
|
||||||
color: Color,
|
path: Option<SharedString>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn svg() -> Svg {
|
||||||
|
Svg {
|
||||||
|
interactivity: Interactivity::default(),
|
||||||
|
path: None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Svg {
|
impl Svg {
|
||||||
pub fn new(path: impl Into<Cow<'static, str>>) -> Self {
|
pub fn path(mut self, path: impl Into<SharedString>) -> Self {
|
||||||
Self {
|
self.path = Some(path.into());
|
||||||
path: path.into(),
|
|
||||||
color: Color::black(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn for_style<V: 'static>(style: SvgStyle) -> impl Element<V> {
|
|
||||||
Self::new(style.asset)
|
|
||||||
.with_color(style.color)
|
|
||||||
.constrained()
|
|
||||||
.with_width(style.dimensions.width)
|
|
||||||
.with_height(style.dimensions.height)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_color(mut self, color: Color) -> Self {
|
|
||||||
self.color = color;
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: 'static> Element<V> for Svg {
|
impl Element for Svg {
|
||||||
type LayoutState = Option<usvg::Tree>;
|
type State = InteractiveElementState;
|
||||||
type PaintState = ();
|
|
||||||
|
|
||||||
fn layout(
|
fn request_layout(
|
||||||
&mut self,
|
&mut self,
|
||||||
constraint: SizeConstraint,
|
element_state: Option<Self::State>,
|
||||||
_: &mut V,
|
cx: &mut WindowContext,
|
||||||
cx: &mut ViewContext<V>,
|
) -> (LayoutId, Self::State) {
|
||||||
) -> (Vector2F, Self::LayoutState) {
|
self.interactivity.layout(element_state, cx, |style, cx| {
|
||||||
match cx.asset_cache.svg(&self.path) {
|
cx.request_layout(&style, None)
|
||||||
Ok(tree) => {
|
})
|
||||||
let size = constrain_size_preserving_aspect_ratio(
|
|
||||||
constraint.max,
|
|
||||||
from_usvg_rect(tree.svg_node().view_box.rect).size(),
|
|
||||||
);
|
|
||||||
(size, Some(tree))
|
|
||||||
}
|
|
||||||
Err(_error) => {
|
|
||||||
#[cfg(not(any(test, feature = "test-support")))]
|
|
||||||
log::error!("{}", _error);
|
|
||||||
(constraint.min, None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn paint(
|
fn paint(
|
||||||
&mut self,
|
&mut self,
|
||||||
bounds: RectF,
|
bounds: Bounds<Pixels>,
|
||||||
_visible_bounds: RectF,
|
element_state: &mut Self::State,
|
||||||
svg: &mut Self::LayoutState,
|
cx: &mut WindowContext,
|
||||||
_: &mut V,
|
) where
|
||||||
cx: &mut ViewContext<V>,
|
Self: Sized,
|
||||||
) {
|
{
|
||||||
if let Some(svg) = svg.clone() {
|
self.interactivity
|
||||||
cx.scene().push_icon(scene::Icon {
|
.paint(bounds, bounds.size, element_state, cx, |style, _, cx| {
|
||||||
bounds,
|
if let Some((path, color)) = self.path.as_ref().zip(style.text.color) {
|
||||||
svg,
|
cx.paint_svg(bounds, path.clone(), color).log_err();
|
||||||
path: self.path.clone(),
|
}
|
||||||
color: self.color,
|
})
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
|
||||||
&self,
|
|
||||||
_: Range<usize>,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
_: &Self::LayoutState,
|
|
||||||
_: &Self::PaintState,
|
|
||||||
_: &V,
|
|
||||||
_: &ViewContext<V>,
|
|
||||||
) -> Option<RectF> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn debug(
|
|
||||||
&self,
|
|
||||||
bounds: RectF,
|
|
||||||
_: &Self::LayoutState,
|
|
||||||
_: &Self::PaintState,
|
|
||||||
_: &V,
|
|
||||||
_: &ViewContext<V>,
|
|
||||||
) -> serde_json::Value {
|
|
||||||
json!({
|
|
||||||
"type": "Svg",
|
|
||||||
"bounds": bounds.to_json(),
|
|
||||||
"path": self.path,
|
|
||||||
"color": self.color.to_json(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
impl IntoElement for Svg {
|
||||||
pub struct SvgStyle {
|
type Element = Self;
|
||||||
pub color: Color,
|
|
||||||
pub asset: String,
|
|
||||||
pub dimensions: Dimensions,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
fn element_id(&self) -> Option<ElementId> {
|
||||||
pub struct Dimensions {
|
self.interactivity.element_id.clone()
|
||||||
pub width: f32,
|
}
|
||||||
pub height: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Dimensions {
|
fn into_element(self) -> Self::Element {
|
||||||
pub fn to_vec(&self) -> Vector2F {
|
self
|
||||||
vec2f(self.width, self.height)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_usvg_rect(rect: usvg::Rect) -> RectF {
|
impl Styled for Svg {
|
||||||
RectF::new(
|
fn style(&mut self) -> &mut StyleRefinement {
|
||||||
vec2f(rect.x() as f32, rect.y() as f32),
|
&mut self.interactivity.base_style
|
||||||
vec2f(rect.width() as f32, rect.height() as f32),
|
}
|
||||||
)
|
}
|
||||||
|
|
||||||
|
impl InteractiveElement for Svg {
|
||||||
|
fn interactivity(&mut self) -> &mut Interactivity {
|
||||||
|
&mut self.interactivity
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,438 +1,423 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
color::Color,
|
Bounds, DispatchPhase, Element, ElementId, HighlightStyle, IntoElement, LayoutId,
|
||||||
fonts::{HighlightStyle, TextStyle},
|
MouseDownEvent, MouseUpEvent, Pixels, Point, SharedString, Size, TextRun, TextStyle,
|
||||||
geometry::{
|
WhiteSpace, WindowContext, WrappedLine,
|
||||||
rect::RectF,
|
|
||||||
vector::{vec2f, Vector2F},
|
|
||||||
},
|
|
||||||
json::{ToJson, Value},
|
|
||||||
text_layout::{Line, RunStyle, ShapedBoundary},
|
|
||||||
Element, FontCache, SizeConstraint, TextLayoutCache, ViewContext, WindowContext,
|
|
||||||
};
|
};
|
||||||
use log::warn;
|
use anyhow::anyhow;
|
||||||
use serde_json::json;
|
use parking_lot::{Mutex, MutexGuard};
|
||||||
use std::{borrow::Cow, ops::Range, sync::Arc};
|
use smallvec::SmallVec;
|
||||||
|
use std::{cell::Cell, mem, ops::Range, rc::Rc, sync::Arc};
|
||||||
|
use util::ResultExt;
|
||||||
|
|
||||||
pub struct Text {
|
impl Element for &'static str {
|
||||||
text: Cow<'static, str>,
|
type State = TextState;
|
||||||
style: TextStyle,
|
|
||||||
soft_wrap: bool,
|
|
||||||
highlights: Option<Box<[(Range<usize>, HighlightStyle)]>>,
|
|
||||||
custom_runs: Option<(
|
|
||||||
Box<[Range<usize>]>,
|
|
||||||
Box<dyn FnMut(usize, RectF, &mut WindowContext)>,
|
|
||||||
)>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct LayoutState {
|
fn request_layout(
|
||||||
shaped_lines: Vec<Line>,
|
&mut self,
|
||||||
wrap_boundaries: Vec<Vec<ShapedBoundary>>,
|
_: Option<Self::State>,
|
||||||
line_height: f32,
|
cx: &mut WindowContext,
|
||||||
}
|
) -> (LayoutId, Self::State) {
|
||||||
|
let mut state = TextState::default();
|
||||||
impl Text {
|
let layout_id = state.layout(SharedString::from(*self), None, cx);
|
||||||
pub fn new<I: Into<Cow<'static, str>>>(text: I, style: TextStyle) -> Self {
|
(layout_id, state)
|
||||||
Self {
|
|
||||||
text: text.into(),
|
|
||||||
style,
|
|
||||||
soft_wrap: true,
|
|
||||||
highlights: None,
|
|
||||||
custom_runs: None,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_default_color(mut self, color: Color) -> Self {
|
fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut TextState, cx: &mut WindowContext) {
|
||||||
self.style.color = color;
|
state.paint(bounds, self, cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoElement for &'static str {
|
||||||
|
type Element = Self;
|
||||||
|
|
||||||
|
fn element_id(&self) -> Option<ElementId> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_element(self) -> Self::Element {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element for SharedString {
|
||||||
|
type State = TextState;
|
||||||
|
|
||||||
|
fn request_layout(
|
||||||
|
&mut self,
|
||||||
|
_: Option<Self::State>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> (LayoutId, Self::State) {
|
||||||
|
let mut state = TextState::default();
|
||||||
|
let layout_id = state.layout(self.clone(), None, cx);
|
||||||
|
(layout_id, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut TextState, cx: &mut WindowContext) {
|
||||||
|
let text_str: &str = self.as_ref();
|
||||||
|
state.paint(bounds, text_str, cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoElement for SharedString {
|
||||||
|
type Element = Self;
|
||||||
|
|
||||||
|
fn element_id(&self) -> Option<ElementId> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_element(self) -> Self::Element {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders text with runs of different styles.
|
||||||
|
///
|
||||||
|
/// Callers are responsible for setting the correct style for each run.
|
||||||
|
/// For text with a uniform style, you can usually avoid calling this constructor
|
||||||
|
/// and just pass text directly.
|
||||||
|
pub struct StyledText {
|
||||||
|
text: SharedString,
|
||||||
|
runs: Option<Vec<TextRun>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StyledText {
|
||||||
|
pub fn new(text: impl Into<SharedString>) -> Self {
|
||||||
|
StyledText {
|
||||||
|
text: text.into(),
|
||||||
|
runs: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn with_highlights(
|
pub fn with_highlights(
|
||||||
mut self,
|
mut self,
|
||||||
runs: impl Into<Box<[(Range<usize>, HighlightStyle)]>>,
|
default_style: &TextStyle,
|
||||||
|
highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
self.highlights = Some(runs.into());
|
let mut runs = Vec::new();
|
||||||
self
|
let mut ix = 0;
|
||||||
}
|
for (range, highlight) in highlights {
|
||||||
|
if ix < range.start {
|
||||||
pub fn with_custom_runs(
|
runs.push(default_style.clone().to_run(range.start - ix));
|
||||||
mut self,
|
}
|
||||||
runs: impl Into<Box<[Range<usize>]>>,
|
runs.push(
|
||||||
callback: impl 'static + FnMut(usize, RectF, &mut WindowContext),
|
default_style
|
||||||
) -> Self {
|
.clone()
|
||||||
self.custom_runs = Some((runs.into(), Box::new(callback)));
|
.highlight(highlight)
|
||||||
self
|
.to_run(range.len()),
|
||||||
}
|
);
|
||||||
|
ix = range.end;
|
||||||
pub fn with_soft_wrap(mut self, soft_wrap: bool) -> Self {
|
}
|
||||||
self.soft_wrap = soft_wrap;
|
if ix < self.text.len() {
|
||||||
|
runs.push(default_style.to_run(self.text.len() - ix));
|
||||||
|
}
|
||||||
|
self.runs = Some(runs);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: 'static> Element<V> for Text {
|
impl Element for StyledText {
|
||||||
type LayoutState = LayoutState;
|
type State = TextState;
|
||||||
type PaintState = ();
|
|
||||||
|
|
||||||
fn layout(
|
fn request_layout(
|
||||||
&mut self,
|
&mut self,
|
||||||
constraint: SizeConstraint,
|
_: Option<Self::State>,
|
||||||
_: &mut V,
|
cx: &mut WindowContext,
|
||||||
cx: &mut ViewContext<V>,
|
) -> (LayoutId, Self::State) {
|
||||||
) -> (Vector2F, Self::LayoutState) {
|
let mut state = TextState::default();
|
||||||
// Convert the string and highlight ranges into an iterator of highlighted chunks.
|
let layout_id = state.layout(self.text.clone(), self.runs.take(), cx);
|
||||||
|
(layout_id, state)
|
||||||
let mut offset = 0;
|
|
||||||
let mut highlight_ranges = self
|
|
||||||
.highlights
|
|
||||||
.as_ref()
|
|
||||||
.map_or(Default::default(), AsRef::as_ref)
|
|
||||||
.iter()
|
|
||||||
.peekable();
|
|
||||||
let chunks = std::iter::from_fn(|| {
|
|
||||||
let result;
|
|
||||||
if let Some((range, highlight_style)) = highlight_ranges.peek() {
|
|
||||||
if offset < range.start {
|
|
||||||
result = Some((&self.text[offset..range.start], None));
|
|
||||||
offset = range.start;
|
|
||||||
} else if range.end <= self.text.len() {
|
|
||||||
result = Some((&self.text[range.clone()], Some(*highlight_style)));
|
|
||||||
highlight_ranges.next();
|
|
||||||
offset = range.end;
|
|
||||||
} else {
|
|
||||||
warn!(
|
|
||||||
"Highlight out of text range. Text len: {}, Highlight range: {}..{}",
|
|
||||||
self.text.len(),
|
|
||||||
range.start,
|
|
||||||
range.end
|
|
||||||
);
|
|
||||||
result = None;
|
|
||||||
}
|
|
||||||
} else if offset < self.text.len() {
|
|
||||||
result = Some((&self.text[offset..], None));
|
|
||||||
offset = self.text.len();
|
|
||||||
} else {
|
|
||||||
result = None;
|
|
||||||
}
|
|
||||||
result
|
|
||||||
});
|
|
||||||
|
|
||||||
// Perform shaping on these highlighted chunks
|
|
||||||
let shaped_lines = layout_highlighted_chunks(
|
|
||||||
chunks,
|
|
||||||
&self.style,
|
|
||||||
cx.text_layout_cache(),
|
|
||||||
&cx.font_cache,
|
|
||||||
usize::MAX,
|
|
||||||
self.text.matches('\n').count() + 1,
|
|
||||||
);
|
|
||||||
|
|
||||||
// If line wrapping is enabled, wrap each of the shaped lines.
|
|
||||||
let font_id = self.style.font_id;
|
|
||||||
let mut line_count = 0;
|
|
||||||
let mut max_line_width = 0_f32;
|
|
||||||
let mut wrap_boundaries = Vec::new();
|
|
||||||
let mut wrapper = cx.font_cache.line_wrapper(font_id, self.style.font_size);
|
|
||||||
for (line, shaped_line) in self.text.split('\n').zip(&shaped_lines) {
|
|
||||||
if self.soft_wrap {
|
|
||||||
let boundaries = wrapper
|
|
||||||
.wrap_shaped_line(line, shaped_line, constraint.max.x())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
line_count += boundaries.len() + 1;
|
|
||||||
wrap_boundaries.push(boundaries);
|
|
||||||
} else {
|
|
||||||
line_count += 1;
|
|
||||||
}
|
|
||||||
max_line_width = max_line_width.max(shaped_line.width());
|
|
||||||
}
|
|
||||||
|
|
||||||
let line_height = cx.font_cache.line_height(self.style.font_size);
|
|
||||||
let size = vec2f(
|
|
||||||
max_line_width
|
|
||||||
.ceil()
|
|
||||||
.max(constraint.min.x())
|
|
||||||
.min(constraint.max.x()),
|
|
||||||
(line_height * line_count as f32).ceil(),
|
|
||||||
);
|
|
||||||
(
|
|
||||||
size,
|
|
||||||
LayoutState {
|
|
||||||
shaped_lines,
|
|
||||||
wrap_boundaries,
|
|
||||||
line_height,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn paint(
|
fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
|
||||||
&mut self,
|
state.paint(bounds, &self.text, cx)
|
||||||
bounds: RectF,
|
|
||||||
visible_bounds: RectF,
|
|
||||||
layout: &mut Self::LayoutState,
|
|
||||||
_: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> Self::PaintState {
|
|
||||||
let mut origin = bounds.origin();
|
|
||||||
let empty = Vec::new();
|
|
||||||
let mut callback = |_, _, _: &mut WindowContext| {};
|
|
||||||
|
|
||||||
let mouse_runs;
|
|
||||||
let custom_run_callback;
|
|
||||||
if let Some((runs, build_region)) = &mut self.custom_runs {
|
|
||||||
mouse_runs = runs.iter();
|
|
||||||
custom_run_callback = build_region.as_mut();
|
|
||||||
} else {
|
|
||||||
mouse_runs = [].iter();
|
|
||||||
custom_run_callback = &mut callback;
|
|
||||||
}
|
|
||||||
let mut custom_runs = mouse_runs.enumerate().peekable();
|
|
||||||
|
|
||||||
let mut offset = 0;
|
|
||||||
for (ix, line) in layout.shaped_lines.iter().enumerate() {
|
|
||||||
let wrap_boundaries = layout.wrap_boundaries.get(ix).unwrap_or(&empty);
|
|
||||||
let boundaries = RectF::new(
|
|
||||||
origin,
|
|
||||||
vec2f(
|
|
||||||
bounds.width(),
|
|
||||||
(wrap_boundaries.len() + 1) as f32 * layout.line_height,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if boundaries.intersects(visible_bounds) {
|
|
||||||
if self.soft_wrap {
|
|
||||||
line.paint_wrapped(
|
|
||||||
origin,
|
|
||||||
visible_bounds,
|
|
||||||
layout.line_height,
|
|
||||||
wrap_boundaries,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
line.paint(origin, visible_bounds, layout.line_height, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Paint any custom runs that intersect this line.
|
|
||||||
let end_offset = offset + line.len();
|
|
||||||
if let Some((custom_run_ix, custom_run_range)) = custom_runs.peek().cloned() {
|
|
||||||
if custom_run_range.start < end_offset {
|
|
||||||
let mut current_custom_run = None;
|
|
||||||
if custom_run_range.start <= offset {
|
|
||||||
current_custom_run = Some((custom_run_ix, custom_run_range.end, origin));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut glyph_origin = origin;
|
|
||||||
let mut prev_position = 0.;
|
|
||||||
let mut wrap_boundaries = wrap_boundaries.iter().copied().peekable();
|
|
||||||
for (run_ix, glyph_ix, glyph) in
|
|
||||||
line.runs().iter().enumerate().flat_map(|(run_ix, run)| {
|
|
||||||
run.glyphs()
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(move |(ix, glyph)| (run_ix, ix, glyph))
|
|
||||||
})
|
|
||||||
{
|
|
||||||
glyph_origin.set_x(glyph_origin.x() + glyph.position.x() - prev_position);
|
|
||||||
prev_position = glyph.position.x();
|
|
||||||
|
|
||||||
// If we've reached a soft wrap position, move down one line. If there
|
|
||||||
// is a custom run in-progress, paint it.
|
|
||||||
if wrap_boundaries
|
|
||||||
.peek()
|
|
||||||
.map_or(false, |b| b.run_ix == run_ix && b.glyph_ix == glyph_ix)
|
|
||||||
{
|
|
||||||
if let Some((run_ix, _, run_origin)) = &mut current_custom_run {
|
|
||||||
let bounds = RectF::from_points(
|
|
||||||
*run_origin,
|
|
||||||
glyph_origin + vec2f(0., layout.line_height),
|
|
||||||
);
|
|
||||||
custom_run_callback(*run_ix, bounds, cx);
|
|
||||||
*run_origin =
|
|
||||||
vec2f(origin.x(), glyph_origin.y() + layout.line_height);
|
|
||||||
}
|
|
||||||
wrap_boundaries.next();
|
|
||||||
glyph_origin = vec2f(origin.x(), glyph_origin.y() + layout.line_height);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we've reached the end of the current custom run, paint it.
|
|
||||||
if let Some((run_ix, run_end_offset, run_origin)) = current_custom_run {
|
|
||||||
if offset + glyph.index == run_end_offset {
|
|
||||||
current_custom_run.take();
|
|
||||||
let bounds = RectF::from_points(
|
|
||||||
run_origin,
|
|
||||||
glyph_origin + vec2f(0., layout.line_height),
|
|
||||||
);
|
|
||||||
custom_run_callback(run_ix, bounds, cx);
|
|
||||||
custom_runs.next();
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some((_, run_range)) = custom_runs.peek() {
|
|
||||||
if run_range.start >= end_offset {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if run_range.start == offset + glyph.index {
|
|
||||||
current_custom_run =
|
|
||||||
Some((run_ix, run_range.end, glyph_origin));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we've reached the start of a new custom run, start tracking it.
|
|
||||||
if let Some((run_ix, run_range)) = custom_runs.peek() {
|
|
||||||
if offset + glyph.index == run_range.start {
|
|
||||||
current_custom_run = Some((*run_ix, run_range.end, glyph_origin));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If a custom run extends beyond the end of the line, paint it.
|
|
||||||
if let Some((run_ix, run_end_offset, run_origin)) = current_custom_run {
|
|
||||||
let line_end = glyph_origin + vec2f(line.width() - prev_position, 0.);
|
|
||||||
let bounds = RectF::from_points(
|
|
||||||
run_origin,
|
|
||||||
line_end + vec2f(0., layout.line_height),
|
|
||||||
);
|
|
||||||
custom_run_callback(run_ix, bounds, cx);
|
|
||||||
if end_offset == run_end_offset {
|
|
||||||
custom_runs.next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
offset = end_offset + 1;
|
|
||||||
origin.set_y(boundaries.max_y());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn rect_for_text_range(
|
impl IntoElement for StyledText {
|
||||||
&self,
|
type Element = Self;
|
||||||
_: Range<usize>,
|
|
||||||
_: RectF,
|
fn element_id(&self) -> Option<crate::ElementId> {
|
||||||
_: RectF,
|
|
||||||
_: &Self::LayoutState,
|
|
||||||
_: &Self::PaintState,
|
|
||||||
_: &V,
|
|
||||||
_: &ViewContext<V>,
|
|
||||||
) -> Option<RectF> {
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn debug(
|
fn into_element(self) -> Self::Element {
|
||||||
&self,
|
self
|
||||||
bounds: RectF,
|
|
||||||
_: &Self::LayoutState,
|
|
||||||
_: &Self::PaintState,
|
|
||||||
_: &V,
|
|
||||||
_: &ViewContext<V>,
|
|
||||||
) -> Value {
|
|
||||||
json!({
|
|
||||||
"type": "Text",
|
|
||||||
"bounds": bounds.to_json(),
|
|
||||||
"text": &self.text,
|
|
||||||
"style": self.style.to_json(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Perform text layout on a series of highlighted chunks of text.
|
#[derive(Default, Clone)]
|
||||||
pub fn layout_highlighted_chunks<'a>(
|
pub struct TextState(Arc<Mutex<Option<TextStateInner>>>);
|
||||||
chunks: impl Iterator<Item = (&'a str, Option<HighlightStyle>)>,
|
|
||||||
text_style: &TextStyle,
|
|
||||||
text_layout_cache: &TextLayoutCache,
|
|
||||||
font_cache: &Arc<FontCache>,
|
|
||||||
max_line_len: usize,
|
|
||||||
max_line_count: usize,
|
|
||||||
) -> Vec<Line> {
|
|
||||||
let mut layouts = Vec::with_capacity(max_line_count);
|
|
||||||
let mut line = String::new();
|
|
||||||
let mut styles = Vec::new();
|
|
||||||
let mut row = 0;
|
|
||||||
let mut line_exceeded_max_len = false;
|
|
||||||
for (chunk, highlight_style) in chunks.chain([("\n", Default::default())]) {
|
|
||||||
for (ix, mut line_chunk) in chunk.split('\n').enumerate() {
|
|
||||||
if ix > 0 {
|
|
||||||
layouts.push(text_layout_cache.layout_str(&line, text_style.font_size, &styles));
|
|
||||||
line.clear();
|
|
||||||
styles.clear();
|
|
||||||
row += 1;
|
|
||||||
line_exceeded_max_len = false;
|
|
||||||
if row == max_line_count {
|
|
||||||
return layouts;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !line_chunk.is_empty() && !line_exceeded_max_len {
|
struct TextStateInner {
|
||||||
let text_style = if let Some(style) = highlight_style {
|
lines: SmallVec<[WrappedLine; 1]>,
|
||||||
text_style
|
line_height: Pixels,
|
||||||
.clone()
|
wrap_width: Option<Pixels>,
|
||||||
.highlight(style, font_cache)
|
size: Option<Size<Pixels>>,
|
||||||
.map(Cow::Owned)
|
}
|
||||||
.unwrap_or_else(|_| Cow::Borrowed(text_style))
|
|
||||||
|
impl TextState {
|
||||||
|
fn lock(&self) -> MutexGuard<Option<TextStateInner>> {
|
||||||
|
self.0.lock()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
text: SharedString,
|
||||||
|
runs: Option<Vec<TextRun>>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> LayoutId {
|
||||||
|
let text_style = cx.text_style();
|
||||||
|
let font_size = text_style.font_size.to_pixels(cx.rem_size());
|
||||||
|
let line_height = text_style
|
||||||
|
.line_height
|
||||||
|
.to_pixels(font_size.into(), cx.rem_size());
|
||||||
|
|
||||||
|
let runs = if let Some(runs) = runs {
|
||||||
|
runs
|
||||||
|
} else {
|
||||||
|
vec![text_style.to_run(text.len())]
|
||||||
|
};
|
||||||
|
|
||||||
|
let layout_id = cx.request_measured_layout(Default::default(), {
|
||||||
|
let element_state = self.clone();
|
||||||
|
|
||||||
|
move |known_dimensions, available_space, cx| {
|
||||||
|
let wrap_width = if text_style.white_space == WhiteSpace::Normal {
|
||||||
|
known_dimensions.width.or(match available_space.width {
|
||||||
|
crate::AvailableSpace::Definite(x) => Some(x),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
Cow::Borrowed(text_style)
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
if line.len() + line_chunk.len() > max_line_len {
|
if let Some(text_state) = element_state.0.lock().as_ref() {
|
||||||
let mut chunk_len = max_line_len - line.len();
|
if text_state.size.is_some()
|
||||||
while !line_chunk.is_char_boundary(chunk_len) {
|
&& (wrap_width.is_none() || wrap_width == text_state.wrap_width)
|
||||||
chunk_len -= 1;
|
{
|
||||||
|
return text_state.size.unwrap();
|
||||||
}
|
}
|
||||||
line_chunk = &line_chunk[..chunk_len];
|
|
||||||
line_exceeded_max_len = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
line.push_str(line_chunk);
|
let Some(lines) = cx
|
||||||
styles.push((
|
.text_system()
|
||||||
line_chunk.len(),
|
.shape_text(
|
||||||
RunStyle {
|
&text, font_size, &runs, wrap_width, // Wrap if we know the width.
|
||||||
font_id: text_style.font_id,
|
)
|
||||||
color: text_style.color,
|
.log_err()
|
||||||
underline: text_style.underline,
|
else {
|
||||||
},
|
element_state.lock().replace(TextStateInner {
|
||||||
));
|
lines: Default::default(),
|
||||||
|
line_height,
|
||||||
|
wrap_width,
|
||||||
|
size: Some(Size::default()),
|
||||||
|
});
|
||||||
|
return Size::default();
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut size: Size<Pixels> = Size::default();
|
||||||
|
for line in &lines {
|
||||||
|
let line_size = line.size(line_height);
|
||||||
|
size.height += line_size.height;
|
||||||
|
size.width = size.width.max(line_size.width).ceil();
|
||||||
|
}
|
||||||
|
|
||||||
|
element_state.lock().replace(TextStateInner {
|
||||||
|
lines,
|
||||||
|
line_height,
|
||||||
|
wrap_width,
|
||||||
|
size: Some(size),
|
||||||
|
});
|
||||||
|
|
||||||
|
size
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
layout_id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(&mut self, bounds: Bounds<Pixels>, text: &str, cx: &mut WindowContext) {
|
||||||
|
let element_state = self.lock();
|
||||||
|
let element_state = element_state
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| anyhow!("measurement has not been performed on {}", text))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let line_height = element_state.line_height;
|
||||||
|
let mut line_origin = bounds.origin;
|
||||||
|
for line in &element_state.lines {
|
||||||
|
line.paint(line_origin, line_height, cx).log_err();
|
||||||
|
line_origin.y += line.size(line_height).height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn index_for_position(&self, bounds: Bounds<Pixels>, position: Point<Pixels>) -> Option<usize> {
|
||||||
|
if !bounds.contains(&position) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let element_state = self.lock();
|
||||||
|
let element_state = element_state
|
||||||
|
.as_ref()
|
||||||
|
.expect("measurement has not been performed");
|
||||||
|
|
||||||
|
let line_height = element_state.line_height;
|
||||||
|
let mut line_origin = bounds.origin;
|
||||||
|
let mut line_start_ix = 0;
|
||||||
|
for line in &element_state.lines {
|
||||||
|
let line_bottom = line_origin.y + line.size(line_height).height;
|
||||||
|
if position.y > line_bottom {
|
||||||
|
line_origin.y = line_bottom;
|
||||||
|
line_start_ix += line.len() + 1;
|
||||||
|
} else {
|
||||||
|
let position_within_line = position - line_origin;
|
||||||
|
let index_within_line =
|
||||||
|
line.index_for_position(position_within_line, line_height)?;
|
||||||
|
return Some(line_start_ix + index_within_line);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
layouts
|
None
|
||||||
}
|
}
|
||||||
|
}
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
pub struct InteractiveText {
|
||||||
use super::*;
|
element_id: ElementId,
|
||||||
use crate::{elements::Empty, fonts, AnyElement, AppContext, Entity, View, ViewContext};
|
text: StyledText,
|
||||||
|
click_listener:
|
||||||
#[crate::test(self)]
|
Option<Box<dyn Fn(&[Range<usize>], InteractiveTextClickEvent, &mut WindowContext<'_>)>>,
|
||||||
fn test_soft_wrapping_with_carriage_returns(cx: &mut AppContext) {
|
clickable_ranges: Vec<Range<usize>>,
|
||||||
cx.add_window(Default::default(), |cx| {
|
}
|
||||||
let mut view = TestView;
|
|
||||||
fonts::with_font_cache(cx.font_cache().clone(), || {
|
struct InteractiveTextClickEvent {
|
||||||
let mut text = Text::new("Hello\r\n", Default::default()).with_soft_wrap(true);
|
mouse_down_index: usize,
|
||||||
let (_, state) = text.layout(
|
mouse_up_index: usize,
|
||||||
SizeConstraint::new(Default::default(), vec2f(f32::INFINITY, f32::INFINITY)),
|
}
|
||||||
&mut view,
|
|
||||||
cx,
|
pub struct InteractiveTextState {
|
||||||
);
|
text_state: TextState,
|
||||||
assert_eq!(state.shaped_lines.len(), 2);
|
mouse_down_index: Rc<Cell<Option<usize>>>,
|
||||||
assert_eq!(state.wrap_boundaries.len(), 2);
|
}
|
||||||
});
|
|
||||||
view
|
impl InteractiveText {
|
||||||
});
|
pub fn new(id: impl Into<ElementId>, text: StyledText) -> Self {
|
||||||
}
|
Self {
|
||||||
|
element_id: id.into(),
|
||||||
struct TestView;
|
text,
|
||||||
|
click_listener: None,
|
||||||
impl Entity for TestView {
|
clickable_ranges: Vec::new(),
|
||||||
type Event = ();
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl View for TestView {
|
pub fn on_click(
|
||||||
fn ui_name() -> &'static str {
|
mut self,
|
||||||
"TestView"
|
ranges: Vec<Range<usize>>,
|
||||||
}
|
listener: impl Fn(usize, &mut WindowContext<'_>) + 'static,
|
||||||
|
) -> Self {
|
||||||
fn render(&mut self, _: &mut ViewContext<Self>) -> AnyElement<Self> {
|
self.click_listener = Some(Box::new(move |ranges, event, cx| {
|
||||||
Empty::new().into_any()
|
for (range_ix, range) in ranges.iter().enumerate() {
|
||||||
}
|
if range.contains(&event.mouse_down_index) && range.contains(&event.mouse_up_index)
|
||||||
|
{
|
||||||
|
listener(range_ix, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
self.clickable_ranges = ranges;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element for InteractiveText {
|
||||||
|
type State = InteractiveTextState;
|
||||||
|
|
||||||
|
fn request_layout(
|
||||||
|
&mut self,
|
||||||
|
state: Option<Self::State>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> (LayoutId, Self::State) {
|
||||||
|
if let Some(InteractiveTextState {
|
||||||
|
mouse_down_index, ..
|
||||||
|
}) = state
|
||||||
|
{
|
||||||
|
let (layout_id, text_state) = self.text.request_layout(None, cx);
|
||||||
|
let element_state = InteractiveTextState {
|
||||||
|
text_state,
|
||||||
|
mouse_down_index,
|
||||||
|
};
|
||||||
|
(layout_id, element_state)
|
||||||
|
} else {
|
||||||
|
let (layout_id, text_state) = self.text.request_layout(None, cx);
|
||||||
|
let element_state = InteractiveTextState {
|
||||||
|
text_state,
|
||||||
|
mouse_down_index: Rc::default(),
|
||||||
|
};
|
||||||
|
(layout_id, element_state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
|
||||||
|
if let Some(click_listener) = self.click_listener.take() {
|
||||||
|
let mouse_position = cx.mouse_position();
|
||||||
|
if let Some(ix) = state.text_state.index_for_position(bounds, mouse_position) {
|
||||||
|
if self
|
||||||
|
.clickable_ranges
|
||||||
|
.iter()
|
||||||
|
.any(|range| range.contains(&ix))
|
||||||
|
&& cx.was_top_layer(&mouse_position, cx.stacking_order())
|
||||||
|
{
|
||||||
|
cx.set_cursor_style(crate::CursorStyle::PointingHand)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let text_state = state.text_state.clone();
|
||||||
|
let mouse_down = state.mouse_down_index.clone();
|
||||||
|
if let Some(mouse_down_index) = mouse_down.get() {
|
||||||
|
let clickable_ranges = mem::take(&mut self.clickable_ranges);
|
||||||
|
cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| {
|
||||||
|
if phase == DispatchPhase::Bubble {
|
||||||
|
if let Some(mouse_up_index) =
|
||||||
|
text_state.index_for_position(bounds, event.position)
|
||||||
|
{
|
||||||
|
click_listener(
|
||||||
|
&clickable_ranges,
|
||||||
|
InteractiveTextClickEvent {
|
||||||
|
mouse_down_index,
|
||||||
|
mouse_up_index,
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
mouse_down.take();
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
|
||||||
|
if phase == DispatchPhase::Bubble {
|
||||||
|
if let Some(mouse_down_index) =
|
||||||
|
text_state.index_for_position(bounds, event.position)
|
||||||
|
{
|
||||||
|
mouse_down.set(Some(mouse_down_index));
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.text.paint(bounds, &mut state.text_state, cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoElement for InteractiveText {
|
||||||
|
type Element = Self;
|
||||||
|
|
||||||
|
fn element_id(&self) -> Option<ElementId> {
|
||||||
|
Some(self.element_id.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_element(self) -> Self::Element {
|
||||||
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,244 +0,0 @@
|
|||||||
use super::{
|
|
||||||
AnyElement, ContainerStyle, Element, Flex, KeystrokeLabel, MouseEventHandler, Overlay,
|
|
||||||
OverlayFitMode, ParentElement, Text,
|
|
||||||
};
|
|
||||||
use crate::{
|
|
||||||
fonts::TextStyle,
|
|
||||||
geometry::{rect::RectF, vector::Vector2F},
|
|
||||||
json::json,
|
|
||||||
Action, Axis, ElementStateHandle, SizeConstraint, Task, TypeTag, ViewContext,
|
|
||||||
};
|
|
||||||
use schemars::JsonSchema;
|
|
||||||
use serde::Deserialize;
|
|
||||||
use std::{
|
|
||||||
borrow::Cow,
|
|
||||||
cell::{Cell, RefCell},
|
|
||||||
ops::Range,
|
|
||||||
rc::Rc,
|
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
use util::ResultExt;
|
|
||||||
|
|
||||||
const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(500);
|
|
||||||
|
|
||||||
pub struct Tooltip<V> {
|
|
||||||
child: AnyElement<V>,
|
|
||||||
tooltip: Option<AnyElement<V>>,
|
|
||||||
_state: ElementStateHandle<Rc<TooltipState>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
struct TooltipState {
|
|
||||||
visible: Cell<bool>,
|
|
||||||
position: Cell<Vector2F>,
|
|
||||||
debounce: RefCell<Option<Task<()>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
|
||||||
pub struct TooltipStyle {
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub container: ContainerStyle,
|
|
||||||
pub text: TextStyle,
|
|
||||||
keystroke: KeystrokeStyle,
|
|
||||||
pub max_text_width: Option<f32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
|
||||||
pub struct KeystrokeStyle {
|
|
||||||
#[serde(flatten)]
|
|
||||||
container: ContainerStyle,
|
|
||||||
#[serde(flatten)]
|
|
||||||
text: TextStyle,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V: 'static> Tooltip<V> {
|
|
||||||
pub fn new<Tag: 'static>(
|
|
||||||
id: usize,
|
|
||||||
text: impl Into<Cow<'static, str>>,
|
|
||||||
action: Option<Box<dyn Action>>,
|
|
||||||
style: TooltipStyle,
|
|
||||||
child: AnyElement<V>,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> Self {
|
|
||||||
Self::new_dynamic(TypeTag::new::<Tag>(), id, text, action, style, child, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_dynamic(
|
|
||||||
mut tag: TypeTag,
|
|
||||||
id: usize,
|
|
||||||
text: impl Into<Cow<'static, str>>,
|
|
||||||
action: Option<Box<dyn Action>>,
|
|
||||||
style: TooltipStyle,
|
|
||||||
child: AnyElement<V>,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> Self {
|
|
||||||
tag = tag.compose(TypeTag::new::<Self>());
|
|
||||||
|
|
||||||
let focused_view_id = cx.focused_view_id();
|
|
||||||
|
|
||||||
let state_handle = cx.default_element_state_dynamic::<Rc<TooltipState>>(tag, id);
|
|
||||||
let state = state_handle.read(cx).clone();
|
|
||||||
let text = text.into();
|
|
||||||
|
|
||||||
let tooltip = if state.visible.get() {
|
|
||||||
let mut collapsed_tooltip = Self::render_tooltip(
|
|
||||||
focused_view_id,
|
|
||||||
text.clone(),
|
|
||||||
style.clone(),
|
|
||||||
action.as_ref().map(|a| a.boxed_clone()),
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
Some(
|
|
||||||
Overlay::new(
|
|
||||||
Self::render_tooltip(focused_view_id, text, style, action, false)
|
|
||||||
.constrained()
|
|
||||||
.dynamically(move |constraint, view, cx| {
|
|
||||||
SizeConstraint::strict_along(
|
|
||||||
Axis::Vertical,
|
|
||||||
collapsed_tooltip.layout(constraint, view, cx).0.y(),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.with_fit_mode(OverlayFitMode::SwitchAnchor)
|
|
||||||
.with_anchor_position(state.position.get())
|
|
||||||
.into_any(),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
let child = MouseEventHandler::new_dynamic(tag, id, cx, |_, _| child)
|
|
||||||
.on_hover(move |e, _, cx| {
|
|
||||||
let position = e.position;
|
|
||||||
if e.started {
|
|
||||||
if !state.visible.get() {
|
|
||||||
state.position.set(position);
|
|
||||||
|
|
||||||
let mut debounce = state.debounce.borrow_mut();
|
|
||||||
if debounce.is_none() {
|
|
||||||
*debounce = Some(cx.spawn({
|
|
||||||
let state = state.clone();
|
|
||||||
|view, mut cx| async move {
|
|
||||||
cx.background().timer(DEBOUNCE_TIMEOUT).await;
|
|
||||||
state.visible.set(true);
|
|
||||||
view.update(&mut cx, |_, cx| cx.notify()).log_err();
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
state.visible.set(false);
|
|
||||||
state.debounce.take();
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.into_any();
|
|
||||||
Self {
|
|
||||||
child,
|
|
||||||
tooltip,
|
|
||||||
_state: state_handle,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn render_tooltip(
|
|
||||||
focused_view_id: Option<usize>,
|
|
||||||
text: impl Into<Cow<'static, str>>,
|
|
||||||
style: TooltipStyle,
|
|
||||||
action: Option<Box<dyn Action>>,
|
|
||||||
measure: bool,
|
|
||||||
) -> impl Element<V> {
|
|
||||||
Flex::row()
|
|
||||||
.with_child({
|
|
||||||
let text = if let Some(max_text_width) = style.max_text_width {
|
|
||||||
Text::new(text, style.text)
|
|
||||||
.constrained()
|
|
||||||
.with_max_width(max_text_width)
|
|
||||||
} else {
|
|
||||||
Text::new(text, style.text).constrained()
|
|
||||||
};
|
|
||||||
|
|
||||||
if measure {
|
|
||||||
text.flex(1., false).into_any()
|
|
||||||
} else {
|
|
||||||
text.flex(1., false).aligned().into_any()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.with_children(action.and_then(|action| {
|
|
||||||
let keystroke_label = KeystrokeLabel::new(
|
|
||||||
focused_view_id?,
|
|
||||||
action,
|
|
||||||
style.keystroke.container,
|
|
||||||
style.keystroke.text,
|
|
||||||
);
|
|
||||||
if measure {
|
|
||||||
Some(keystroke_label.into_any())
|
|
||||||
} else {
|
|
||||||
Some(keystroke_label.aligned().into_any())
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
.contained()
|
|
||||||
.with_style(style.container)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V: 'static> Element<V> for Tooltip<V> {
|
|
||||||
type LayoutState = ();
|
|
||||||
type PaintState = ();
|
|
||||||
|
|
||||||
fn layout(
|
|
||||||
&mut self,
|
|
||||||
constraint: SizeConstraint,
|
|
||||||
view: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> (Vector2F, Self::LayoutState) {
|
|
||||||
let size = self.child.layout(constraint, view, cx);
|
|
||||||
if let Some(tooltip) = self.tooltip.as_mut() {
|
|
||||||
tooltip.layout(
|
|
||||||
SizeConstraint::new(Vector2F::zero(), cx.window_size()),
|
|
||||||
view,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
(size, ())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn paint(
|
|
||||||
&mut self,
|
|
||||||
bounds: RectF,
|
|
||||||
visible_bounds: RectF,
|
|
||||||
_: &mut Self::LayoutState,
|
|
||||||
view: &mut V,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) {
|
|
||||||
self.child.paint(bounds.origin(), visible_bounds, view, cx);
|
|
||||||
if let Some(tooltip) = self.tooltip.as_mut() {
|
|
||||||
tooltip.paint(bounds.origin(), visible_bounds, view, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
|
||||||
&self,
|
|
||||||
range: Range<usize>,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
_: &Self::LayoutState,
|
|
||||||
_: &Self::PaintState,
|
|
||||||
view: &V,
|
|
||||||
cx: &ViewContext<V>,
|
|
||||||
) -> Option<RectF> {
|
|
||||||
self.child.rect_for_text_range(range, view, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn debug(
|
|
||||||
&self,
|
|
||||||
_: RectF,
|
|
||||||
_: &Self::LayoutState,
|
|
||||||
_: &Self::PaintState,
|
|
||||||
view: &V,
|
|
||||||
cx: &ViewContext<V>,
|
|
||||||
) -> serde_json::Value {
|
|
||||||
json!({
|
|
||||||
"child": self.child.debug(view, cx),
|
|
||||||
"tooltip": self.tooltip.as_ref().map(|t| t.debug(view, cx)),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,354 +1,316 @@
|
|||||||
use super::{Element, SizeConstraint};
|
|
||||||
use crate::{
|
use crate::{
|
||||||
geometry::{
|
point, px, size, AnyElement, AvailableSpace, BorrowWindow, Bounds, ContentMask, Element,
|
||||||
rect::RectF,
|
ElementId, InteractiveElement, InteractiveElementState, Interactivity, IntoElement, LayoutId,
|
||||||
vector::{vec2f, Vector2F},
|
Pixels, Point, Render, Size, StyleRefinement, Styled, View, ViewContext, WindowContext,
|
||||||
},
|
|
||||||
json::{self, json},
|
|
||||||
platform::ScrollWheelEvent,
|
|
||||||
AnyElement, MouseRegion, ViewContext,
|
|
||||||
};
|
};
|
||||||
use json::ToJson;
|
use smallvec::SmallVec;
|
||||||
use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
|
use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
|
||||||
|
use taffy::style::Overflow;
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
/// uniform_list provides lazy rendering for a set of items that are of uniform height.
|
||||||
pub struct UniformListState(Rc<RefCell<StateInner>>);
|
/// When rendered into a container with overflow-y: hidden and a fixed (or max) height,
|
||||||
|
/// uniform_list will only render the visible subset of items.
|
||||||
|
#[track_caller]
|
||||||
|
pub fn uniform_list<I, R, V>(
|
||||||
|
view: View<V>,
|
||||||
|
id: I,
|
||||||
|
item_count: usize,
|
||||||
|
f: impl 'static + Fn(&mut V, Range<usize>, &mut ViewContext<V>) -> Vec<R>,
|
||||||
|
) -> UniformList
|
||||||
|
where
|
||||||
|
I: Into<ElementId>,
|
||||||
|
R: IntoElement,
|
||||||
|
V: Render,
|
||||||
|
{
|
||||||
|
let id = id.into();
|
||||||
|
let mut base_style = StyleRefinement::default();
|
||||||
|
base_style.overflow.y = Some(Overflow::Scroll);
|
||||||
|
|
||||||
#[derive(Debug)]
|
let render_range = move |range, cx: &mut WindowContext| {
|
||||||
pub enum ScrollTarget {
|
view.update(cx, |this, cx| {
|
||||||
Show(usize),
|
f(this, range, cx)
|
||||||
Center(usize),
|
.into_iter()
|
||||||
|
.map(|component| component.into_any_element())
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
UniformList {
|
||||||
|
id: id.clone(),
|
||||||
|
item_count,
|
||||||
|
item_to_measure_index: 0,
|
||||||
|
render_items: Box::new(render_range),
|
||||||
|
interactivity: Interactivity {
|
||||||
|
element_id: Some(id),
|
||||||
|
base_style: Box::new(base_style),
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
location: Some(*core::panic::Location::caller()),
|
||||||
|
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
scroll_handle: None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UniformListState {
|
pub struct UniformList {
|
||||||
pub fn scroll_to(&self, scroll_to: ScrollTarget) {
|
id: ElementId,
|
||||||
self.0.borrow_mut().scroll_to = Some(scroll_to);
|
item_count: usize,
|
||||||
|
item_to_measure_index: usize,
|
||||||
|
render_items:
|
||||||
|
Box<dyn for<'a> Fn(Range<usize>, &'a mut WindowContext) -> SmallVec<[AnyElement; 64]>>,
|
||||||
|
interactivity: Interactivity,
|
||||||
|
scroll_handle: Option<UniformListScrollHandle>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
pub struct UniformListScrollHandle(Rc<RefCell<Option<ScrollHandleState>>>);
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct ScrollHandleState {
|
||||||
|
item_height: Pixels,
|
||||||
|
list_height: Pixels,
|
||||||
|
scroll_offset: Rc<RefCell<Point<Pixels>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UniformListScrollHandle {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self(Rc::new(RefCell::new(None)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn scroll_top(&self) -> f32 {
|
pub fn scroll_to_item(&self, ix: usize) {
|
||||||
self.0.borrow().scroll_top
|
if let Some(state) = &*self.0.borrow() {
|
||||||
|
let mut scroll_offset = state.scroll_offset.borrow_mut();
|
||||||
|
let item_top = state.item_height * ix;
|
||||||
|
let item_bottom = item_top + state.item_height;
|
||||||
|
let scroll_top = -scroll_offset.y;
|
||||||
|
if item_top < scroll_top {
|
||||||
|
scroll_offset.y = -item_top;
|
||||||
|
} else if item_bottom > scroll_top + state.list_height {
|
||||||
|
scroll_offset.y = -(item_bottom - state.list_height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scroll_top(&self) -> Pixels {
|
||||||
|
if let Some(state) = &*self.0.borrow() {
|
||||||
|
-state.scroll_offset.borrow().y
|
||||||
|
} else {
|
||||||
|
Pixels::ZERO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Styled for UniformList {
|
||||||
|
fn style(&mut self) -> &mut StyleRefinement {
|
||||||
|
&mut self.interactivity.base_style
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct StateInner {
|
pub struct UniformListState {
|
||||||
scroll_top: f32,
|
interactive: InteractiveElementState,
|
||||||
scroll_to: Option<ScrollTarget>,
|
item_size: Size<Pixels>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct UniformListLayoutState<V> {
|
impl Element for UniformList {
|
||||||
scroll_max: f32,
|
type State = UniformListState;
|
||||||
item_height: f32,
|
|
||||||
items: Vec<AnyElement<V>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct UniformList<V> {
|
fn request_layout(
|
||||||
state: UniformListState,
|
|
||||||
item_count: usize,
|
|
||||||
#[allow(clippy::type_complexity)]
|
|
||||||
append_items: Box<dyn Fn(&mut V, Range<usize>, &mut Vec<AnyElement<V>>, &mut ViewContext<V>)>,
|
|
||||||
padding_top: f32,
|
|
||||||
padding_bottom: f32,
|
|
||||||
get_width_from_item: Option<usize>,
|
|
||||||
view_id: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V: 'static> UniformList<V> {
|
|
||||||
pub fn new<F>(
|
|
||||||
state: UniformListState,
|
|
||||||
item_count: usize,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
append_items: F,
|
|
||||||
) -> Self
|
|
||||||
where
|
|
||||||
F: 'static + Fn(&mut V, Range<usize>, &mut Vec<AnyElement<V>>, &mut ViewContext<V>),
|
|
||||||
{
|
|
||||||
Self {
|
|
||||||
state,
|
|
||||||
item_count,
|
|
||||||
append_items: Box::new(append_items),
|
|
||||||
padding_top: 0.,
|
|
||||||
padding_bottom: 0.,
|
|
||||||
get_width_from_item: None,
|
|
||||||
view_id: cx.handle().id(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_width_from_item(mut self, item_ix: Option<usize>) -> Self {
|
|
||||||
self.get_width_from_item = item_ix;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_padding_top(mut self, padding: f32) -> Self {
|
|
||||||
self.padding_top = padding;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_padding_bottom(mut self, padding: f32) -> Self {
|
|
||||||
self.padding_bottom = padding;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn scroll(
|
|
||||||
state: UniformListState,
|
|
||||||
_: Vector2F,
|
|
||||||
mut delta: Vector2F,
|
|
||||||
precise: bool,
|
|
||||||
scroll_max: f32,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> bool {
|
|
||||||
if !precise {
|
|
||||||
delta *= 20.;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut state = state.0.borrow_mut();
|
|
||||||
state.scroll_top = (state.scroll_top - delta.y()).max(0.0).min(scroll_max);
|
|
||||||
cx.notify();
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn autoscroll(&mut self, scroll_max: f32, list_height: f32, item_height: f32) {
|
|
||||||
let mut state = self.state.0.borrow_mut();
|
|
||||||
|
|
||||||
if let Some(scroll_to) = state.scroll_to.take() {
|
|
||||||
let item_ix;
|
|
||||||
let center;
|
|
||||||
match scroll_to {
|
|
||||||
ScrollTarget::Show(ix) => {
|
|
||||||
item_ix = ix;
|
|
||||||
center = false;
|
|
||||||
}
|
|
||||||
ScrollTarget::Center(ix) => {
|
|
||||||
item_ix = ix;
|
|
||||||
center = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let item_top = self.padding_top + item_ix as f32 * item_height;
|
|
||||||
let item_bottom = item_top + item_height;
|
|
||||||
if center {
|
|
||||||
let item_center = item_top + item_height / 2.;
|
|
||||||
state.scroll_top = (item_center - list_height / 2.).max(0.);
|
|
||||||
} else {
|
|
||||||
let scroll_bottom = state.scroll_top + list_height;
|
|
||||||
if item_top < state.scroll_top {
|
|
||||||
state.scroll_top = item_top;
|
|
||||||
} else if item_bottom > scroll_bottom {
|
|
||||||
state.scroll_top = item_bottom - list_height;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if state.scroll_top > scroll_max {
|
|
||||||
state.scroll_top = scroll_max;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn scroll_top(&self) -> f32 {
|
|
||||||
self.state.0.borrow().scroll_top
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<V: 'static> Element<V> for UniformList<V> {
|
|
||||||
type LayoutState = UniformListLayoutState<V>;
|
|
||||||
type PaintState = ();
|
|
||||||
|
|
||||||
fn layout(
|
|
||||||
&mut self,
|
&mut self,
|
||||||
constraint: SizeConstraint,
|
state: Option<Self::State>,
|
||||||
view: &mut V,
|
cx: &mut WindowContext,
|
||||||
cx: &mut ViewContext<V>,
|
) -> (LayoutId, Self::State) {
|
||||||
) -> (Vector2F, Self::LayoutState) {
|
let max_items = self.item_count;
|
||||||
if constraint.max.y().is_infinite() {
|
let item_size = state
|
||||||
unimplemented!(
|
.as_ref()
|
||||||
"UniformList does not support being rendered with an unconstrained height"
|
.map(|s| s.item_size)
|
||||||
);
|
.unwrap_or_else(|| self.measure_item(None, cx));
|
||||||
}
|
|
||||||
|
|
||||||
let no_items = (
|
let (layout_id, interactive) =
|
||||||
constraint.min,
|
self.interactivity
|
||||||
UniformListLayoutState {
|
.layout(state.map(|s| s.interactive), cx, |style, cx| {
|
||||||
item_height: 0.,
|
cx.request_measured_layout(
|
||||||
scroll_max: 0.,
|
style,
|
||||||
items: Default::default(),
|
move |known_dimensions, available_space, _cx| {
|
||||||
},
|
let desired_height = item_size.height * max_items;
|
||||||
);
|
let width =
|
||||||
|
known_dimensions
|
||||||
|
.width
|
||||||
|
.unwrap_or(match available_space.width {
|
||||||
|
AvailableSpace::Definite(x) => x,
|
||||||
|
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
|
||||||
|
item_size.width
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if self.item_count == 0 {
|
let height = match available_space.height {
|
||||||
return no_items;
|
AvailableSpace::Definite(height) => desired_height.min(height),
|
||||||
}
|
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
|
||||||
|
desired_height
|
||||||
|
}
|
||||||
|
};
|
||||||
|
size(width, height)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
let mut items = Vec::new();
|
let element_state = UniformListState {
|
||||||
let mut size = constraint.max;
|
interactive,
|
||||||
let mut item_size;
|
item_size,
|
||||||
let sample_item_ix;
|
|
||||||
let sample_item;
|
|
||||||
if let Some(sample_ix) = self.get_width_from_item {
|
|
||||||
(self.append_items)(view, sample_ix..sample_ix + 1, &mut items, cx);
|
|
||||||
sample_item_ix = sample_ix;
|
|
||||||
|
|
||||||
if let Some(mut item) = items.pop() {
|
|
||||||
item_size = item.layout(constraint, view, cx);
|
|
||||||
size.set_x(item_size.x());
|
|
||||||
sample_item = item;
|
|
||||||
} else {
|
|
||||||
return no_items;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
(self.append_items)(view, 0..1, &mut items, cx);
|
|
||||||
sample_item_ix = 0;
|
|
||||||
if let Some(mut item) = items.pop() {
|
|
||||||
item_size = item.layout(
|
|
||||||
SizeConstraint::new(
|
|
||||||
vec2f(constraint.max.x(), 0.0),
|
|
||||||
vec2f(constraint.max.x(), f32::INFINITY),
|
|
||||||
),
|
|
||||||
view,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
item_size.set_x(size.x());
|
|
||||||
sample_item = item
|
|
||||||
} else {
|
|
||||||
return no_items;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let item_constraint = SizeConstraint {
|
|
||||||
min: item_size,
|
|
||||||
max: vec2f(constraint.max.x(), item_size.y()),
|
|
||||||
};
|
};
|
||||||
let item_height = item_size.y();
|
|
||||||
|
|
||||||
let scroll_height = self.item_count as f32 * item_height;
|
(layout_id, element_state)
|
||||||
if scroll_height < size.y() {
|
|
||||||
size.set_y(size.y().min(scroll_height).max(constraint.min.y()));
|
|
||||||
}
|
|
||||||
|
|
||||||
let scroll_height =
|
|
||||||
item_height * self.item_count as f32 + self.padding_top + self.padding_bottom;
|
|
||||||
let scroll_max = (scroll_height - size.y()).max(0.);
|
|
||||||
self.autoscroll(scroll_max, size.y(), item_height);
|
|
||||||
|
|
||||||
let start = cmp::min(
|
|
||||||
((self.scroll_top() - self.padding_top) / item_height.max(1.)) as usize,
|
|
||||||
self.item_count,
|
|
||||||
);
|
|
||||||
let end = cmp::min(
|
|
||||||
self.item_count,
|
|
||||||
start + (size.y() / item_height.max(1.)).ceil() as usize + 1,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (start..end).contains(&sample_item_ix) {
|
|
||||||
if sample_item_ix > start {
|
|
||||||
(self.append_items)(view, start..sample_item_ix, &mut items, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
items.push(sample_item);
|
|
||||||
|
|
||||||
if sample_item_ix < end {
|
|
||||||
(self.append_items)(view, sample_item_ix + 1..end, &mut items, cx);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
(self.append_items)(view, start..end, &mut items, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
for item in &mut items {
|
|
||||||
let item_size = item.layout(item_constraint, view, cx);
|
|
||||||
if item_size.x() > size.x() {
|
|
||||||
size.set_x(item_size.x());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(
|
|
||||||
size,
|
|
||||||
UniformListLayoutState {
|
|
||||||
item_height,
|
|
||||||
scroll_max,
|
|
||||||
items,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn paint(
|
fn paint(
|
||||||
&mut self,
|
&mut self,
|
||||||
bounds: RectF,
|
bounds: Bounds<crate::Pixels>,
|
||||||
visible_bounds: RectF,
|
element_state: &mut Self::State,
|
||||||
layout: &mut Self::LayoutState,
|
cx: &mut WindowContext,
|
||||||
view: &mut V,
|
) {
|
||||||
cx: &mut ViewContext<V>,
|
let style =
|
||||||
) -> Self::PaintState {
|
self.interactivity
|
||||||
let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default();
|
.compute_style(Some(bounds), &mut element_state.interactive, cx);
|
||||||
|
let border = style.border_widths.to_pixels(cx.rem_size());
|
||||||
|
let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size());
|
||||||
|
|
||||||
cx.scene().push_layer(Some(visible_bounds));
|
let padded_bounds = Bounds::from_corners(
|
||||||
|
bounds.origin + point(border.left + padding.left, border.top + padding.top),
|
||||||
cx.scene().push_mouse_region(
|
bounds.lower_right()
|
||||||
MouseRegion::new::<Self>(self.view_id, 0, visible_bounds).on_scroll({
|
- point(border.right + padding.right, border.bottom + padding.bottom),
|
||||||
let scroll_max = layout.scroll_max;
|
|
||||||
let state = self.state.clone();
|
|
||||||
move |event, _, cx| {
|
|
||||||
let ScrollWheelEvent {
|
|
||||||
position, delta, ..
|
|
||||||
} = event.platform_event;
|
|
||||||
if !Self::scroll(
|
|
||||||
state.clone(),
|
|
||||||
position,
|
|
||||||
*delta.raw(),
|
|
||||||
delta.precise(),
|
|
||||||
scroll_max,
|
|
||||||
cx,
|
|
||||||
) {
|
|
||||||
cx.propagate_event();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut item_origin = bounds.origin()
|
let item_size = element_state.item_size;
|
||||||
- vec2f(
|
let content_size = Size {
|
||||||
0.,
|
width: padded_bounds.size.width,
|
||||||
(self.state.scroll_top() - self.padding_top) % layout.item_height,
|
height: item_size.height * self.item_count + padding.top + padding.bottom,
|
||||||
);
|
};
|
||||||
|
|
||||||
for item in &mut layout.items {
|
let shared_scroll_offset = element_state
|
||||||
item.paint(item_origin, visible_bounds, view, cx);
|
.interactive
|
||||||
item_origin += vec2f(0.0, layout.item_height);
|
.scroll_offset
|
||||||
}
|
.get_or_insert_with(|| {
|
||||||
|
if let Some(scroll_handle) = self.scroll_handle.as_ref() {
|
||||||
|
if let Some(scroll_handle) = scroll_handle.0.borrow().as_ref() {
|
||||||
|
return scroll_handle.scroll_offset.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cx.scene().pop_layer();
|
Rc::default()
|
||||||
}
|
})
|
||||||
|
.clone();
|
||||||
|
|
||||||
fn rect_for_text_range(
|
let item_height = self.measure_item(Some(padded_bounds.size.width), cx).height;
|
||||||
&self,
|
|
||||||
range: Range<usize>,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
layout: &Self::LayoutState,
|
|
||||||
_: &Self::PaintState,
|
|
||||||
view: &V,
|
|
||||||
cx: &ViewContext<V>,
|
|
||||||
) -> Option<RectF> {
|
|
||||||
layout
|
|
||||||
.items
|
|
||||||
.iter()
|
|
||||||
.find_map(|child| child.rect_for_text_range(range.clone(), view, cx))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn debug(
|
self.interactivity.paint(
|
||||||
&self,
|
bounds,
|
||||||
bounds: RectF,
|
content_size,
|
||||||
layout: &Self::LayoutState,
|
&mut element_state.interactive,
|
||||||
_: &Self::PaintState,
|
cx,
|
||||||
view: &V,
|
|style, mut scroll_offset, cx| {
|
||||||
cx: &ViewContext<V>,
|
let border = style.border_widths.to_pixels(cx.rem_size());
|
||||||
) -> json::Value {
|
let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size());
|
||||||
json!({
|
|
||||||
"type": "UniformList",
|
|
||||||
"bounds": bounds.to_json(),
|
|
||||||
"scroll_max": layout.scroll_max,
|
|
||||||
"item_height": layout.item_height,
|
|
||||||
"items": layout.items.iter().map(|item| item.debug(view, cx)).collect::<Vec<json::Value>>()
|
|
||||||
|
|
||||||
})
|
let padded_bounds = Bounds::from_corners(
|
||||||
|
bounds.origin + point(border.left + padding.left, border.top),
|
||||||
|
bounds.lower_right() - point(border.right + padding.right, border.bottom),
|
||||||
|
);
|
||||||
|
|
||||||
|
if self.item_count > 0 {
|
||||||
|
let content_height =
|
||||||
|
item_height * self.item_count + padding.top + padding.bottom;
|
||||||
|
let min_scroll_offset = padded_bounds.size.height - content_height;
|
||||||
|
let is_scrolled = scroll_offset.y != px(0.);
|
||||||
|
|
||||||
|
if is_scrolled && scroll_offset.y < min_scroll_offset {
|
||||||
|
shared_scroll_offset.borrow_mut().y = min_scroll_offset;
|
||||||
|
scroll_offset.y = min_scroll_offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(scroll_handle) = self.scroll_handle.clone() {
|
||||||
|
scroll_handle.0.borrow_mut().replace(ScrollHandleState {
|
||||||
|
item_height,
|
||||||
|
list_height: padded_bounds.size.height,
|
||||||
|
scroll_offset: shared_scroll_offset,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let first_visible_element_ix =
|
||||||
|
(-(scroll_offset.y + padding.top) / item_height).floor() as usize;
|
||||||
|
let last_visible_element_ix = ((-scroll_offset.y + padded_bounds.size.height)
|
||||||
|
/ item_height)
|
||||||
|
.ceil() as usize;
|
||||||
|
let visible_range = first_visible_element_ix
|
||||||
|
..cmp::min(last_visible_element_ix, self.item_count);
|
||||||
|
|
||||||
|
let mut items = (self.render_items)(visible_range.clone(), cx);
|
||||||
|
cx.with_z_index(1, |cx| {
|
||||||
|
let content_mask = ContentMask { bounds };
|
||||||
|
cx.with_content_mask(Some(content_mask), |cx| {
|
||||||
|
for (item, ix) in items.iter_mut().zip(visible_range) {
|
||||||
|
let item_origin = padded_bounds.origin
|
||||||
|
+ point(
|
||||||
|
px(0.),
|
||||||
|
item_height * ix + scroll_offset.y + padding.top,
|
||||||
|
);
|
||||||
|
let available_space = size(
|
||||||
|
AvailableSpace::Definite(padded_bounds.size.width),
|
||||||
|
AvailableSpace::Definite(item_height),
|
||||||
|
);
|
||||||
|
item.draw(item_origin, available_space, cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoElement for UniformList {
|
||||||
|
type Element = Self;
|
||||||
|
|
||||||
|
fn element_id(&self) -> Option<crate::ElementId> {
|
||||||
|
Some(self.id.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_element(self) -> Self::Element {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UniformList {
|
||||||
|
pub fn with_width_from_item(mut self, item_index: Option<usize>) -> Self {
|
||||||
|
self.item_to_measure_index = item_index.unwrap_or(0);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn measure_item(&self, list_width: Option<Pixels>, cx: &mut WindowContext) -> Size<Pixels> {
|
||||||
|
if self.item_count == 0 {
|
||||||
|
return Size::default();
|
||||||
|
}
|
||||||
|
|
||||||
|
let item_ix = cmp::min(self.item_to_measure_index, self.item_count - 1);
|
||||||
|
let mut items = (self.render_items)(item_ix..item_ix + 1, cx);
|
||||||
|
let mut item_to_measure = items.pop().unwrap();
|
||||||
|
let available_space = size(
|
||||||
|
list_width.map_or(AvailableSpace::MinContent, |width| {
|
||||||
|
AvailableSpace::Definite(width)
|
||||||
|
}),
|
||||||
|
AvailableSpace::MinContent,
|
||||||
|
);
|
||||||
|
item_to_measure.measure(available_space, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn track_scroll(mut self, handle: UniformListScrollHandle) -> Self {
|
||||||
|
self.scroll_handle = Some(handle);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InteractiveElement for UniformList {
|
||||||
|
fn interactivity(&mut self) -> &mut crate::Interactivity {
|
||||||
|
&mut self.interactivity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,330 +0,0 @@
|
|||||||
use crate::{
|
|
||||||
fonts::{Features, FontId, Metrics, Properties},
|
|
||||||
geometry::vector::{vec2f, Vector2F},
|
|
||||||
platform,
|
|
||||||
text_layout::LineWrapper,
|
|
||||||
};
|
|
||||||
use anyhow::{anyhow, Result};
|
|
||||||
use ordered_float::OrderedFloat;
|
|
||||||
use parking_lot::{RwLock, RwLockUpgradableReadGuard};
|
|
||||||
use schemars::JsonSchema;
|
|
||||||
use std::{
|
|
||||||
collections::HashMap,
|
|
||||||
ops::{Deref, DerefMut},
|
|
||||||
sync::Arc,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, JsonSchema)]
|
|
||||||
pub struct FamilyId(usize);
|
|
||||||
|
|
||||||
struct Family {
|
|
||||||
name: Arc<str>,
|
|
||||||
font_features: Features,
|
|
||||||
font_ids: Vec<FontId>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct FontCache(RwLock<FontCacheState>);
|
|
||||||
|
|
||||||
pub struct FontCacheState {
|
|
||||||
font_system: Arc<dyn platform::FontSystem>,
|
|
||||||
families: Vec<Family>,
|
|
||||||
default_family: Option<FamilyId>,
|
|
||||||
font_selections: HashMap<FamilyId, HashMap<Properties, FontId>>,
|
|
||||||
metrics: HashMap<FontId, Metrics>,
|
|
||||||
wrapper_pool: HashMap<(FontId, OrderedFloat<f32>), Vec<LineWrapper>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct LineWrapperHandle {
|
|
||||||
wrapper: Option<LineWrapper>,
|
|
||||||
font_cache: Arc<FontCache>,
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe impl Send for FontCache {}
|
|
||||||
|
|
||||||
impl FontCache {
|
|
||||||
pub fn new(fonts: Arc<dyn platform::FontSystem>) -> Self {
|
|
||||||
Self(RwLock::new(FontCacheState {
|
|
||||||
font_system: fonts,
|
|
||||||
families: Default::default(),
|
|
||||||
default_family: None,
|
|
||||||
font_selections: Default::default(),
|
|
||||||
metrics: Default::default(),
|
|
||||||
wrapper_pool: Default::default(),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn family_name(&self, family_id: FamilyId) -> Result<Arc<str>> {
|
|
||||||
self.0
|
|
||||||
.read()
|
|
||||||
.families
|
|
||||||
.get(family_id.0)
|
|
||||||
.ok_or_else(|| anyhow!("invalid family id"))
|
|
||||||
.map(|family| family.name.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn load_family(&self, names: &[&str], features: &Features) -> Result<FamilyId> {
|
|
||||||
for name in names {
|
|
||||||
let state = self.0.upgradable_read();
|
|
||||||
|
|
||||||
if let Some(ix) = state
|
|
||||||
.families
|
|
||||||
.iter()
|
|
||||||
.position(|f| f.name.as_ref() == *name && f.font_features == *features)
|
|
||||||
{
|
|
||||||
return Ok(FamilyId(ix));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut state = RwLockUpgradableReadGuard::upgrade(state);
|
|
||||||
|
|
||||||
if let Ok(font_ids) = state.font_system.load_family(name, features) {
|
|
||||||
if font_ids.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let family_id = FamilyId(state.families.len());
|
|
||||||
for font_id in &font_ids {
|
|
||||||
if state.font_system.glyph_for_char(*font_id, 'm').is_none() {
|
|
||||||
return Err(anyhow!("font must contain a glyph for the 'm' character"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
state.families.push(Family {
|
|
||||||
name: Arc::from(*name),
|
|
||||||
font_features: features.clone(),
|
|
||||||
font_ids,
|
|
||||||
});
|
|
||||||
return Ok(family_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(anyhow!(
|
|
||||||
"could not find a non-empty font family matching one of the given names: {}",
|
|
||||||
names
|
|
||||||
.iter()
|
|
||||||
.map(|name| format!("`{name}`"))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(", ")
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns an arbitrary font family that is available on the system.
|
|
||||||
pub fn known_existing_family(&self) -> FamilyId {
|
|
||||||
if let Some(family_id) = self.0.read().default_family {
|
|
||||||
return family_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
let default_family = self
|
|
||||||
.load_family(
|
|
||||||
&["Courier", "Helvetica", "Arial", "Verdana"],
|
|
||||||
&Default::default(),
|
|
||||||
)
|
|
||||||
.unwrap_or_else(|_| {
|
|
||||||
let all_family_names = self.0.read().font_system.all_families();
|
|
||||||
let all_family_names: Vec<_> = all_family_names
|
|
||||||
.iter()
|
|
||||||
.map(|string| string.as_str())
|
|
||||||
.collect();
|
|
||||||
self.load_family(&all_family_names, &Default::default())
|
|
||||||
.expect("could not load any default font family")
|
|
||||||
});
|
|
||||||
|
|
||||||
self.0.write().default_family = Some(default_family);
|
|
||||||
default_family
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn default_font(&self, family_id: FamilyId) -> FontId {
|
|
||||||
self.select_font(family_id, &Properties::default()).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn select_font(&self, family_id: FamilyId, properties: &Properties) -> Result<FontId> {
|
|
||||||
let inner = self.0.upgradable_read();
|
|
||||||
if let Some(font_id) = inner
|
|
||||||
.font_selections
|
|
||||||
.get(&family_id)
|
|
||||||
.and_then(|f| f.get(properties))
|
|
||||||
{
|
|
||||||
Ok(*font_id)
|
|
||||||
} else {
|
|
||||||
let mut inner = RwLockUpgradableReadGuard::upgrade(inner);
|
|
||||||
let family = &inner.families[family_id.0];
|
|
||||||
let font_id = inner
|
|
||||||
.font_system
|
|
||||||
.select_font(&family.font_ids, properties)
|
|
||||||
.unwrap_or(family.font_ids[0]);
|
|
||||||
|
|
||||||
inner
|
|
||||||
.font_selections
|
|
||||||
.entry(family_id)
|
|
||||||
.or_default()
|
|
||||||
.insert(*properties, font_id);
|
|
||||||
Ok(font_id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn metric<F, T>(&self, font_id: FontId, f: F) -> T
|
|
||||||
where
|
|
||||||
F: FnOnce(&Metrics) -> T,
|
|
||||||
T: 'static,
|
|
||||||
{
|
|
||||||
let state = self.0.upgradable_read();
|
|
||||||
if let Some(metrics) = state.metrics.get(&font_id) {
|
|
||||||
f(metrics)
|
|
||||||
} else {
|
|
||||||
let metrics = state.font_system.font_metrics(font_id);
|
|
||||||
let metric = f(&metrics);
|
|
||||||
let mut state = RwLockUpgradableReadGuard::upgrade(state);
|
|
||||||
state.metrics.insert(font_id, metrics);
|
|
||||||
metric
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn bounding_box(&self, font_id: FontId, font_size: f32) -> Vector2F {
|
|
||||||
let bounding_box = self.metric(font_id, |m| m.bounding_box);
|
|
||||||
let width = bounding_box.width() * self.em_scale(font_id, font_size);
|
|
||||||
let height = bounding_box.height() * self.em_scale(font_id, font_size);
|
|
||||||
vec2f(width, height)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn em_width(&self, font_id: FontId, font_size: f32) -> f32 {
|
|
||||||
let glyph_id;
|
|
||||||
let bounds;
|
|
||||||
{
|
|
||||||
let state = self.0.read();
|
|
||||||
glyph_id = state.font_system.glyph_for_char(font_id, 'm').unwrap();
|
|
||||||
bounds = state
|
|
||||||
.font_system
|
|
||||||
.typographic_bounds(font_id, glyph_id)
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
bounds.width() * self.em_scale(font_id, font_size)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn em_advance(&self, font_id: FontId, font_size: f32) -> f32 {
|
|
||||||
let glyph_id;
|
|
||||||
let advance;
|
|
||||||
{
|
|
||||||
let state = self.0.read();
|
|
||||||
glyph_id = state.font_system.glyph_for_char(font_id, 'm').unwrap();
|
|
||||||
advance = state.font_system.advance(font_id, glyph_id).unwrap();
|
|
||||||
}
|
|
||||||
advance.x() * self.em_scale(font_id, font_size)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn line_height(&self, font_size: f32) -> f32 {
|
|
||||||
(font_size * 1.618).round()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cap_height(&self, font_id: FontId, font_size: f32) -> f32 {
|
|
||||||
self.metric(font_id, |m| m.cap_height) * self.em_scale(font_id, font_size)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn x_height(&self, font_id: FontId, font_size: f32) -> f32 {
|
|
||||||
self.metric(font_id, |m| m.x_height) * self.em_scale(font_id, font_size)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ascent(&self, font_id: FontId, font_size: f32) -> f32 {
|
|
||||||
self.metric(font_id, |m| m.ascent) * self.em_scale(font_id, font_size)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn descent(&self, font_id: FontId, font_size: f32) -> f32 {
|
|
||||||
self.metric(font_id, |m| -m.descent) * self.em_scale(font_id, font_size)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn em_scale(&self, font_id: FontId, font_size: f32) -> f32 {
|
|
||||||
font_size / self.metric(font_id, |m| m.units_per_em as f32)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn baseline_offset(&self, font_id: FontId, font_size: f32) -> f32 {
|
|
||||||
let line_height = self.line_height(font_size);
|
|
||||||
let ascent = self.ascent(font_id, font_size);
|
|
||||||
let descent = self.descent(font_id, font_size);
|
|
||||||
let padding_top = (line_height - ascent - descent) / 2.;
|
|
||||||
padding_top + ascent
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn line_wrapper(self: &Arc<Self>, font_id: FontId, font_size: f32) -> LineWrapperHandle {
|
|
||||||
let mut state = self.0.write();
|
|
||||||
let wrappers = state
|
|
||||||
.wrapper_pool
|
|
||||||
.entry((font_id, OrderedFloat(font_size)))
|
|
||||||
.or_default();
|
|
||||||
let wrapper = wrappers
|
|
||||||
.pop()
|
|
||||||
.unwrap_or_else(|| LineWrapper::new(font_id, font_size, state.font_system.clone()));
|
|
||||||
LineWrapperHandle {
|
|
||||||
wrapper: Some(wrapper),
|
|
||||||
font_cache: self.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for LineWrapperHandle {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
let mut state = self.font_cache.0.write();
|
|
||||||
let wrapper = self.wrapper.take().unwrap();
|
|
||||||
state
|
|
||||||
.wrapper_pool
|
|
||||||
.get_mut(&(wrapper.font_id, OrderedFloat(wrapper.font_size)))
|
|
||||||
.unwrap()
|
|
||||||
.push(wrapper);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for LineWrapperHandle {
|
|
||||||
type Target = LineWrapper;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
self.wrapper.as_ref().unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DerefMut for LineWrapperHandle {
|
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
||||||
self.wrapper.as_mut().unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::{
|
|
||||||
fonts::{Style, Weight},
|
|
||||||
platform::{test, Platform as _},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_select_font() {
|
|
||||||
let platform = test::platform();
|
|
||||||
let fonts = FontCache::new(platform.fonts());
|
|
||||||
let arial = fonts
|
|
||||||
.load_family(
|
|
||||||
&["Arial"],
|
|
||||||
&Features {
|
|
||||||
calt: Some(false),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let arial_regular = fonts.select_font(arial, &Properties::new()).unwrap();
|
|
||||||
let arial_italic = fonts
|
|
||||||
.select_font(arial, Properties::new().style(Style::Italic))
|
|
||||||
.unwrap();
|
|
||||||
let arial_bold = fonts
|
|
||||||
.select_font(arial, Properties::new().weight(Weight::BOLD))
|
|
||||||
.unwrap();
|
|
||||||
assert_ne!(arial_regular, arial_italic);
|
|
||||||
assert_ne!(arial_regular, arial_bold);
|
|
||||||
assert_ne!(arial_italic, arial_bold);
|
|
||||||
|
|
||||||
let arial_with_calt = fonts
|
|
||||||
.load_family(
|
|
||||||
&["Arial"],
|
|
||||||
&Features {
|
|
||||||
calt: Some(true),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert_ne!(arial_with_calt, arial);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,636 +0,0 @@
|
|||||||
use crate::{
|
|
||||||
color::Color,
|
|
||||||
font_cache::FamilyId,
|
|
||||||
json::{json, ToJson},
|
|
||||||
text_layout::RunStyle,
|
|
||||||
FontCache,
|
|
||||||
};
|
|
||||||
use anyhow::{anyhow, Result};
|
|
||||||
pub use font_kit::{
|
|
||||||
metrics::Metrics,
|
|
||||||
properties::{Properties, Stretch, Style, Weight},
|
|
||||||
};
|
|
||||||
use ordered_float::OrderedFloat;
|
|
||||||
use refineable::Refineable;
|
|
||||||
use schemars::JsonSchema;
|
|
||||||
use serde::{de, Deserialize, Serialize};
|
|
||||||
use serde_json::Value;
|
|
||||||
use std::{cell::RefCell, sync::Arc};
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, JsonSchema)]
|
|
||||||
pub struct FontId(pub usize);
|
|
||||||
|
|
||||||
pub type GlyphId = u32;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
|
|
||||||
pub struct Features {
|
|
||||||
pub calt: Option<bool>,
|
|
||||||
pub case: Option<bool>,
|
|
||||||
pub cpsp: Option<bool>,
|
|
||||||
pub frac: Option<bool>,
|
|
||||||
pub liga: Option<bool>,
|
|
||||||
pub onum: Option<bool>,
|
|
||||||
pub ordn: Option<bool>,
|
|
||||||
pub pnum: Option<bool>,
|
|
||||||
pub ss01: Option<bool>,
|
|
||||||
pub ss02: Option<bool>,
|
|
||||||
pub ss03: Option<bool>,
|
|
||||||
pub ss04: Option<bool>,
|
|
||||||
pub ss05: Option<bool>,
|
|
||||||
pub ss06: Option<bool>,
|
|
||||||
pub ss07: Option<bool>,
|
|
||||||
pub ss08: Option<bool>,
|
|
||||||
pub ss09: Option<bool>,
|
|
||||||
pub ss10: Option<bool>,
|
|
||||||
pub ss11: Option<bool>,
|
|
||||||
pub ss12: Option<bool>,
|
|
||||||
pub ss13: Option<bool>,
|
|
||||||
pub ss14: Option<bool>,
|
|
||||||
pub ss15: Option<bool>,
|
|
||||||
pub ss16: Option<bool>,
|
|
||||||
pub ss17: Option<bool>,
|
|
||||||
pub ss18: Option<bool>,
|
|
||||||
pub ss19: Option<bool>,
|
|
||||||
pub ss20: Option<bool>,
|
|
||||||
pub subs: Option<bool>,
|
|
||||||
pub sups: Option<bool>,
|
|
||||||
pub swsh: Option<bool>,
|
|
||||||
pub titl: Option<bool>,
|
|
||||||
pub tnum: Option<bool>,
|
|
||||||
pub zero: Option<bool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, JsonSchema)]
|
|
||||||
pub struct TextStyle {
|
|
||||||
pub color: Color,
|
|
||||||
pub font_family_name: Arc<str>,
|
|
||||||
pub font_family_id: FamilyId,
|
|
||||||
pub font_id: FontId,
|
|
||||||
pub font_size: f32,
|
|
||||||
#[schemars(with = "PropertiesDef")]
|
|
||||||
pub font_properties: Properties,
|
|
||||||
pub underline: Underline,
|
|
||||||
pub soft_wrap: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TextStyle {
|
|
||||||
pub fn for_color(color: Color) -> Self {
|
|
||||||
Self {
|
|
||||||
color,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TextStyle {
|
|
||||||
pub fn refine(
|
|
||||||
&mut self,
|
|
||||||
refinement: &TextStyleRefinement,
|
|
||||||
font_cache: &FontCache,
|
|
||||||
) -> Result<()> {
|
|
||||||
if let Some(font_size) = refinement.font_size {
|
|
||||||
self.font_size = font_size;
|
|
||||||
}
|
|
||||||
if let Some(color) = refinement.color {
|
|
||||||
self.color = color;
|
|
||||||
}
|
|
||||||
if let Some(underline) = refinement.underline {
|
|
||||||
self.underline = underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut update_font_id = false;
|
|
||||||
if let Some(font_family) = refinement.font_family.clone() {
|
|
||||||
self.font_family_id = font_cache.load_family(&[&font_family], &Default::default())?;
|
|
||||||
self.font_family_name = font_family;
|
|
||||||
update_font_id = true;
|
|
||||||
}
|
|
||||||
if let Some(font_weight) = refinement.font_weight {
|
|
||||||
self.font_properties.weight = font_weight;
|
|
||||||
update_font_id = true;
|
|
||||||
}
|
|
||||||
if let Some(font_style) = refinement.font_style {
|
|
||||||
self.font_properties.style = font_style;
|
|
||||||
update_font_id = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if update_font_id {
|
|
||||||
self.font_id = font_cache.select_font(self.font_family_id, &self.font_properties)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
|
||||||
pub struct TextStyleRefinement {
|
|
||||||
pub color: Option<Color>,
|
|
||||||
pub font_family: Option<Arc<str>>,
|
|
||||||
pub font_size: Option<f32>,
|
|
||||||
pub font_weight: Option<Weight>,
|
|
||||||
pub font_style: Option<Style>,
|
|
||||||
pub underline: Option<Underline>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Refineable for TextStyleRefinement {
|
|
||||||
type Refinement = Self;
|
|
||||||
|
|
||||||
fn refine(&mut self, refinement: &Self::Refinement) {
|
|
||||||
if refinement.color.is_some() {
|
|
||||||
self.color = refinement.color;
|
|
||||||
}
|
|
||||||
if refinement.font_family.is_some() {
|
|
||||||
self.font_family = refinement.font_family.clone();
|
|
||||||
}
|
|
||||||
if refinement.font_size.is_some() {
|
|
||||||
self.font_size = refinement.font_size;
|
|
||||||
}
|
|
||||||
if refinement.font_weight.is_some() {
|
|
||||||
self.font_weight = refinement.font_weight;
|
|
||||||
}
|
|
||||||
if refinement.font_style.is_some() {
|
|
||||||
self.font_style = refinement.font_style;
|
|
||||||
}
|
|
||||||
if refinement.underline.is_some() {
|
|
||||||
self.underline = refinement.underline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn refined(mut self, refinement: Self::Refinement) -> Self {
|
|
||||||
self.refine(&refinement);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(JsonSchema)]
|
|
||||||
#[serde(remote = "Properties")]
|
|
||||||
pub struct PropertiesDef {
|
|
||||||
/// The font style, as defined in CSS.
|
|
||||||
pub style: StyleDef,
|
|
||||||
/// The font weight, as defined in CSS.
|
|
||||||
pub weight: f32,
|
|
||||||
/// The font stretchiness, as defined in CSS.
|
|
||||||
pub stretch: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(JsonSchema)]
|
|
||||||
#[schemars(remote = "Style")]
|
|
||||||
pub enum StyleDef {
|
|
||||||
/// A face that is neither italic not obliqued.
|
|
||||||
Normal,
|
|
||||||
/// A form that is generally cursive in nature.
|
|
||||||
Italic,
|
|
||||||
/// A typically-sloped version of the regular face.
|
|
||||||
Oblique,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Default, PartialEq, JsonSchema)]
|
|
||||||
pub struct HighlightStyle {
|
|
||||||
pub color: Option<Color>,
|
|
||||||
#[schemars(with = "Option::<f32>")]
|
|
||||||
pub weight: Option<Weight>,
|
|
||||||
pub italic: Option<bool>,
|
|
||||||
pub underline: Option<Underline>,
|
|
||||||
pub fade_out: Option<f32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Eq for HighlightStyle {}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, JsonSchema)]
|
|
||||||
pub struct Underline {
|
|
||||||
pub color: Option<Color>,
|
|
||||||
#[schemars(with = "f32")]
|
|
||||||
pub thickness: OrderedFloat<f32>,
|
|
||||||
pub squiggly: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(non_camel_case_types)]
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
enum WeightJson {
|
|
||||||
thin,
|
|
||||||
extra_light,
|
|
||||||
light,
|
|
||||||
normal,
|
|
||||||
medium,
|
|
||||||
semibold,
|
|
||||||
bold,
|
|
||||||
extra_bold,
|
|
||||||
black,
|
|
||||||
}
|
|
||||||
|
|
||||||
thread_local! {
|
|
||||||
static FONT_CACHE: RefCell<Option<Arc<FontCache>>> = Default::default();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct TextStyleJson {
|
|
||||||
color: Color,
|
|
||||||
family: String,
|
|
||||||
#[serde(default)]
|
|
||||||
features: Features,
|
|
||||||
weight: Option<WeightJson>,
|
|
||||||
size: f32,
|
|
||||||
#[serde(default)]
|
|
||||||
italic: bool,
|
|
||||||
#[serde(default)]
|
|
||||||
underline: UnderlineStyleJson,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct HighlightStyleJson {
|
|
||||||
color: Option<Color>,
|
|
||||||
weight: Option<WeightJson>,
|
|
||||||
italic: Option<bool>,
|
|
||||||
underline: Option<UnderlineStyleJson>,
|
|
||||||
fade_out: Option<f32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
#[serde(untagged)]
|
|
||||||
enum UnderlineStyleJson {
|
|
||||||
Underlined(bool),
|
|
||||||
UnderlinedWithProperties {
|
|
||||||
#[serde(default)]
|
|
||||||
color: Option<Color>,
|
|
||||||
#[serde(default)]
|
|
||||||
thickness: Option<f32>,
|
|
||||||
#[serde(default)]
|
|
||||||
squiggly: bool,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TextStyle {
|
|
||||||
pub fn new(
|
|
||||||
font_family_name: impl Into<Arc<str>>,
|
|
||||||
font_size: f32,
|
|
||||||
font_properties: Properties,
|
|
||||||
font_features: Features,
|
|
||||||
underline: Underline,
|
|
||||||
color: Color,
|
|
||||||
font_cache: &FontCache,
|
|
||||||
) -> Result<Self> {
|
|
||||||
let font_family_name = font_family_name.into();
|
|
||||||
let font_family_id = font_cache.load_family(&[&font_family_name], &font_features)?;
|
|
||||||
let font_id = font_cache.select_font(font_family_id, &font_properties)?;
|
|
||||||
Ok(Self {
|
|
||||||
color,
|
|
||||||
font_family_name,
|
|
||||||
font_family_id,
|
|
||||||
font_id,
|
|
||||||
font_size,
|
|
||||||
font_properties,
|
|
||||||
underline,
|
|
||||||
soft_wrap: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn default(font_cache: &FontCache) -> Self {
|
|
||||||
let font_family_id = font_cache.known_existing_family();
|
|
||||||
let font_id = font_cache
|
|
||||||
.select_font(font_family_id, &Default::default())
|
|
||||||
.expect("did not have any font in system-provided family");
|
|
||||||
let font_family_name = font_cache
|
|
||||||
.family_name(font_family_id)
|
|
||||||
.expect("we loaded this family from the font cache, so this should work");
|
|
||||||
|
|
||||||
Self {
|
|
||||||
color: Color::default(),
|
|
||||||
font_family_name,
|
|
||||||
font_family_id,
|
|
||||||
font_id,
|
|
||||||
font_size: 14.,
|
|
||||||
font_properties: Default::default(),
|
|
||||||
underline: Default::default(),
|
|
||||||
soft_wrap: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_font_size(mut self, font_size: f32) -> Self {
|
|
||||||
self.font_size = font_size;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn highlight(mut self, style: HighlightStyle, font_cache: &FontCache) -> Result<Self> {
|
|
||||||
let mut font_properties = self.font_properties;
|
|
||||||
if let Some(weight) = style.weight {
|
|
||||||
font_properties.weight(weight);
|
|
||||||
}
|
|
||||||
if let Some(italic) = style.italic {
|
|
||||||
if italic {
|
|
||||||
font_properties.style(Style::Italic);
|
|
||||||
} else {
|
|
||||||
font_properties.style(Style::Normal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.font_properties != font_properties {
|
|
||||||
self.font_id = font_cache.select_font(self.font_family_id, &font_properties)?;
|
|
||||||
}
|
|
||||||
if let Some(color) = style.color {
|
|
||||||
self.color = Color::blend(color, self.color);
|
|
||||||
}
|
|
||||||
if let Some(factor) = style.fade_out {
|
|
||||||
self.color.fade_out(factor);
|
|
||||||
}
|
|
||||||
if let Some(underline) = style.underline {
|
|
||||||
self.underline = underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_run(&self) -> RunStyle {
|
|
||||||
RunStyle {
|
|
||||||
font_id: self.font_id,
|
|
||||||
color: self.color,
|
|
||||||
underline: self.underline,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_json(json: TextStyleJson) -> Result<Self> {
|
|
||||||
FONT_CACHE.with(|font_cache| {
|
|
||||||
if let Some(font_cache) = font_cache.borrow().as_ref() {
|
|
||||||
let font_properties = properties_from_json(json.weight, json.italic);
|
|
||||||
Self::new(
|
|
||||||
json.family,
|
|
||||||
json.size,
|
|
||||||
font_properties,
|
|
||||||
json.features,
|
|
||||||
underline_from_json(json.underline),
|
|
||||||
json.color,
|
|
||||||
font_cache,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Err(anyhow!(
|
|
||||||
"TextStyle can only be deserialized within a call to with_font_cache"
|
|
||||||
))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn line_height(&self, font_cache: &FontCache) -> f32 {
|
|
||||||
font_cache.line_height(self.font_size)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cap_height(&self, font_cache: &FontCache) -> f32 {
|
|
||||||
font_cache.cap_height(self.font_id, self.font_size)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn x_height(&self, font_cache: &FontCache) -> f32 {
|
|
||||||
font_cache.x_height(self.font_id, self.font_size)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn em_width(&self, font_cache: &FontCache) -> f32 {
|
|
||||||
font_cache.em_width(self.font_id, self.font_size)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn em_advance(&self, font_cache: &FontCache) -> f32 {
|
|
||||||
font_cache.em_advance(self.font_id, self.font_size)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn descent(&self, font_cache: &FontCache) -> f32 {
|
|
||||||
font_cache.metric(self.font_id, |m| m.descent) * self.em_scale(font_cache)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn baseline_offset(&self, font_cache: &FontCache) -> f32 {
|
|
||||||
font_cache.baseline_offset(self.font_id, self.font_size)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn em_scale(&self, font_cache: &FontCache) -> f32 {
|
|
||||||
font_cache.em_scale(self.font_id, self.font_size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<TextStyle> for HighlightStyle {
|
|
||||||
fn from(other: TextStyle) -> Self {
|
|
||||||
Self::from(&other)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&TextStyle> for HighlightStyle {
|
|
||||||
fn from(other: &TextStyle) -> Self {
|
|
||||||
Self {
|
|
||||||
color: Some(other.color),
|
|
||||||
weight: Some(other.font_properties.weight),
|
|
||||||
italic: Some(other.font_properties.style == Style::Italic),
|
|
||||||
underline: Some(other.underline),
|
|
||||||
fade_out: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for UnderlineStyleJson {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Underlined(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for TextStyle {
|
|
||||||
fn default() -> Self {
|
|
||||||
FONT_CACHE.with(|font_cache| {
|
|
||||||
let font_cache = font_cache.borrow();
|
|
||||||
let font_cache = font_cache
|
|
||||||
.as_ref()
|
|
||||||
.expect("TextStyle::default can only be called within a call to with_font_cache");
|
|
||||||
Self::default(font_cache)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HighlightStyle {
|
|
||||||
fn from_json(json: HighlightStyleJson) -> Self {
|
|
||||||
Self {
|
|
||||||
color: json.color,
|
|
||||||
weight: json.weight.map(weight_from_json),
|
|
||||||
italic: json.italic,
|
|
||||||
underline: json.underline.map(underline_from_json),
|
|
||||||
fade_out: json.fade_out,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn highlight(&mut self, other: HighlightStyle) {
|
|
||||||
match (self.color, other.color) {
|
|
||||||
(Some(self_color), Some(other_color)) => {
|
|
||||||
self.color = Some(Color::blend(other_color, self_color));
|
|
||||||
}
|
|
||||||
(None, Some(other_color)) => {
|
|
||||||
self.color = Some(other_color);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
if other.weight.is_some() {
|
|
||||||
self.weight = other.weight;
|
|
||||||
}
|
|
||||||
|
|
||||||
if other.italic.is_some() {
|
|
||||||
self.italic = other.italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
if other.underline.is_some() {
|
|
||||||
self.underline = other.underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
match (other.fade_out, self.fade_out) {
|
|
||||||
(Some(source_fade), None) => self.fade_out = Some(source_fade),
|
|
||||||
(Some(source_fade), Some(dest_fade)) => {
|
|
||||||
let source_alpha = 1. - source_fade;
|
|
||||||
let dest_alpha = 1. - dest_fade;
|
|
||||||
let blended_alpha = source_alpha + (dest_alpha * source_fade);
|
|
||||||
let blended_fade = 1. - blended_alpha;
|
|
||||||
self.fade_out = Some(blended_fade);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Color> for HighlightStyle {
|
|
||||||
fn from(color: Color) -> Self {
|
|
||||||
Self {
|
|
||||||
color: Some(color),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for TextStyle {
|
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
||||||
where
|
|
||||||
D: serde::Deserializer<'de>,
|
|
||||||
{
|
|
||||||
Self::from_json(TextStyleJson::deserialize(deserializer)?).map_err(de::Error::custom)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ToJson for TextStyle {
|
|
||||||
fn to_json(&self) -> Value {
|
|
||||||
json!({
|
|
||||||
"color": self.color.to_json(),
|
|
||||||
"font_family": self.font_family_name.as_ref(),
|
|
||||||
"font_properties": self.font_properties.to_json(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for HighlightStyle {
|
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
||||||
where
|
|
||||||
D: serde::Deserializer<'de>,
|
|
||||||
{
|
|
||||||
let json = serde_json::Value::deserialize(deserializer)?;
|
|
||||||
if json.is_object() {
|
|
||||||
Ok(Self::from_json(
|
|
||||||
serde_json::from_value(json).map_err(de::Error::custom)?,
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
Ok(Self {
|
|
||||||
color: serde_json::from_value(json).map_err(de::Error::custom)?,
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn underline_from_json(json: UnderlineStyleJson) -> Underline {
|
|
||||||
match json {
|
|
||||||
UnderlineStyleJson::Underlined(false) => Underline::default(),
|
|
||||||
UnderlineStyleJson::Underlined(true) => Underline {
|
|
||||||
color: None,
|
|
||||||
thickness: 1.0.into(),
|
|
||||||
squiggly: false,
|
|
||||||
},
|
|
||||||
UnderlineStyleJson::UnderlinedWithProperties {
|
|
||||||
color,
|
|
||||||
thickness,
|
|
||||||
squiggly,
|
|
||||||
} => Underline {
|
|
||||||
color,
|
|
||||||
thickness: thickness.unwrap_or(1.).into(),
|
|
||||||
squiggly,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn properties_from_json(weight: Option<WeightJson>, italic: bool) -> Properties {
|
|
||||||
let weight = weight.map(weight_from_json).unwrap_or_default();
|
|
||||||
let style = if italic { Style::Italic } else { Style::Normal };
|
|
||||||
*Properties::new().weight(weight).style(style)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn weight_from_json(weight: WeightJson) -> Weight {
|
|
||||||
match weight {
|
|
||||||
WeightJson::thin => Weight::THIN,
|
|
||||||
WeightJson::extra_light => Weight::EXTRA_LIGHT,
|
|
||||||
WeightJson::light => Weight::LIGHT,
|
|
||||||
WeightJson::normal => Weight::NORMAL,
|
|
||||||
WeightJson::medium => Weight::MEDIUM,
|
|
||||||
WeightJson::semibold => Weight::SEMIBOLD,
|
|
||||||
WeightJson::bold => Weight::BOLD,
|
|
||||||
WeightJson::extra_bold => Weight::EXTRA_BOLD,
|
|
||||||
WeightJson::black => Weight::BLACK,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ToJson for Properties {
|
|
||||||
fn to_json(&self) -> crate::json::Value {
|
|
||||||
json!({
|
|
||||||
"style": self.style.to_json(),
|
|
||||||
"weight": self.weight.to_json(),
|
|
||||||
"stretch": self.stretch.to_json(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ToJson for Style {
|
|
||||||
fn to_json(&self) -> crate::json::Value {
|
|
||||||
match self {
|
|
||||||
Style::Normal => json!("normal"),
|
|
||||||
Style::Italic => json!("italic"),
|
|
||||||
Style::Oblique => json!("oblique"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ToJson for Weight {
|
|
||||||
fn to_json(&self) -> crate::json::Value {
|
|
||||||
if self.0 == Weight::THIN.0 {
|
|
||||||
json!("thin")
|
|
||||||
} else if self.0 == Weight::EXTRA_LIGHT.0 {
|
|
||||||
json!("extra light")
|
|
||||||
} else if self.0 == Weight::LIGHT.0 {
|
|
||||||
json!("light")
|
|
||||||
} else if self.0 == Weight::NORMAL.0 {
|
|
||||||
json!("normal")
|
|
||||||
} else if self.0 == Weight::MEDIUM.0 {
|
|
||||||
json!("medium")
|
|
||||||
} else if self.0 == Weight::SEMIBOLD.0 {
|
|
||||||
json!("semibold")
|
|
||||||
} else if self.0 == Weight::BOLD.0 {
|
|
||||||
json!("bold")
|
|
||||||
} else if self.0 == Weight::EXTRA_BOLD.0 {
|
|
||||||
json!("extra bold")
|
|
||||||
} else if self.0 == Weight::BLACK.0 {
|
|
||||||
json!("black")
|
|
||||||
} else {
|
|
||||||
json!(self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ToJson for Stretch {
|
|
||||||
fn to_json(&self) -> serde_json::Value {
|
|
||||||
json!(self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_font_cache<F, T>(font_cache: Arc<FontCache>, callback: F) -> T
|
|
||||||
where
|
|
||||||
F: FnOnce() -> T,
|
|
||||||
{
|
|
||||||
FONT_CACHE.with(|cache| {
|
|
||||||
*cache.borrow_mut() = Some(font_cache);
|
|
||||||
let result = callback();
|
|
||||||
cache.borrow_mut().take();
|
|
||||||
result
|
|
||||||
})
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
@ -1,40 +1,215 @@
|
|||||||
|
#[macro_use]
|
||||||
|
mod action;
|
||||||
mod app;
|
mod app;
|
||||||
mod image_cache;
|
|
||||||
pub use app::*;
|
mod arena;
|
||||||
mod assets;
|
mod assets;
|
||||||
|
mod color;
|
||||||
|
mod element;
|
||||||
|
mod elements;
|
||||||
|
mod executor;
|
||||||
|
mod geometry;
|
||||||
|
mod image_cache;
|
||||||
|
mod input;
|
||||||
|
mod interactive;
|
||||||
|
mod key_dispatch;
|
||||||
|
mod keymap;
|
||||||
|
mod platform;
|
||||||
|
pub mod prelude;
|
||||||
|
mod scene;
|
||||||
|
mod shared_string;
|
||||||
|
mod style;
|
||||||
|
mod styled;
|
||||||
|
mod subscription;
|
||||||
|
mod svg_renderer;
|
||||||
|
mod taffy;
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub mod test;
|
pub mod test;
|
||||||
pub use assets::*;
|
mod text_system;
|
||||||
pub mod elements;
|
|
||||||
pub mod font_cache;
|
|
||||||
mod image_data;
|
|
||||||
pub use crate::image_data::ImageData;
|
|
||||||
pub use taffy;
|
|
||||||
pub mod views;
|
|
||||||
pub use font_cache::FontCache;
|
|
||||||
mod clipboard;
|
|
||||||
pub use clipboard::ClipboardItem;
|
|
||||||
pub mod fonts;
|
|
||||||
pub mod geometry;
|
|
||||||
pub mod scene;
|
|
||||||
pub use scene::{Border, CursorRegion, MouseRegion, MouseRegionId, Quad, Scene, SceneBuilder};
|
|
||||||
pub mod text_layout;
|
|
||||||
pub use text_layout::TextLayoutCache;
|
|
||||||
mod util;
|
mod util;
|
||||||
pub use elements::{AnyElement, Element};
|
mod view;
|
||||||
pub mod executor;
|
mod window;
|
||||||
pub use executor::Task;
|
|
||||||
pub mod color;
|
|
||||||
pub mod json;
|
|
||||||
pub mod keymap_matcher;
|
|
||||||
pub mod platform;
|
|
||||||
pub use gpui_macros::{test, Element};
|
|
||||||
pub use usvg;
|
|
||||||
pub use window::{
|
|
||||||
Axis, Layout, LayoutEngine, LayoutId, RectFExt, SizeConstraint, Vector2FExt, WindowContext,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub use anyhow;
|
mod private {
|
||||||
|
/// A mechanism for restricting implementations of a trait to only those in GPUI.
|
||||||
|
/// See: https://predr.ag/blog/definitive-guide-to-sealed-traits-in-rust/
|
||||||
|
pub trait Sealed {}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub use action::*;
|
||||||
|
pub use anyhow::Result;
|
||||||
|
pub use app::*;
|
||||||
|
pub(crate) use arena::*;
|
||||||
|
pub use assets::*;
|
||||||
|
pub use color::*;
|
||||||
|
pub use ctor::ctor;
|
||||||
|
pub use element::*;
|
||||||
|
pub use elements::*;
|
||||||
|
pub use executor::*;
|
||||||
|
pub use geometry::*;
|
||||||
|
pub use gpui2_macros::*;
|
||||||
|
pub use image_cache::*;
|
||||||
|
pub use input::*;
|
||||||
|
pub use interactive::*;
|
||||||
|
pub use key_dispatch::*;
|
||||||
|
pub use keymap::*;
|
||||||
|
pub use linkme;
|
||||||
|
pub use platform::*;
|
||||||
|
use private::Sealed;
|
||||||
|
pub use refineable::*;
|
||||||
|
pub use scene::*;
|
||||||
|
pub use serde;
|
||||||
|
pub use serde_derive;
|
||||||
pub use serde_json;
|
pub use serde_json;
|
||||||
|
pub use shared_string::*;
|
||||||
|
pub use smallvec;
|
||||||
|
pub use smol::Timer;
|
||||||
|
pub use style::*;
|
||||||
|
pub use styled::*;
|
||||||
|
pub use subscription::*;
|
||||||
|
pub use svg_renderer::*;
|
||||||
|
pub use taffy::{AvailableSpace, LayoutId};
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub use test::*;
|
||||||
|
pub use text_system::*;
|
||||||
|
pub use util::arc_cow::ArcCow;
|
||||||
|
pub use view::*;
|
||||||
|
pub use window::*;
|
||||||
|
|
||||||
actions!(zed, [NoAction]);
|
use std::{
|
||||||
|
any::{Any, TypeId},
|
||||||
|
borrow::BorrowMut,
|
||||||
|
};
|
||||||
|
use taffy::TaffyLayoutEngine;
|
||||||
|
|
||||||
|
pub trait Context {
|
||||||
|
type Result<T>;
|
||||||
|
|
||||||
|
fn new_model<T: 'static>(
|
||||||
|
&mut self,
|
||||||
|
build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T,
|
||||||
|
) -> Self::Result<Model<T>>;
|
||||||
|
|
||||||
|
fn update_model<T, R>(
|
||||||
|
&mut self,
|
||||||
|
handle: &Model<T>,
|
||||||
|
update: impl FnOnce(&mut T, &mut ModelContext<'_, T>) -> R,
|
||||||
|
) -> Self::Result<R>
|
||||||
|
where
|
||||||
|
T: 'static;
|
||||||
|
|
||||||
|
fn read_model<T, R>(
|
||||||
|
&self,
|
||||||
|
handle: &Model<T>,
|
||||||
|
read: impl FnOnce(&T, &AppContext) -> R,
|
||||||
|
) -> Self::Result<R>
|
||||||
|
where
|
||||||
|
T: 'static;
|
||||||
|
|
||||||
|
fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Result<T>
|
||||||
|
where
|
||||||
|
F: FnOnce(AnyView, &mut WindowContext<'_>) -> T;
|
||||||
|
|
||||||
|
fn read_window<T, R>(
|
||||||
|
&self,
|
||||||
|
window: &WindowHandle<T>,
|
||||||
|
read: impl FnOnce(View<T>, &AppContext) -> R,
|
||||||
|
) -> Result<R>
|
||||||
|
where
|
||||||
|
T: 'static;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait VisualContext: Context {
|
||||||
|
fn new_view<V>(
|
||||||
|
&mut self,
|
||||||
|
build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V,
|
||||||
|
) -> Self::Result<View<V>>
|
||||||
|
where
|
||||||
|
V: 'static + Render;
|
||||||
|
|
||||||
|
fn update_view<V: 'static, R>(
|
||||||
|
&mut self,
|
||||||
|
view: &View<V>,
|
||||||
|
update: impl FnOnce(&mut V, &mut ViewContext<'_, V>) -> R,
|
||||||
|
) -> Self::Result<R>;
|
||||||
|
|
||||||
|
fn replace_root_view<V>(
|
||||||
|
&mut self,
|
||||||
|
build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V,
|
||||||
|
) -> Self::Result<View<V>>
|
||||||
|
where
|
||||||
|
V: 'static + Render;
|
||||||
|
|
||||||
|
fn focus_view<V>(&mut self, view: &View<V>) -> Self::Result<()>
|
||||||
|
where
|
||||||
|
V: FocusableView;
|
||||||
|
|
||||||
|
fn dismiss_view<V>(&mut self, view: &View<V>) -> Self::Result<()>
|
||||||
|
where
|
||||||
|
V: ManagedView;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Entity<T>: Sealed {
|
||||||
|
type Weak: 'static;
|
||||||
|
|
||||||
|
fn entity_id(&self) -> EntityId;
|
||||||
|
fn downgrade(&self) -> Self::Weak;
|
||||||
|
fn upgrade_from(weak: &Self::Weak) -> Option<Self>
|
||||||
|
where
|
||||||
|
Self: Sized;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait EventEmitter<E: Any>: 'static {}
|
||||||
|
|
||||||
|
pub enum GlobalKey {
|
||||||
|
Numeric(usize),
|
||||||
|
View(EntityId),
|
||||||
|
Type(TypeId),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait BorrowAppContext {
|
||||||
|
fn with_text_style<F, R>(&mut self, style: Option<TextStyleRefinement>, f: F) -> R
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut Self) -> R;
|
||||||
|
|
||||||
|
fn set_global<T: 'static>(&mut self, global: T);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C> BorrowAppContext for C
|
||||||
|
where
|
||||||
|
C: BorrowMut<AppContext>,
|
||||||
|
{
|
||||||
|
fn with_text_style<F, R>(&mut self, style: Option<TextStyleRefinement>, f: F) -> R
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut Self) -> R,
|
||||||
|
{
|
||||||
|
if let Some(style) = style {
|
||||||
|
self.borrow_mut().push_text_style(style);
|
||||||
|
let result = f(self);
|
||||||
|
self.borrow_mut().pop_text_style();
|
||||||
|
result
|
||||||
|
} else {
|
||||||
|
f(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_global<G: 'static>(&mut self, global: G) {
|
||||||
|
self.borrow_mut().set_global(global)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Flatten<T> {
|
||||||
|
fn flatten(self) -> Result<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Flatten<T> for Result<Result<T>> {
|
||||||
|
fn flatten(self) -> Result<T> {
|
||||||
|
self?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Flatten<T> for Result<T> {
|
||||||
|
fn flatten(self) -> Result<T> {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,18 +1,19 @@
|
|||||||
use std::sync::Arc;
|
use crate::{ImageData, ImageId, SharedString};
|
||||||
|
|
||||||
use crate::ImageData;
|
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use futures::{
|
use futures::{
|
||||||
future::{BoxFuture, Shared},
|
future::{BoxFuture, Shared},
|
||||||
AsyncReadExt, FutureExt,
|
AsyncReadExt, FutureExt, TryFutureExt,
|
||||||
};
|
};
|
||||||
use image::ImageError;
|
use image::ImageError;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
use std::sync::Arc;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use util::{
|
use util::http::{self, HttpClient};
|
||||||
arc_cow::ArcCow,
|
|
||||||
http::{self, HttpClient},
|
#[derive(PartialEq, Eq, Hash, Clone)]
|
||||||
};
|
pub struct RenderImageParams {
|
||||||
|
pub(crate) image_id: ImageId,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error, Clone)]
|
#[derive(Debug, Error, Clone)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
@ -43,7 +44,7 @@ impl From<ImageError> for Error {
|
|||||||
|
|
||||||
pub struct ImageCache {
|
pub struct ImageCache {
|
||||||
client: Arc<dyn HttpClient>,
|
client: Arc<dyn HttpClient>,
|
||||||
images: Arc<Mutex<HashMap<ArcCow<'static, str>, FetchImageFuture>>>,
|
images: Arc<Mutex<HashMap<SharedString, FetchImageFuture>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
type FetchImageFuture = Shared<BoxFuture<'static, Result<Arc<ImageData>, Error>>>;
|
type FetchImageFuture = Shared<BoxFuture<'static, Result<Arc<ImageData>, Error>>>;
|
||||||
@ -58,12 +59,12 @@ impl ImageCache {
|
|||||||
|
|
||||||
pub fn get(
|
pub fn get(
|
||||||
&self,
|
&self,
|
||||||
uri: impl Into<ArcCow<'static, str>>,
|
uri: impl Into<SharedString>,
|
||||||
) -> Shared<BoxFuture<'static, Result<Arc<ImageData>, Error>>> {
|
) -> Shared<BoxFuture<'static, Result<Arc<ImageData>, Error>>> {
|
||||||
let uri = uri.into();
|
let uri = uri.into();
|
||||||
let mut images = self.images.lock();
|
let mut images = self.images.lock();
|
||||||
|
|
||||||
match images.get(uri.as_ref()) {
|
match images.get(&uri) {
|
||||||
Some(future) => future.clone(),
|
Some(future) => future.clone(),
|
||||||
None => {
|
None => {
|
||||||
let client = self.client.clone();
|
let client = self.client.clone();
|
||||||
@ -84,9 +85,17 @@ impl ImageCache {
|
|||||||
let format = image::guess_format(&body)?;
|
let format = image::guess_format(&body)?;
|
||||||
let image =
|
let image =
|
||||||
image::load_from_memory_with_format(&body, format)?.into_bgra8();
|
image::load_from_memory_with_format(&body, format)?.into_bgra8();
|
||||||
Ok(ImageData::new(image))
|
Ok(Arc::new(ImageData::new(image)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.map_err({
|
||||||
|
let uri = uri.clone();
|
||||||
|
|
||||||
|
move |error| {
|
||||||
|
log::log!(log::Level::Error, "{:?} {:?}", &uri, &error);
|
||||||
|
error
|
||||||
|
}
|
||||||
|
})
|
||||||
.boxed()
|
.boxed()
|
||||||
.shared();
|
.shared();
|
||||||
|
|
||||||
|
@ -1,43 +0,0 @@
|
|||||||
use crate::geometry::vector::{vec2i, Vector2I};
|
|
||||||
use image::{Bgra, ImageBuffer};
|
|
||||||
use std::{
|
|
||||||
fmt,
|
|
||||||
sync::{
|
|
||||||
atomic::{AtomicUsize, Ordering::SeqCst},
|
|
||||||
Arc,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct ImageData {
|
|
||||||
pub id: usize,
|
|
||||||
data: ImageBuffer<Bgra<u8>, Vec<u8>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ImageData {
|
|
||||||
pub fn new(data: ImageBuffer<Bgra<u8>, Vec<u8>>) -> Arc<Self> {
|
|
||||||
static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
|
|
||||||
|
|
||||||
Arc::new(Self {
|
|
||||||
id: NEXT_ID.fetch_add(1, SeqCst),
|
|
||||||
data,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn as_bytes(&self) -> &[u8] {
|
|
||||||
&self.data
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn size(&self) -> Vector2I {
|
|
||||||
let (width, height) = self.data.dimensions();
|
|
||||||
vec2i(width as i32, height as i32)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Debug for ImageData {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
f.debug_struct("ImageData")
|
|
||||||
.field("id", &self.id)
|
|
||||||
.field("size", &self.data.dimensions())
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
pub use serde_json::*;
|
|
||||||
|
|
||||||
pub trait ToJson {
|
|
||||||
fn to_json(&self) -> Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: ToJson> ToJson for Option<T> {
|
|
||||||
fn to_json(&self) -> Value {
|
|
||||||
if let Some(value) = self.as_ref() {
|
|
||||||
value.to_json()
|
|
||||||
} else {
|
|
||||||
json!(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,587 +0,0 @@
|
|||||||
mod binding;
|
|
||||||
mod keymap;
|
|
||||||
mod keymap_context;
|
|
||||||
mod keystroke;
|
|
||||||
|
|
||||||
use std::{any::TypeId, fmt::Debug};
|
|
||||||
|
|
||||||
use collections::HashMap;
|
|
||||||
use smallvec::SmallVec;
|
|
||||||
|
|
||||||
use crate::{Action, NoAction};
|
|
||||||
|
|
||||||
pub use binding::{Binding, BindingMatchResult};
|
|
||||||
pub use keymap::Keymap;
|
|
||||||
pub use keymap_context::{KeymapContext, KeymapContextPredicate};
|
|
||||||
pub use keystroke::Keystroke;
|
|
||||||
|
|
||||||
pub struct KeymapMatcher {
|
|
||||||
pub contexts: Vec<KeymapContext>,
|
|
||||||
pending_views: HashMap<usize, KeymapContext>,
|
|
||||||
pending_keystrokes: Vec<Keystroke>,
|
|
||||||
keymap: Keymap,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl KeymapMatcher {
|
|
||||||
pub fn new(keymap: Keymap) -> Self {
|
|
||||||
Self {
|
|
||||||
contexts: Vec::new(),
|
|
||||||
pending_views: Default::default(),
|
|
||||||
pending_keystrokes: Vec::new(),
|
|
||||||
keymap,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_keymap(&mut self, keymap: Keymap) {
|
|
||||||
self.clear_pending();
|
|
||||||
self.keymap = keymap;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_bindings<T: IntoIterator<Item = Binding>>(&mut self, bindings: T) {
|
|
||||||
self.clear_pending();
|
|
||||||
self.keymap.add_bindings(bindings);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clear_bindings(&mut self) {
|
|
||||||
self.clear_pending();
|
|
||||||
self.keymap.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn bindings_for_action(&self, action_id: TypeId) -> impl Iterator<Item = &Binding> {
|
|
||||||
self.keymap.bindings_for_action(action_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clear_pending(&mut self) {
|
|
||||||
self.pending_keystrokes.clear();
|
|
||||||
self.pending_views.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn has_pending_keystrokes(&self) -> bool {
|
|
||||||
!self.pending_keystrokes.is_empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Pushes a keystroke onto the matcher.
|
|
||||||
/// The result of the new keystroke is returned:
|
|
||||||
/// MatchResult::None =>
|
|
||||||
/// No match is valid for this key given any pending keystrokes.
|
|
||||||
/// MatchResult::Pending =>
|
|
||||||
/// There exist bindings which are still waiting for more keys.
|
|
||||||
/// MatchResult::Complete(matches) =>
|
|
||||||
/// 1 or more bindings have received the necessary key presses.
|
|
||||||
/// The order of the matched actions is by position of the matching first,
|
|
||||||
// and order in the keymap second.
|
|
||||||
pub fn push_keystroke(
|
|
||||||
&mut self,
|
|
||||||
keystroke: Keystroke,
|
|
||||||
mut dispatch_path: Vec<(usize, KeymapContext)>,
|
|
||||||
) -> MatchResult {
|
|
||||||
// Collect matched bindings into an ordered list using the position in the matching binding first,
|
|
||||||
// and then the order the binding matched in the view tree second.
|
|
||||||
// The key is the reverse position of the binding in the bindings list so that later bindings
|
|
||||||
// match before earlier ones in the user's config
|
|
||||||
let mut matched_bindings: Vec<(usize, Box<dyn Action>)> = Default::default();
|
|
||||||
let no_action_id = (NoAction {}).id();
|
|
||||||
|
|
||||||
let first_keystroke = self.pending_keystrokes.is_empty();
|
|
||||||
let mut pending_key = None;
|
|
||||||
let mut previous_keystrokes = self.pending_keystrokes.clone();
|
|
||||||
|
|
||||||
self.contexts.clear();
|
|
||||||
self.contexts
|
|
||||||
.extend(dispatch_path.iter_mut().map(|e| std::mem::take(&mut e.1)));
|
|
||||||
|
|
||||||
// Find the bindings which map the pending keystrokes and current context
|
|
||||||
for (i, (view_id, _)) in dispatch_path.iter().enumerate() {
|
|
||||||
// Don't require pending view entry if there are no pending keystrokes
|
|
||||||
if !first_keystroke && !self.pending_views.contains_key(view_id) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there is a previous view context, invalidate that view if it
|
|
||||||
// has changed
|
|
||||||
if let Some(previous_view_context) = self.pending_views.remove(view_id) {
|
|
||||||
if previous_view_context != self.contexts[i] {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for binding in self.keymap.bindings().iter().rev() {
|
|
||||||
for possibility in keystroke.match_possibilities() {
|
|
||||||
previous_keystrokes.push(possibility.clone());
|
|
||||||
match binding.match_keys_and_context(&previous_keystrokes, &self.contexts[i..])
|
|
||||||
{
|
|
||||||
BindingMatchResult::Complete(action) => {
|
|
||||||
if action.id() != no_action_id {
|
|
||||||
matched_bindings.push((*view_id, action));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BindingMatchResult::Partial => {
|
|
||||||
if pending_key == None || pending_key == Some(possibility.clone()) {
|
|
||||||
self.pending_views
|
|
||||||
.insert(*view_id, self.contexts[i].clone());
|
|
||||||
pending_key = Some(possibility)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
previous_keystrokes.pop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if pending_key.is_some() {
|
|
||||||
self.pending_keystrokes.push(pending_key.unwrap());
|
|
||||||
} else {
|
|
||||||
self.clear_pending();
|
|
||||||
}
|
|
||||||
|
|
||||||
if !matched_bindings.is_empty() {
|
|
||||||
// Collect the sorted matched bindings into the final vec for ease of use
|
|
||||||
// Matched bindings are in order by precedence
|
|
||||||
MatchResult::Matches(matched_bindings)
|
|
||||||
} else if !self.pending_keystrokes.is_empty() {
|
|
||||||
MatchResult::Pending
|
|
||||||
} else {
|
|
||||||
MatchResult::None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn keystrokes_for_action(
|
|
||||||
&self,
|
|
||||||
action: &dyn Action,
|
|
||||||
contexts: &[KeymapContext],
|
|
||||||
) -> Option<SmallVec<[Keystroke; 2]>> {
|
|
||||||
self.keymap
|
|
||||||
.bindings()
|
|
||||||
.iter()
|
|
||||||
.rev()
|
|
||||||
.find_map(|binding| binding.keystrokes_for_action(action, contexts))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for KeymapMatcher {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new(Keymap::default())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum MatchResult {
|
|
||||||
None,
|
|
||||||
Pending,
|
|
||||||
Matches(Vec<(usize, Box<dyn Action>)>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Debug for MatchResult {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
MatchResult::None => f.debug_struct("MatchResult::None").finish(),
|
|
||||||
MatchResult::Pending => f.debug_struct("MatchResult::Pending").finish(),
|
|
||||||
MatchResult::Matches(matches) => f
|
|
||||||
.debug_list()
|
|
||||||
.entries(
|
|
||||||
matches
|
|
||||||
.iter()
|
|
||||||
.map(|(view_id, action)| format!("{view_id}, {}", action.name())),
|
|
||||||
)
|
|
||||||
.finish(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialEq for MatchResult {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
match (self, other) {
|
|
||||||
(MatchResult::None, MatchResult::None) => true,
|
|
||||||
(MatchResult::Pending, MatchResult::Pending) => true,
|
|
||||||
(MatchResult::Matches(matches), MatchResult::Matches(other_matches)) => {
|
|
||||||
matches.len() == other_matches.len()
|
|
||||||
&& matches.iter().zip(other_matches.iter()).all(
|
|
||||||
|((view_id, action), (other_view_id, other_action))| {
|
|
||||||
view_id == other_view_id && action.eq(other_action.as_ref())
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Eq for MatchResult {}
|
|
||||||
|
|
||||||
impl Clone for MatchResult {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
match self {
|
|
||||||
MatchResult::None => MatchResult::None,
|
|
||||||
MatchResult::Pending => MatchResult::Pending,
|
|
||||||
MatchResult::Matches(matches) => MatchResult::Matches(
|
|
||||||
matches
|
|
||||||
.iter()
|
|
||||||
.map(|(view_id, action)| (*view_id, Action::boxed_clone(action.as_ref())))
|
|
||||||
.collect(),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use anyhow::Result;
|
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
use crate::{actions, impl_actions, keymap_matcher::KeymapContext};
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_keymap_and_view_ordering() -> Result<()> {
|
|
||||||
actions!(test, [EditorAction, ProjectPanelAction]);
|
|
||||||
|
|
||||||
let mut editor = KeymapContext::default();
|
|
||||||
editor.add_identifier("Editor");
|
|
||||||
|
|
||||||
let mut project_panel = KeymapContext::default();
|
|
||||||
project_panel.add_identifier("ProjectPanel");
|
|
||||||
|
|
||||||
// Editor 'deeper' in than project panel
|
|
||||||
let dispatch_path = vec![(2, editor), (1, project_panel)];
|
|
||||||
|
|
||||||
// But editor actions 'higher' up in keymap
|
|
||||||
let keymap = Keymap::new(vec![
|
|
||||||
Binding::new("left", EditorAction, Some("Editor")),
|
|
||||||
Binding::new("left", ProjectPanelAction, Some("ProjectPanel")),
|
|
||||||
]);
|
|
||||||
|
|
||||||
let mut matcher = KeymapMatcher::new(keymap);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
matcher.push_keystroke(Keystroke::parse("left")?, dispatch_path.clone()),
|
|
||||||
MatchResult::Matches(vec![
|
|
||||||
(2, Box::new(EditorAction)),
|
|
||||||
(1, Box::new(ProjectPanelAction)),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_push_keystroke() -> Result<()> {
|
|
||||||
actions!(test, [B, AB, C, D, DA, E, EF]);
|
|
||||||
|
|
||||||
let mut context1 = KeymapContext::default();
|
|
||||||
context1.add_identifier("1");
|
|
||||||
|
|
||||||
let mut context2 = KeymapContext::default();
|
|
||||||
context2.add_identifier("2");
|
|
||||||
|
|
||||||
let dispatch_path = vec![(2, context2), (1, context1)];
|
|
||||||
|
|
||||||
let keymap = Keymap::new(vec![
|
|
||||||
Binding::new("a b", AB, Some("1")),
|
|
||||||
Binding::new("b", B, Some("2")),
|
|
||||||
Binding::new("c", C, Some("2")),
|
|
||||||
Binding::new("d", D, Some("1")),
|
|
||||||
Binding::new("d", D, Some("2")),
|
|
||||||
Binding::new("d a", DA, Some("2")),
|
|
||||||
]);
|
|
||||||
|
|
||||||
let mut matcher = KeymapMatcher::new(keymap);
|
|
||||||
|
|
||||||
// Binding with pending prefix always takes precedence
|
|
||||||
assert_eq!(
|
|
||||||
matcher.push_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),
|
|
||||||
MatchResult::Pending,
|
|
||||||
);
|
|
||||||
// B alone doesn't match because a was pending, so AB is returned instead
|
|
||||||
assert_eq!(
|
|
||||||
matcher.push_keystroke(Keystroke::parse("b")?, dispatch_path.clone()),
|
|
||||||
MatchResult::Matches(vec![(1, Box::new(AB))]),
|
|
||||||
);
|
|
||||||
assert!(!matcher.has_pending_keystrokes());
|
|
||||||
|
|
||||||
// Without an a prefix, B is dispatched like expected
|
|
||||||
assert_eq!(
|
|
||||||
matcher.push_keystroke(Keystroke::parse("b")?, dispatch_path.clone()),
|
|
||||||
MatchResult::Matches(vec![(2, Box::new(B))]),
|
|
||||||
);
|
|
||||||
assert!(!matcher.has_pending_keystrokes());
|
|
||||||
|
|
||||||
// If a is prefixed, C will not be dispatched because there
|
|
||||||
// was a pending binding for it
|
|
||||||
assert_eq!(
|
|
||||||
matcher.push_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),
|
|
||||||
MatchResult::Pending,
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
matcher.push_keystroke(Keystroke::parse("c")?, dispatch_path.clone()),
|
|
||||||
MatchResult::None,
|
|
||||||
);
|
|
||||||
assert!(!matcher.has_pending_keystrokes());
|
|
||||||
|
|
||||||
// If a single keystroke matches multiple bindings in the tree
|
|
||||||
// all of them are returned so that we can fallback if the action
|
|
||||||
// handler decides to propagate the action
|
|
||||||
assert_eq!(
|
|
||||||
matcher.push_keystroke(Keystroke::parse("d")?, dispatch_path.clone()),
|
|
||||||
MatchResult::Matches(vec![(2, Box::new(D)), (1, Box::new(D))]),
|
|
||||||
);
|
|
||||||
|
|
||||||
// If none of the d action handlers consume the binding, a pending
|
|
||||||
// binding may then be used
|
|
||||||
assert_eq!(
|
|
||||||
matcher.push_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),
|
|
||||||
MatchResult::Matches(vec![(2, Box::new(DA))]),
|
|
||||||
);
|
|
||||||
assert!(!matcher.has_pending_keystrokes());
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_keystroke_parsing() -> Result<()> {
|
|
||||||
assert_eq!(
|
|
||||||
Keystroke::parse("ctrl-p")?,
|
|
||||||
Keystroke {
|
|
||||||
key: "p".into(),
|
|
||||||
ctrl: true,
|
|
||||||
alt: false,
|
|
||||||
shift: false,
|
|
||||||
cmd: false,
|
|
||||||
function: false,
|
|
||||||
ime_key: None,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
Keystroke::parse("alt-shift-down")?,
|
|
||||||
Keystroke {
|
|
||||||
key: "down".into(),
|
|
||||||
ctrl: false,
|
|
||||||
alt: true,
|
|
||||||
shift: true,
|
|
||||||
cmd: false,
|
|
||||||
function: false,
|
|
||||||
ime_key: None,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
Keystroke::parse("shift-cmd--")?,
|
|
||||||
Keystroke {
|
|
||||||
key: "-".into(),
|
|
||||||
ctrl: false,
|
|
||||||
alt: false,
|
|
||||||
shift: true,
|
|
||||||
cmd: true,
|
|
||||||
function: false,
|
|
||||||
ime_key: None,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_context_predicate_parsing() -> Result<()> {
|
|
||||||
use KeymapContextPredicate::*;
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
KeymapContextPredicate::parse("a && (b == c || d != e)")?,
|
|
||||||
And(
|
|
||||||
Box::new(Identifier("a".into())),
|
|
||||||
Box::new(Or(
|
|
||||||
Box::new(Equal("b".into(), "c".into())),
|
|
||||||
Box::new(NotEqual("d".into(), "e".into())),
|
|
||||||
))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
KeymapContextPredicate::parse("!a")?,
|
|
||||||
Not(Box::new(Identifier("a".into())),)
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_context_predicate_eval() {
|
|
||||||
let predicate = KeymapContextPredicate::parse("a && b || c == d").unwrap();
|
|
||||||
|
|
||||||
let mut context = KeymapContext::default();
|
|
||||||
context.add_identifier("a");
|
|
||||||
assert!(!predicate.eval(&[context]));
|
|
||||||
|
|
||||||
let mut context = KeymapContext::default();
|
|
||||||
context.add_identifier("a");
|
|
||||||
context.add_identifier("b");
|
|
||||||
assert!(predicate.eval(&[context]));
|
|
||||||
|
|
||||||
let mut context = KeymapContext::default();
|
|
||||||
context.add_identifier("a");
|
|
||||||
context.add_key("c", "x");
|
|
||||||
assert!(!predicate.eval(&[context]));
|
|
||||||
|
|
||||||
let mut context = KeymapContext::default();
|
|
||||||
context.add_identifier("a");
|
|
||||||
context.add_key("c", "d");
|
|
||||||
assert!(predicate.eval(&[context]));
|
|
||||||
|
|
||||||
let predicate = KeymapContextPredicate::parse("!a").unwrap();
|
|
||||||
assert!(predicate.eval(&[KeymapContext::default()]));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_context_child_predicate_eval() {
|
|
||||||
let predicate = KeymapContextPredicate::parse("a && b > c").unwrap();
|
|
||||||
let contexts = [
|
|
||||||
context_set(&["e", "f"]),
|
|
||||||
context_set(&["c", "d"]), // match this context
|
|
||||||
context_set(&["a", "b"]),
|
|
||||||
];
|
|
||||||
|
|
||||||
assert!(!predicate.eval(&contexts[0..]));
|
|
||||||
assert!(predicate.eval(&contexts[1..]));
|
|
||||||
assert!(!predicate.eval(&contexts[2..]));
|
|
||||||
|
|
||||||
let predicate = KeymapContextPredicate::parse("a && b > c && !d > e").unwrap();
|
|
||||||
let contexts = [
|
|
||||||
context_set(&["f"]),
|
|
||||||
context_set(&["e"]), // only match this context
|
|
||||||
context_set(&["c"]),
|
|
||||||
context_set(&["a", "b"]),
|
|
||||||
context_set(&["e"]),
|
|
||||||
context_set(&["c", "d"]),
|
|
||||||
context_set(&["a", "b"]),
|
|
||||||
];
|
|
||||||
|
|
||||||
assert!(!predicate.eval(&contexts[0..]));
|
|
||||||
assert!(predicate.eval(&contexts[1..]));
|
|
||||||
assert!(!predicate.eval(&contexts[2..]));
|
|
||||||
assert!(!predicate.eval(&contexts[3..]));
|
|
||||||
assert!(!predicate.eval(&contexts[4..]));
|
|
||||||
assert!(!predicate.eval(&contexts[5..]));
|
|
||||||
assert!(!predicate.eval(&contexts[6..]));
|
|
||||||
|
|
||||||
fn context_set(names: &[&str]) -> KeymapContext {
|
|
||||||
let mut keymap = KeymapContext::new();
|
|
||||||
names
|
|
||||||
.iter()
|
|
||||||
.for_each(|name| keymap.add_identifier(name.to_string()));
|
|
||||||
keymap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_matcher() -> Result<()> {
|
|
||||||
#[derive(Clone, Deserialize, PartialEq, Eq, Debug)]
|
|
||||||
pub struct A(pub String);
|
|
||||||
impl_actions!(test, [A]);
|
|
||||||
actions!(test, [B, Ab, Dollar, Quote, Ess, Backtick]);
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
||||||
struct ActionArg {
|
|
||||||
a: &'static str,
|
|
||||||
}
|
|
||||||
|
|
||||||
let keymap = Keymap::new(vec![
|
|
||||||
Binding::new("a", A("x".to_string()), Some("a")),
|
|
||||||
Binding::new("b", B, Some("a")),
|
|
||||||
Binding::new("a b", Ab, Some("a || b")),
|
|
||||||
Binding::new("$", Dollar, Some("a")),
|
|
||||||
Binding::new("\"", Quote, Some("a")),
|
|
||||||
Binding::new("alt-s", Ess, Some("a")),
|
|
||||||
Binding::new("ctrl-`", Backtick, Some("a")),
|
|
||||||
]);
|
|
||||||
|
|
||||||
let mut context_a = KeymapContext::default();
|
|
||||||
context_a.add_identifier("a");
|
|
||||||
|
|
||||||
let mut context_b = KeymapContext::default();
|
|
||||||
context_b.add_identifier("b");
|
|
||||||
|
|
||||||
let mut matcher = KeymapMatcher::new(keymap);
|
|
||||||
|
|
||||||
// Basic match
|
|
||||||
assert_eq!(
|
|
||||||
matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, context_a.clone())]),
|
|
||||||
MatchResult::Matches(vec![(1, Box::new(A("x".to_string())))])
|
|
||||||
);
|
|
||||||
matcher.clear_pending();
|
|
||||||
|
|
||||||
// Multi-keystroke match
|
|
||||||
assert_eq!(
|
|
||||||
matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, context_b.clone())]),
|
|
||||||
MatchResult::Pending
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
matcher.push_keystroke(Keystroke::parse("b")?, vec![(1, context_b.clone())]),
|
|
||||||
MatchResult::Matches(vec![(1, Box::new(Ab))])
|
|
||||||
);
|
|
||||||
matcher.clear_pending();
|
|
||||||
|
|
||||||
// Failed matches don't interfere with matching subsequent keys
|
|
||||||
assert_eq!(
|
|
||||||
matcher.push_keystroke(Keystroke::parse("x")?, vec![(1, context_a.clone())]),
|
|
||||||
MatchResult::None
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, context_a.clone())]),
|
|
||||||
MatchResult::Matches(vec![(1, Box::new(A("x".to_string())))])
|
|
||||||
);
|
|
||||||
matcher.clear_pending();
|
|
||||||
|
|
||||||
// Pending keystrokes are cleared when the context changes
|
|
||||||
assert_eq!(
|
|
||||||
matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, context_b.clone())]),
|
|
||||||
MatchResult::Pending
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
matcher.push_keystroke(Keystroke::parse("b")?, vec![(1, context_a.clone())]),
|
|
||||||
MatchResult::None
|
|
||||||
);
|
|
||||||
matcher.clear_pending();
|
|
||||||
|
|
||||||
let mut context_c = KeymapContext::default();
|
|
||||||
context_c.add_identifier("c");
|
|
||||||
|
|
||||||
// Pending keystrokes are maintained per-view
|
|
||||||
assert_eq!(
|
|
||||||
matcher.push_keystroke(
|
|
||||||
Keystroke::parse("a")?,
|
|
||||||
vec![(1, context_b.clone()), (2, context_c.clone())]
|
|
||||||
),
|
|
||||||
MatchResult::Pending
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
matcher.push_keystroke(Keystroke::parse("b")?, vec![(1, context_b.clone())]),
|
|
||||||
MatchResult::Matches(vec![(1, Box::new(Ab))])
|
|
||||||
);
|
|
||||||
|
|
||||||
// handle Czech $ (option + 4 key)
|
|
||||||
assert_eq!(
|
|
||||||
matcher.push_keystroke(Keystroke::parse("alt-ç->$")?, vec![(1, context_a.clone())]),
|
|
||||||
MatchResult::Matches(vec![(1, Box::new(Dollar))])
|
|
||||||
);
|
|
||||||
|
|
||||||
// handle Brazillian quote (quote key then space key)
|
|
||||||
assert_eq!(
|
|
||||||
matcher.push_keystroke(Keystroke::parse("space->\"")?, vec![(1, context_a.clone())]),
|
|
||||||
MatchResult::Matches(vec![(1, Box::new(Quote))])
|
|
||||||
);
|
|
||||||
|
|
||||||
// handle ctrl+` on a brazillian keyboard
|
|
||||||
assert_eq!(
|
|
||||||
matcher.push_keystroke(Keystroke::parse("ctrl-->`")?, vec![(1, context_a.clone())]),
|
|
||||||
MatchResult::Matches(vec![(1, Box::new(Backtick))])
|
|
||||||
);
|
|
||||||
|
|
||||||
// handle alt-s on a US keyboard
|
|
||||||
assert_eq!(
|
|
||||||
matcher.push_keystroke(Keystroke::parse("alt-s->ß")?, vec![(1, context_a.clone())]),
|
|
||||||
MatchResult::Matches(vec![(1, Box::new(Ess))])
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,111 +0,0 @@
|
|||||||
use anyhow::Result;
|
|
||||||
use smallvec::SmallVec;
|
|
||||||
|
|
||||||
use crate::Action;
|
|
||||||
|
|
||||||
use super::{KeymapContext, KeymapContextPredicate, Keystroke};
|
|
||||||
|
|
||||||
pub struct Binding {
|
|
||||||
action: Box<dyn Action>,
|
|
||||||
pub(super) keystrokes: SmallVec<[Keystroke; 2]>,
|
|
||||||
pub(super) context_predicate: Option<KeymapContextPredicate>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Debug for Binding {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"Binding {{ keystrokes: {:?}, action: {}::{}, context_predicate: {:?} }}",
|
|
||||||
self.keystrokes,
|
|
||||||
self.action.namespace(),
|
|
||||||
self.action.name(),
|
|
||||||
self.context_predicate
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Clone for Binding {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
Self {
|
|
||||||
action: self.action.boxed_clone(),
|
|
||||||
keystrokes: self.keystrokes.clone(),
|
|
||||||
context_predicate: self.context_predicate.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Binding {
|
|
||||||
pub fn new<A: Action>(keystrokes: &str, action: A, context: Option<&str>) -> Self {
|
|
||||||
Self::load(keystrokes, Box::new(action), context).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn load(keystrokes: &str, action: Box<dyn Action>, context: Option<&str>) -> Result<Self> {
|
|
||||||
let context = if let Some(context) = context {
|
|
||||||
Some(KeymapContextPredicate::parse(context)?)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let keystrokes = keystrokes
|
|
||||||
.split_whitespace()
|
|
||||||
.map(Keystroke::parse)
|
|
||||||
.collect::<Result<_>>()?;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
keystrokes,
|
|
||||||
action,
|
|
||||||
context_predicate: context,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn match_context(&self, contexts: &[KeymapContext]) -> bool {
|
|
||||||
self.context_predicate
|
|
||||||
.as_ref()
|
|
||||||
.map(|predicate| predicate.eval(contexts))
|
|
||||||
.unwrap_or(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn match_keys_and_context(
|
|
||||||
&self,
|
|
||||||
pending_keystrokes: &Vec<Keystroke>,
|
|
||||||
contexts: &[KeymapContext],
|
|
||||||
) -> BindingMatchResult {
|
|
||||||
if self.keystrokes.as_ref().starts_with(&pending_keystrokes) && self.match_context(contexts)
|
|
||||||
{
|
|
||||||
// If the binding is completed, push it onto the matches list
|
|
||||||
if self.keystrokes.as_ref().len() == pending_keystrokes.len() {
|
|
||||||
BindingMatchResult::Complete(self.action.boxed_clone())
|
|
||||||
} else {
|
|
||||||
BindingMatchResult::Partial
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
BindingMatchResult::Fail
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn keystrokes_for_action(
|
|
||||||
&self,
|
|
||||||
action: &dyn Action,
|
|
||||||
contexts: &[KeymapContext],
|
|
||||||
) -> Option<SmallVec<[Keystroke; 2]>> {
|
|
||||||
if self.action.eq(action) && self.match_context(contexts) {
|
|
||||||
Some(self.keystrokes.clone())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn keystrokes(&self) -> &[Keystroke] {
|
|
||||||
self.keystrokes.as_slice()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn action(&self) -> &dyn Action {
|
|
||||||
self.action.as_ref()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum BindingMatchResult {
|
|
||||||
Complete(Box<dyn Action>),
|
|
||||||
Partial,
|
|
||||||
Fail,
|
|
||||||
}
|
|
@ -1,392 +0,0 @@
|
|||||||
use collections::HashSet;
|
|
||||||
use smallvec::SmallVec;
|
|
||||||
use std::{any::TypeId, collections::HashMap};
|
|
||||||
|
|
||||||
use crate::{Action, NoAction};
|
|
||||||
|
|
||||||
use super::{Binding, KeymapContextPredicate, Keystroke};
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct Keymap {
|
|
||||||
bindings: Vec<Binding>,
|
|
||||||
binding_indices_by_action_id: HashMap<TypeId, SmallVec<[usize; 3]>>,
|
|
||||||
disabled_keystrokes: HashMap<SmallVec<[Keystroke; 2]>, HashSet<Option<KeymapContextPredicate>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Keymap {
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(super) fn new(bindings: Vec<Binding>) -> Self {
|
|
||||||
let mut this = Self::default();
|
|
||||||
this.add_bindings(bindings);
|
|
||||||
this
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn bindings_for_action(
|
|
||||||
&self,
|
|
||||||
action_id: TypeId,
|
|
||||||
) -> impl Iterator<Item = &'_ Binding> {
|
|
||||||
self.binding_indices_by_action_id
|
|
||||||
.get(&action_id)
|
|
||||||
.map(SmallVec::as_slice)
|
|
||||||
.unwrap_or(&[])
|
|
||||||
.iter()
|
|
||||||
.map(|ix| &self.bindings[*ix])
|
|
||||||
.filter(|binding| !self.binding_disabled(binding))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn add_bindings<T: IntoIterator<Item = Binding>>(&mut self, bindings: T) {
|
|
||||||
let no_action_id = (NoAction {}).id();
|
|
||||||
let mut new_bindings = Vec::new();
|
|
||||||
let mut has_new_disabled_keystrokes = false;
|
|
||||||
for binding in bindings {
|
|
||||||
if binding.action().id() == no_action_id {
|
|
||||||
has_new_disabled_keystrokes |= self
|
|
||||||
.disabled_keystrokes
|
|
||||||
.entry(binding.keystrokes)
|
|
||||||
.or_default()
|
|
||||||
.insert(binding.context_predicate);
|
|
||||||
} else {
|
|
||||||
new_bindings.push(binding);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if has_new_disabled_keystrokes {
|
|
||||||
self.binding_indices_by_action_id.retain(|_, indices| {
|
|
||||||
indices.retain(|ix| {
|
|
||||||
let binding = &self.bindings[*ix];
|
|
||||||
match self.disabled_keystrokes.get(&binding.keystrokes) {
|
|
||||||
Some(disabled_predicates) => {
|
|
||||||
!disabled_predicates.contains(&binding.context_predicate)
|
|
||||||
}
|
|
||||||
None => true,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
!indices.is_empty()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for new_binding in new_bindings {
|
|
||||||
if !self.binding_disabled(&new_binding) {
|
|
||||||
self.binding_indices_by_action_id
|
|
||||||
.entry(new_binding.action().id())
|
|
||||||
.or_default()
|
|
||||||
.push(self.bindings.len());
|
|
||||||
self.bindings.push(new_binding);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn clear(&mut self) {
|
|
||||||
self.bindings.clear();
|
|
||||||
self.binding_indices_by_action_id.clear();
|
|
||||||
self.disabled_keystrokes.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn bindings(&self) -> Vec<&Binding> {
|
|
||||||
self.bindings
|
|
||||||
.iter()
|
|
||||||
.filter(|binding| !self.binding_disabled(binding))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn binding_disabled(&self, binding: &Binding) -> bool {
|
|
||||||
match self.disabled_keystrokes.get(&binding.keystrokes) {
|
|
||||||
Some(disabled_predicates) => disabled_predicates.contains(&binding.context_predicate),
|
|
||||||
None => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use crate::actions;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
actions!(
|
|
||||||
keymap_test,
|
|
||||||
[Present1, Present2, Present3, Duplicate, Missing]
|
|
||||||
);
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn regular_keymap() {
|
|
||||||
let present_1 = Binding::new("ctrl-q", Present1 {}, None);
|
|
||||||
let present_2 = Binding::new("ctrl-w", Present2 {}, Some("pane"));
|
|
||||||
let present_3 = Binding::new("ctrl-e", Present3 {}, Some("editor"));
|
|
||||||
let keystroke_duplicate_to_1 = Binding::new("ctrl-q", Duplicate {}, None);
|
|
||||||
let full_duplicate_to_2 = Binding::new("ctrl-w", Present2 {}, Some("pane"));
|
|
||||||
let missing = Binding::new("ctrl-r", Missing {}, None);
|
|
||||||
let all_bindings = [
|
|
||||||
&present_1,
|
|
||||||
&present_2,
|
|
||||||
&present_3,
|
|
||||||
&keystroke_duplicate_to_1,
|
|
||||||
&full_duplicate_to_2,
|
|
||||||
&missing,
|
|
||||||
];
|
|
||||||
|
|
||||||
let mut keymap = Keymap::default();
|
|
||||||
assert_absent(&keymap, &all_bindings);
|
|
||||||
assert!(keymap.bindings().is_empty());
|
|
||||||
|
|
||||||
keymap.add_bindings([present_1.clone(), present_2.clone(), present_3.clone()]);
|
|
||||||
assert_absent(&keymap, &[&keystroke_duplicate_to_1, &missing]);
|
|
||||||
assert_present(
|
|
||||||
&keymap,
|
|
||||||
&[(&present_1, "q"), (&present_2, "w"), (&present_3, "e")],
|
|
||||||
);
|
|
||||||
|
|
||||||
keymap.add_bindings([
|
|
||||||
keystroke_duplicate_to_1.clone(),
|
|
||||||
full_duplicate_to_2.clone(),
|
|
||||||
]);
|
|
||||||
assert_absent(&keymap, &[&missing]);
|
|
||||||
assert!(
|
|
||||||
!keymap.binding_disabled(&keystroke_duplicate_to_1),
|
|
||||||
"Duplicate binding 1 was added and should not be disabled"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
!keymap.binding_disabled(&full_duplicate_to_2),
|
|
||||||
"Duplicate binding 2 was added and should not be disabled"
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
keymap
|
|
||||||
.bindings_for_action(keystroke_duplicate_to_1.action().id())
|
|
||||||
.map(|binding| &binding.keystrokes)
|
|
||||||
.flatten()
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
vec![&Keystroke {
|
|
||||||
ctrl: true,
|
|
||||||
alt: false,
|
|
||||||
shift: false,
|
|
||||||
cmd: false,
|
|
||||||
function: false,
|
|
||||||
key: "q".to_string(),
|
|
||||||
ime_key: None,
|
|
||||||
}],
|
|
||||||
"{keystroke_duplicate_to_1:?} should have the expected keystroke in the keymap"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
keymap
|
|
||||||
.bindings_for_action(full_duplicate_to_2.action().id())
|
|
||||||
.map(|binding| &binding.keystrokes)
|
|
||||||
.flatten()
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
vec![
|
|
||||||
&Keystroke {
|
|
||||||
ctrl: true,
|
|
||||||
alt: false,
|
|
||||||
shift: false,
|
|
||||||
cmd: false,
|
|
||||||
function: false,
|
|
||||||
key: "w".to_string(),
|
|
||||||
ime_key: None,
|
|
||||||
},
|
|
||||||
&Keystroke {
|
|
||||||
ctrl: true,
|
|
||||||
alt: false,
|
|
||||||
shift: false,
|
|
||||||
cmd: false,
|
|
||||||
function: false,
|
|
||||||
key: "w".to_string(),
|
|
||||||
ime_key: None,
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"{full_duplicate_to_2:?} should have a duplicated keystroke in the keymap"
|
|
||||||
);
|
|
||||||
|
|
||||||
let updated_bindings = keymap.bindings();
|
|
||||||
let expected_updated_bindings = vec![
|
|
||||||
&present_1,
|
|
||||||
&present_2,
|
|
||||||
&present_3,
|
|
||||||
&keystroke_duplicate_to_1,
|
|
||||||
&full_duplicate_to_2,
|
|
||||||
];
|
|
||||||
assert_eq!(
|
|
||||||
updated_bindings.len(),
|
|
||||||
expected_updated_bindings.len(),
|
|
||||||
"Unexpected updated keymap bindings {updated_bindings:?}"
|
|
||||||
);
|
|
||||||
for (i, expected) in expected_updated_bindings.iter().enumerate() {
|
|
||||||
let keymap_binding = &updated_bindings[i];
|
|
||||||
assert_eq!(
|
|
||||||
keymap_binding.context_predicate, expected.context_predicate,
|
|
||||||
"Unexpected context predicate for keymap {i} element: {keymap_binding:?}"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
keymap_binding.keystrokes, expected.keystrokes,
|
|
||||||
"Unexpected keystrokes for keymap {i} element: {keymap_binding:?}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
keymap.clear();
|
|
||||||
assert_absent(&keymap, &all_bindings);
|
|
||||||
assert!(keymap.bindings().is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn keymap_with_ignored() {
|
|
||||||
let present_1 = Binding::new("ctrl-q", Present1 {}, None);
|
|
||||||
let present_2 = Binding::new("ctrl-w", Present2 {}, Some("pane"));
|
|
||||||
let present_3 = Binding::new("ctrl-e", Present3 {}, Some("editor"));
|
|
||||||
let keystroke_duplicate_to_1 = Binding::new("ctrl-q", Duplicate {}, None);
|
|
||||||
let full_duplicate_to_2 = Binding::new("ctrl-w", Present2 {}, Some("pane"));
|
|
||||||
let ignored_1 = Binding::new("ctrl-q", NoAction {}, None);
|
|
||||||
let ignored_2 = Binding::new("ctrl-w", NoAction {}, Some("pane"));
|
|
||||||
let ignored_3_with_other_context =
|
|
||||||
Binding::new("ctrl-e", NoAction {}, Some("other_context"));
|
|
||||||
|
|
||||||
let mut keymap = Keymap::default();
|
|
||||||
|
|
||||||
keymap.add_bindings([
|
|
||||||
ignored_1.clone(),
|
|
||||||
ignored_2.clone(),
|
|
||||||
ignored_3_with_other_context.clone(),
|
|
||||||
]);
|
|
||||||
assert_absent(&keymap, &[&present_3]);
|
|
||||||
assert_disabled(
|
|
||||||
&keymap,
|
|
||||||
&[
|
|
||||||
&present_1,
|
|
||||||
&present_2,
|
|
||||||
&ignored_1,
|
|
||||||
&ignored_2,
|
|
||||||
&ignored_3_with_other_context,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
assert!(keymap.bindings().is_empty());
|
|
||||||
keymap.clear();
|
|
||||||
|
|
||||||
keymap.add_bindings([
|
|
||||||
present_1.clone(),
|
|
||||||
present_2.clone(),
|
|
||||||
present_3.clone(),
|
|
||||||
ignored_1.clone(),
|
|
||||||
ignored_2.clone(),
|
|
||||||
ignored_3_with_other_context.clone(),
|
|
||||||
]);
|
|
||||||
assert_present(&keymap, &[(&present_3, "e")]);
|
|
||||||
assert_disabled(
|
|
||||||
&keymap,
|
|
||||||
&[
|
|
||||||
&present_1,
|
|
||||||
&present_2,
|
|
||||||
&ignored_1,
|
|
||||||
&ignored_2,
|
|
||||||
&ignored_3_with_other_context,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
keymap.clear();
|
|
||||||
|
|
||||||
keymap.add_bindings([
|
|
||||||
present_1.clone(),
|
|
||||||
present_2.clone(),
|
|
||||||
present_3.clone(),
|
|
||||||
ignored_1.clone(),
|
|
||||||
]);
|
|
||||||
assert_present(&keymap, &[(&present_2, "w"), (&present_3, "e")]);
|
|
||||||
assert_disabled(&keymap, &[&present_1, &ignored_1]);
|
|
||||||
assert_absent(&keymap, &[&ignored_2, &ignored_3_with_other_context]);
|
|
||||||
keymap.clear();
|
|
||||||
|
|
||||||
keymap.add_bindings([
|
|
||||||
present_1.clone(),
|
|
||||||
present_2.clone(),
|
|
||||||
present_3.clone(),
|
|
||||||
keystroke_duplicate_to_1.clone(),
|
|
||||||
full_duplicate_to_2.clone(),
|
|
||||||
ignored_1.clone(),
|
|
||||||
ignored_2.clone(),
|
|
||||||
ignored_3_with_other_context.clone(),
|
|
||||||
]);
|
|
||||||
assert_present(&keymap, &[(&present_3, "e")]);
|
|
||||||
assert_disabled(
|
|
||||||
&keymap,
|
|
||||||
&[
|
|
||||||
&present_1,
|
|
||||||
&present_2,
|
|
||||||
&keystroke_duplicate_to_1,
|
|
||||||
&full_duplicate_to_2,
|
|
||||||
&ignored_1,
|
|
||||||
&ignored_2,
|
|
||||||
&ignored_3_with_other_context,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
keymap.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[track_caller]
|
|
||||||
fn assert_present(keymap: &Keymap, expected_bindings: &[(&Binding, &str)]) {
|
|
||||||
let keymap_bindings = keymap.bindings();
|
|
||||||
assert_eq!(
|
|
||||||
expected_bindings.len(),
|
|
||||||
keymap_bindings.len(),
|
|
||||||
"Unexpected keymap bindings {keymap_bindings:?}"
|
|
||||||
);
|
|
||||||
for (i, (expected, expected_key)) in expected_bindings.iter().enumerate() {
|
|
||||||
assert!(
|
|
||||||
!keymap.binding_disabled(expected),
|
|
||||||
"{expected:?} should not be disabled as it was added into keymap for element {i}"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
keymap
|
|
||||||
.bindings_for_action(expected.action().id())
|
|
||||||
.map(|binding| &binding.keystrokes)
|
|
||||||
.flatten()
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
vec![&Keystroke {
|
|
||||||
ctrl: true,
|
|
||||||
alt: false,
|
|
||||||
shift: false,
|
|
||||||
cmd: false,
|
|
||||||
function: false,
|
|
||||||
key: expected_key.to_string(),
|
|
||||||
ime_key: None,
|
|
||||||
}],
|
|
||||||
"{expected:?} should have the expected keystroke with key '{expected_key}' in the keymap for element {i}"
|
|
||||||
);
|
|
||||||
|
|
||||||
let keymap_binding = &keymap_bindings[i];
|
|
||||||
assert_eq!(
|
|
||||||
keymap_binding.context_predicate, expected.context_predicate,
|
|
||||||
"Unexpected context predicate for keymap {i} element: {keymap_binding:?}"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
keymap_binding.keystrokes, expected.keystrokes,
|
|
||||||
"Unexpected keystrokes for keymap {i} element: {keymap_binding:?}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[track_caller]
|
|
||||||
fn assert_absent(keymap: &Keymap, bindings: &[&Binding]) {
|
|
||||||
for binding in bindings.iter() {
|
|
||||||
assert!(
|
|
||||||
!keymap.binding_disabled(binding),
|
|
||||||
"{binding:?} should not be disabled in the keymap where was not added"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
keymap.bindings_for_action(binding.action().id()).count(),
|
|
||||||
0,
|
|
||||||
"{binding:?} should have no actions in the keymap where was not added"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[track_caller]
|
|
||||||
fn assert_disabled(keymap: &Keymap, bindings: &[&Binding]) {
|
|
||||||
for binding in bindings.iter() {
|
|
||||||
assert!(
|
|
||||||
keymap.binding_disabled(binding),
|
|
||||||
"{binding:?} should be disabled in the keymap"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
keymap.bindings_for_action(binding.action().id()).count(),
|
|
||||||
0,
|
|
||||||
"{binding:?} should have no actions in the keymap where it was disabled"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,326 +0,0 @@
|
|||||||
use std::borrow::Cow;
|
|
||||||
|
|
||||||
use anyhow::{anyhow, Result};
|
|
||||||
use collections::{HashMap, HashSet};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
|
||||||
pub struct KeymapContext {
|
|
||||||
set: HashSet<Cow<'static, str>>,
|
|
||||||
map: HashMap<Cow<'static, str>, Cow<'static, str>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl KeymapContext {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
KeymapContext {
|
|
||||||
set: HashSet::default(),
|
|
||||||
map: HashMap::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clear(&mut self) {
|
|
||||||
self.set.clear();
|
|
||||||
self.map.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn extend(&mut self, other: &Self) {
|
|
||||||
for v in &other.set {
|
|
||||||
self.set.insert(v.clone());
|
|
||||||
}
|
|
||||||
for (k, v) in &other.map {
|
|
||||||
self.map.insert(k.clone(), v.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_identifier<I: Into<Cow<'static, str>>>(&mut self, identifier: I) {
|
|
||||||
self.set.insert(identifier.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_key<S1: Into<Cow<'static, str>>, S2: Into<Cow<'static, str>>>(
|
|
||||||
&mut self,
|
|
||||||
key: S1,
|
|
||||||
value: S2,
|
|
||||||
) {
|
|
||||||
self.map.insert(key.into(), value.into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
|
||||||
pub enum KeymapContextPredicate {
|
|
||||||
Identifier(String),
|
|
||||||
Equal(String, String),
|
|
||||||
NotEqual(String, String),
|
|
||||||
Child(Box<KeymapContextPredicate>, Box<KeymapContextPredicate>),
|
|
||||||
Not(Box<KeymapContextPredicate>),
|
|
||||||
And(Box<KeymapContextPredicate>, Box<KeymapContextPredicate>),
|
|
||||||
Or(Box<KeymapContextPredicate>, Box<KeymapContextPredicate>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl KeymapContextPredicate {
|
|
||||||
pub fn parse(source: &str) -> Result<Self> {
|
|
||||||
let source = Self::skip_whitespace(source);
|
|
||||||
let (predicate, rest) = Self::parse_expr(source, 0)?;
|
|
||||||
if let Some(next) = rest.chars().next() {
|
|
||||||
Err(anyhow!("unexpected character {next:?}"))
|
|
||||||
} else {
|
|
||||||
Ok(predicate)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn eval(&self, contexts: &[KeymapContext]) -> bool {
|
|
||||||
let Some(context) = contexts.first() else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
match self {
|
|
||||||
Self::Identifier(name) => (&context.set).contains(name.as_str()),
|
|
||||||
Self::Equal(left, right) => context
|
|
||||||
.map
|
|
||||||
.get(left.as_str())
|
|
||||||
.map(|value| value == right)
|
|
||||||
.unwrap_or(false),
|
|
||||||
Self::NotEqual(left, right) => context
|
|
||||||
.map
|
|
||||||
.get(left.as_str())
|
|
||||||
.map(|value| value != right)
|
|
||||||
.unwrap_or(true),
|
|
||||||
Self::Not(pred) => !pred.eval(contexts),
|
|
||||||
Self::Child(parent, child) => parent.eval(&contexts[1..]) && child.eval(contexts),
|
|
||||||
Self::And(left, right) => left.eval(contexts) && right.eval(contexts),
|
|
||||||
Self::Or(left, right) => left.eval(contexts) || right.eval(contexts),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_expr(mut source: &str, min_precedence: u32) -> anyhow::Result<(Self, &str)> {
|
|
||||||
type Op =
|
|
||||||
fn(KeymapContextPredicate, KeymapContextPredicate) -> Result<KeymapContextPredicate>;
|
|
||||||
|
|
||||||
let (mut predicate, rest) = Self::parse_primary(source)?;
|
|
||||||
source = rest;
|
|
||||||
|
|
||||||
'parse: loop {
|
|
||||||
for (operator, precedence, constructor) in [
|
|
||||||
(">", PRECEDENCE_CHILD, Self::new_child as Op),
|
|
||||||
("&&", PRECEDENCE_AND, Self::new_and as Op),
|
|
||||||
("||", PRECEDENCE_OR, Self::new_or as Op),
|
|
||||||
("==", PRECEDENCE_EQ, Self::new_eq as Op),
|
|
||||||
("!=", PRECEDENCE_EQ, Self::new_neq as Op),
|
|
||||||
] {
|
|
||||||
if source.starts_with(operator) && precedence >= min_precedence {
|
|
||||||
source = Self::skip_whitespace(&source[operator.len()..]);
|
|
||||||
let (right, rest) = Self::parse_expr(source, precedence + 1)?;
|
|
||||||
predicate = constructor(predicate, right)?;
|
|
||||||
source = rest;
|
|
||||||
continue 'parse;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok((predicate, source))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_primary(mut source: &str) -> anyhow::Result<(Self, &str)> {
|
|
||||||
let next = source
|
|
||||||
.chars()
|
|
||||||
.next()
|
|
||||||
.ok_or_else(|| anyhow!("unexpected eof"))?;
|
|
||||||
match next {
|
|
||||||
'(' => {
|
|
||||||
source = Self::skip_whitespace(&source[1..]);
|
|
||||||
let (predicate, rest) = Self::parse_expr(source, 0)?;
|
|
||||||
if rest.starts_with(')') {
|
|
||||||
source = Self::skip_whitespace(&rest[1..]);
|
|
||||||
Ok((predicate, source))
|
|
||||||
} else {
|
|
||||||
Err(anyhow!("expected a ')'"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
'!' => {
|
|
||||||
let source = Self::skip_whitespace(&source[1..]);
|
|
||||||
let (predicate, source) = Self::parse_expr(&source, PRECEDENCE_NOT)?;
|
|
||||||
Ok((KeymapContextPredicate::Not(Box::new(predicate)), source))
|
|
||||||
}
|
|
||||||
_ if next.is_alphanumeric() || next == '_' => {
|
|
||||||
let len = source
|
|
||||||
.find(|c: char| !(c.is_alphanumeric() || c == '_'))
|
|
||||||
.unwrap_or(source.len());
|
|
||||||
let (identifier, rest) = source.split_at(len);
|
|
||||||
source = Self::skip_whitespace(rest);
|
|
||||||
Ok((
|
|
||||||
KeymapContextPredicate::Identifier(identifier.into()),
|
|
||||||
source,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
_ => Err(anyhow!("unexpected character {next:?}")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn skip_whitespace(source: &str) -> &str {
|
|
||||||
let len = source
|
|
||||||
.find(|c: char| !c.is_whitespace())
|
|
||||||
.unwrap_or(source.len());
|
|
||||||
&source[len..]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn new_or(self, other: Self) -> Result<Self> {
|
|
||||||
Ok(Self::Or(Box::new(self), Box::new(other)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn new_and(self, other: Self) -> Result<Self> {
|
|
||||||
Ok(Self::And(Box::new(self), Box::new(other)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn new_child(self, other: Self) -> Result<Self> {
|
|
||||||
Ok(Self::Child(Box::new(self), Box::new(other)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn new_eq(self, other: Self) -> Result<Self> {
|
|
||||||
if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) {
|
|
||||||
Ok(Self::Equal(left, right))
|
|
||||||
} else {
|
|
||||||
Err(anyhow!("operands must be identifiers"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn new_neq(self, other: Self) -> Result<Self> {
|
|
||||||
if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) {
|
|
||||||
Ok(Self::NotEqual(left, right))
|
|
||||||
} else {
|
|
||||||
Err(anyhow!("operands must be identifiers"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const PRECEDENCE_CHILD: u32 = 1;
|
|
||||||
const PRECEDENCE_OR: u32 = 2;
|
|
||||||
const PRECEDENCE_AND: u32 = 3;
|
|
||||||
const PRECEDENCE_EQ: u32 = 4;
|
|
||||||
const PRECEDENCE_NOT: u32 = 5;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::KeymapContextPredicate::{self, *};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_identifiers() {
|
|
||||||
// Identifiers
|
|
||||||
assert_eq!(
|
|
||||||
KeymapContextPredicate::parse("abc12").unwrap(),
|
|
||||||
Identifier("abc12".into())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
KeymapContextPredicate::parse("_1a").unwrap(),
|
|
||||||
Identifier("_1a".into())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_negations() {
|
|
||||||
assert_eq!(
|
|
||||||
KeymapContextPredicate::parse("!abc").unwrap(),
|
|
||||||
Not(Box::new(Identifier("abc".into())))
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
KeymapContextPredicate::parse(" ! ! abc").unwrap(),
|
|
||||||
Not(Box::new(Not(Box::new(Identifier("abc".into())))))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_equality_operators() {
|
|
||||||
assert_eq!(
|
|
||||||
KeymapContextPredicate::parse("a == b").unwrap(),
|
|
||||||
Equal("a".into(), "b".into())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
KeymapContextPredicate::parse("c!=d").unwrap(),
|
|
||||||
NotEqual("c".into(), "d".into())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
KeymapContextPredicate::parse("c == !d")
|
|
||||||
.unwrap_err()
|
|
||||||
.to_string(),
|
|
||||||
"operands must be identifiers"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_boolean_operators() {
|
|
||||||
assert_eq!(
|
|
||||||
KeymapContextPredicate::parse("a || b").unwrap(),
|
|
||||||
Or(
|
|
||||||
Box::new(Identifier("a".into())),
|
|
||||||
Box::new(Identifier("b".into()))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
KeymapContextPredicate::parse("a || !b && c").unwrap(),
|
|
||||||
Or(
|
|
||||||
Box::new(Identifier("a".into())),
|
|
||||||
Box::new(And(
|
|
||||||
Box::new(Not(Box::new(Identifier("b".into())))),
|
|
||||||
Box::new(Identifier("c".into()))
|
|
||||||
))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
KeymapContextPredicate::parse("a && b || c&&d").unwrap(),
|
|
||||||
Or(
|
|
||||||
Box::new(And(
|
|
||||||
Box::new(Identifier("a".into())),
|
|
||||||
Box::new(Identifier("b".into()))
|
|
||||||
)),
|
|
||||||
Box::new(And(
|
|
||||||
Box::new(Identifier("c".into())),
|
|
||||||
Box::new(Identifier("d".into()))
|
|
||||||
))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
KeymapContextPredicate::parse("a == b && c || d == e && f").unwrap(),
|
|
||||||
Or(
|
|
||||||
Box::new(And(
|
|
||||||
Box::new(Equal("a".into(), "b".into())),
|
|
||||||
Box::new(Identifier("c".into()))
|
|
||||||
)),
|
|
||||||
Box::new(And(
|
|
||||||
Box::new(Equal("d".into(), "e".into())),
|
|
||||||
Box::new(Identifier("f".into()))
|
|
||||||
))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
KeymapContextPredicate::parse("a && b && c && d").unwrap(),
|
|
||||||
And(
|
|
||||||
Box::new(And(
|
|
||||||
Box::new(And(
|
|
||||||
Box::new(Identifier("a".into())),
|
|
||||||
Box::new(Identifier("b".into()))
|
|
||||||
)),
|
|
||||||
Box::new(Identifier("c".into())),
|
|
||||||
)),
|
|
||||||
Box::new(Identifier("d".into()))
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_parenthesized_expressions() {
|
|
||||||
assert_eq!(
|
|
||||||
KeymapContextPredicate::parse("a && (b == c || d != e)").unwrap(),
|
|
||||||
And(
|
|
||||||
Box::new(Identifier("a".into())),
|
|
||||||
Box::new(Or(
|
|
||||||
Box::new(Equal("b".into(), "c".into())),
|
|
||||||
Box::new(NotEqual("d".into(), "e".into())),
|
|
||||||
)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
KeymapContextPredicate::parse(" ( a || b ) ").unwrap(),
|
|
||||||
Or(
|
|
||||||
Box::new(Identifier("a".into())),
|
|
||||||
Box::new(Identifier("b".into())),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,141 +0,0 @@
|
|||||||
use std::fmt::Write;
|
|
||||||
|
|
||||||
use anyhow::anyhow;
|
|
||||||
use serde::Deserialize;
|
|
||||||
use smallvec::SmallVec;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
|
|
||||||
pub struct Keystroke {
|
|
||||||
pub ctrl: bool,
|
|
||||||
pub alt: bool,
|
|
||||||
pub shift: bool,
|
|
||||||
pub cmd: bool,
|
|
||||||
pub function: bool,
|
|
||||||
/// key is the character printed on the key that was pressed
|
|
||||||
/// e.g. for option-s, key is "s"
|
|
||||||
pub key: String,
|
|
||||||
/// ime_key is the character inserted by the IME engine when that key was pressed.
|
|
||||||
/// e.g. for option-s, ime_key is "ß"
|
|
||||||
pub ime_key: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Keystroke {
|
|
||||||
// When matching a key we cannot know whether the user intended to type
|
|
||||||
// the ime_key or the key. On some non-US keyboards keys we use in our
|
|
||||||
// bindings are behind option (for example `$` is typed `alt-ç` on a Czech keyboard),
|
|
||||||
// and on some keyboards the IME handler converts a sequence of keys into a
|
|
||||||
// specific character (for example `"` is typed as `" space` on a brazillian keyboard).
|
|
||||||
pub fn match_possibilities(&self) -> SmallVec<[Keystroke; 2]> {
|
|
||||||
let mut possibilities = SmallVec::new();
|
|
||||||
match self.ime_key.as_ref() {
|
|
||||||
None => possibilities.push(self.clone()),
|
|
||||||
Some(ime_key) => {
|
|
||||||
possibilities.push(Keystroke {
|
|
||||||
ctrl: self.ctrl,
|
|
||||||
alt: false,
|
|
||||||
shift: false,
|
|
||||||
cmd: false,
|
|
||||||
function: false,
|
|
||||||
key: ime_key.to_string(),
|
|
||||||
ime_key: None,
|
|
||||||
});
|
|
||||||
possibilities.push(Keystroke {
|
|
||||||
ime_key: None,
|
|
||||||
..self.clone()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
possibilities
|
|
||||||
}
|
|
||||||
|
|
||||||
/// key syntax is:
|
|
||||||
/// [ctrl-][alt-][shift-][cmd-][fn-]key[->ime_key]
|
|
||||||
/// ime_key is only used for generating test events,
|
|
||||||
/// when matching a key with an ime_key set will be matched without it.
|
|
||||||
pub fn parse(source: &str) -> anyhow::Result<Self> {
|
|
||||||
let mut ctrl = false;
|
|
||||||
let mut alt = false;
|
|
||||||
let mut shift = false;
|
|
||||||
let mut cmd = false;
|
|
||||||
let mut function = false;
|
|
||||||
let mut key = None;
|
|
||||||
let mut ime_key = None;
|
|
||||||
|
|
||||||
let mut components = source.split('-').peekable();
|
|
||||||
while let Some(component) = components.next() {
|
|
||||||
match component {
|
|
||||||
"ctrl" => ctrl = true,
|
|
||||||
"alt" => alt = true,
|
|
||||||
"shift" => shift = true,
|
|
||||||
"cmd" => cmd = true,
|
|
||||||
"fn" => function = true,
|
|
||||||
_ => {
|
|
||||||
if let Some(next) = components.peek() {
|
|
||||||
if next.is_empty() && source.ends_with('-') {
|
|
||||||
key = Some(String::from("-"));
|
|
||||||
break;
|
|
||||||
} else if next.len() > 1 && next.starts_with('>') {
|
|
||||||
key = Some(String::from(component));
|
|
||||||
ime_key = Some(String::from(&next[1..]));
|
|
||||||
components.next();
|
|
||||||
} else {
|
|
||||||
return Err(anyhow!("Invalid keystroke `{}`", source));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
key = Some(String::from(component));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let key = key.ok_or_else(|| anyhow!("Invalid keystroke `{}`", source))?;
|
|
||||||
|
|
||||||
Ok(Keystroke {
|
|
||||||
ctrl,
|
|
||||||
alt,
|
|
||||||
shift,
|
|
||||||
cmd,
|
|
||||||
function,
|
|
||||||
key,
|
|
||||||
ime_key,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn modified(&self) -> bool {
|
|
||||||
self.ctrl || self.alt || self.shift || self.cmd
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for Keystroke {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
if self.ctrl {
|
|
||||||
f.write_char('^')?;
|
|
||||||
}
|
|
||||||
if self.alt {
|
|
||||||
f.write_char('⌥')?;
|
|
||||||
}
|
|
||||||
if self.cmd {
|
|
||||||
f.write_char('⌘')?;
|
|
||||||
}
|
|
||||||
if self.shift {
|
|
||||||
f.write_char('⇧')?;
|
|
||||||
}
|
|
||||||
let key = match self.key.as_str() {
|
|
||||||
"backspace" => '⌫',
|
|
||||||
"up" => '↑',
|
|
||||||
"down" => '↓',
|
|
||||||
"left" => '←',
|
|
||||||
"right" => '→',
|
|
||||||
"tab" => '⇥',
|
|
||||||
"escape" => '⎋',
|
|
||||||
key => {
|
|
||||||
if key.len() == 1 {
|
|
||||||
key.chars().next().unwrap().to_ascii_uppercase()
|
|
||||||
} else {
|
|
||||||
return f.write_str(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
f.write_char(key)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,37 +1,27 @@
|
|||||||
mod event;
|
mod app_menu;
|
||||||
|
mod keystroke;
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
pub mod mac;
|
mod mac;
|
||||||
pub mod test;
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub mod current {
|
mod test;
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
pub use super::mac::*;
|
|
||||||
}
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
executor,
|
point, size, Action, AnyWindowHandle, BackgroundExecutor, Bounds, DevicePixels, Font, FontId,
|
||||||
fonts::{
|
FontMetrics, FontRun, ForegroundExecutor, GlobalPixels, GlyphId, InputEvent, Keymap,
|
||||||
Features as FontFeatures, FontId, GlyphId, Metrics as FontMetrics,
|
LineLayout, Pixels, Point, RenderGlyphParams, RenderImageParams, RenderSvgParams, Result,
|
||||||
Properties as FontProperties,
|
Scene, SharedString, Size, TaskLabel,
|
||||||
},
|
|
||||||
geometry::{
|
|
||||||
rect::{RectF, RectI},
|
|
||||||
vector::Vector2F,
|
|
||||||
},
|
|
||||||
keymap_matcher::KeymapMatcher,
|
|
||||||
text_layout::{LineLayout, RunStyle},
|
|
||||||
Action, AnyWindowHandle, ClipboardItem, Menu, Scene,
|
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, bail, Result};
|
use anyhow::{anyhow, bail};
|
||||||
use async_task::Runnable;
|
use async_task::Runnable;
|
||||||
pub use event::*;
|
use futures::channel::oneshot;
|
||||||
use pathfinder_geometry::vector::vec2f;
|
use parking::Unparker;
|
||||||
use postage::oneshot;
|
use seahash::SeaHasher;
|
||||||
use schemars::JsonSchema;
|
use serde::{Deserialize, Serialize};
|
||||||
use serde::Deserialize;
|
use sqlez::bindable::{Bind, Column, StaticColumnCount};
|
||||||
use sqlez::{
|
use sqlez::statement::Statement;
|
||||||
bindable::{Bind, Column, StaticColumnCount},
|
use std::borrow::Cow;
|
||||||
statement::Statement,
|
use std::hash::{Hash, Hasher};
|
||||||
};
|
use std::time::Duration;
|
||||||
use std::{
|
use std::{
|
||||||
any::Any,
|
any::Any,
|
||||||
fmt::{self, Debug, Display},
|
fmt::{self, Debug, Display},
|
||||||
@ -41,86 +31,294 @@ use std::{
|
|||||||
str::FromStr,
|
str::FromStr,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
use time::UtcOffset;
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub trait Platform: Send + Sync {
|
pub use app_menu::*;
|
||||||
fn dispatcher(&self) -> Arc<dyn Dispatcher>;
|
pub use keystroke::*;
|
||||||
fn fonts(&self) -> Arc<dyn FontSystem>;
|
#[cfg(target_os = "macos")]
|
||||||
|
pub use mac::*;
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub use test::*;
|
||||||
|
pub use time::UtcOffset;
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
pub(crate) fn current_platform() -> Rc<dyn Platform> {
|
||||||
|
Rc::new(MacPlatform::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type DrawWindow = Box<dyn FnMut() -> Result<Scene>>;
|
||||||
|
|
||||||
|
pub(crate) trait Platform: 'static {
|
||||||
|
fn background_executor(&self) -> BackgroundExecutor;
|
||||||
|
fn foreground_executor(&self) -> ForegroundExecutor;
|
||||||
|
fn text_system(&self) -> Arc<dyn PlatformTextSystem>;
|
||||||
|
|
||||||
|
fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>);
|
||||||
|
fn quit(&self);
|
||||||
|
fn restart(&self);
|
||||||
fn activate(&self, ignoring_other_apps: bool);
|
fn activate(&self, ignoring_other_apps: bool);
|
||||||
fn hide(&self);
|
fn hide(&self);
|
||||||
fn hide_other_apps(&self);
|
fn hide_other_apps(&self);
|
||||||
fn unhide_other_apps(&self);
|
fn unhide_other_apps(&self);
|
||||||
fn quit(&self);
|
|
||||||
|
|
||||||
fn screen_by_id(&self, id: Uuid) -> Option<Rc<dyn Screen>>;
|
|
||||||
fn screens(&self) -> Vec<Rc<dyn Screen>>;
|
|
||||||
|
|
||||||
|
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
|
||||||
|
fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>>;
|
||||||
|
fn active_window(&self) -> Option<AnyWindowHandle>;
|
||||||
fn open_window(
|
fn open_window(
|
||||||
&self,
|
&self,
|
||||||
handle: AnyWindowHandle,
|
handle: AnyWindowHandle,
|
||||||
options: WindowOptions,
|
options: WindowOptions,
|
||||||
executor: Rc<executor::Foreground>,
|
draw: DrawWindow,
|
||||||
) -> Box<dyn Window>;
|
) -> Box<dyn PlatformWindow>;
|
||||||
fn main_window(&self) -> Option<AnyWindowHandle>;
|
|
||||||
|
|
||||||
fn add_status_item(&self, handle: AnyWindowHandle) -> Box<dyn Window>;
|
fn set_display_link_output_callback(
|
||||||
|
&self,
|
||||||
|
display_id: DisplayId,
|
||||||
|
callback: Box<dyn FnMut(&VideoTimestamp, &VideoTimestamp) + Send>,
|
||||||
|
);
|
||||||
|
fn start_display_link(&self, display_id: DisplayId);
|
||||||
|
fn stop_display_link(&self, display_id: DisplayId);
|
||||||
|
// fn add_status_item(&self, _handle: AnyWindowHandle) -> Box<dyn PlatformWindow>;
|
||||||
|
|
||||||
fn write_to_clipboard(&self, item: ClipboardItem);
|
|
||||||
fn read_from_clipboard(&self) -> Option<ClipboardItem>;
|
|
||||||
fn open_url(&self, url: &str);
|
fn open_url(&self, url: &str);
|
||||||
|
|
||||||
fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Result<()>;
|
|
||||||
fn read_credentials(&self, url: &str) -> Result<Option<(String, Vec<u8>)>>;
|
|
||||||
fn delete_credentials(&self, url: &str) -> Result<()>;
|
|
||||||
|
|
||||||
fn set_cursor_style(&self, style: CursorStyle);
|
|
||||||
fn should_auto_hide_scrollbars(&self) -> bool;
|
|
||||||
|
|
||||||
fn local_timezone(&self) -> UtcOffset;
|
|
||||||
|
|
||||||
fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
|
|
||||||
fn app_path(&self) -> Result<PathBuf>;
|
|
||||||
fn app_version(&self) -> Result<AppVersion>;
|
|
||||||
fn os_name(&self) -> &'static str;
|
|
||||||
fn os_version(&self) -> Result<AppVersion>;
|
|
||||||
fn restart(&self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) trait ForegroundPlatform {
|
|
||||||
fn on_become_active(&self, callback: Box<dyn FnMut()>);
|
|
||||||
fn on_resign_active(&self, callback: Box<dyn FnMut()>);
|
|
||||||
fn on_quit(&self, callback: Box<dyn FnMut()>);
|
|
||||||
|
|
||||||
/// Handle the application being re-activated with no windows open.
|
|
||||||
fn on_reopen(&self, callback: Box<dyn FnMut()>);
|
|
||||||
|
|
||||||
fn on_event(&self, callback: Box<dyn FnMut(Event) -> bool>);
|
|
||||||
fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>);
|
fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>);
|
||||||
fn run(&self, on_finish_launching: Box<dyn FnOnce()>);
|
|
||||||
|
|
||||||
fn on_menu_command(&self, callback: Box<dyn FnMut(&dyn Action)>);
|
|
||||||
fn on_validate_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
|
|
||||||
fn on_will_open_menu(&self, callback: Box<dyn FnMut()>);
|
|
||||||
fn set_menus(&self, menus: Vec<Menu>, matcher: &KeymapMatcher);
|
|
||||||
fn prompt_for_paths(
|
fn prompt_for_paths(
|
||||||
&self,
|
&self,
|
||||||
options: PathPromptOptions,
|
options: PathPromptOptions,
|
||||||
) -> oneshot::Receiver<Option<Vec<PathBuf>>>;
|
) -> oneshot::Receiver<Option<Vec<PathBuf>>>;
|
||||||
fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>>;
|
fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>>;
|
||||||
fn reveal_path(&self, path: &Path);
|
fn reveal_path(&self, path: &Path);
|
||||||
|
|
||||||
|
fn on_become_active(&self, callback: Box<dyn FnMut()>);
|
||||||
|
fn on_resign_active(&self, callback: Box<dyn FnMut()>);
|
||||||
|
fn on_quit(&self, callback: Box<dyn FnMut()>);
|
||||||
|
fn on_reopen(&self, callback: Box<dyn FnMut()>);
|
||||||
|
fn on_event(&self, callback: Box<dyn FnMut(InputEvent) -> bool>);
|
||||||
|
|
||||||
|
fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
|
||||||
|
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
|
||||||
|
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
|
||||||
|
fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
|
||||||
|
|
||||||
|
fn os_name(&self) -> &'static str;
|
||||||
|
fn os_version(&self) -> Result<SemanticVersion>;
|
||||||
|
fn app_version(&self) -> Result<SemanticVersion>;
|
||||||
|
fn app_path(&self) -> Result<PathBuf>;
|
||||||
|
fn local_timezone(&self) -> UtcOffset;
|
||||||
|
fn double_click_interval(&self) -> Duration;
|
||||||
|
fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
|
||||||
|
|
||||||
|
fn set_cursor_style(&self, style: CursorStyle);
|
||||||
|
fn should_auto_hide_scrollbars(&self) -> bool;
|
||||||
|
|
||||||
|
fn write_to_clipboard(&self, item: ClipboardItem);
|
||||||
|
fn read_from_clipboard(&self) -> Option<ClipboardItem>;
|
||||||
|
|
||||||
|
fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Result<()>;
|
||||||
|
fn read_credentials(&self, url: &str) -> Result<Option<(String, Vec<u8>)>>;
|
||||||
|
fn delete_credentials(&self, url: &str) -> Result<()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Dispatcher: Send + Sync {
|
pub trait PlatformDisplay: Send + Sync + Debug {
|
||||||
|
fn id(&self) -> DisplayId;
|
||||||
|
/// Returns a stable identifier for this display that can be persisted and used
|
||||||
|
/// across system restarts.
|
||||||
|
fn uuid(&self) -> Result<Uuid>;
|
||||||
|
fn as_any(&self) -> &dyn Any;
|
||||||
|
fn bounds(&self) -> Bounds<GlobalPixels>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Hash, Copy, Clone)]
|
||||||
|
pub struct DisplayId(pub(crate) u32);
|
||||||
|
|
||||||
|
impl Debug for DisplayId {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "DisplayId({})", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl Send for DisplayId {}
|
||||||
|
|
||||||
|
pub trait PlatformWindow {
|
||||||
|
fn bounds(&self) -> WindowBounds;
|
||||||
|
fn content_size(&self) -> Size<Pixels>;
|
||||||
|
fn scale_factor(&self) -> f32;
|
||||||
|
fn titlebar_height(&self) -> Pixels;
|
||||||
|
fn appearance(&self) -> WindowAppearance;
|
||||||
|
fn display(&self) -> Rc<dyn PlatformDisplay>;
|
||||||
|
fn mouse_position(&self) -> Point<Pixels>;
|
||||||
|
fn modifiers(&self) -> Modifiers;
|
||||||
|
fn as_any_mut(&mut self) -> &mut dyn Any;
|
||||||
|
fn set_input_handler(&mut self, input_handler: Box<dyn PlatformInputHandler>);
|
||||||
|
fn clear_input_handler(&mut self);
|
||||||
|
fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver<usize>;
|
||||||
|
fn activate(&self);
|
||||||
|
fn set_title(&mut self, title: &str);
|
||||||
|
fn set_edited(&mut self, edited: bool);
|
||||||
|
fn show_character_palette(&self);
|
||||||
|
fn minimize(&self);
|
||||||
|
fn zoom(&self);
|
||||||
|
fn toggle_full_screen(&self);
|
||||||
|
fn on_input(&self, callback: Box<dyn FnMut(InputEvent) -> bool>);
|
||||||
|
fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>);
|
||||||
|
fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>);
|
||||||
|
fn on_fullscreen(&self, callback: Box<dyn FnMut(bool)>);
|
||||||
|
fn on_moved(&self, callback: Box<dyn FnMut()>);
|
||||||
|
fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>);
|
||||||
|
fn on_close(&self, callback: Box<dyn FnOnce()>);
|
||||||
|
fn on_appearance_changed(&self, callback: Box<dyn FnMut()>);
|
||||||
|
fn is_topmost_for_position(&self, position: Point<Pixels>) -> bool;
|
||||||
|
fn invalidate(&self);
|
||||||
|
|
||||||
|
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
fn as_test(&mut self) -> Option<&mut TestWindow> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait PlatformDispatcher: Send + Sync {
|
||||||
fn is_main_thread(&self) -> bool;
|
fn is_main_thread(&self) -> bool;
|
||||||
fn run_on_main_thread(&self, task: Runnable);
|
fn dispatch(&self, runnable: Runnable, label: Option<TaskLabel>);
|
||||||
|
fn dispatch_on_main_thread(&self, runnable: Runnable);
|
||||||
|
fn dispatch_after(&self, duration: Duration, runnable: Runnable);
|
||||||
|
fn tick(&self, background_only: bool) -> bool;
|
||||||
|
fn park(&self);
|
||||||
|
fn unparker(&self) -> Unparker;
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
fn as_test(&self) -> Option<&TestDispatcher> {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait InputHandler {
|
pub trait PlatformTextSystem: Send + Sync {
|
||||||
fn selected_text_range(&self) -> Option<Range<usize>>;
|
fn add_fonts(&self, fonts: &[Arc<Vec<u8>>]) -> Result<()>;
|
||||||
fn marked_text_range(&self) -> Option<Range<usize>>;
|
fn all_font_families(&self) -> Vec<String>;
|
||||||
fn text_for_range(&self, range_utf16: Range<usize>) -> Option<String>;
|
fn font_id(&self, descriptor: &Font) -> Result<FontId>;
|
||||||
|
fn font_metrics(&self, font_id: FontId) -> FontMetrics;
|
||||||
|
fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>>;
|
||||||
|
fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>>;
|
||||||
|
fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId>;
|
||||||
|
fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>>;
|
||||||
|
fn rasterize_glyph(
|
||||||
|
&self,
|
||||||
|
params: &RenderGlyphParams,
|
||||||
|
raster_bounds: Bounds<DevicePixels>,
|
||||||
|
) -> Result<(Size<DevicePixels>, Vec<u8>)>;
|
||||||
|
fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
|
||||||
|
fn wrap_line(
|
||||||
|
&self,
|
||||||
|
text: &str,
|
||||||
|
font_id: FontId,
|
||||||
|
font_size: Pixels,
|
||||||
|
width: Pixels,
|
||||||
|
) -> Vec<usize>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct AppMetadata {
|
||||||
|
pub os_name: &'static str,
|
||||||
|
pub os_version: Option<SemanticVersion>,
|
||||||
|
pub app_version: Option<SemanticVersion>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Hash, Clone)]
|
||||||
|
pub enum AtlasKey {
|
||||||
|
Glyph(RenderGlyphParams),
|
||||||
|
Svg(RenderSvgParams),
|
||||||
|
Image(RenderImageParams),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AtlasKey {
|
||||||
|
pub(crate) fn texture_kind(&self) -> AtlasTextureKind {
|
||||||
|
match self {
|
||||||
|
AtlasKey::Glyph(params) => {
|
||||||
|
if params.is_emoji {
|
||||||
|
AtlasTextureKind::Polychrome
|
||||||
|
} else {
|
||||||
|
AtlasTextureKind::Monochrome
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AtlasKey::Svg(_) => AtlasTextureKind::Monochrome,
|
||||||
|
AtlasKey::Image(_) => AtlasTextureKind::Polychrome,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RenderGlyphParams> for AtlasKey {
|
||||||
|
fn from(params: RenderGlyphParams) -> Self {
|
||||||
|
Self::Glyph(params)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RenderSvgParams> for AtlasKey {
|
||||||
|
fn from(params: RenderSvgParams) -> Self {
|
||||||
|
Self::Svg(params)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RenderImageParams> for AtlasKey {
|
||||||
|
fn from(params: RenderImageParams) -> Self {
|
||||||
|
Self::Image(params)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait PlatformAtlas: Send + Sync {
|
||||||
|
fn get_or_insert_with<'a>(
|
||||||
|
&self,
|
||||||
|
key: &AtlasKey,
|
||||||
|
build: &mut dyn FnMut() -> Result<(Size<DevicePixels>, Cow<'a, [u8]>)>,
|
||||||
|
) -> Result<AtlasTile>;
|
||||||
|
|
||||||
|
fn clear(&self);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct AtlasTile {
|
||||||
|
pub(crate) texture_id: AtlasTextureId,
|
||||||
|
pub(crate) tile_id: TileId,
|
||||||
|
pub(crate) bounds: Bounds<DevicePixels>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||||
|
#[repr(C)]
|
||||||
|
pub(crate) struct AtlasTextureId {
|
||||||
|
// We use u32 instead of usize for Metal Shader Language compatibility
|
||||||
|
pub(crate) index: u32,
|
||||||
|
pub(crate) kind: AtlasTextureKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||||
|
#[repr(C)]
|
||||||
|
pub(crate) enum AtlasTextureKind {
|
||||||
|
Monochrome = 0,
|
||||||
|
Polychrome = 1,
|
||||||
|
Path = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
#[repr(C)]
|
||||||
|
pub(crate) struct TileId(pub(crate) u32);
|
||||||
|
|
||||||
|
impl From<etagere::AllocId> for TileId {
|
||||||
|
fn from(id: etagere::AllocId) -> Self {
|
||||||
|
Self(id.serialize())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TileId> for etagere::AllocId {
|
||||||
|
fn from(id: TileId) -> Self {
|
||||||
|
Self::deserialize(id.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait PlatformInputHandler: 'static {
|
||||||
|
fn selected_text_range(&mut self) -> Option<Range<usize>>;
|
||||||
|
fn marked_text_range(&mut self) -> Option<Range<usize>>;
|
||||||
|
fn text_for_range(&mut self, range_utf16: Range<usize>) -> Option<String>;
|
||||||
fn replace_text_in_range(&mut self, replacement_range: Option<Range<usize>>, text: &str);
|
fn replace_text_in_range(&mut self, replacement_range: Option<Range<usize>>, text: &str);
|
||||||
fn replace_and_mark_text_in_range(
|
fn replace_and_mark_text_in_range(
|
||||||
&mut self,
|
&mut self,
|
||||||
@ -129,75 +327,45 @@ pub trait InputHandler {
|
|||||||
new_selected_range: Option<Range<usize>>,
|
new_selected_range: Option<Range<usize>>,
|
||||||
);
|
);
|
||||||
fn unmark_text(&mut self);
|
fn unmark_text(&mut self);
|
||||||
fn rect_for_range(&self, range_utf16: Range<usize>) -> Option<RectF>;
|
fn bounds_for_range(&mut self, range_utf16: Range<usize>) -> Option<Bounds<Pixels>>;
|
||||||
}
|
|
||||||
|
|
||||||
pub trait Screen: Debug {
|
|
||||||
fn as_any(&self) -> &dyn Any;
|
|
||||||
fn bounds(&self) -> RectF;
|
|
||||||
fn content_bounds(&self) -> RectF;
|
|
||||||
fn display_uuid(&self) -> Option<Uuid>;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait Window {
|
|
||||||
fn bounds(&self) -> WindowBounds;
|
|
||||||
fn content_size(&self) -> Vector2F;
|
|
||||||
fn scale_factor(&self) -> f32;
|
|
||||||
fn titlebar_height(&self) -> f32;
|
|
||||||
fn appearance(&self) -> Appearance;
|
|
||||||
fn screen(&self) -> Rc<dyn Screen>;
|
|
||||||
fn mouse_position(&self) -> Vector2F;
|
|
||||||
|
|
||||||
fn as_any_mut(&mut self) -> &mut dyn Any;
|
|
||||||
fn set_input_handler(&mut self, input_handler: Box<dyn InputHandler>);
|
|
||||||
fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver<usize>;
|
|
||||||
fn activate(&self);
|
|
||||||
fn set_title(&mut self, title: &str);
|
|
||||||
fn set_edited(&mut self, edited: bool);
|
|
||||||
fn show_character_palette(&self);
|
|
||||||
fn minimize(&self);
|
|
||||||
fn zoom(&self);
|
|
||||||
fn present_scene(&mut self, scene: Scene);
|
|
||||||
fn toggle_full_screen(&self);
|
|
||||||
|
|
||||||
fn on_event(&mut self, callback: Box<dyn FnMut(Event) -> bool>);
|
|
||||||
fn on_active_status_change(&mut self, callback: Box<dyn FnMut(bool)>);
|
|
||||||
fn on_resize(&mut self, callback: Box<dyn FnMut()>);
|
|
||||||
fn on_fullscreen(&mut self, callback: Box<dyn FnMut(bool)>);
|
|
||||||
fn on_moved(&mut self, callback: Box<dyn FnMut()>);
|
|
||||||
fn on_should_close(&mut self, callback: Box<dyn FnMut() -> bool>);
|
|
||||||
fn on_close(&mut self, callback: Box<dyn FnOnce()>);
|
|
||||||
fn on_appearance_changed(&mut self, callback: Box<dyn FnMut()>);
|
|
||||||
fn is_topmost_for_position(&self, position: Vector2F) -> bool;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct WindowOptions<'a> {
|
pub struct WindowOptions {
|
||||||
pub bounds: WindowBounds,
|
pub bounds: WindowBounds,
|
||||||
pub titlebar: Option<TitlebarOptions<'a>>,
|
pub titlebar: Option<TitlebarOptions>,
|
||||||
pub center: bool,
|
pub center: bool,
|
||||||
pub focus: bool,
|
pub focus: bool,
|
||||||
pub show: bool,
|
pub show: bool,
|
||||||
pub kind: WindowKind,
|
pub kind: WindowKind,
|
||||||
pub is_movable: bool,
|
pub is_movable: bool,
|
||||||
pub screen: Option<Rc<dyn Screen>>,
|
pub display_id: Option<DisplayId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> WindowOptions<'a> {
|
impl Default for WindowOptions {
|
||||||
pub fn with_bounds(bounds: Vector2F) -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
bounds: WindowBounds::Fixed(RectF::new(vec2f(0., 0.), bounds)),
|
bounds: WindowBounds::default(),
|
||||||
center: true,
|
titlebar: Some(TitlebarOptions {
|
||||||
..Default::default()
|
title: Default::default(),
|
||||||
|
appears_transparent: Default::default(),
|
||||||
|
traffic_light_position: Default::default(),
|
||||||
|
}),
|
||||||
|
center: false,
|
||||||
|
focus: true,
|
||||||
|
show: true,
|
||||||
|
kind: WindowKind::Normal,
|
||||||
|
is_movable: true,
|
||||||
|
display_id: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct TitlebarOptions<'a> {
|
pub struct TitlebarOptions {
|
||||||
pub title: Option<&'a str>,
|
pub title: Option<SharedString>,
|
||||||
pub appears_transparent: bool,
|
pub appears_transparent: bool,
|
||||||
pub traffic_light_position: Option<Vector2F>,
|
pub traffic_light_position: Option<Point<Pixels>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
@ -220,11 +388,12 @@ pub enum WindowKind {
|
|||||||
PopUp,
|
PopUp,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
#[derive(Copy, Clone, Debug, PartialEq, Default)]
|
||||||
pub enum WindowBounds {
|
pub enum WindowBounds {
|
||||||
Fullscreen,
|
Fullscreen,
|
||||||
|
#[default]
|
||||||
Maximized,
|
Maximized,
|
||||||
Fixed(RectF),
|
Fixed(Bounds<GlobalPixels>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StaticColumnCount for WindowBounds {
|
impl StaticColumnCount for WindowBounds {
|
||||||
@ -253,10 +422,10 @@ impl Bind for WindowBounds {
|
|||||||
statement.bind(
|
statement.bind(
|
||||||
®ion.map(|region| {
|
®ion.map(|region| {
|
||||||
(
|
(
|
||||||
region.min_x(),
|
region.origin.x,
|
||||||
region.min_y(),
|
region.origin.y,
|
||||||
region.width(),
|
region.size.width,
|
||||||
region.height(),
|
region.size.height,
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
next_index,
|
next_index,
|
||||||
@ -272,10 +441,14 @@ impl Column for WindowBounds {
|
|||||||
"Maximized" => WindowBounds::Maximized,
|
"Maximized" => WindowBounds::Maximized,
|
||||||
"Fixed" => {
|
"Fixed" => {
|
||||||
let ((x, y, width, height), _) = Column::column(statement, next_index)?;
|
let ((x, y, width, height), _) = Column::column(statement, next_index)?;
|
||||||
WindowBounds::Fixed(RectF::new(
|
let x: f64 = x;
|
||||||
Vector2F::new(x, y),
|
let y: f64 = y;
|
||||||
Vector2F::new(width, height),
|
let width: f64 = width;
|
||||||
))
|
let height: f64 = height;
|
||||||
|
WindowBounds::Fixed(Bounds {
|
||||||
|
origin: point(x.into(), y.into()),
|
||||||
|
size: size(width.into(), height.into()),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
_ => bail!("Window State did not have a valid string"),
|
_ => bail!("Window State did not have a valid string"),
|
||||||
};
|
};
|
||||||
@ -284,25 +457,55 @@ impl Column for WindowBounds {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
pub enum WindowAppearance {
|
||||||
|
Light,
|
||||||
|
VibrantLight,
|
||||||
|
Dark,
|
||||||
|
VibrantDark,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for WindowAppearance {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Light
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
pub struct PathPromptOptions {
|
pub struct PathPromptOptions {
|
||||||
pub files: bool,
|
pub files: bool,
|
||||||
pub directories: bool,
|
pub directories: bool,
|
||||||
pub multiple: bool,
|
pub multiple: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
pub enum PromptLevel {
|
pub enum PromptLevel {
|
||||||
Info,
|
Info,
|
||||||
Warning,
|
Warning,
|
||||||
Critical,
|
Critical,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Deserialize, JsonSchema)]
|
/// The style of the cursor (pointer)
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
pub enum CursorStyle {
|
pub enum CursorStyle {
|
||||||
Arrow,
|
Arrow,
|
||||||
ResizeLeftRight,
|
|
||||||
ResizeUpDown,
|
|
||||||
PointingHand,
|
|
||||||
IBeam,
|
IBeam,
|
||||||
|
Crosshair,
|
||||||
|
ClosedHand,
|
||||||
|
OpenHand,
|
||||||
|
PointingHand,
|
||||||
|
ResizeLeft,
|
||||||
|
ResizeRight,
|
||||||
|
ResizeLeftRight,
|
||||||
|
ResizeUp,
|
||||||
|
ResizeDown,
|
||||||
|
ResizeUpDown,
|
||||||
|
DisappearingItem,
|
||||||
|
IBeamCursorForVerticalLayout,
|
||||||
|
OperationNotAllowed,
|
||||||
|
DragLink,
|
||||||
|
DragCopy,
|
||||||
|
ContextualMenu,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for CursorStyle {
|
impl Default for CursorStyle {
|
||||||
@ -311,14 +514,14 @@ impl Default for CursorStyle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd, Serialize)]
|
||||||
pub struct AppVersion {
|
pub struct SemanticVersion {
|
||||||
major: usize,
|
major: usize,
|
||||||
minor: usize,
|
minor: usize,
|
||||||
patch: usize,
|
patch: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for AppVersion {
|
impl FromStr for SemanticVersion {
|
||||||
type Err = anyhow::Error;
|
type Err = anyhow::Error;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
@ -343,59 +546,47 @@ impl FromStr for AppVersion {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for AppVersion {
|
impl Display for SemanticVersion {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
|
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub enum RasterizationOptions {
|
pub struct ClipboardItem {
|
||||||
Alpha,
|
pub(crate) text: String,
|
||||||
Bgra,
|
pub(crate) metadata: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait FontSystem: Send + Sync {
|
impl ClipboardItem {
|
||||||
fn add_fonts(&self, fonts: &[Arc<Vec<u8>>]) -> anyhow::Result<()>;
|
pub fn new(text: String) -> Self {
|
||||||
fn all_families(&self) -> Vec<String>;
|
|
||||||
fn load_family(&self, name: &str, features: &FontFeatures) -> anyhow::Result<Vec<FontId>>;
|
|
||||||
fn select_font(
|
|
||||||
&self,
|
|
||||||
font_ids: &[FontId],
|
|
||||||
properties: &FontProperties,
|
|
||||||
) -> anyhow::Result<FontId>;
|
|
||||||
fn font_metrics(&self, font_id: FontId) -> FontMetrics;
|
|
||||||
fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> anyhow::Result<RectF>;
|
|
||||||
fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> anyhow::Result<Vector2F>;
|
|
||||||
fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId>;
|
|
||||||
fn rasterize_glyph(
|
|
||||||
&self,
|
|
||||||
font_id: FontId,
|
|
||||||
font_size: f32,
|
|
||||||
glyph_id: GlyphId,
|
|
||||||
subpixel_shift: Vector2F,
|
|
||||||
scale_factor: f32,
|
|
||||||
options: RasterizationOptions,
|
|
||||||
) -> Option<(RectI, Vec<u8>)>;
|
|
||||||
fn layout_line(&self, text: &str, font_size: f32, runs: &[(usize, RunStyle)]) -> LineLayout;
|
|
||||||
fn wrap_line(&self, text: &str, font_id: FontId, font_size: f32, width: f32) -> Vec<usize>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Default for WindowOptions<'a> {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
bounds: WindowBounds::Maximized,
|
text,
|
||||||
titlebar: Some(TitlebarOptions {
|
metadata: None,
|
||||||
title: Default::default(),
|
|
||||||
appears_transparent: Default::default(),
|
|
||||||
traffic_light_position: Default::default(),
|
|
||||||
}),
|
|
||||||
center: false,
|
|
||||||
focus: true,
|
|
||||||
show: true,
|
|
||||||
kind: WindowKind::Normal,
|
|
||||||
is_movable: true,
|
|
||||||
screen: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_metadata<T: Serialize>(mut self, metadata: T) -> Self {
|
||||||
|
self.metadata = Some(serde_json::to_string(&metadata).unwrap());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn text(&self) -> &String {
|
||||||
|
&self.text
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn metadata<T>(&self) -> Option<T>
|
||||||
|
where
|
||||||
|
T: for<'a> Deserialize<'a>,
|
||||||
|
{
|
||||||
|
self.metadata
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|m| serde_json::from_str(m).ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn text_hash(text: &str) -> u64 {
|
||||||
|
let mut hasher = SeaHasher::new();
|
||||||
|
text.hash(&mut hasher);
|
||||||
|
hasher.finish()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user