mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-07 20:39:04 +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",
|
||||
"editor",
|
||||
"futures 0.3.28",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"language",
|
||||
"project",
|
||||
"settings",
|
||||
@ -82,7 +82,7 @@ dependencies = [
|
||||
"async-trait",
|
||||
"bincode",
|
||||
"futures 0.3.28",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"isahc",
|
||||
"language",
|
||||
"lazy_static",
|
||||
@ -312,7 +312,7 @@ dependencies = [
|
||||
"env_logger",
|
||||
"fs",
|
||||
"futures 0.3.28",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"indoc",
|
||||
"isahc",
|
||||
"language",
|
||||
@ -662,7 +662,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"collections",
|
||||
"futures 0.3.28",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"log",
|
||||
"parking_lot 0.11.2",
|
||||
"rodio",
|
||||
@ -676,7 +676,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"db",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"isahc",
|
||||
"lazy_static",
|
||||
"log",
|
||||
@ -1010,7 +1010,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"collections",
|
||||
"editor",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"itertools 0.10.5",
|
||||
"language",
|
||||
"outline",
|
||||
@ -1111,7 +1111,7 @@ dependencies = [
|
||||
"collections",
|
||||
"fs",
|
||||
"futures 0.3.28",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"image",
|
||||
"language",
|
||||
"live_kit_client",
|
||||
@ -1200,7 +1200,7 @@ dependencies = [
|
||||
"db",
|
||||
"feature_flags",
|
||||
"futures 0.3.28",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"image",
|
||||
"language",
|
||||
"lazy_static",
|
||||
@ -1374,7 +1374,7 @@ dependencies = [
|
||||
"db",
|
||||
"feature_flags",
|
||||
"futures 0.3.28",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"image",
|
||||
"lazy_static",
|
||||
"log",
|
||||
@ -1470,7 +1470,7 @@ dependencies = [
|
||||
"fs",
|
||||
"futures 0.3.28",
|
||||
"git",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"hyper",
|
||||
"indoc",
|
||||
"language",
|
||||
@ -1534,7 +1534,7 @@ dependencies = [
|
||||
"feedback",
|
||||
"futures 0.3.28",
|
||||
"fuzzy",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"language",
|
||||
"lazy_static",
|
||||
"log",
|
||||
@ -1603,7 +1603,7 @@ dependencies = [
|
||||
"env_logger",
|
||||
"fuzzy",
|
||||
"go_to_line",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"language",
|
||||
"menu",
|
||||
"picker",
|
||||
@ -1702,7 +1702,7 @@ dependencies = [
|
||||
"collections",
|
||||
"fs",
|
||||
"futures 0.3.28",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"language",
|
||||
"log",
|
||||
"lsp",
|
||||
@ -1727,7 +1727,7 @@ dependencies = [
|
||||
"editor",
|
||||
"fs",
|
||||
"futures 0.3.28",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"language",
|
||||
"settings",
|
||||
"smol",
|
||||
@ -2127,7 +2127,7 @@ dependencies = [
|
||||
"async-trait",
|
||||
"collections",
|
||||
"env_logger",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"indoc",
|
||||
"lazy_static",
|
||||
"log",
|
||||
@ -2229,7 +2229,7 @@ dependencies = [
|
||||
"collections",
|
||||
"editor",
|
||||
"futures 0.3.28",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"language",
|
||||
"log",
|
||||
"lsp",
|
||||
@ -2397,7 +2397,7 @@ dependencies = [
|
||||
"futures 0.3.28",
|
||||
"fuzzy",
|
||||
"git",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"indoc",
|
||||
"itertools 0.10.5",
|
||||
"language",
|
||||
@ -2606,7 +2606,7 @@ name = "feature_flags"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2619,7 +2619,7 @@ dependencies = [
|
||||
"db",
|
||||
"editor",
|
||||
"futures 0.3.28",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"human_bytes",
|
||||
"isahc",
|
||||
"language",
|
||||
@ -2653,7 +2653,7 @@ dependencies = [
|
||||
"editor",
|
||||
"env_logger",
|
||||
"fuzzy",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"language",
|
||||
"menu",
|
||||
"picker",
|
||||
@ -2822,7 +2822,7 @@ dependencies = [
|
||||
"fsevent",
|
||||
"futures 0.3.28",
|
||||
"git2",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"log",
|
||||
@ -3014,7 +3014,7 @@ dependencies = [
|
||||
name = "fuzzy"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"util",
|
||||
]
|
||||
|
||||
@ -3149,7 +3149,7 @@ name = "go_to_line"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"editor",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"menu",
|
||||
"postage",
|
||||
"serde",
|
||||
@ -3164,68 +3164,6 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gpui"
|
||||
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 = [
|
||||
"anyhow",
|
||||
"async-task",
|
||||
@ -3277,7 +3215,7 @@ dependencies = [
|
||||
"smol",
|
||||
"sqlez",
|
||||
"sum_tree",
|
||||
"taffy 0.3.11 (git+https://github.com/DioxusLabs/taffy?rev=1876f72bee5e376023eaa518aa7b8a34c769bd1b)",
|
||||
"taffy",
|
||||
"thiserror",
|
||||
"time",
|
||||
"tiny-skia",
|
||||
@ -3306,12 +3244,6 @@ dependencies = [
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "grid"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eec1c01eb1de97451ee0d60de7d81cf1e72aabefb021616027f3d1c3ec1c723c"
|
||||
|
||||
[[package]]
|
||||
name = "grid"
|
||||
version = "0.11.0"
|
||||
@ -3694,7 +3626,7 @@ name = "install_cli"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"log",
|
||||
"smol",
|
||||
"util",
|
||||
@ -3847,7 +3779,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"dirs 4.0.0",
|
||||
"editor",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"log",
|
||||
"schemars",
|
||||
"serde",
|
||||
@ -3940,7 +3872,7 @@ dependencies = [
|
||||
"fuzzy",
|
||||
"git",
|
||||
"globset",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"indoc",
|
||||
"lazy_static",
|
||||
"log",
|
||||
@ -3985,7 +3917,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"editor",
|
||||
"fuzzy",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"language",
|
||||
"picker",
|
||||
"project",
|
||||
@ -4006,7 +3938,7 @@ dependencies = [
|
||||
"editor",
|
||||
"env_logger",
|
||||
"futures 0.3.28",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"language",
|
||||
"lsp",
|
||||
"project",
|
||||
@ -4181,7 +4113,7 @@ dependencies = [
|
||||
"core-graphics",
|
||||
"foreign-types",
|
||||
"futures 0.3.28",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"hmac 0.12.1",
|
||||
"jwt",
|
||||
"live_kit_server",
|
||||
@ -4247,7 +4179,7 @@ dependencies = [
|
||||
"ctor",
|
||||
"env_logger",
|
||||
"futures 0.3.28",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"log",
|
||||
"lsp-types",
|
||||
"parking_lot 0.11.2",
|
||||
@ -4399,7 +4331,7 @@ dependencies = [
|
||||
name = "menu"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@ -4569,7 +4501,7 @@ dependencies = [
|
||||
"env_logger",
|
||||
"futures 0.3.28",
|
||||
"git",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"indoc",
|
||||
"itertools 0.10.5",
|
||||
"language",
|
||||
@ -4761,7 +4693,7 @@ dependencies = [
|
||||
"collections",
|
||||
"db",
|
||||
"feature_flags",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"rpc",
|
||||
"settings",
|
||||
"sum_tree",
|
||||
@ -5162,7 +5094,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"editor",
|
||||
"fuzzy",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"language",
|
||||
"ordered-float 2.10.0",
|
||||
"picker",
|
||||
@ -5386,7 +5318,7 @@ dependencies = [
|
||||
"ctor",
|
||||
"editor",
|
||||
"env_logger",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"menu",
|
||||
"parking_lot 0.11.2",
|
||||
"serde_json",
|
||||
@ -5570,7 +5502,7 @@ dependencies = [
|
||||
"collections",
|
||||
"fs",
|
||||
"futures 0.3.28",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"language",
|
||||
"log",
|
||||
"lsp",
|
||||
@ -5687,7 +5619,7 @@ dependencies = [
|
||||
"git",
|
||||
"git2",
|
||||
"globset",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"ignore",
|
||||
"itertools 0.10.5",
|
||||
"language",
|
||||
@ -5730,7 +5662,7 @@ dependencies = [
|
||||
"db",
|
||||
"editor",
|
||||
"futures 0.3.28",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"language",
|
||||
"menu",
|
||||
"postage",
|
||||
@ -5758,7 +5690,7 @@ dependencies = [
|
||||
"editor",
|
||||
"futures 0.3.28",
|
||||
"fuzzy",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"language",
|
||||
"lsp",
|
||||
"ordered-float 2.10.0",
|
||||
@ -5935,7 +5867,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"assistant",
|
||||
"editor",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"search",
|
||||
"ui",
|
||||
"workspace",
|
||||
@ -6109,7 +6041,7 @@ dependencies = [
|
||||
"editor",
|
||||
"futures 0.3.28",
|
||||
"fuzzy",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"language",
|
||||
"ordered-float 2.10.0",
|
||||
"picker",
|
||||
@ -6306,7 +6238,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"collections",
|
||||
"futures 0.3.28",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"language",
|
||||
"lazy_static",
|
||||
"pulldown-cmark",
|
||||
@ -6397,7 +6329,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"arrayvec 0.7.4",
|
||||
"bromberg_sl2",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"log",
|
||||
"rand 0.8.5",
|
||||
"smallvec",
|
||||
@ -6427,7 +6359,7 @@ dependencies = [
|
||||
"ctor",
|
||||
"env_logger",
|
||||
"futures 0.3.28",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"parking_lot 0.11.2",
|
||||
"prost 0.8.0",
|
||||
"prost-build",
|
||||
@ -6887,7 +6819,7 @@ dependencies = [
|
||||
"collections",
|
||||
"editor",
|
||||
"futures 0.3.28",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"language",
|
||||
"log",
|
||||
"menu",
|
||||
@ -6943,7 +6875,7 @@ dependencies = [
|
||||
"env_logger",
|
||||
"futures 0.3.28",
|
||||
"globset",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"language",
|
||||
"lazy_static",
|
||||
"log",
|
||||
@ -7110,7 +7042,7 @@ dependencies = [
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"futures 0.3.28",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"indoc",
|
||||
"lazy_static",
|
||||
"postage",
|
||||
@ -7714,7 +7646,7 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
name = "story"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"itertools 0.10.5",
|
||||
"smallvec",
|
||||
]
|
||||
@ -7730,7 +7662,7 @@ dependencies = [
|
||||
"dialoguer",
|
||||
"editor",
|
||||
"fuzzy",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"indoc",
|
||||
"itertools 0.11.0",
|
||||
"language",
|
||||
@ -7957,18 +7889,7 @@ version = "0.3.11"
|
||||
source = "git+https://github.com/DioxusLabs/taffy?rev=1876f72bee5e376023eaa518aa7b8a34c769bd1b#1876f72bee5e376023eaa518aa7b8a34c769bd1b"
|
||||
dependencies = [
|
||||
"arrayvec 0.7.4",
|
||||
"grid 0.11.0",
|
||||
"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",
|
||||
"grid",
|
||||
"num-traits",
|
||||
"slotmap",
|
||||
]
|
||||
@ -8032,7 +7953,7 @@ dependencies = [
|
||||
"db",
|
||||
"dirs 4.0.0",
|
||||
"futures 0.3.28",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"itertools 0.10.5",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
@ -8062,7 +7983,7 @@ dependencies = [
|
||||
"dirs 4.0.0",
|
||||
"editor",
|
||||
"futures 0.3.28",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"itertools 0.10.5",
|
||||
"language",
|
||||
"lazy_static",
|
||||
@ -8096,7 +8017,7 @@ dependencies = [
|
||||
"ctor",
|
||||
"digest 0.9.0",
|
||||
"env_logger",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"parking_lot 0.11.2",
|
||||
@ -8121,7 +8042,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"fs",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"indexmap 1.9.3",
|
||||
"itertools 0.11.0",
|
||||
"parking_lot 0.11.2",
|
||||
@ -8145,7 +8066,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"clap 4.4.4",
|
||||
"convert_case 0.6.0",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"indexmap 1.9.3",
|
||||
"json_comments",
|
||||
"log",
|
||||
@ -8168,7 +8089,7 @@ dependencies = [
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"fuzzy",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"log",
|
||||
"parking_lot 0.11.2",
|
||||
"picker",
|
||||
@ -8985,7 +8906,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"itertools 0.11.0",
|
||||
"menu",
|
||||
"rand 0.8.5",
|
||||
@ -9238,7 +9159,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"fs",
|
||||
"fuzzy",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"picker",
|
||||
"ui",
|
||||
"util",
|
||||
@ -9263,7 +9184,7 @@ dependencies = [
|
||||
"diagnostics",
|
||||
"editor",
|
||||
"futures 0.3.28",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"indoc",
|
||||
"itertools 0.10.5",
|
||||
"language",
|
||||
@ -9682,7 +9603,7 @@ dependencies = [
|
||||
"editor",
|
||||
"fs",
|
||||
"fuzzy",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"install_cli",
|
||||
"log",
|
||||
"picker",
|
||||
@ -9951,7 +9872,7 @@ dependencies = [
|
||||
"env_logger",
|
||||
"fs",
|
||||
"futures 0.3.28",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"indoc",
|
||||
"install_cli",
|
||||
"itertools 0.10.5",
|
||||
@ -10091,7 +10012,7 @@ dependencies = [
|
||||
"fsevent",
|
||||
"futures 0.3.28",
|
||||
"go_to_line",
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"ignore",
|
||||
"image",
|
||||
"indexmap 1.9.3",
|
||||
@ -10187,7 +10108,7 @@ dependencies = [
|
||||
name = "zed_actions"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"gpui2",
|
||||
"gpui",
|
||||
"serde",
|
||||
]
|
||||
|
||||
|
@ -35,7 +35,7 @@ members = [
|
||||
"crates/go_to_line",
|
||||
"crates/gpui",
|
||||
"crates/gpui_macros",
|
||||
"crates/gpui2",
|
||||
"crates/gpui",
|
||||
"crates/gpui2_macros",
|
||||
"crates/install_cli",
|
||||
"crates/journal",
|
||||
|
@ -12,7 +12,7 @@ doctest = false
|
||||
auto_update = { path = "../auto_update" }
|
||||
editor = { path = "../editor" }
|
||||
language = { path = "../language" }
|
||||
gpui = { path = "../gpui2", package = "gpui2" }
|
||||
gpui = { path = "../gpui" }
|
||||
project = { path = "../project" }
|
||||
settings = { path = "../settings" }
|
||||
ui = { path = "../ui" }
|
||||
|
@ -12,7 +12,7 @@ doctest = false
|
||||
test-support = []
|
||||
|
||||
[dependencies]
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
gpui = { path = "../gpui" }
|
||||
util = { path = "../util" }
|
||||
language = { path = "../language" }
|
||||
async-trait.workspace = true
|
||||
@ -35,4 +35,4 @@ rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
|
||||
bincode = "1.3.3"
|
||||
|
||||
[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"}
|
||||
editor = { path = "../editor" }
|
||||
fs = { path = "../fs" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
gpui = { path = "../gpui" }
|
||||
language = { path = "../language" }
|
||||
menu = { path = "../menu" }
|
||||
multi_buffer = { path = "../multi_buffer" }
|
||||
|
@ -9,7 +9,7 @@ path = "src/audio.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
gpui = { path = "../gpui" }
|
||||
collections = { path = "../collections" }
|
||||
util = { path = "../util" }
|
||||
|
||||
|
@ -11,7 +11,7 @@ doctest = false
|
||||
[dependencies]
|
||||
db = { path = "../db" }
|
||||
client = { path = "../client" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
gpui = { path = "../gpui" }
|
||||
menu = { path = "../menu" }
|
||||
project = { path = "../project" }
|
||||
settings = { path = "../settings" }
|
||||
|
@ -11,7 +11,7 @@ doctest = false
|
||||
[dependencies]
|
||||
collections = { path = "../collections" }
|
||||
editor = { path = "../editor" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
gpui = { path = "../gpui" }
|
||||
ui = { path = "../ui" }
|
||||
language = { path = "../language" }
|
||||
project = { path = "../project" }
|
||||
@ -24,5 +24,5 @@ itertools = "0.10"
|
||||
|
||||
[dev-dependencies]
|
||||
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"] }
|
||||
|
@ -22,7 +22,7 @@ test-support = [
|
||||
audio = { path = "../audio" }
|
||||
client = { path = "../client" }
|
||||
collections = { path = "../collections" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
gpui = { path = "../gpui" }
|
||||
log.workspace = true
|
||||
live_kit_client = { path = "../live_kit_client" }
|
||||
fs = { path = "../fs" }
|
||||
@ -48,7 +48,7 @@ client = { path = "../client", features = ["test-support"] }
|
||||
fs = { path = "../fs", features = ["test-support"] }
|
||||
language = { path = "../language", 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"] }
|
||||
project = { path = "../project", 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" }
|
||||
collections = { path = "../collections" }
|
||||
db = { path = "../db" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
gpui = { path = "../gpui" }
|
||||
util = { path = "../util" }
|
||||
rpc = { path = "../rpc" }
|
||||
text = { path = "../text" }
|
||||
@ -47,7 +47,7 @@ tempfile = "3"
|
||||
|
||||
[dev-dependencies]
|
||||
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"] }
|
||||
client = { path = "../client", 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"] }
|
||||
collections = { path = "../collections" }
|
||||
db = { path = "../db" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
gpui = { path = "../gpui" }
|
||||
util = { path = "../util" }
|
||||
rpc = { path = "../rpc" }
|
||||
text = { path = "../text" }
|
||||
@ -47,7 +47,7 @@ url = "2.2"
|
||||
|
||||
[dev-dependencies]
|
||||
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"] }
|
||||
settings = { path = "../settings", features = ["test-support"] }
|
||||
util = { path = "../util", features = ["test-support"] }
|
||||
|
@ -62,7 +62,7 @@ uuid.workspace = true
|
||||
[dev-dependencies]
|
||||
audio = { path = "../audio" }
|
||||
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"] }
|
||||
client = { path = "../client", features = ["test-support"] }
|
||||
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" }
|
||||
feedback = { path = "../feedback" }
|
||||
fuzzy = { path = "../fuzzy" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
gpui = { path = "../gpui" }
|
||||
language = { path = "../language" }
|
||||
menu = { path = "../menu" }
|
||||
notifications = { path = "../notifications" }
|
||||
@ -69,7 +69,7 @@ call = { path = "../call", features = ["test-support"] }
|
||||
client = { path = "../client", features = ["test-support"] }
|
||||
collections = { path = "../collections", 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"] }
|
||||
project = { path = "../project", features = ["test-support"] }
|
||||
rpc = { path = "../rpc", features = ["test-support"] }
|
||||
|
@ -12,7 +12,7 @@ doctest = false
|
||||
collections = { path = "../collections" }
|
||||
editor = { path = "../editor" }
|
||||
fuzzy = { path = "../fuzzy" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
gpui = { path = "../gpui" }
|
||||
picker = { path = "../picker" }
|
||||
project = { path = "../project" }
|
||||
settings = { path = "../settings" }
|
||||
@ -25,7 +25,7 @@ anyhow.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
language = { path = "../language", features = ["test-support"] }
|
||||
project = { path = "../project", features = ["test-support"] }
|
||||
|
@ -21,7 +21,7 @@ test-support = [
|
||||
[dependencies]
|
||||
collections = { path = "../collections" }
|
||||
# context_menu = { path = "../context_menu" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
gpui = { path = "../gpui" }
|
||||
language = { path = "../language" }
|
||||
settings = { path = "../settings" }
|
||||
theme = { path = "../theme" }
|
||||
@ -43,7 +43,7 @@ parking_lot.workspace = true
|
||||
clock = { path = "../clock" }
|
||||
collections = { path = "../collections", 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"] }
|
||||
lsp = { path = "../lsp", features = ["test-support"] }
|
||||
rpc = { path = "../rpc", features = ["test-support"] }
|
||||
|
@ -13,7 +13,7 @@ copilot = { path = "../copilot" }
|
||||
editor = { path = "../editor" }
|
||||
fs = { path = "../fs" }
|
||||
zed_actions = { path = "../zed_actions"}
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
gpui = { path = "../gpui" }
|
||||
language = { path = "../language" }
|
||||
settings = { path = "../settings" }
|
||||
theme = { path = "../theme" }
|
||||
|
@ -13,7 +13,7 @@ test-support = []
|
||||
|
||||
[dependencies]
|
||||
collections = { path = "../collections" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
gpui = { path = "../gpui" }
|
||||
sqlez = { path = "../sqlez" }
|
||||
sqlez_macros = { path = "../sqlez_macros" }
|
||||
util = { path = "../util" }
|
||||
@ -28,6 +28,6 @@ serde_derive.workspace = true
|
||||
smol.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
env_logger.workspace = true
|
||||
tempdir.workspace = true
|
||||
|
@ -11,7 +11,7 @@ doctest = false
|
||||
[dependencies]
|
||||
collections = { path = "../collections" }
|
||||
editor = { path = "../editor" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
gpui = { path = "../gpui" }
|
||||
ui = { path = "../ui" }
|
||||
language = { path = "../language" }
|
||||
lsp = { path = "../lsp" }
|
||||
@ -35,7 +35,7 @@ client = { path = "../client", features = ["test-support"] }
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
language = { path = "../language", 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"] }
|
||||
theme = { path = "../theme", features = ["test-support"] }
|
||||
|
||||
|
@ -31,7 +31,7 @@ collections = { path = "../collections" }
|
||||
# context_menu = { path = "../context_menu" }
|
||||
fuzzy = { path = "../fuzzy" }
|
||||
git = { path = "../git" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
gpui = { path = "../gpui" }
|
||||
language = { path = "../language" }
|
||||
lsp = { path = "../lsp" }
|
||||
multi_buffer = { path = "../multi_buffer" }
|
||||
@ -76,7 +76,7 @@ copilot = { path = "../copilot", features = ["test-support"] }
|
||||
text = { path = "../text", features = ["test-support"] }
|
||||
language = { path = "../language", 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"] }
|
||||
project = { path = "../project", features = ["test-support"] }
|
||||
settings = { path = "../settings", features = ["test-support"] }
|
||||
|
@ -8,5 +8,5 @@ publish = false
|
||||
path = "src/feature_flags.rs"
|
||||
|
||||
[dependencies]
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
gpui = { path = "../gpui" }
|
||||
anyhow.workspace = true
|
||||
|
@ -14,7 +14,7 @@ test-support = []
|
||||
client = { path = "../client" }
|
||||
db = { path = "../db" }
|
||||
editor = { path = "../editor" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
gpui = { path = "../gpui" }
|
||||
language = { path = "../language" }
|
||||
menu = { path = "../menu" }
|
||||
project = { path = "../project" }
|
||||
|
@ -12,7 +12,7 @@ doctest = false
|
||||
editor = { path = "../editor" }
|
||||
collections = { path = "../collections" }
|
||||
fuzzy = { path = "../fuzzy" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
gpui = { path = "../gpui" }
|
||||
menu = { path = "../menu" }
|
||||
picker = { path = "../picker" }
|
||||
project = { path = "../project" }
|
||||
@ -27,7 +27,7 @@ serde.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
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"] }
|
||||
workspace = { path = "../workspace", features = ["test-support"] }
|
||||
theme = { path = "../theme", features = ["test-support"] }
|
||||
|
@ -31,10 +31,10 @@ log.workspace = true
|
||||
libc = "0.2"
|
||||
time.workspace = true
|
||||
|
||||
gpui = { package = "gpui2", path = "../gpui2", optional = true}
|
||||
gpui = { path = "../gpui", optional = true}
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
|
||||
[features]
|
||||
test-support = ["gpui/test-support"]
|
||||
|
@ -9,5 +9,5 @@ path = "src/fuzzy.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
gpui = { path = "../gpui" }
|
||||
util = { path = "../util" }
|
||||
|
@ -10,7 +10,7 @@ doctest = false
|
||||
|
||||
[dependencies]
|
||||
editor = { path = "../editor" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
gpui = { path = "../gpui" }
|
||||
menu = { path = "../menu" }
|
||||
serde.workspace = true
|
||||
settings = { path = "../settings" }
|
||||
|
@ -1,27 +1,28 @@
|
||||
[package]
|
||||
authors = ["Nathan Sobo <nathansobo@gmail.com>"]
|
||||
edition = "2021"
|
||||
name = "gpui"
|
||||
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
|
||||
|
||||
[features]
|
||||
test-support = ["backtrace", "dhat", "env_logger", "collections/test-support", "util/test-support"]
|
||||
|
||||
[lib]
|
||||
path = "src/gpui.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = ["backtrace", "dhat", "env_logger", "collections/test-support", "util/test-support"]
|
||||
|
||||
[dependencies]
|
||||
collections = { path = "../collections" }
|
||||
gpui_macros = { path = "../gpui_macros" }
|
||||
gpui2_macros = { path = "../gpui2_macros" }
|
||||
util = { path = "../util" }
|
||||
sum_tree = { path = "../sum_tree" }
|
||||
sqlez = { path = "../sqlez" }
|
||||
async-task = "4.0.3"
|
||||
backtrace = { version = "0.3", optional = true }
|
||||
ctor.workspace = true
|
||||
linkme = "0.3"
|
||||
derive_more.workspace = true
|
||||
dhat = { version = "0.3", optional = true }
|
||||
env_logger = { version = "0.9", optional = true }
|
||||
@ -35,30 +36,27 @@ num_cpus = "1.13"
|
||||
ordered-float.workspace = true
|
||||
parking = "2.0.0"
|
||||
parking_lot.workspace = true
|
||||
pathfinder_color = "0.5"
|
||||
pathfinder_geometry = "0.5"
|
||||
postage.workspace = true
|
||||
rand.workspace = true
|
||||
refineable.workspace = true
|
||||
resvg = "0.14"
|
||||
schemars = "0.8"
|
||||
seahash = "4.1"
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
serde_json.workspace = true
|
||||
smallvec.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
|
||||
time.workspace = true
|
||||
tiny-skia = "0.5"
|
||||
usvg = { version = "0.14", features = [] }
|
||||
uuid.workspace = true
|
||||
uuid = { version = "1.1.2", features = ["v4"] }
|
||||
waker-fn = "1.1.0"
|
||||
|
||||
[build-dependencies]
|
||||
bindgen = "0.65.1"
|
||||
cc = "1.0.67"
|
||||
slotmap = "1.0.6"
|
||||
schemars.workspace = true
|
||||
bitflags = "2.4.0"
|
||||
|
||||
[dev-dependencies]
|
||||
backtrace = "0.3"
|
||||
@ -69,6 +67,10 @@ png = "0.16"
|
||||
simplelog = "0.9"
|
||||
util = { path = "../util", features = ["test-support"] }
|
||||
|
||||
[build-dependencies]
|
||||
bindgen = "0.65.1"
|
||||
cbindgen = "0.26.0"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
media = { path = "../media" }
|
||||
anyhow.workspace = true
|
||||
|
@ -1,13 +1,15 @@
|
||||
use std::{
|
||||
env,
|
||||
path::PathBuf,
|
||||
path::{Path, PathBuf},
|
||||
process::{self, Command},
|
||||
};
|
||||
|
||||
use cbindgen::Config;
|
||||
|
||||
fn main() {
|
||||
generate_dispatch_bindings();
|
||||
compile_metal_shaders();
|
||||
generate_shader_bindings();
|
||||
let header_path = generate_shader_bindings();
|
||||
compile_metal_shaders(&header_path);
|
||||
}
|
||||
|
||||
fn generate_dispatch_bindings() {
|
||||
@ -17,7 +19,12 @@ fn generate_dispatch_bindings() {
|
||||
let bindings = bindgen::Builder::default()
|
||||
.header("src/platform/mac/dispatch.h")
|
||||
.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_after_f")
|
||||
.allowlist_function("dispatch_time")
|
||||
.parse_callbacks(Box::new(bindgen::CargoCallbacks))
|
||||
.layout_tests(false)
|
||||
.generate()
|
||||
@ -29,14 +36,61 @@ fn generate_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() {
|
||||
let shader_path = "./src/platform/mac/shaders/shaders.metal";
|
||||
output_path
|
||||
}
|
||||
|
||||
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 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);
|
||||
|
||||
let output = Command::new("xcrun")
|
||||
@ -49,6 +103,8 @@ fn compile_metal_shaders() {
|
||||
"-MO",
|
||||
"-c",
|
||||
shader_path,
|
||||
"-include",
|
||||
&header_path.to_str().unwrap(),
|
||||
"-o",
|
||||
])
|
||||
.arg(&air_output_path)
|
||||
@ -79,18 +135,3 @@ fn compile_metal_shaders() {
|
||||
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 image::ImageFormat;
|
||||
use std::{borrow::Cow, cell::RefCell, collections::HashMap, sync::Arc};
|
||||
|
||||
use crate::ImageData;
|
||||
use crate::{size, DevicePixels, Result, SharedString, Size};
|
||||
use anyhow::anyhow;
|
||||
use image::{Bgra, ImageBuffer};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
fmt,
|
||||
hash::Hash,
|
||||
sync::atomic::{AtomicUsize, Ordering::SeqCst},
|
||||
};
|
||||
|
||||
pub trait AssetSource: 'static + Send + Sync {
|
||||
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 () {
|
||||
@ -17,49 +21,44 @@ impl AssetSource for () {
|
||||
))
|
||||
}
|
||||
|
||||
fn list(&self, _: &str) -> Vec<Cow<'static, str>> {
|
||||
vec![]
|
||||
fn list(&self, _path: &str) -> Result<Vec<SharedString>> {
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AssetCache {
|
||||
source: Box<dyn AssetSource>,
|
||||
svgs: RefCell<HashMap<String, usvg::Tree>>,
|
||||
pngs: RefCell<HashMap<String, Arc<ImageData>>>,
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
pub struct ImageId(usize);
|
||||
|
||||
pub struct ImageData {
|
||||
pub id: ImageId,
|
||||
data: ImageBuffer<Bgra<u8>, Vec<u8>>,
|
||||
}
|
||||
|
||||
impl AssetCache {
|
||||
pub fn new(source: impl AssetSource) -> Self {
|
||||
impl ImageData {
|
||||
pub fn new(data: ImageBuffer<Bgra<u8>, Vec<u8>>) -> Self {
|
||||
static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
Self {
|
||||
source: Box::new(source),
|
||||
svgs: RefCell::new(HashMap::new()),
|
||||
pngs: RefCell::new(HashMap::new()),
|
||||
id: ImageId(NEXT_ID.fetch_add(1, SeqCst)),
|
||||
data,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn svg(&self, path: &str) -> Result<usvg::Tree> {
|
||||
let mut svgs = self.svgs.borrow_mut();
|
||||
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 as_bytes(&self) -> &[u8] {
|
||||
&self.data
|
||||
}
|
||||
|
||||
pub fn png(&self, path: &str) -> Result<Arc<ImageData>> {
|
||||
let mut pngs = self.pngs.borrow_mut();
|
||||
if let Some(png) = pngs.get(path) {
|
||||
Ok(png.clone())
|
||||
} else {
|
||||
let bytes = self.source.load(path)?;
|
||||
let image = ImageData::new(
|
||||
image::load_from_memory_with_format(&bytes, ImageFormat::Png)?.into_bgra8(),
|
||||
);
|
||||
pngs.insert(path.to_string(), image.clone());
|
||||
Ok(image)
|
||||
}
|
||||
pub fn size(&self) -> Size<DevicePixels> {
|
||||
let (width, height) = self.data.dimensions();
|
||||
size(width.into(), height.into())
|
||||
}
|
||||
}
|
||||
|
||||
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,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::{
|
||||
borrow::Cow,
|
||||
fmt,
|
||||
ops::{Deref, DerefMut},
|
||||
};
|
||||
use anyhow::bail;
|
||||
use serde::de::{self, Deserialize, Deserializer, Visitor};
|
||||
use std::fmt;
|
||||
|
||||
use crate::json::ToJson;
|
||||
use pathfinder_color::{ColorF, ColorU};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{
|
||||
de::{self, Unexpected},
|
||||
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<C: From<Rgba>>(hex: u32) -> C {
|
||||
let r = ((hex >> 16) & 0xFF) as f32 / 255.0;
|
||||
let g = ((hex >> 8) & 0xFF) as f32 / 255.0;
|
||||
let b = (hex & 0xFF) as f32 / 255.0;
|
||||
Rgba { r, g, b, a: 1.0 }.into()
|
||||
}
|
||||
|
||||
pub fn rgb(r: f32, g: f32, b: f32) -> Color {
|
||||
Color(ColorF::new(r, g, b, 1.).to_u8())
|
||||
pub fn rgba(hex: u32) -> Rgba {
|
||||
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 {
|
||||
Color(ColorF::new(r, g, b, a).to_u8())
|
||||
#[derive(PartialEq, Clone, Copy, Default)]
|
||||
pub struct Rgba {
|
||||
pub r: f32,
|
||||
pub g: f32,
|
||||
pub b: f32,
|
||||
pub a: f32,
|
||||
}
|
||||
|
||||
pub fn transparent_black() -> Color {
|
||||
Color(ColorU::transparent_black())
|
||||
impl fmt::Debug for Rgba {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "rgba({:#010x})", u32::from(*self))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn black() -> Color {
|
||||
Color(ColorU::black())
|
||||
impl Rgba {
|
||||
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 {
|
||||
Color(ColorU::white())
|
||||
impl From<Rgba> for u32 {
|
||||
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 {
|
||||
color(0xff0000ff)
|
||||
struct RgbaVisitor;
|
||||
|
||||
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 {
|
||||
color(0x00ff00ff)
|
||||
impl<'de> Deserialize<'de> for Rgba {
|
||||
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
deserializer.deserialize_str(RgbaVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn blue() -> Color {
|
||||
color(0x0000ffff)
|
||||
impl From<Hsla> for Rgba {
|
||||
fn from(color: Hsla) -> Self {
|
||||
let h = color.h;
|
||||
let s = color.s;
|
||||
let l = color.l;
|
||||
|
||||
let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
|
||||
let x = c * (1.0 - ((h * 6.0) % 2.0 - 1.0).abs());
|
||||
let m = l - c / 2.0;
|
||||
let cm = c + m;
|
||||
let xm = x + m;
|
||||
|
||||
let (r, g, b) = match (h * 6.0).floor() as i32 {
|
||||
0 | 6 => (cm, xm, m),
|
||||
1 => (xm, cm, m),
|
||||
2 => (m, cm, xm),
|
||||
3 => (m, xm, cm),
|
||||
4 => (xm, m, cm),
|
||||
_ => (cm, m, xm),
|
||||
};
|
||||
|
||||
Rgba {
|
||||
r,
|
||||
g,
|
||||
b,
|
||||
a: color.a,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn yellow() -> Color {
|
||||
color(0xffff00ff)
|
||||
impl TryFrom<&'_ str> for Rgba {
|
||||
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 {
|
||||
pub fn transparent_black() -> Self {
|
||||
transparent_black()
|
||||
#[derive(Default, Copy, Clone, Debug)]
|
||||
#[repr(C)]
|
||||
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 {
|
||||
@ -70,107 +228,230 @@ impl Color {
|
||||
white()
|
||||
}
|
||||
|
||||
pub fn red() -> Self {
|
||||
Color::from_u32(0xff0000ff)
|
||||
}
|
||||
|
||||
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;
|
||||
pub fn transparent_black() -> Self {
|
||||
transparent_black()
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let literal: Cow<str> = Deserialize::deserialize(deserializer)?;
|
||||
if let Some(digits) = literal.strip_prefix('#') {
|
||||
if let Ok(value) = u32::from_str_radix(digits, 16) {
|
||||
if digits.len() == 6 {
|
||||
return Ok(Color::from_u32((value << 8) | 0xFF));
|
||||
} else if digits.len() == 8 {
|
||||
return Ok(Color::from_u32(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(de::Error::invalid_value(
|
||||
Unexpected::Str(literal.as_ref()),
|
||||
&"#RRGGBB[AA]",
|
||||
))
|
||||
// First, deserialize it into Rgba
|
||||
let rgba = Rgba::deserialize(deserializer)?;
|
||||
|
||||
// Then, use the From<Rgba> for Hsla implementation to convert it
|
||||
Ok(Hsla::from(rgba))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u32> for Color {
|
||||
fn from(value: u32) -> Self {
|
||||
Self(ColorU::from_u32(value))
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
impl ToJson for Color {
|
||||
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
|
||||
))
|
||||
}
|
||||
}
|
||||
use super::*;
|
||||
|
||||
impl Deref for Color {
|
||||
type Target = ColorU;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn test_deserialize_three_value_hex_to_rgba() {
|
||||
let actual: Rgba = serde_json::from_value(json!("#f09")).unwrap();
|
||||
|
||||
impl DerefMut for Color {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
assert_eq!(actual, rgba(0xff0099ff))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Color {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
#[test]
|
||||
fn test_deserialize_four_value_hex_to_rgba() {
|
||||
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::{
|
||||
json::{self, json},
|
||||
ViewContext,
|
||||
};
|
||||
use json::ToJson;
|
||||
use pathfinder_geometry::{
|
||||
rect::RectF,
|
||||
vector::{vec2f, Vector2F},
|
||||
};
|
||||
use crate::{Bounds, Element, IntoElement, Pixels, Style, StyleRefinement, Styled, WindowContext};
|
||||
|
||||
pub struct Canvas<V, F>(F, PhantomData<V>);
|
||||
|
||||
impl<V, F> Canvas<V, F>
|
||||
where
|
||||
F: FnMut(RectF, RectF, &mut V, &mut ViewContext<V>),
|
||||
{
|
||||
pub fn new(f: F) -> Self {
|
||||
Self(f, PhantomData)
|
||||
pub fn canvas(callback: impl 'static + FnOnce(&Bounds<Pixels>, &mut WindowContext)) -> Canvas {
|
||||
Canvas {
|
||||
paint_callback: Some(Box::new(callback)),
|
||||
style: StyleRefinement::default(),
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: 'static, F> Element<V> for Canvas<V, F>
|
||||
where
|
||||
F: 'static + FnMut(RectF, RectF, &mut V, &mut ViewContext<V>),
|
||||
{
|
||||
type LayoutState = ();
|
||||
type PaintState = ();
|
||||
pub struct Canvas {
|
||||
paint_callback: Option<Box<dyn FnOnce(&Bounds<Pixels>, &mut WindowContext)>>,
|
||||
style: StyleRefinement,
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut 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), ())
|
||||
}
|
||||
impl IntoElement for Canvas {
|
||||
type Element = Self;
|
||||
|
||||
fn paint(
|
||||
&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> {
|
||||
fn element_id(&self) -> Option<crate::ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn debug(
|
||||
&self,
|
||||
bounds: RectF,
|
||||
_: &Self::LayoutState,
|
||||
_: &Self::PaintState,
|
||||
_: &V,
|
||||
_: &ViewContext<V>,
|
||||
) -> json::Value {
|
||||
json!({"type": "Canvas", "bounds": bounds.to_json()})
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for Canvas {
|
||||
type State = Style;
|
||||
|
||||
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::{
|
||||
geometry::{rect::RectF, vector::Vector2F},
|
||||
json::ToJson,
|
||||
AnyElement, Axis, Element, MouseRegion, SizeConstraint, ViewContext,
|
||||
point, AnyElement, BorrowWindow, Bounds, Element, IntoElement, LayoutId, ParentElement, Pixels,
|
||||
Point, Size, Style, WindowContext,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
pub struct Overlay<V> {
|
||||
child: AnyElement<V>,
|
||||
anchor_position: Option<Vector2F>,
|
||||
anchor_corner: AnchorCorner,
|
||||
fit_mode: OverlayFitMode,
|
||||
position_mode: OverlayPositionMode,
|
||||
hoverable: bool,
|
||||
z_index: Option<usize>,
|
||||
pub struct OverlayState {
|
||||
child_layout_ids: SmallVec<[LayoutId; 4]>,
|
||||
}
|
||||
|
||||
#[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 {
|
||||
SnapToWindow,
|
||||
SwitchAnchor,
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub enum OverlayPositionMode {
|
||||
Window,
|
||||
Local,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
@ -39,18 +193,32 @@ pub enum 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 {
|
||||
Self::TopLeft => RectF::from_points(anchor_position, anchor_position + size),
|
||||
Self::TopRight => RectF::from_points(
|
||||
anchor_position - Vector2F::new(size.x(), 0.),
|
||||
anchor_position + Vector2F::new(0., size.y()),
|
||||
),
|
||||
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),
|
||||
Self::TopLeft => bounds.origin,
|
||||
Self::TopRight => bounds.upper_right(),
|
||||
Self::BottomLeft => bounds.lower_left(),
|
||||
Self::BottomRight => bounds.lower_right(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -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::{
|
||||
color::Color,
|
||||
geometry::{
|
||||
rect::RectF,
|
||||
vector::{vec2f, Vector2F},
|
||||
},
|
||||
scene, Element, SizeConstraint, ViewContext,
|
||||
Bounds, Element, ElementId, InteractiveElement, InteractiveElementState, Interactivity,
|
||||
IntoElement, LayoutId, Pixels, SharedString, StyleRefinement, Styled, WindowContext,
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use serde_derive::Deserialize;
|
||||
use serde_json::json;
|
||||
use std::{borrow::Cow, ops::Range};
|
||||
use util::ResultExt;
|
||||
|
||||
pub struct Svg {
|
||||
path: Cow<'static, str>,
|
||||
color: Color,
|
||||
interactivity: Interactivity,
|
||||
path: Option<SharedString>,
|
||||
}
|
||||
|
||||
pub fn svg() -> Svg {
|
||||
Svg {
|
||||
interactivity: Interactivity::default(),
|
||||
path: None,
|
||||
}
|
||||
}
|
||||
|
||||
impl Svg {
|
||||
pub fn new(path: impl Into<Cow<'static, str>>) -> Self {
|
||||
Self {
|
||||
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;
|
||||
pub fn path(mut self, path: impl Into<SharedString>) -> Self {
|
||||
self.path = Some(path.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: 'static> Element<V> for Svg {
|
||||
type LayoutState = Option<usvg::Tree>;
|
||||
type PaintState = ();
|
||||
impl Element for Svg {
|
||||
type State = InteractiveElementState;
|
||||
|
||||
fn layout(
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
constraint: SizeConstraint,
|
||||
_: &mut V,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
match cx.asset_cache.svg(&self.path) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
element_state: Option<Self::State>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::State) {
|
||||
self.interactivity.layout(element_state, cx, |style, cx| {
|
||||
cx.request_layout(&style, None)
|
||||
})
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: RectF,
|
||||
_visible_bounds: RectF,
|
||||
svg: &mut Self::LayoutState,
|
||||
_: &mut V,
|
||||
cx: &mut ViewContext<V>,
|
||||
) {
|
||||
if let Some(svg) = svg.clone() {
|
||||
cx.scene().push_icon(scene::Icon {
|
||||
bounds,
|
||||
svg,
|
||||
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(),
|
||||
})
|
||||
bounds: Bounds<Pixels>,
|
||||
element_state: &mut Self::State,
|
||||
cx: &mut WindowContext,
|
||||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
self.interactivity
|
||||
.paint(bounds, bounds.size, element_state, cx, |style, _, cx| {
|
||||
if let Some((path, color)) = self.path.as_ref().zip(style.text.color) {
|
||||
cx.paint_svg(bounds, path.clone(), color).log_err();
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct SvgStyle {
|
||||
pub color: Color,
|
||||
pub asset: String,
|
||||
pub dimensions: Dimensions,
|
||||
}
|
||||
impl IntoElement for Svg {
|
||||
type Element = Self;
|
||||
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct Dimensions {
|
||||
pub width: f32,
|
||||
pub height: f32,
|
||||
}
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
self.interactivity.element_id.clone()
|
||||
}
|
||||
|
||||
impl Dimensions {
|
||||
pub fn to_vec(&self) -> Vector2F {
|
||||
vec2f(self.width, self.height)
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn from_usvg_rect(rect: usvg::Rect) -> RectF {
|
||||
RectF::new(
|
||||
vec2f(rect.x() as f32, rect.y() as f32),
|
||||
vec2f(rect.width() as f32, rect.height() as f32),
|
||||
)
|
||||
impl Styled for Svg {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
&mut self.interactivity.base_style
|
||||
}
|
||||
}
|
||||
|
||||
impl InteractiveElement for Svg {
|
||||
fn interactivity(&mut self) -> &mut Interactivity {
|
||||
&mut self.interactivity
|
||||
}
|
||||
}
|
||||
|
@ -1,438 +1,423 @@
|
||||
use crate::{
|
||||
color::Color,
|
||||
fonts::{HighlightStyle, TextStyle},
|
||||
geometry::{
|
||||
rect::RectF,
|
||||
vector::{vec2f, Vector2F},
|
||||
},
|
||||
json::{ToJson, Value},
|
||||
text_layout::{Line, RunStyle, ShapedBoundary},
|
||||
Element, FontCache, SizeConstraint, TextLayoutCache, ViewContext, WindowContext,
|
||||
Bounds, DispatchPhase, Element, ElementId, HighlightStyle, IntoElement, LayoutId,
|
||||
MouseDownEvent, MouseUpEvent, Pixels, Point, SharedString, Size, TextRun, TextStyle,
|
||||
WhiteSpace, WindowContext, WrappedLine,
|
||||
};
|
||||
use log::warn;
|
||||
use serde_json::json;
|
||||
use std::{borrow::Cow, ops::Range, sync::Arc};
|
||||
use anyhow::anyhow;
|
||||
use parking_lot::{Mutex, MutexGuard};
|
||||
use smallvec::SmallVec;
|
||||
use std::{cell::Cell, mem, ops::Range, rc::Rc, sync::Arc};
|
||||
use util::ResultExt;
|
||||
|
||||
pub struct Text {
|
||||
text: Cow<'static, str>,
|
||||
style: TextStyle,
|
||||
soft_wrap: bool,
|
||||
highlights: Option<Box<[(Range<usize>, HighlightStyle)]>>,
|
||||
custom_runs: Option<(
|
||||
Box<[Range<usize>]>,
|
||||
Box<dyn FnMut(usize, RectF, &mut WindowContext)>,
|
||||
)>,
|
||||
}
|
||||
impl Element for &'static str {
|
||||
type State = TextState;
|
||||
|
||||
pub struct LayoutState {
|
||||
shaped_lines: Vec<Line>,
|
||||
wrap_boundaries: Vec<Vec<ShapedBoundary>>,
|
||||
line_height: f32,
|
||||
}
|
||||
|
||||
impl Text {
|
||||
pub fn new<I: Into<Cow<'static, str>>>(text: I, style: TextStyle) -> Self {
|
||||
Self {
|
||||
text: text.into(),
|
||||
style,
|
||||
soft_wrap: true,
|
||||
highlights: None,
|
||||
custom_runs: None,
|
||||
}
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
_: Option<Self::State>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::State) {
|
||||
let mut state = TextState::default();
|
||||
let layout_id = state.layout(SharedString::from(*self), None, cx);
|
||||
(layout_id, state)
|
||||
}
|
||||
|
||||
pub fn with_default_color(mut self, color: Color) -> Self {
|
||||
self.style.color = color;
|
||||
fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut TextState, cx: &mut WindowContext) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
mut self,
|
||||
runs: impl Into<Box<[(Range<usize>, HighlightStyle)]>>,
|
||||
default_style: &TextStyle,
|
||||
highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
|
||||
) -> Self {
|
||||
self.highlights = Some(runs.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_custom_runs(
|
||||
mut self,
|
||||
runs: impl Into<Box<[Range<usize>]>>,
|
||||
callback: impl 'static + FnMut(usize, RectF, &mut WindowContext),
|
||||
) -> Self {
|
||||
self.custom_runs = Some((runs.into(), Box::new(callback)));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_soft_wrap(mut self, soft_wrap: bool) -> Self {
|
||||
self.soft_wrap = soft_wrap;
|
||||
let mut runs = Vec::new();
|
||||
let mut ix = 0;
|
||||
for (range, highlight) in highlights {
|
||||
if ix < range.start {
|
||||
runs.push(default_style.clone().to_run(range.start - ix));
|
||||
}
|
||||
runs.push(
|
||||
default_style
|
||||
.clone()
|
||||
.highlight(highlight)
|
||||
.to_run(range.len()),
|
||||
);
|
||||
ix = range.end;
|
||||
}
|
||||
if ix < self.text.len() {
|
||||
runs.push(default_style.to_run(self.text.len() - ix));
|
||||
}
|
||||
self.runs = Some(runs);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: 'static> Element<V> for Text {
|
||||
type LayoutState = LayoutState;
|
||||
type PaintState = ();
|
||||
impl Element for StyledText {
|
||||
type State = TextState;
|
||||
|
||||
fn layout(
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
constraint: SizeConstraint,
|
||||
_: &mut V,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
// Convert the string and highlight ranges into an iterator of highlighted chunks.
|
||||
|
||||
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,
|
||||
},
|
||||
)
|
||||
_: Option<Self::State>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::State) {
|
||||
let mut state = TextState::default();
|
||||
let layout_id = state.layout(self.text.clone(), self.runs.take(), cx);
|
||||
(layout_id, state)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
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 paint(&mut self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
|
||||
state.paint(bounds, &self.text, cx)
|
||||
}
|
||||
}
|
||||
|
||||
fn rect_for_text_range(
|
||||
&self,
|
||||
_: Range<usize>,
|
||||
_: RectF,
|
||||
_: RectF,
|
||||
_: &Self::LayoutState,
|
||||
_: &Self::PaintState,
|
||||
_: &V,
|
||||
_: &ViewContext<V>,
|
||||
) -> Option<RectF> {
|
||||
impl IntoElement for StyledText {
|
||||
type Element = Self;
|
||||
|
||||
fn element_id(&self) -> Option<crate::ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn debug(
|
||||
&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(),
|
||||
})
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform text layout on a series of highlighted chunks of text.
|
||||
pub fn layout_highlighted_chunks<'a>(
|
||||
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;
|
||||
}
|
||||
}
|
||||
#[derive(Default, Clone)]
|
||||
pub struct TextState(Arc<Mutex<Option<TextStateInner>>>);
|
||||
|
||||
if !line_chunk.is_empty() && !line_exceeded_max_len {
|
||||
let text_style = if let Some(style) = highlight_style {
|
||||
text_style
|
||||
.clone()
|
||||
.highlight(style, font_cache)
|
||||
.map(Cow::Owned)
|
||||
.unwrap_or_else(|_| Cow::Borrowed(text_style))
|
||||
struct TextStateInner {
|
||||
lines: SmallVec<[WrappedLine; 1]>,
|
||||
line_height: Pixels,
|
||||
wrap_width: Option<Pixels>,
|
||||
size: Option<Size<Pixels>>,
|
||||
}
|
||||
|
||||
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 {
|
||||
Cow::Borrowed(text_style)
|
||||
None
|
||||
};
|
||||
|
||||
if line.len() + line_chunk.len() > max_line_len {
|
||||
let mut chunk_len = max_line_len - line.len();
|
||||
while !line_chunk.is_char_boundary(chunk_len) {
|
||||
chunk_len -= 1;
|
||||
if let Some(text_state) = element_state.0.lock().as_ref() {
|
||||
if text_state.size.is_some()
|
||||
&& (wrap_width.is_none() || wrap_width == text_state.wrap_width)
|
||||
{
|
||||
return text_state.size.unwrap();
|
||||
}
|
||||
line_chunk = &line_chunk[..chunk_len];
|
||||
line_exceeded_max_len = true;
|
||||
}
|
||||
|
||||
line.push_str(line_chunk);
|
||||
styles.push((
|
||||
line_chunk.len(),
|
||||
RunStyle {
|
||||
font_id: text_style.font_id,
|
||||
color: text_style.color,
|
||||
underline: text_style.underline,
|
||||
},
|
||||
));
|
||||
let Some(lines) = cx
|
||||
.text_system()
|
||||
.shape_text(
|
||||
&text, font_size, &runs, wrap_width, // Wrap if we know the width.
|
||||
)
|
||||
.log_err()
|
||||
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
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{elements::Empty, fonts, AnyElement, AppContext, Entity, View, ViewContext};
|
||||
|
||||
#[crate::test(self)]
|
||||
fn test_soft_wrapping_with_carriage_returns(cx: &mut AppContext) {
|
||||
cx.add_window(Default::default(), |cx| {
|
||||
let mut view = TestView;
|
||||
fonts::with_font_cache(cx.font_cache().clone(), || {
|
||||
let mut text = Text::new("Hello\r\n", Default::default()).with_soft_wrap(true);
|
||||
let (_, state) = text.layout(
|
||||
SizeConstraint::new(Default::default(), vec2f(f32::INFINITY, f32::INFINITY)),
|
||||
&mut view,
|
||||
cx,
|
||||
);
|
||||
assert_eq!(state.shaped_lines.len(), 2);
|
||||
assert_eq!(state.wrap_boundaries.len(), 2);
|
||||
});
|
||||
view
|
||||
});
|
||||
}
|
||||
|
||||
struct TestView;
|
||||
|
||||
impl Entity for TestView {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for TestView {
|
||||
fn ui_name() -> &'static str {
|
||||
"TestView"
|
||||
}
|
||||
|
||||
fn render(&mut self, _: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
Empty::new().into_any()
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InteractiveText {
|
||||
element_id: ElementId,
|
||||
text: StyledText,
|
||||
click_listener:
|
||||
Option<Box<dyn Fn(&[Range<usize>], InteractiveTextClickEvent, &mut WindowContext<'_>)>>,
|
||||
clickable_ranges: Vec<Range<usize>>,
|
||||
}
|
||||
|
||||
struct InteractiveTextClickEvent {
|
||||
mouse_down_index: usize,
|
||||
mouse_up_index: usize,
|
||||
}
|
||||
|
||||
pub struct InteractiveTextState {
|
||||
text_state: TextState,
|
||||
mouse_down_index: Rc<Cell<Option<usize>>>,
|
||||
}
|
||||
|
||||
impl InteractiveText {
|
||||
pub fn new(id: impl Into<ElementId>, text: StyledText) -> Self {
|
||||
Self {
|
||||
element_id: id.into(),
|
||||
text,
|
||||
click_listener: None,
|
||||
clickable_ranges: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_click(
|
||||
mut self,
|
||||
ranges: Vec<Range<usize>>,
|
||||
listener: impl Fn(usize, &mut WindowContext<'_>) + 'static,
|
||||
) -> Self {
|
||||
self.click_listener = Some(Box::new(move |ranges, event, cx| {
|
||||
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::{
|
||||
geometry::{
|
||||
rect::RectF,
|
||||
vector::{vec2f, Vector2F},
|
||||
},
|
||||
json::{self, json},
|
||||
platform::ScrollWheelEvent,
|
||||
AnyElement, MouseRegion, ViewContext,
|
||||
point, px, size, AnyElement, AvailableSpace, BorrowWindow, Bounds, ContentMask, Element,
|
||||
ElementId, InteractiveElement, InteractiveElementState, Interactivity, IntoElement, LayoutId,
|
||||
Pixels, Point, Render, Size, StyleRefinement, Styled, View, ViewContext, WindowContext,
|
||||
};
|
||||
use json::ToJson;
|
||||
use smallvec::SmallVec;
|
||||
use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
|
||||
use taffy::style::Overflow;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct UniformListState(Rc<RefCell<StateInner>>);
|
||||
/// uniform_list provides lazy rendering for a set of items that are of uniform height.
|
||||
/// 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)]
|
||||
pub enum ScrollTarget {
|
||||
Show(usize),
|
||||
Center(usize),
|
||||
let render_range = move |range, cx: &mut WindowContext| {
|
||||
view.update(cx, |this, cx| {
|
||||
f(this, range, cx)
|
||||
.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 fn scroll_to(&self, scroll_to: ScrollTarget) {
|
||||
self.0.borrow_mut().scroll_to = Some(scroll_to);
|
||||
pub struct UniformList {
|
||||
id: ElementId,
|
||||
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 {
|
||||
self.0.borrow().scroll_top
|
||||
pub fn scroll_to_item(&self, ix: usize) {
|
||||
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)]
|
||||
struct StateInner {
|
||||
scroll_top: f32,
|
||||
scroll_to: Option<ScrollTarget>,
|
||||
pub struct UniformListState {
|
||||
interactive: InteractiveElementState,
|
||||
item_size: Size<Pixels>,
|
||||
}
|
||||
|
||||
pub struct UniformListLayoutState<V> {
|
||||
scroll_max: f32,
|
||||
item_height: f32,
|
||||
items: Vec<AnyElement<V>>,
|
||||
}
|
||||
impl Element for UniformList {
|
||||
type State = UniformListState;
|
||||
|
||||
pub struct UniformList<V> {
|
||||
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(
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
constraint: SizeConstraint,
|
||||
view: &mut V,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
if constraint.max.y().is_infinite() {
|
||||
unimplemented!(
|
||||
"UniformList does not support being rendered with an unconstrained height"
|
||||
);
|
||||
}
|
||||
state: Option<Self::State>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::State) {
|
||||
let max_items = self.item_count;
|
||||
let item_size = state
|
||||
.as_ref()
|
||||
.map(|s| s.item_size)
|
||||
.unwrap_or_else(|| self.measure_item(None, cx));
|
||||
|
||||
let no_items = (
|
||||
constraint.min,
|
||||
UniformListLayoutState {
|
||||
item_height: 0.,
|
||||
scroll_max: 0.,
|
||||
items: Default::default(),
|
||||
},
|
||||
);
|
||||
let (layout_id, interactive) =
|
||||
self.interactivity
|
||||
.layout(state.map(|s| s.interactive), cx, |style, cx| {
|
||||
cx.request_measured_layout(
|
||||
style,
|
||||
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 {
|
||||
return no_items;
|
||||
}
|
||||
let height = match available_space.height {
|
||||
AvailableSpace::Definite(height) => desired_height.min(height),
|
||||
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
|
||||
desired_height
|
||||
}
|
||||
};
|
||||
size(width, height)
|
||||
},
|
||||
)
|
||||
});
|
||||
|
||||
let mut items = Vec::new();
|
||||
let mut size = constraint.max;
|
||||
let mut 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 element_state = UniformListState {
|
||||
interactive,
|
||||
item_size,
|
||||
};
|
||||
let item_height = item_size.y();
|
||||
|
||||
let scroll_height = self.item_count as f32 * item_height;
|
||||
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,
|
||||
},
|
||||
)
|
||||
(layout_id, element_state)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: RectF,
|
||||
visible_bounds: RectF,
|
||||
layout: &mut Self::LayoutState,
|
||||
view: &mut V,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> Self::PaintState {
|
||||
let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default();
|
||||
bounds: Bounds<crate::Pixels>,
|
||||
element_state: &mut Self::State,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
let style =
|
||||
self.interactivity
|
||||
.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));
|
||||
|
||||
cx.scene().push_mouse_region(
|
||||
MouseRegion::new::<Self>(self.view_id, 0, visible_bounds).on_scroll({
|
||||
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 padded_bounds = Bounds::from_corners(
|
||||
bounds.origin + point(border.left + padding.left, border.top + padding.top),
|
||||
bounds.lower_right()
|
||||
- point(border.right + padding.right, border.bottom + padding.bottom),
|
||||
);
|
||||
|
||||
let mut item_origin = bounds.origin()
|
||||
- vec2f(
|
||||
0.,
|
||||
(self.state.scroll_top() - self.padding_top) % layout.item_height,
|
||||
);
|
||||
let item_size = element_state.item_size;
|
||||
let content_size = Size {
|
||||
width: padded_bounds.size.width,
|
||||
height: item_size.height * self.item_count + padding.top + padding.bottom,
|
||||
};
|
||||
|
||||
for item in &mut layout.items {
|
||||
item.paint(item_origin, visible_bounds, view, cx);
|
||||
item_origin += vec2f(0.0, layout.item_height);
|
||||
}
|
||||
let shared_scroll_offset = element_state
|
||||
.interactive
|
||||
.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(
|
||||
&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))
|
||||
}
|
||||
let item_height = self.measure_item(Some(padded_bounds.size.width), cx).height;
|
||||
|
||||
fn debug(
|
||||
&self,
|
||||
bounds: RectF,
|
||||
layout: &Self::LayoutState,
|
||||
_: &Self::PaintState,
|
||||
view: &V,
|
||||
cx: &ViewContext<V>,
|
||||
) -> json::Value {
|
||||
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>>()
|
||||
self.interactivity.paint(
|
||||
bounds,
|
||||
content_size,
|
||||
&mut element_state.interactive,
|
||||
cx,
|
||||
|style, mut scroll_offset, cx| {
|
||||
let border = style.border_widths.to_pixels(cx.rem_size());
|
||||
let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size());
|
||||
|
||||
})
|
||||
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 image_cache;
|
||||
pub use app::*;
|
||||
|
||||
mod arena;
|
||||
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"))]
|
||||
pub mod test;
|
||||
pub use assets::*;
|
||||
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 text_system;
|
||||
mod util;
|
||||
pub use elements::{AnyElement, Element};
|
||||
pub mod executor;
|
||||
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,
|
||||
};
|
||||
mod view;
|
||||
mod window;
|
||||
|
||||
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 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;
|
||||
use crate::{ImageData, ImageId, SharedString};
|
||||
use collections::HashMap;
|
||||
use futures::{
|
||||
future::{BoxFuture, Shared},
|
||||
AsyncReadExt, FutureExt,
|
||||
AsyncReadExt, FutureExt, TryFutureExt,
|
||||
};
|
||||
use image::ImageError;
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::Arc;
|
||||
use thiserror::Error;
|
||||
use util::{
|
||||
arc_cow::ArcCow,
|
||||
http::{self, HttpClient},
|
||||
};
|
||||
use util::http::{self, HttpClient};
|
||||
|
||||
#[derive(PartialEq, Eq, Hash, Clone)]
|
||||
pub struct RenderImageParams {
|
||||
pub(crate) image_id: ImageId,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error, Clone)]
|
||||
pub enum Error {
|
||||
@ -43,7 +44,7 @@ impl From<ImageError> for Error {
|
||||
|
||||
pub struct ImageCache {
|
||||
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>>>;
|
||||
@ -58,12 +59,12 @@ impl ImageCache {
|
||||
|
||||
pub fn get(
|
||||
&self,
|
||||
uri: impl Into<ArcCow<'static, str>>,
|
||||
uri: impl Into<SharedString>,
|
||||
) -> Shared<BoxFuture<'static, Result<Arc<ImageData>, Error>>> {
|
||||
let uri = uri.into();
|
||||
let mut images = self.images.lock();
|
||||
|
||||
match images.get(uri.as_ref()) {
|
||||
match images.get(&uri) {
|
||||
Some(future) => future.clone(),
|
||||
None => {
|
||||
let client = self.client.clone();
|
||||
@ -84,9 +85,17 @@ impl ImageCache {
|
||||
let format = image::guess_format(&body)?;
|
||||
let image =
|
||||
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()
|
||||
.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")]
|
||||
pub mod mac;
|
||||
pub mod test;
|
||||
pub mod current {
|
||||
#[cfg(target_os = "macos")]
|
||||
pub use super::mac::*;
|
||||
}
|
||||
mod mac;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
mod test;
|
||||
|
||||
use crate::{
|
||||
executor,
|
||||
fonts::{
|
||||
Features as FontFeatures, FontId, GlyphId, Metrics as FontMetrics,
|
||||
Properties as FontProperties,
|
||||
},
|
||||
geometry::{
|
||||
rect::{RectF, RectI},
|
||||
vector::Vector2F,
|
||||
},
|
||||
keymap_matcher::KeymapMatcher,
|
||||
text_layout::{LineLayout, RunStyle},
|
||||
Action, AnyWindowHandle, ClipboardItem, Menu, Scene,
|
||||
point, size, Action, AnyWindowHandle, BackgroundExecutor, Bounds, DevicePixels, Font, FontId,
|
||||
FontMetrics, FontRun, ForegroundExecutor, GlobalPixels, GlyphId, InputEvent, Keymap,
|
||||
LineLayout, Pixels, Point, RenderGlyphParams, RenderImageParams, RenderSvgParams, Result,
|
||||
Scene, SharedString, Size, TaskLabel,
|
||||
};
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use anyhow::{anyhow, bail};
|
||||
use async_task::Runnable;
|
||||
pub use event::*;
|
||||
use pathfinder_geometry::vector::vec2f;
|
||||
use postage::oneshot;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use sqlez::{
|
||||
bindable::{Bind, Column, StaticColumnCount},
|
||||
statement::Statement,
|
||||
};
|
||||
use futures::channel::oneshot;
|
||||
use parking::Unparker;
|
||||
use seahash::SeaHasher;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlez::bindable::{Bind, Column, StaticColumnCount};
|
||||
use sqlez::statement::Statement;
|
||||
use std::borrow::Cow;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::time::Duration;
|
||||
use std::{
|
||||
any::Any,
|
||||
fmt::{self, Debug, Display},
|
||||
@ -41,86 +31,294 @@ use std::{
|
||||
str::FromStr,
|
||||
sync::Arc,
|
||||
};
|
||||
use time::UtcOffset;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub trait Platform: Send + Sync {
|
||||
fn dispatcher(&self) -> Arc<dyn Dispatcher>;
|
||||
fn fonts(&self) -> Arc<dyn FontSystem>;
|
||||
pub use app_menu::*;
|
||||
pub use keystroke::*;
|
||||
#[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 hide(&self);
|
||||
fn hide_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(
|
||||
&self,
|
||||
handle: AnyWindowHandle,
|
||||
options: WindowOptions,
|
||||
executor: Rc<executor::Foreground>,
|
||||
) -> Box<dyn Window>;
|
||||
fn main_window(&self) -> Option<AnyWindowHandle>;
|
||||
draw: DrawWindow,
|
||||
) -> Box<dyn PlatformWindow>;
|
||||
|
||||
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 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 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(
|
||||
&self,
|
||||
options: PathPromptOptions,
|
||||
) -> oneshot::Receiver<Option<Vec<PathBuf>>>;
|
||||
fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>>;
|
||||
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 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 {
|
||||
fn selected_text_range(&self) -> Option<Range<usize>>;
|
||||
fn marked_text_range(&self) -> Option<Range<usize>>;
|
||||
fn text_for_range(&self, range_utf16: Range<usize>) -> Option<String>;
|
||||
pub trait PlatformTextSystem: Send + Sync {
|
||||
fn add_fonts(&self, fonts: &[Arc<Vec<u8>>]) -> Result<()>;
|
||||
fn all_font_families(&self) -> Vec<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_and_mark_text_in_range(
|
||||
&mut self,
|
||||
@ -129,75 +327,45 @@ pub trait InputHandler {
|
||||
new_selected_range: Option<Range<usize>>,
|
||||
);
|
||||
fn unmark_text(&mut self);
|
||||
fn rect_for_range(&self, range_utf16: Range<usize>) -> Option<RectF>;
|
||||
}
|
||||
|
||||
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;
|
||||
fn bounds_for_range(&mut self, range_utf16: Range<usize>) -> Option<Bounds<Pixels>>;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WindowOptions<'a> {
|
||||
pub struct WindowOptions {
|
||||
pub bounds: WindowBounds,
|
||||
pub titlebar: Option<TitlebarOptions<'a>>,
|
||||
pub titlebar: Option<TitlebarOptions>,
|
||||
pub center: bool,
|
||||
pub focus: bool,
|
||||
pub show: bool,
|
||||
pub kind: WindowKind,
|
||||
pub is_movable: bool,
|
||||
pub screen: Option<Rc<dyn Screen>>,
|
||||
pub display_id: Option<DisplayId>,
|
||||
}
|
||||
|
||||
impl<'a> WindowOptions<'a> {
|
||||
pub fn with_bounds(bounds: Vector2F) -> Self {
|
||||
impl Default for WindowOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
bounds: WindowBounds::Fixed(RectF::new(vec2f(0., 0.), bounds)),
|
||||
center: true,
|
||||
..Default::default()
|
||||
bounds: WindowBounds::default(),
|
||||
titlebar: Some(TitlebarOptions {
|
||||
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)]
|
||||
pub struct TitlebarOptions<'a> {
|
||||
pub title: Option<&'a str>,
|
||||
pub struct TitlebarOptions {
|
||||
pub title: Option<SharedString>,
|
||||
pub appears_transparent: bool,
|
||||
pub traffic_light_position: Option<Vector2F>,
|
||||
pub traffic_light_position: Option<Point<Pixels>>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
@ -220,11 +388,12 @@ pub enum WindowKind {
|
||||
PopUp,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Default)]
|
||||
pub enum WindowBounds {
|
||||
Fullscreen,
|
||||
#[default]
|
||||
Maximized,
|
||||
Fixed(RectF),
|
||||
Fixed(Bounds<GlobalPixels>),
|
||||
}
|
||||
|
||||
impl StaticColumnCount for WindowBounds {
|
||||
@ -253,10 +422,10 @@ impl Bind for WindowBounds {
|
||||
statement.bind(
|
||||
®ion.map(|region| {
|
||||
(
|
||||
region.min_x(),
|
||||
region.min_y(),
|
||||
region.width(),
|
||||
region.height(),
|
||||
region.origin.x,
|
||||
region.origin.y,
|
||||
region.size.width,
|
||||
region.size.height,
|
||||
)
|
||||
}),
|
||||
next_index,
|
||||
@ -272,10 +441,14 @@ impl Column for WindowBounds {
|
||||
"Maximized" => WindowBounds::Maximized,
|
||||
"Fixed" => {
|
||||
let ((x, y, width, height), _) = Column::column(statement, next_index)?;
|
||||
WindowBounds::Fixed(RectF::new(
|
||||
Vector2F::new(x, y),
|
||||
Vector2F::new(width, height),
|
||||
))
|
||||
let x: f64 = x;
|
||||
let y: f64 = y;
|
||||
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"),
|
||||
};
|
||||
@ -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 files: bool,
|
||||
pub directories: bool,
|
||||
pub multiple: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum PromptLevel {
|
||||
Info,
|
||||
Warning,
|
||||
Critical,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Deserialize, JsonSchema)]
|
||||
/// The style of the cursor (pointer)
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum CursorStyle {
|
||||
Arrow,
|
||||
ResizeLeftRight,
|
||||
ResizeUpDown,
|
||||
PointingHand,
|
||||
IBeam,
|
||||
Crosshair,
|
||||
ClosedHand,
|
||||
OpenHand,
|
||||
PointingHand,
|
||||
ResizeLeft,
|
||||
ResizeRight,
|
||||
ResizeLeftRight,
|
||||
ResizeUp,
|
||||
ResizeDown,
|
||||
ResizeUpDown,
|
||||
DisappearingItem,
|
||||
IBeamCursorForVerticalLayout,
|
||||
OperationNotAllowed,
|
||||
DragLink,
|
||||
DragCopy,
|
||||
ContextualMenu,
|
||||
}
|
||||
|
||||
impl Default for CursorStyle {
|
||||
@ -311,14 +514,14 @@ impl Default for CursorStyle {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct AppVersion {
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd, Serialize)]
|
||||
pub struct SemanticVersion {
|
||||
major: usize,
|
||||
minor: usize,
|
||||
patch: usize,
|
||||
}
|
||||
|
||||
impl FromStr for AppVersion {
|
||||
impl FromStr for SemanticVersion {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
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 {
|
||||
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum RasterizationOptions {
|
||||
Alpha,
|
||||
Bgra,
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ClipboardItem {
|
||||
pub(crate) text: String,
|
||||
pub(crate) metadata: Option<String>,
|
||||
}
|
||||
|
||||
pub trait FontSystem: Send + Sync {
|
||||
fn add_fonts(&self, fonts: &[Arc<Vec<u8>>]) -> anyhow::Result<()>;
|
||||
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 {
|
||||
impl ClipboardItem {
|
||||
pub fn new(text: String) -> Self {
|
||||
Self {
|
||||
bounds: WindowBounds::Maximized,
|
||||
titlebar: Some(TitlebarOptions {
|
||||
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,
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user