Remove 2 suffix from gpui

Co-authored-by: Mikayla <mikayla@zed.dev>
This commit is contained in:
Max Brunsfeld 2024-01-03 12:59:39 -08:00
parent 3c81dda8e2
commit f5ba22659b
225 changed files with 8511 additions and 41063 deletions

207
Cargo.lock generated
View File

@ -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",
]

View File

@ -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",

View File

@ -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" }

View File

@ -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"] }

View File

@ -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" }

View File

@ -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" }

View File

@ -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" }

View File

@ -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"] }

View File

@ -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"] }

View File

@ -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"] }

View File

@ -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"] }

View File

@ -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
View 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"]

View File

@ -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"] }

View File

@ -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"] }

View File

@ -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"] }

View File

@ -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" }

View File

@ -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

View File

@ -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"] }

View File

@ -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"] }

View File

@ -8,5 +8,5 @@ publish = false
path = "src/feature_flags.rs"
[dependencies]
gpui = { package = "gpui2", path = "../gpui2" }
gpui = { path = "../gpui" }
anyhow.workspace = true

View File

@ -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" }

View File

@ -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"] }

View File

@ -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"]

View File

@ -9,5 +9,5 @@ path = "src/fuzzy.rs"
doctest = false
[dependencies]
gpui = { package = "gpui2", path = "../gpui2" }
gpui = { path = "../gpui" }
util = { path = "../util" }

View File

@ -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" }

View File

@ -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

View File

@ -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");
}

View File

@ -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()
}
}
}

View File

@ -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!()
}
}

View File

@ -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

View File

@ -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
}
};
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}));
}

View File

@ -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);
}
}

View File

@ -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

View File

@ -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)
})
}
}

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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))
}
}

View File

@ -1 +0,0 @@

View File

@ -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())
}
}

View File

@ -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),
})
}
}

View File

@ -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
}
}

View File

@ -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)
})
}
}

View File

@ -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)),
})
}
}

View File

@ -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)})
}
}

View File

@ -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()
})
}
}

View File

@ -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(),
})
}
}

View File

@ -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)
})
}
}

View File

@ -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)
})
}
}

View File

@ -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),
})
}
}

View File

@ -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(),
})
}
}

View File

@ -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)
})
}
}

View File

@ -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

View File

@ -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),
})
}
}

View File

@ -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),
})
}
}

View File

@ -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),
})
}
}

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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)),
})
}
}

View File

@ -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

View File

@ -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);
}
}

View File

@ -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

View File

@ -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
}
}

View File

@ -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();

View File

@ -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()
}
}

View File

@ -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)
}
}
}

View File

@ -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(())
}
}

View File

@ -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,
}

View File

@ -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"
);
}
}
}

View File

@ -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())),
)
);
}
}

View File

@ -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)
}
}

View File

@ -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(
&region.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