migrate to gpui-component
This commit is contained in:
532
Cargo.lock
generated
532
Cargo.lock
generated
@@ -260,6 +260,15 @@ version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
|
||||
|
||||
[[package]]
|
||||
name = "arc-swap"
|
||||
version = "1.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arg_enum_proc_macro"
|
||||
version = "0.3.4"
|
||||
@@ -763,6 +772,12 @@ dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base62"
|
||||
version = "2.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd637ac531c60eb7fbc4684dc061c2d7d90d73d758181aa02eeff0464b9eee4b"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
@@ -1170,6 +1185,7 @@ dependencies = [
|
||||
"common",
|
||||
"flume 0.11.1",
|
||||
"gpui",
|
||||
"gpui-component",
|
||||
"gpui_tokio",
|
||||
"itertools 0.13.0",
|
||||
"linkify",
|
||||
@@ -1185,8 +1201,6 @@ dependencies = [
|
||||
"smallvec",
|
||||
"smol",
|
||||
"state",
|
||||
"theme",
|
||||
"ui",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1396,6 +1410,7 @@ dependencies = [
|
||||
"dirs 5.0.1",
|
||||
"futures",
|
||||
"gpui",
|
||||
"gpui-component",
|
||||
"itertools 0.13.0",
|
||||
"log",
|
||||
"nostr",
|
||||
@@ -1497,6 +1512,7 @@ dependencies = [
|
||||
"device",
|
||||
"futures",
|
||||
"gpui",
|
||||
"gpui-component",
|
||||
"gpui_linux",
|
||||
"gpui_macos",
|
||||
"gpui_platform",
|
||||
@@ -1517,10 +1533,8 @@ dependencies = [
|
||||
"smallvec",
|
||||
"smol",
|
||||
"state",
|
||||
"theme",
|
||||
"title_bar",
|
||||
"tracing-subscriber",
|
||||
"ui",
|
||||
"webbrowser",
|
||||
]
|
||||
|
||||
@@ -1553,10 +1567,8 @@ dependencies = [
|
||||
"smallvec",
|
||||
"smol",
|
||||
"state",
|
||||
"theme",
|
||||
"tracing-subscriber",
|
||||
"tracing-wasm",
|
||||
"ui",
|
||||
"wasm-bindgen",
|
||||
"webbrowser",
|
||||
]
|
||||
@@ -1857,6 +1869,7 @@ dependencies = [
|
||||
"common",
|
||||
"flume 0.11.1",
|
||||
"gpui",
|
||||
"gpui-component",
|
||||
"itertools 0.13.0",
|
||||
"log",
|
||||
"nostr-sdk",
|
||||
@@ -1866,8 +1879,6 @@ dependencies = [
|
||||
"smallvec",
|
||||
"smol",
|
||||
"state",
|
||||
"theme",
|
||||
"ui",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2053,6 +2064,26 @@ version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
|
||||
|
||||
[[package]]
|
||||
name = "enum-iterator"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4549325971814bda7a44061bf3fe7e487d447cba01e4220a4b454d630d7a016"
|
||||
dependencies = [
|
||||
"enum-iterator-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enum-iterator-derive"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "685adfa4d6f3d765a26bc5dbc936577de9abf756c1feeb3089b01dd395034842"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enumflags2"
|
||||
version = "0.7.12"
|
||||
@@ -2451,6 +2482,15 @@ version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||
|
||||
[[package]]
|
||||
name = "fsevent-sys"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ftree"
|
||||
version = "1.3.0"
|
||||
@@ -2733,6 +2773,17 @@ dependencies = [
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "globwalk"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"ignore",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gloo-timers"
|
||||
version = "0.3.0"
|
||||
@@ -2881,6 +2932,59 @@ dependencies = [
|
||||
"zed-scap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gpui-component"
|
||||
version = "0.5.2"
|
||||
source = "git+https://github.com/reyakov/gpui-component#af5eb24c00da145d5145cbad87f14e0f9234b094"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
"async-channel 2.5.0",
|
||||
"chrono",
|
||||
"core-text",
|
||||
"enum-iterator",
|
||||
"futures",
|
||||
"gpui",
|
||||
"gpui-component-macros",
|
||||
"gpui_macros",
|
||||
"html5ever",
|
||||
"instant",
|
||||
"itertools 0.13.0",
|
||||
"log",
|
||||
"lsp-types",
|
||||
"markdown",
|
||||
"markup5ever_rcdom",
|
||||
"notify",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"paste",
|
||||
"regex",
|
||||
"ropey",
|
||||
"rust-i18n",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
"smallvec",
|
||||
"smol",
|
||||
"tracing",
|
||||
"tree-sitter",
|
||||
"tree-sitter-json",
|
||||
"unicode-segmentation",
|
||||
"uuid",
|
||||
"zed-sum-tree",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gpui-component-macros"
|
||||
version = "0.5.1"
|
||||
source = "git+https://github.com/reyakov/gpui-component#af5eb24c00da145d5145cbad87f14e0f9234b094"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gpui_linux"
|
||||
version = "0.1.0"
|
||||
@@ -3331,6 +3435,20 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "html5ever"
|
||||
version = "0.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4"
|
||||
dependencies = [
|
||||
"log",
|
||||
"mac",
|
||||
"markup5ever",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.4.1"
|
||||
@@ -3598,6 +3716,22 @@ dependencies = [
|
||||
"icu_properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ignore"
|
||||
version = "0.4.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a"
|
||||
dependencies = [
|
||||
"crossbeam-deque",
|
||||
"globset",
|
||||
"log",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"same-file",
|
||||
"walkdir",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.25.10"
|
||||
@@ -3665,6 +3799,26 @@ dependencies = [
|
||||
"ftree",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fdd168d97690d0b8c412d6b6c10360277f4d7ee495c5d0d5d5fe0854923255cc"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"inotify-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify-sys"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.4"
|
||||
@@ -3682,6 +3836,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3747,6 +3904,15 @@ version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.13.0"
|
||||
@@ -3884,6 +4050,26 @@ version = "3.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
|
||||
|
||||
[[package]]
|
||||
name = "kqueue"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "273c0752728918e0ac4976f2b275b6fefb9ecd400585dec929419f3844cd87b5"
|
||||
dependencies = [
|
||||
"kqueue-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kqueue-sys"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kurbo"
|
||||
version = "0.11.3"
|
||||
@@ -4201,6 +4387,42 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5cab8f2cadc416a82d2e783a1946388b31654d391d1c7d92cc1f03e295b1deb"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"unicode-id",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markup5ever"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45"
|
||||
dependencies = [
|
||||
"log",
|
||||
"phf 0.11.3",
|
||||
"phf_codegen",
|
||||
"string_cache",
|
||||
"string_cache_codegen",
|
||||
"tendril",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markup5ever_rcdom"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edaa21ab3701bfee5099ade5f7e1f84553fd19228cf332f13cd6e964bf59be18"
|
||||
dependencies = [
|
||||
"html5ever",
|
||||
"markup5ever",
|
||||
"tendril",
|
||||
"xml5ever",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.2.0"
|
||||
@@ -4332,6 +4554,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
@@ -4465,6 +4688,15 @@ version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
|
||||
|
||||
[[package]]
|
||||
name = "normpath"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9985ef7269fa99f3b12437bb698381da2428743ab90f20393f399fa14cab21a"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nostr"
|
||||
version = "0.44.1"
|
||||
@@ -4577,6 +4809,34 @@ dependencies = [
|
||||
"universal-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify"
|
||||
version = "7.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c533b4c39709f9ba5005d8002048266593c1cfaf3c5f0739d5b8ab0c6c504009"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"filetime",
|
||||
"fsevent-sys",
|
||||
"inotify",
|
||||
"kqueue",
|
||||
"libc",
|
||||
"log",
|
||||
"mio",
|
||||
"notify-types",
|
||||
"walkdir",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify-types"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "585d3cb5e12e01aed9e8a1f70d5c6b5e86fe2a6e48fc8cd0b3e0b8df6f6eb174"
|
||||
dependencies = [
|
||||
"instant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ntapi"
|
||||
version = "0.4.3"
|
||||
@@ -5195,6 +5455,16 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_codegen"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
|
||||
dependencies = [
|
||||
"phf_generator 0.11.3",
|
||||
"phf_shared 0.11.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_generator"
|
||||
version = "0.11.3"
|
||||
@@ -5433,6 +5703,12 @@ dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "precomputed-hash"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
|
||||
|
||||
[[package]]
|
||||
name = "presser"
|
||||
version = "0.3.1"
|
||||
@@ -5946,14 +6222,13 @@ dependencies = [
|
||||
"common",
|
||||
"flume 0.11.1",
|
||||
"gpui",
|
||||
"gpui-component",
|
||||
"log",
|
||||
"nostr-sdk",
|
||||
"settings",
|
||||
"smallvec",
|
||||
"smol",
|
||||
"state",
|
||||
"theme",
|
||||
"ui",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6059,18 +6334,12 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rope"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d"
|
||||
name = "ropey"
|
||||
version = "2.0.0-beta.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4045a00dc327d084a2bbf126976e14125b54f23bd30511d45b842eba76c52d74"
|
||||
dependencies = [
|
||||
"heapless 0.9.3",
|
||||
"log",
|
||||
"rayon",
|
||||
"sum_tree",
|
||||
"tracing",
|
||||
"unicode-segmentation",
|
||||
"util",
|
||||
"ztracing",
|
||||
"str_indices",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6114,6 +6383,54 @@ dependencies = [
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-i18n"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55691a65892c33ee2de49c15ea5600c6f4a70e8eeb8e6c3cd96d2a231d230c40"
|
||||
dependencies = [
|
||||
"globwalk",
|
||||
"regex",
|
||||
"rust-i18n-macro",
|
||||
"rust-i18n-support",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-i18n-macro"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30de488acadcf767d97cd48518a8da8ea9777b1c9a5beca4eab78bbf77d07309"
|
||||
dependencies = [
|
||||
"glob",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rust-i18n-support",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-i18n-support"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aea0fef8a93c06326b66392c95a115120e609674cb2132d37d276a6b05b545b4"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"base62",
|
||||
"globwalk",
|
||||
"itertools 0.11.0",
|
||||
"normpath",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"siphasher",
|
||||
"toml 0.8.23",
|
||||
"triomphe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-ini"
|
||||
version = "0.17.0"
|
||||
@@ -6600,6 +6917,19 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_yaml"
|
||||
version = "0.9.34+deprecated"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
"unsafe-libyaml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "settings"
|
||||
version = "1.0.0-beta4"
|
||||
@@ -6614,7 +6944,6 @@ dependencies = [
|
||||
"serde_json",
|
||||
"smallvec",
|
||||
"smol",
|
||||
"theme",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6889,6 +7218,18 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "str_indices"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d08889ec5408683408db66ad89e0e1f93dff55c73a4ccc71c427d5b277ee47e6"
|
||||
|
||||
[[package]]
|
||||
name = "streaming-iterator"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520"
|
||||
|
||||
[[package]]
|
||||
name = "strict-num"
|
||||
version = "0.1.1"
|
||||
@@ -6898,6 +7239,31 @@ dependencies = [
|
||||
"float-cmp",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "string_cache"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f"
|
||||
dependencies = [
|
||||
"new_debug_unreachable",
|
||||
"parking_lot",
|
||||
"phf_shared 0.11.3",
|
||||
"precomputed-hash",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "string_cache_codegen"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0"
|
||||
dependencies = [
|
||||
"phf_generator 0.11.3",
|
||||
"phf_shared 0.11.3",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
@@ -7195,20 +7561,6 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "theme"
|
||||
version = "1.0.0-beta4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gpui",
|
||||
"log",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"smallvec",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
@@ -7339,13 +7691,12 @@ dependencies = [
|
||||
"anyhow",
|
||||
"common",
|
||||
"gpui",
|
||||
"gpui-component",
|
||||
"linicon",
|
||||
"log",
|
||||
"nostr-sdk",
|
||||
"smallvec",
|
||||
"smol",
|
||||
"theme",
|
||||
"ui",
|
||||
"windows 0.61.3",
|
||||
]
|
||||
|
||||
@@ -7648,6 +7999,47 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter"
|
||||
version = "0.26.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4dab76d0b724ba557954125188cf0633a1ca43199ced82d95c7b9c32cc3de1f3"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"regex",
|
||||
"regex-syntax",
|
||||
"serde_json",
|
||||
"streaming-iterator",
|
||||
"tree-sitter-language",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-json"
|
||||
version = "0.24.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d727acca406c0020cffc6cf35516764f36c8e3dc4408e5ebe2cb35a947ec471"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter-language",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-language"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "009994f150cc0cd50ff54917d5bc8bffe8cad10ca10d81c34da2ec421ae61782"
|
||||
|
||||
[[package]]
|
||||
name = "triomphe"
|
||||
version = "0.1.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd69c5aa8f924c7519d6372789a74eac5b94fb0f8fcf0d4a97eb0bfc3e785f39"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"serde",
|
||||
"stable_deref_trait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "try-lock"
|
||||
version = "0.2.5"
|
||||
@@ -7705,29 +8097,6 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ui"
|
||||
version = "1.0.0-beta4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"common",
|
||||
"gpui",
|
||||
"image",
|
||||
"itertools 0.13.0",
|
||||
"log",
|
||||
"lsp-types",
|
||||
"regex",
|
||||
"rope",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"smallvec",
|
||||
"smol",
|
||||
"sum_tree",
|
||||
"theme",
|
||||
"unicode-segmentation",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.9.0"
|
||||
@@ -7752,6 +8121,12 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-id"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70ba288e709927c043cbe476718d37be306be53fb1fafecd0dbe36d072be2580"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
@@ -7825,6 +8200,12 @@ version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f942cacf37a673350e8f252dff4691fb8e525a5fff6e1f2c4b6c2f3b8349248d"
|
||||
|
||||
[[package]]
|
||||
name = "unsafe-libyaml"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
@@ -9325,6 +9706,12 @@ dependencies = [
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "workspace-hack"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "beffa227304dbaea3ad6a06ac674f9bc83a3dec3b7f63eeb442de37e7cb6bb01"
|
||||
|
||||
[[package]]
|
||||
name = "writeable"
|
||||
version = "0.6.3"
|
||||
@@ -9438,6 +9825,17 @@ version = "0.8.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f"
|
||||
|
||||
[[package]]
|
||||
name = "xml5ever"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9bbb26405d8e919bc1547a5aa9abc95cbfa438f04844f5fdd9dc7596b748bf69"
|
||||
dependencies = [
|
||||
"log",
|
||||
"mac",
|
||||
"markup5ever",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xmlwriter"
|
||||
version = "0.1.0"
|
||||
@@ -9681,6 +10079,18 @@ dependencies = [
|
||||
"xcb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed-sum-tree"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d490156d0d7311855564d6e1d6dccab992405a0c0e15e1c8ef18920c02177e35"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"log",
|
||||
"rayon",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed-xim"
|
||||
version = "0.4.0-zed"
|
||||
|
||||
@@ -16,6 +16,7 @@ gpui_linux = { git = "https://github.com/zed-industries/zed" }
|
||||
gpui_windows = { git = "https://github.com/zed-industries/zed" }
|
||||
gpui_macos = { git = "https://github.com/zed-industries/zed" }
|
||||
gpui_tokio = { git = "https://github.com/zed-industries/zed" }
|
||||
gpui-component = { git = "https://github.com/reyakov/gpui-component" }
|
||||
reqwest_client = { git = "https://github.com/zed-industries/zed" }
|
||||
|
||||
# Nostr
|
||||
|
||||
@@ -6,8 +6,6 @@ publish.workspace = true
|
||||
|
||||
[dependencies]
|
||||
state = { path = "../state" }
|
||||
ui = { path = "../ui" }
|
||||
theme = { path = "../theme" }
|
||||
common = { path = "../common" }
|
||||
person = { path = "../person" }
|
||||
chat = { path = "../chat" }
|
||||
@@ -15,6 +13,7 @@ settings = { path = "../settings" }
|
||||
|
||||
gpui.workspace = true
|
||||
gpui_tokio.workspace = true
|
||||
gpui-component.workspace = true
|
||||
|
||||
nostr-sdk.workspace = true
|
||||
anyhow.workspace = true
|
||||
|
||||
@@ -4,15 +4,26 @@ use std::sync::Arc;
|
||||
pub use actions::*;
|
||||
use anyhow::{Context as AnyhowContext, Error};
|
||||
use chat::{ChatRegistry, Message, RenderedMessage, Room, RoomEvent, SendReport, SendStatus};
|
||||
use common::{TimestampExt, coop_cache};
|
||||
use common::{CoopIcon, TimestampExt, coop_cache};
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, InteractiveElement, IntoElement, ListAlignment, ListOffset, ListState, MouseButton,
|
||||
ObjectFit, ParentElement, PathPromptOptions, Render, SharedString, SharedUri,
|
||||
AnyElement, App, AppContext, ClipboardItem, Context, Element, Entity, EventEmitter,
|
||||
FocusHandle, Focusable, InteractiveElement, IntoElement, ListAlignment, ListOffset, ListState,
|
||||
MouseButton, ObjectFit, ParentElement, PathPromptOptions, Render, SharedString, SharedUri,
|
||||
StatefulInteractiveElement, Styled, StyledImage, Subscription, Task, WeakEntity, Window,
|
||||
deferred, div, img, list, px, red, relative, svg, white,
|
||||
};
|
||||
use gpui_component::avatar::Avatar;
|
||||
use gpui_component::button::{Button, ButtonVariants};
|
||||
use gpui_component::dock::{Panel, PanelEvent};
|
||||
use gpui_component::input::{Input, InputEvent, InputState};
|
||||
use gpui_component::menu::DropdownMenu;
|
||||
use gpui_component::notification::Notification;
|
||||
use gpui_component::scroll::Scrollbar;
|
||||
use gpui_component::{
|
||||
ActiveTheme, Disableable, Icon, InteractiveElementExt, Sizable, StyledExt, WindowExt, h_flex,
|
||||
v_flex,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use nostr_sdk::prelude::*;
|
||||
use person::{Person, PersonRegistry};
|
||||
@@ -20,18 +31,6 @@ use settings::{AppSettings, SignerKind};
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use smol::lock::RwLock;
|
||||
use state::{NostrRegistry, upload};
|
||||
use theme::ActiveTheme;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock::{Panel, PanelEvent};
|
||||
use ui::input::{InputEvent, InputState, TextInput};
|
||||
use ui::menu::DropdownMenu;
|
||||
use ui::notification::Notification;
|
||||
use ui::scroll::Scrollbar;
|
||||
use ui::{
|
||||
Disableable, Icon, IconName, InteractiveElementExt, Sizable, StyledExt, WindowExtension,
|
||||
h_flex, v_flex,
|
||||
};
|
||||
|
||||
use crate::text::RenderedText;
|
||||
|
||||
@@ -119,7 +118,7 @@ impl ChatPanel {
|
||||
InputState::new(window, cx)
|
||||
.placeholder(format!("Message {}", name))
|
||||
.auto_grow(1, 20)
|
||||
.prevent_new_line_on_enter()
|
||||
.submit_on_enter(true)
|
||||
.clean_on_escape()
|
||||
});
|
||||
|
||||
@@ -674,8 +673,8 @@ impl ChatPanel {
|
||||
let chat = ChatRegistry::global(cx);
|
||||
let seen_on = chat.read(cx).rumor_seen_on(id);
|
||||
|
||||
window.open_modal(cx, move |this, _window, cx| {
|
||||
this.title("Seen on").show_close(true).child(
|
||||
window.open_dialog(cx, move |this, _window, cx| {
|
||||
this.title("Seen on").close_button(true).child(
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.when_none(&seen_on, |this| {
|
||||
@@ -684,7 +683,7 @@ impl ChatPanel {
|
||||
.h_10()
|
||||
.justify_center()
|
||||
.text_sm()
|
||||
.bg(cx.theme().elevated_surface_background)
|
||||
.bg(cx.theme().muted)
|
||||
.rounded(cx.theme().radius)
|
||||
.child("Message isn't traced yet"),
|
||||
)
|
||||
@@ -699,7 +698,7 @@ impl ChatPanel {
|
||||
.h_7()
|
||||
.px_2()
|
||||
.gap_2()
|
||||
.bg(cx.theme().elevated_surface_background)
|
||||
.bg(cx.theme().muted)
|
||||
.rounded(cx.theme().radius)
|
||||
.text_sm()
|
||||
.child(div().size_1p5().rounded_full().bg(gpui::green()))
|
||||
@@ -717,11 +716,11 @@ impl ChatPanel {
|
||||
fn open_relays(&mut self, public_key: &PublicKey, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let profile = self.profile(public_key, cx);
|
||||
|
||||
window.open_modal(cx, move |this, _window, cx| {
|
||||
window.open_dialog(cx, move |this, _window, cx| {
|
||||
let relays = profile.messaging_relays();
|
||||
|
||||
this.title("Messaging Relays")
|
||||
.show_close(true)
|
||||
.close_button(true)
|
||||
.child(v_flex().gap_1().children({
|
||||
let mut items = vec![];
|
||||
|
||||
@@ -731,7 +730,7 @@ impl ChatPanel {
|
||||
.h_7()
|
||||
.px_2()
|
||||
.gap_2()
|
||||
.bg(cx.theme().elevated_surface_background)
|
||||
.bg(cx.theme().muted)
|
||||
.rounded(cx.theme().radius)
|
||||
.text_sm()
|
||||
.child(div().size_1p5().rounded_full().bg(gpui::green()))
|
||||
@@ -760,13 +759,13 @@ impl ChatPanel {
|
||||
.justify_center()
|
||||
.text_center()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().text_placeholder)
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.line_height(relative(1.3))
|
||||
.child(
|
||||
svg()
|
||||
.path("brand/coop.svg")
|
||||
.size_12()
|
||||
.text_color(cx.theme().ghost_element_active),
|
||||
.text_color(cx.theme().muted_foreground),
|
||||
)
|
||||
.child(SharedString::from(ANNOUNCEMENT))
|
||||
.into_any_element()
|
||||
@@ -789,9 +788,9 @@ impl ChatPanel {
|
||||
.size_8()
|
||||
.justify_center()
|
||||
.rounded_full()
|
||||
.bg(cx.theme().warning_background)
|
||||
.bg(cx.theme().warning)
|
||||
.text_color(cx.theme().warning_foreground)
|
||||
.child(Icon::new(IconName::Warning).small()),
|
||||
.child(Icon::new(CoopIcon::Warning).small()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
@@ -867,15 +866,11 @@ impl ChatPanel {
|
||||
.gap_3()
|
||||
.when(!hide_avatar, |this| {
|
||||
this.child(
|
||||
Avatar::new(author.avatar())
|
||||
Avatar::new()
|
||||
.src(author.avatar())
|
||||
.name(author.name())
|
||||
.flex_shrink_0()
|
||||
.relative()
|
||||
.dropdown_menu(move |this, _window, _cx| {
|
||||
this.menu("Public Key", Box::new(Command::Copy(pk)))
|
||||
.menu("View Relays", Box::new(Command::Relays(pk)))
|
||||
.separator()
|
||||
.menu("View on njump.me", Box::new(Command::Njump(pk)))
|
||||
}),
|
||||
.relative(),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
@@ -888,17 +883,17 @@ impl ChatPanel {
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().text_placeholder)
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(
|
||||
div()
|
||||
.font_semibold()
|
||||
.text_color(cx.theme().text)
|
||||
.text_color(cx.theme().foreground)
|
||||
.child(author.name()),
|
||||
)
|
||||
.when(encrypted_by_dekey, |this| {
|
||||
this.child(
|
||||
Button::new(format!("dekey-{ix}"))
|
||||
.icon(IconName::Shield)
|
||||
.icon(CoopIcon::Shield)
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.px_4()
|
||||
@@ -920,13 +915,13 @@ impl ChatPanel {
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.group_hover("", |this| this.bg(cx.theme().element_active))
|
||||
.group_hover("", |this| this.bg(cx.theme().primary_active))
|
||||
.absolute()
|
||||
.left_0()
|
||||
.top_0()
|
||||
.w(px(2.))
|
||||
.h_full()
|
||||
.bg(cx.theme().border_transparent),
|
||||
.bg(cx.theme().transparent),
|
||||
)
|
||||
.child(self.render_actions(&id, &pk, cx))
|
||||
.on_mouse_down(
|
||||
@@ -938,7 +933,7 @@ impl ChatPanel {
|
||||
.on_double_click(cx.listener(move |this, _, _window, cx| {
|
||||
this.reply_to(&id, cx);
|
||||
}))
|
||||
.hover(|this| this.bg(cx.theme().surface_background))
|
||||
.hover(|this| this.bg(cx.theme().muted))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -953,7 +948,7 @@ impl ChatPanel {
|
||||
return div().child(
|
||||
img(media[0].clone())
|
||||
.border_1()
|
||||
.border_color(cx.theme().border_variant)
|
||||
.border_color(cx.theme().border)
|
||||
.h(px(250.))
|
||||
.object_fit(ObjectFit::Cover)
|
||||
.rounded(cx.theme().radius),
|
||||
@@ -981,7 +976,7 @@ impl ChatPanel {
|
||||
img(item.clone())
|
||||
.h_32()
|
||||
.border_1()
|
||||
.border_color(cx.theme().border_variant)
|
||||
.border_color(cx.theme().border)
|
||||
.rounded(cx.theme().radius),
|
||||
),
|
||||
);
|
||||
@@ -1010,11 +1005,11 @@ impl ChatPanel {
|
||||
.w_full()
|
||||
.px_2()
|
||||
.border_l_2()
|
||||
.border_color(cx.theme().element_selected)
|
||||
.border_color(cx.theme().primary_active)
|
||||
.text_sm()
|
||||
.child(
|
||||
div()
|
||||
.text_color(cx.theme().text_accent)
|
||||
.text_color(cx.theme().accent_foreground)
|
||||
.child(author.name()),
|
||||
)
|
||||
.child(
|
||||
@@ -1024,7 +1019,7 @@ impl ChatPanel {
|
||||
.line_clamp(1)
|
||||
.child(SharedString::from(&message.content)),
|
||||
)
|
||||
.hover(|this| this.bg(cx.theme().elevated_surface_background))
|
||||
.hover(|this| this.bg(cx.theme().muted))
|
||||
.on_click({
|
||||
let id = *id;
|
||||
cx.listener(move |this, _event, _window, _cx| {
|
||||
@@ -1065,15 +1060,15 @@ impl ChatPanel {
|
||||
div()
|
||||
.id(SharedString::from(id.to_hex()))
|
||||
.child(label)
|
||||
.when(failed, |this| this.text_color(cx.theme().text_danger))
|
||||
.when(failed, |this| this.text_color(cx.theme().danger_foreground))
|
||||
.when_some(reports, |this, reports| {
|
||||
this.when(!pending, |this| {
|
||||
this.on_click(move |_e, window, cx| {
|
||||
let reports = reports.clone();
|
||||
|
||||
window.open_modal(cx, move |this, _window, cx| {
|
||||
window.open_dialog(cx, move |this, _window, cx| {
|
||||
this.title(SharedString::from("Sent Reports"))
|
||||
.show_close(true)
|
||||
.close_button(true)
|
||||
.child(v_flex().gap_4().children({
|
||||
let mut items = Vec::with_capacity(reports.len());
|
||||
|
||||
@@ -1107,7 +1102,7 @@ impl ChatPanel {
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.font_semibold()
|
||||
.child(Avatar::new(avatar).small())
|
||||
.child(Avatar::new().src(avatar).name(name.clone()).small())
|
||||
.child(name.clone()),
|
||||
),
|
||||
)
|
||||
@@ -1121,7 +1116,7 @@ impl ChatPanel {
|
||||
.w_full()
|
||||
.text_sm()
|
||||
.rounded(cx.theme().radius)
|
||||
.bg(cx.theme().warning_background)
|
||||
.bg(cx.theme().warning)
|
||||
.text_color(cx.theme().warning_foreground)
|
||||
.child(div().flex_1().w_full().text_center().child(error)),
|
||||
)
|
||||
@@ -1141,7 +1136,7 @@ impl ChatPanel {
|
||||
.p_1()
|
||||
.w_full()
|
||||
.rounded(cx.theme().radius)
|
||||
.bg(cx.theme().danger_background)
|
||||
.bg(cx.theme().danger)
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
@@ -1171,7 +1166,7 @@ impl ChatPanel {
|
||||
.p_1()
|
||||
.w_full()
|
||||
.rounded(cx.theme().radius)
|
||||
.bg(cx.theme().elevated_surface_background)
|
||||
.bg(cx.theme().muted)
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
@@ -1214,7 +1209,7 @@ impl ChatPanel {
|
||||
.bg(cx.theme().background)
|
||||
.child(
|
||||
Button::new("reply")
|
||||
.icon(IconName::Reply)
|
||||
.icon(CoopIcon::Reply)
|
||||
.tooltip("Reply")
|
||||
.small()
|
||||
.ghost()
|
||||
@@ -1227,7 +1222,7 @@ impl ChatPanel {
|
||||
)
|
||||
.child(
|
||||
Button::new("copy")
|
||||
.icon(IconName::Copy)
|
||||
.icon(CoopIcon::Copy)
|
||||
.tooltip("Copy")
|
||||
.small()
|
||||
.ghost()
|
||||
@@ -1241,7 +1236,7 @@ impl ChatPanel {
|
||||
.child(div().flex_shrink_0().h_4().w_px().bg(cx.theme().border))
|
||||
.child(
|
||||
Button::new("advance")
|
||||
.icon(IconName::Ellipsis)
|
||||
.icon(CoopIcon::Ellipsis)
|
||||
.small()
|
||||
.ghost()
|
||||
.dropdown_menu({
|
||||
@@ -1279,7 +1274,7 @@ impl ChatPanel {
|
||||
.justify_center()
|
||||
.rounded_full()
|
||||
.bg(red())
|
||||
.child(Icon::new(IconName::Close).size_2().text_color(white())),
|
||||
.child(Icon::new(CoopIcon::Close).size_2().text_color(white())),
|
||||
)
|
||||
.on_click({
|
||||
let url = url.clone();
|
||||
@@ -1312,7 +1307,7 @@ impl ChatPanel {
|
||||
.w_full()
|
||||
.pl_2()
|
||||
.border_l_2()
|
||||
.border_color(cx.theme().element_active)
|
||||
.border_color(cx.theme().secondary_active)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
@@ -1324,17 +1319,17 @@ impl ChatPanel {
|
||||
.items_baseline()
|
||||
.gap_1()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(SharedString::from("Replying to:"))
|
||||
.child(
|
||||
div()
|
||||
.text_color(cx.theme().text_accent)
|
||||
.text_color(cx.theme().secondary_foreground)
|
||||
.child(profile.name()),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Button::new("remove-reply")
|
||||
.icon(IconName::Close)
|
||||
.icon(CoopIcon::Close)
|
||||
.xsmall()
|
||||
.ghost()
|
||||
.on_click({
|
||||
@@ -1382,7 +1377,7 @@ impl ChatPanel {
|
||||
.unwrap_or((true, SignerKind::default()));
|
||||
|
||||
Button::new("encryption")
|
||||
.icon(IconName::Settings2)
|
||||
.icon(CoopIcon::Settings2)
|
||||
.tooltip("Configuration")
|
||||
.ghost()
|
||||
.large()
|
||||
@@ -1418,11 +1413,11 @@ impl ChatPanel {
|
||||
|
||||
fn render_emoji_menu(&self, _window: &Window, _cx: &Context<Self>) -> impl IntoElement {
|
||||
Button::new("emoji")
|
||||
.icon(IconName::Emoji)
|
||||
.icon(CoopIcon::Emoji)
|
||||
.ghost()
|
||||
.large()
|
||||
.dropdown_menu_with_anchor(gpui::Anchor::BottomLeft, move |this, _window, _cx| {
|
||||
this.horizontal()
|
||||
this.separator()
|
||||
.menu("👍", Box::new(Command::Insert("👍")))
|
||||
.menu("👎", Box::new(Command::Insert("👎")))
|
||||
.menu("😄", Box::new(Command::Insert("😄")))
|
||||
@@ -1436,11 +1431,11 @@ impl ChatPanel {
|
||||
}
|
||||
|
||||
impl Panel for ChatPanel {
|
||||
fn panel_id(&self) -> SharedString {
|
||||
self.id.clone()
|
||||
fn panel_name(&self) -> &'static str {
|
||||
"Chat"
|
||||
}
|
||||
|
||||
fn title(&self, cx: &App) -> AnyElement {
|
||||
fn title(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
self.room
|
||||
.read_with(cx, |this, cx| {
|
||||
let label = this.display_name(cx);
|
||||
@@ -1448,19 +1443,24 @@ impl Panel for ChatPanel {
|
||||
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(Avatar::new(url).xsmall())
|
||||
.child(Avatar::new().src(url).xsmall())
|
||||
.child(label)
|
||||
.into_any_element()
|
||||
})
|
||||
.unwrap_or(div().child("Unknown").into_any_element())
|
||||
.unwrap_or(div().child("Unknown").into_any())
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn toolbar_buttons(&self, _window: &Window, _cx: &App) -> Vec<Button> {
|
||||
fn toolbar_buttons(
|
||||
&mut self,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) -> Option<Vec<Button>> {
|
||||
let subject_bar = self.subject_bar.clone();
|
||||
|
||||
vec![
|
||||
Some(vec![
|
||||
Button::new("subject")
|
||||
.icon(IconName::Input)
|
||||
.icon(CoopIcon::Input)
|
||||
.tooltip("Change subject")
|
||||
.small()
|
||||
.ghost()
|
||||
@@ -1470,7 +1470,7 @@ impl Panel for ChatPanel {
|
||||
cx.notify();
|
||||
});
|
||||
}),
|
||||
]
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1497,15 +1497,10 @@ impl Render for ChatPanel {
|
||||
.gap_2()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().border)
|
||||
.child(
|
||||
TextInput::new(&self.subject_input)
|
||||
.text_sm()
|
||||
.small()
|
||||
.bordered(false),
|
||||
)
|
||||
.child(Input::new(&self.subject_input).text_sm().bordered(false))
|
||||
.child(
|
||||
Button::new("change")
|
||||
.icon(IconName::CheckCircle)
|
||||
.icon(CoopIcon::CheckCircle)
|
||||
.label("Change")
|
||||
.secondary()
|
||||
.disabled(self.uploading)
|
||||
@@ -1543,7 +1538,7 @@ impl Render for ChatPanel {
|
||||
.items_end()
|
||||
.child(
|
||||
Button::new("upload")
|
||||
.icon(IconName::Plus)
|
||||
.icon(CoopIcon::Plus)
|
||||
.tooltip("Upload media")
|
||||
.loading(self.uploading)
|
||||
.disabled(self.uploading)
|
||||
@@ -1553,12 +1548,7 @@ impl Render for ChatPanel {
|
||||
this.upload(window, cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
TextInput::new(&self.input)
|
||||
.appearance(false)
|
||||
.text_sm()
|
||||
.flex_1(),
|
||||
)
|
||||
.child(Input::new(&self.input).appearance(false).text_sm().flex_1())
|
||||
.child(
|
||||
h_flex()
|
||||
.pl_1()
|
||||
@@ -1567,7 +1557,7 @@ impl Render for ChatPanel {
|
||||
.child(self.render_config_menu(window, cx))
|
||||
.child(
|
||||
Button::new("send")
|
||||
.icon(IconName::PaperPlaneFill)
|
||||
.icon(CoopIcon::PaperPlaneFill)
|
||||
.disabled(self.uploading)
|
||||
.ghost()
|
||||
.large()
|
||||
|
||||
@@ -7,8 +7,8 @@ use gpui::{
|
||||
AnyElement, App, ElementId, Entity, FontStyle, FontWeight, HighlightStyle, InteractiveText,
|
||||
IntoElement, SharedString, StrikethroughStyle, StyledText, UnderlineStyle, Window,
|
||||
};
|
||||
use gpui_component::ActiveTheme;
|
||||
use person::PersonRegistry;
|
||||
use theme::ActiveTheme;
|
||||
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
#[allow(dead_code)]
|
||||
@@ -68,8 +68,8 @@ impl RenderedText {
|
||||
}
|
||||
|
||||
pub fn element(&self, id: ElementId, window: &Window, cx: &App) -> AnyElement {
|
||||
let code_background = cx.theme().elevated_surface_background;
|
||||
let color = cx.theme().text_accent;
|
||||
let code_background = cx.theme().secondary;
|
||||
let color = cx.theme().primary_active;
|
||||
|
||||
InteractiveText::new(
|
||||
id,
|
||||
|
||||
@@ -6,6 +6,7 @@ publish.workspace = true
|
||||
|
||||
[dependencies]
|
||||
gpui.workspace = true
|
||||
gpui-component.workspace = true
|
||||
nostr.workspace = true
|
||||
nostr-sdk.workspace = true
|
||||
|
||||
|
||||
132
crates/common/src/icons.rs
Normal file
132
crates/common/src/icons.rs
Normal file
@@ -0,0 +1,132 @@
|
||||
use gpui_component::IconNamed;
|
||||
|
||||
pub enum CoopIcon {
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
Boom,
|
||||
Book,
|
||||
ChevronDown,
|
||||
CaretDown,
|
||||
CaretRight,
|
||||
CaretUp,
|
||||
Check,
|
||||
CheckCircle,
|
||||
Close,
|
||||
CloseCircle,
|
||||
CloseCircleFill,
|
||||
Copy,
|
||||
Device,
|
||||
Door,
|
||||
Ellipsis,
|
||||
Emoji,
|
||||
Eye,
|
||||
Input,
|
||||
Info,
|
||||
Invite,
|
||||
Inbox,
|
||||
InboxFill,
|
||||
Link,
|
||||
Loader,
|
||||
Moon,
|
||||
Plus,
|
||||
PlusCircle,
|
||||
Profile,
|
||||
Reset,
|
||||
Relay,
|
||||
Reply,
|
||||
Refresh,
|
||||
Scan,
|
||||
Search,
|
||||
Settings,
|
||||
Settings2,
|
||||
Sun,
|
||||
Ship,
|
||||
Shield,
|
||||
Group,
|
||||
UserKey,
|
||||
Upload,
|
||||
Usb,
|
||||
PanelLeft,
|
||||
PanelLeftOpen,
|
||||
PanelRight,
|
||||
PanelRightOpen,
|
||||
PanelBottom,
|
||||
PanelBottomOpen,
|
||||
PaperPlaneFill,
|
||||
Warning,
|
||||
WindowClose,
|
||||
WindowMaximize,
|
||||
WindowMinimize,
|
||||
WindowRestore,
|
||||
Fistbump,
|
||||
FistbumpFill,
|
||||
Zoom,
|
||||
}
|
||||
|
||||
impl IconNamed for CoopIcon {
|
||||
fn path(self) -> gpui::SharedString {
|
||||
match self {
|
||||
Self::ArrowLeft => "icons/arrow-left.svg",
|
||||
Self::ArrowRight => "icons/arrow-right.svg",
|
||||
Self::Boom => "icons/boom.svg",
|
||||
Self::Book => "icons/book.svg",
|
||||
Self::ChevronDown => "icons/chevron-down.svg",
|
||||
Self::CaretDown => "icons/caret-down.svg",
|
||||
Self::CaretRight => "icons/caret-right.svg",
|
||||
Self::CaretUp => "icons/caret-up.svg",
|
||||
Self::Check => "icons/check.svg",
|
||||
Self::CheckCircle => "icons/check-circle.svg",
|
||||
Self::Close => "icons/close.svg",
|
||||
Self::CloseCircle => "icons/close-circle.svg",
|
||||
Self::CloseCircleFill => "icons/close-circle-fill.svg",
|
||||
Self::Copy => "icons/copy.svg",
|
||||
Self::Device => "icons/device.svg",
|
||||
Self::Door => "icons/door.svg",
|
||||
Self::Ellipsis => "icons/ellipsis.svg",
|
||||
Self::Emoji => "icons/emoji.svg",
|
||||
Self::Eye => "icons/eye.svg",
|
||||
Self::Input => "icons/input.svg",
|
||||
Self::Info => "icons/info.svg",
|
||||
Self::Invite => "icons/invite.svg",
|
||||
Self::Inbox => "icons/inbox.svg",
|
||||
Self::InboxFill => "icons/inbox-fill.svg",
|
||||
Self::Link => "icons/link.svg",
|
||||
Self::Loader => "icons/loader.svg",
|
||||
Self::Moon => "icons/moon.svg",
|
||||
Self::Plus => "icons/plus.svg",
|
||||
Self::PlusCircle => "icons/plus-circle.svg",
|
||||
Self::Profile => "icons/profile.svg",
|
||||
Self::Reset => "icons/reset.svg",
|
||||
Self::Relay => "icons/relay.svg",
|
||||
Self::Reply => "icons/reply.svg",
|
||||
Self::Refresh => "icons/refresh.svg",
|
||||
Self::Scan => "icons/scan.svg",
|
||||
Self::Search => "icons/search.svg",
|
||||
Self::Settings => "icons/settings.svg",
|
||||
Self::Settings2 => "icons/settings2.svg",
|
||||
Self::Sun => "icons/sun.svg",
|
||||
Self::Ship => "icons/ship.svg",
|
||||
Self::Shield => "icons/shield.svg",
|
||||
Self::UserKey => "icons/user-key.svg",
|
||||
Self::Upload => "icons/upload.svg",
|
||||
Self::Usb => "icons/usb.svg",
|
||||
Self::Group => "icons/group.svg",
|
||||
Self::PanelLeft => "icons/panel-left.svg",
|
||||
Self::PanelLeftOpen => "icons/panel-left-open.svg",
|
||||
Self::PanelRight => "icons/panel-right.svg",
|
||||
Self::PanelRightOpen => "icons/panel-right-open.svg",
|
||||
Self::PanelBottom => "icons/panel-bottom.svg",
|
||||
Self::PanelBottomOpen => "icons/panel-bottom-open.svg",
|
||||
Self::PaperPlaneFill => "icons/paper-plane-fill.svg",
|
||||
Self::Warning => "icons/warning.svg",
|
||||
Self::WindowClose => "icons/window-close.svg",
|
||||
Self::WindowMaximize => "icons/window-maximize.svg",
|
||||
Self::WindowMinimize => "icons/window-minimize.svg",
|
||||
Self::WindowRestore => "icons/window-restore.svg",
|
||||
Self::Fistbump => "icons/fistbump.svg",
|
||||
Self::FistbumpFill => "icons/fistbump-fill.svg",
|
||||
Self::Zoom => "icons/zoom.svg",
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ pub use caching::*;
|
||||
pub use debounced_delay::*;
|
||||
pub use display::*;
|
||||
pub use event::*;
|
||||
pub use icons::*;
|
||||
pub use media_extractor::*;
|
||||
pub use parser::*;
|
||||
pub use paths::*;
|
||||
@@ -11,6 +12,7 @@ mod caching;
|
||||
mod debounced_delay;
|
||||
mod display;
|
||||
mod event;
|
||||
mod icons;
|
||||
mod media_extractor;
|
||||
mod parser;
|
||||
mod paths;
|
||||
|
||||
@@ -8,10 +8,10 @@ publish.workspace = true
|
||||
common = { path = "../common" }
|
||||
state = { path = "../state" }
|
||||
person = { path = "../person" }
|
||||
ui = { path = "../ui" }
|
||||
theme = { path = "../theme" }
|
||||
|
||||
gpui.workspace = true
|
||||
gpui-component.workspace = true
|
||||
|
||||
nostr-sdk.workspace = true
|
||||
|
||||
anyhow.workspace = true
|
||||
|
||||
@@ -9,14 +9,13 @@ use gpui::{
|
||||
App, AppContext, Context, Entity, EventEmitter, Global, IntoElement, ParentElement,
|
||||
SharedString, Styled, Subscription, Task, Window, div, relative,
|
||||
};
|
||||
use gpui_component::avatar::Avatar;
|
||||
use gpui_component::button::Button;
|
||||
use gpui_component::notification::{Notification, NotificationType};
|
||||
use gpui_component::{ActiveTheme, Disableable, Sizable, StyledExt, WindowExt, h_flex, v_flex};
|
||||
use nostr_sdk::prelude::*;
|
||||
use person::PersonRegistry;
|
||||
use state::{Announcement, CLIENT_NAME, NostrRegistry, StateEvent, TIMEOUT};
|
||||
use theme::ActiveTheme;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::Button;
|
||||
use ui::notification::{Notification, NotificationKind};
|
||||
use ui::{Disableable, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
|
||||
|
||||
const IDENTIFIER: &str = "coop:device";
|
||||
const MSG: &str = "You've requested an encryption key from another device. \
|
||||
@@ -553,7 +552,7 @@ impl DeviceRegistry {
|
||||
match task.await {
|
||||
Ok(_) => {
|
||||
cx.update(|window, cx| {
|
||||
window.clear_notification_by_id::<DeviceNotification>(id, cx);
|
||||
window.remove_notification1::<DeviceNotification>(id, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
@@ -595,9 +594,9 @@ impl DeviceRegistry {
|
||||
let key = SharedString::from(event.id.to_hex());
|
||||
|
||||
Notification::new()
|
||||
.type_id::<DeviceNotification>(key)
|
||||
.id1::<DeviceNotification>(key)
|
||||
.autohide(false)
|
||||
.with_kind(NotificationKind::Info)
|
||||
.with_type(NotificationType::Info)
|
||||
.title("Encryption Key Request")
|
||||
.content(move |_this, _window, cx| {
|
||||
v_flex()
|
||||
@@ -620,7 +619,7 @@ impl DeviceRegistry {
|
||||
div()
|
||||
.font_semibold()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(SharedString::from("From:")),
|
||||
)
|
||||
.child(
|
||||
@@ -629,11 +628,16 @@ impl DeviceRegistry {
|
||||
.w_full()
|
||||
.px_2()
|
||||
.rounded(cx.theme().radius)
|
||||
.bg(cx.theme().elevated_surface_background)
|
||||
.bg(cx.theme().muted)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Avatar::new(profile.avatar()).xsmall())
|
||||
.child(
|
||||
Avatar::new()
|
||||
.src(profile.avatar())
|
||||
.name(profile.name())
|
||||
.xsmall(),
|
||||
)
|
||||
.child(profile.name()),
|
||||
),
|
||||
),
|
||||
@@ -646,7 +650,7 @@ impl DeviceRegistry {
|
||||
div()
|
||||
.font_semibold()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(SharedString::from("Client:")),
|
||||
)
|
||||
.child(
|
||||
@@ -655,7 +659,7 @@ impl DeviceRegistry {
|
||||
.w_full()
|
||||
.px_2()
|
||||
.rounded(cx.theme().radius)
|
||||
.bg(cx.theme().elevated_surface_background)
|
||||
.bg(cx.theme().muted)
|
||||
.child(request.client_name()),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -8,10 +8,10 @@ publish.workspace = true
|
||||
state = { path = "../state" }
|
||||
settings = { path = "../settings" }
|
||||
common = { path = "../common" }
|
||||
theme = { path = "../theme" }
|
||||
ui = { path = "../ui" }
|
||||
|
||||
gpui.workspace = true
|
||||
gpui-component.workspace = true
|
||||
|
||||
nostr-sdk.workspace = true
|
||||
|
||||
anyhow.workspace = true
|
||||
|
||||
@@ -10,14 +10,13 @@ use gpui::{
|
||||
App, AppContext, Context, Entity, Global, IntoElement, ParentElement, SharedString, Styled,
|
||||
Task, Window, div, relative,
|
||||
};
|
||||
use gpui_component::button::Button;
|
||||
use gpui_component::notification::{Notification, NotificationType};
|
||||
use gpui_component::{ActiveTheme, Disableable, WindowExt, v_flex};
|
||||
use nostr_sdk::prelude::*;
|
||||
use settings::{AppSettings, AuthMode};
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use state::NostrRegistry;
|
||||
use theme::ActiveTheme;
|
||||
use ui::button::Button;
|
||||
use ui::notification::{Notification, NotificationKind};
|
||||
use ui::{Disableable, WindowExtension, v_flex};
|
||||
|
||||
const AUTH_MESSAGE: &str =
|
||||
"Approve the authentication request to allow Coop to continue sending or receiving events.";
|
||||
@@ -270,7 +269,7 @@ impl RelayAuth {
|
||||
let url = req.url();
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
window.clear_notification_by_id::<AuthNotification>(challenge, cx);
|
||||
window.remove_notification1::<AuthNotification>(challenge, cx);
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
@@ -325,9 +324,9 @@ impl RelayAuth {
|
||||
let loading = Rc::new(Cell::new(false));
|
||||
|
||||
Notification::new()
|
||||
.type_id::<AuthNotification>(challenge)
|
||||
.id1::<AuthNotification>(challenge)
|
||||
.autohide(false)
|
||||
.with_kind(NotificationKind::Info)
|
||||
.with_type(NotificationType::Info)
|
||||
.title("Authentication Required")
|
||||
.content(move |_this, _window, cx| {
|
||||
v_flex()
|
||||
@@ -344,8 +343,8 @@ impl RelayAuth {
|
||||
.px_1p5()
|
||||
.rounded_sm()
|
||||
.text_xs()
|
||||
.bg(cx.theme().elevated_surface_background)
|
||||
.text_color(cx.theme().text)
|
||||
.bg(cx.theme().muted)
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(url.clone()),
|
||||
)
|
||||
.into_any_element()
|
||||
|
||||
@@ -5,7 +5,6 @@ edition.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[dependencies]
|
||||
theme = { path = "../theme" }
|
||||
common = { path = "../common" }
|
||||
|
||||
nostr-sdk.workspace = true
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fmt::Display;
|
||||
use std::rc::Rc;
|
||||
|
||||
use anyhow::{Error, anyhow};
|
||||
use common::config_dir;
|
||||
@@ -8,7 +7,6 @@ use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task, Window}
|
||||
use nostr_sdk::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use theme::{Theme, ThemeFamily, ThemeMode};
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) {
|
||||
AppSettings::set_global(cx.new(|cx| AppSettings::new(window, cx)), cx)
|
||||
@@ -36,8 +34,6 @@ macro_rules! setting_accessors {
|
||||
}
|
||||
|
||||
setting_accessors! {
|
||||
pub theme: Option<String>,
|
||||
pub theme_mode: ThemeMode,
|
||||
pub hide_avatar: bool,
|
||||
pub screening: bool,
|
||||
pub auth_mode: AuthMode,
|
||||
@@ -125,12 +121,6 @@ impl RoomConfig {
|
||||
/// Settings
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Settings {
|
||||
/// Theme
|
||||
pub theme: Option<String>,
|
||||
|
||||
/// Theme mode
|
||||
pub theme_mode: ThemeMode,
|
||||
|
||||
/// Hide user avatars
|
||||
pub hide_avatar: bool,
|
||||
|
||||
@@ -153,8 +143,6 @@ pub struct Settings {
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
theme: None,
|
||||
theme_mode: ThemeMode::default(),
|
||||
hide_avatar: false,
|
||||
screening: true,
|
||||
auth_mode: AuthMode::default(),
|
||||
@@ -238,9 +226,8 @@ impl AppSettings {
|
||||
let settings = task.await.unwrap_or(Settings::default());
|
||||
|
||||
// Update settings
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.update_in(cx, |this, _window, cx| {
|
||||
this.set_settings(settings, cx);
|
||||
this.apply_theme(window, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
@@ -264,43 +251,6 @@ impl AppSettings {
|
||||
task.detach();
|
||||
}
|
||||
|
||||
/// Set theme
|
||||
pub fn set_theme<T>(&mut self, theme: T, window: &mut Window, cx: &mut Context<Self>)
|
||||
where
|
||||
T: Into<String>,
|
||||
{
|
||||
// Update settings
|
||||
self.values.theme = Some(theme.into());
|
||||
cx.notify();
|
||||
|
||||
// Apply the new theme
|
||||
self.apply_theme(window, cx);
|
||||
}
|
||||
|
||||
/// Reset theme
|
||||
pub fn reset_theme(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.values.theme = None;
|
||||
cx.notify();
|
||||
|
||||
self.apply_theme(window, cx);
|
||||
}
|
||||
|
||||
/// Apply theme
|
||||
pub fn apply_theme(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(name) = self.values.theme.as_ref() {
|
||||
let mode = self.values.theme_mode;
|
||||
|
||||
if let Ok(new_theme) = ThemeFamily::from_assets(name) {
|
||||
Theme::apply_theme(Rc::new(new_theme), Some(window), cx);
|
||||
Theme::change(mode, Some(window), cx);
|
||||
} else {
|
||||
log::info!("Failed to load theme: {name}");
|
||||
}
|
||||
} else {
|
||||
Theme::apply_theme(Rc::new(ThemeFamily::default()), Some(window), cx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the given relay is already authenticated
|
||||
pub fn trusted_relay(&self, url: &RelayUrl, _cx: &App) -> bool {
|
||||
self.values.trusted_relays.iter().any(|relay| {
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
[package]
|
||||
name = "theme"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[dependencies]
|
||||
gpui.workspace = true
|
||||
anyhow.workspace = true
|
||||
log.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
schemars.workspace = true
|
||||
smallvec.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.10"
|
||||
@@ -1 +0,0 @@
|
||||
../../LICENSE
|
||||
@@ -1 +0,0 @@
|
||||
CREDITS: [zed/theme](https://github.com/zed-industries/zed/tree/main/crates/theme)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,159 +0,0 @@
|
||||
use std::fmt::{self, Debug, Display, Formatter};
|
||||
|
||||
use gpui::{AbsoluteLength, Axis, Length, Pixels};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A enum for defining the placement of the element.
|
||||
///
|
||||
/// See also: [`Side`] if you need to define the left, right side.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Placement {
|
||||
#[serde(rename = "top")]
|
||||
Top,
|
||||
#[serde(rename = "bottom")]
|
||||
Bottom,
|
||||
#[serde(rename = "left")]
|
||||
Left,
|
||||
#[serde(rename = "right")]
|
||||
Right,
|
||||
}
|
||||
|
||||
impl Display for Placement {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Placement::Top => write!(f, "Top"),
|
||||
Placement::Bottom => write!(f, "Bottom"),
|
||||
Placement::Left => write!(f, "Left"),
|
||||
Placement::Right => write!(f, "Right"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Placement {
|
||||
#[inline]
|
||||
pub fn is_horizontal(&self) -> bool {
|
||||
matches!(self, Placement::Left | Placement::Right)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_vertical(&self) -> bool {
|
||||
matches!(self, Placement::Top | Placement::Bottom)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn axis(&self) -> Axis {
|
||||
match self {
|
||||
Placement::Top | Placement::Bottom => Axis::Vertical,
|
||||
Placement::Left | Placement::Right => Axis::Horizontal,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A enum for defining the side of the element.
|
||||
///
|
||||
/// See also: [`Placement`] if you need to define the 4 edges.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Side {
|
||||
#[serde(rename = "left")]
|
||||
Left,
|
||||
#[serde(rename = "right")]
|
||||
Right,
|
||||
}
|
||||
|
||||
impl Side {
|
||||
/// Returns true if the side is left.
|
||||
#[inline]
|
||||
pub fn is_left(&self) -> bool {
|
||||
matches!(self, Self::Left)
|
||||
}
|
||||
|
||||
/// Returns true if the side is right.
|
||||
#[inline]
|
||||
pub fn is_right(&self) -> bool {
|
||||
matches!(self, Self::Right)
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait to extend the [`Axis`] enum with utility methods.
|
||||
pub trait AxisExt {
|
||||
#[allow(clippy::wrong_self_convention)]
|
||||
fn is_horizontal(self) -> bool;
|
||||
#[allow(clippy::wrong_self_convention)]
|
||||
fn is_vertical(self) -> bool;
|
||||
}
|
||||
|
||||
impl AxisExt for Axis {
|
||||
#[inline]
|
||||
fn is_horizontal(self) -> bool {
|
||||
self == Axis::Horizontal
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_vertical(self) -> bool {
|
||||
self == Axis::Vertical
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for converting [`Pixels`] to `f32` and `f64`.
|
||||
pub trait PixelsExt {
|
||||
fn as_f32(&self) -> f32;
|
||||
#[allow(clippy::wrong_self_convention)]
|
||||
fn as_f64(self) -> f64;
|
||||
}
|
||||
impl PixelsExt for Pixels {
|
||||
fn as_f32(&self) -> f32 {
|
||||
f32::from(self)
|
||||
}
|
||||
|
||||
fn as_f64(self) -> f64 {
|
||||
f64::from(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait to extend the [`Length`] enum with utility methods.
|
||||
pub trait LengthExt {
|
||||
/// Converts the [`Length`] to [`Pixels`] based on a given `base_size` and `rem_size`.
|
||||
///
|
||||
/// If the [`Length`] is [`Length::Auto`], it returns `None`.
|
||||
fn to_pixels(&self, base_size: AbsoluteLength, rem_size: Pixels) -> Option<Pixels>;
|
||||
}
|
||||
|
||||
impl LengthExt for Length {
|
||||
fn to_pixels(&self, base_size: AbsoluteLength, rem_size: Pixels) -> Option<Pixels> {
|
||||
match self {
|
||||
Length::Auto => None,
|
||||
Length::Definite(len) => Some(len.to_pixels(base_size, rem_size)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A struct for defining the edges of an element.
|
||||
///
|
||||
/// A extend version of [`gpui::Edges`] to serialize/deserialize.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, Eq, PartialEq)]
|
||||
#[repr(C)]
|
||||
pub struct Edges<T: Clone + Debug + Default + PartialEq> {
|
||||
/// The size of the top edge.
|
||||
pub top: T,
|
||||
/// The size of the right edge.
|
||||
pub right: T,
|
||||
/// The size of the bottom edge.
|
||||
pub bottom: T,
|
||||
/// The size of the left edge.
|
||||
pub left: T,
|
||||
}
|
||||
|
||||
impl<T> Edges<T>
|
||||
where
|
||||
T: Clone + Debug + Default + PartialEq,
|
||||
{
|
||||
/// Creates a new `Edges` instance with all edges set to the same value.
|
||||
pub fn all(value: T) -> Self {
|
||||
Self {
|
||||
top: value.clone(),
|
||||
right: value.clone(),
|
||||
bottom: value.clone(),
|
||||
left: value,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{App, Global, Pixels, SharedString, Window, px};
|
||||
|
||||
mod colors;
|
||||
mod geometry;
|
||||
mod notification;
|
||||
mod platform_kind;
|
||||
mod registry;
|
||||
mod scale;
|
||||
mod scrollbar_mode;
|
||||
mod theme;
|
||||
|
||||
pub use colors::*;
|
||||
pub use geometry::*;
|
||||
pub use notification::*;
|
||||
pub use platform_kind::PlatformKind;
|
||||
pub use registry::*;
|
||||
pub use scale::*;
|
||||
pub use scrollbar_mode::*;
|
||||
pub use theme::*;
|
||||
|
||||
/// Defines window border radius for platforms that use client side decorations.
|
||||
pub const CLIENT_SIDE_DECORATION_ROUNDING: Pixels = px(10.0);
|
||||
|
||||
/// Defines window shadow size for platforms that use client side decorations.
|
||||
pub const CLIENT_SIDE_DECORATION_SHADOW: Pixels = px(10.0);
|
||||
|
||||
/// Defines window border size for platforms that use client side decorations.
|
||||
pub const CLIENT_SIDE_DECORATION_BORDER: Pixels = px(1.0);
|
||||
|
||||
/// Defines window titlebar height
|
||||
pub const TITLEBAR_HEIGHT: Pixels = px(36.0);
|
||||
|
||||
/// Defines workspace tabbar height
|
||||
pub const TABBAR_HEIGHT: Pixels = px(28.0);
|
||||
|
||||
/// Defines default sidebar width
|
||||
pub const SIDEBAR_WIDTH: Pixels = px(240.);
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
registry::init(cx);
|
||||
|
||||
Theme::sync_system_appearance(None, cx);
|
||||
Theme::sync_scrollbar_appearance(cx);
|
||||
}
|
||||
|
||||
pub trait ActiveTheme {
|
||||
fn theme(&self) -> &Theme;
|
||||
}
|
||||
|
||||
impl ActiveTheme for App {
|
||||
#[inline(always)]
|
||||
fn theme(&self) -> &Theme {
|
||||
Theme::global(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Theme {
|
||||
/// Theme colors
|
||||
pub colors: ThemeColors,
|
||||
|
||||
/// Theme family
|
||||
pub theme: Rc<ThemeFamily>,
|
||||
|
||||
/// The appearance of the theme (light or dark).
|
||||
pub mode: ThemeMode,
|
||||
|
||||
/// The font family for the application.
|
||||
pub font_family: SharedString,
|
||||
|
||||
/// The root font size for the application, default is 15px.
|
||||
pub font_size: Pixels,
|
||||
|
||||
/// Radius for the general elements.
|
||||
pub radius: Pixels,
|
||||
|
||||
/// Radius for the large elements, e.g.: modal, notification.
|
||||
pub radius_lg: Pixels,
|
||||
|
||||
/// Enable shadow for the general elements. default is true
|
||||
pub shadow: bool,
|
||||
|
||||
/// Show the scrollbar mode, default: scrolling
|
||||
pub scrollbar_mode: ScrollbarMode,
|
||||
|
||||
/// Notification settings
|
||||
pub notification: NotificationSettings,
|
||||
|
||||
/// Platform kind
|
||||
pub platform: PlatformKind,
|
||||
}
|
||||
|
||||
impl Deref for Theme {
|
||||
type Target = ThemeColors;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.colors
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for Theme {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.colors
|
||||
}
|
||||
}
|
||||
|
||||
impl Global for Theme {}
|
||||
|
||||
impl Theme {
|
||||
/// Returns the global theme reference
|
||||
pub fn global(cx: &App) -> &Theme {
|
||||
cx.global::<Theme>()
|
||||
}
|
||||
|
||||
/// Returns the global theme mutable reference
|
||||
pub fn global_mut(cx: &mut App) -> &mut Theme {
|
||||
cx.global_mut::<Theme>()
|
||||
}
|
||||
|
||||
/// Returns true if the theme is dark.
|
||||
pub fn is_dark(&self) -> bool {
|
||||
self.mode.is_dark()
|
||||
}
|
||||
|
||||
/// Sync the theme with the system appearance
|
||||
pub fn sync_system_appearance(window: Option<&mut Window>, cx: &mut App) {
|
||||
let appearance = window
|
||||
.as_ref()
|
||||
.map(|window| window.appearance())
|
||||
.unwrap_or_else(|| cx.window_appearance());
|
||||
|
||||
Self::change(appearance, window, cx);
|
||||
}
|
||||
|
||||
/// Sync the Scrollbar showing behavior with the system
|
||||
pub fn sync_scrollbar_appearance(cx: &mut App) {
|
||||
Theme::global_mut(cx).scrollbar_mode = if cx.should_auto_hide_scrollbars() {
|
||||
ScrollbarMode::Scrolling
|
||||
} else {
|
||||
ScrollbarMode::Hover
|
||||
};
|
||||
}
|
||||
|
||||
/// Apply a new theme to the application.
|
||||
pub fn apply_theme(new_theme: Rc<ThemeFamily>, window: Option<&mut Window>, cx: &mut App) {
|
||||
let theme = cx.global_mut::<Theme>();
|
||||
let mode = theme.mode;
|
||||
// Update the theme
|
||||
theme.theme = new_theme;
|
||||
// Emit a theme change event
|
||||
Self::change(mode, window, cx);
|
||||
}
|
||||
|
||||
/// Change the app's appearance
|
||||
pub fn change<M>(mode: M, window: Option<&mut Window>, cx: &mut App)
|
||||
where
|
||||
M: Into<ThemeMode>,
|
||||
{
|
||||
if !cx.has_global::<Theme>() {
|
||||
let default_theme = ThemeFamily::default();
|
||||
let theme = Theme::from(default_theme);
|
||||
|
||||
cx.set_global(theme);
|
||||
}
|
||||
|
||||
let mode = mode.into();
|
||||
let theme = cx.global_mut::<Theme>();
|
||||
|
||||
// Set the theme mode
|
||||
theme.mode = mode;
|
||||
|
||||
// Set the theme colors
|
||||
if mode.is_dark() {
|
||||
theme.colors = *theme.theme.dark();
|
||||
} else {
|
||||
theme.colors = *theme.theme.light();
|
||||
}
|
||||
|
||||
// Refresh the window if available
|
||||
if let Some(window) = window {
|
||||
window.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ThemeFamily> for Theme {
|
||||
fn from(family: ThemeFamily) -> Self {
|
||||
let platform = PlatformKind::platform();
|
||||
let mode = ThemeMode::default();
|
||||
|
||||
// Define the font family based on the platform.
|
||||
// TODO: Use native fonts on Linux too.
|
||||
let font_family = match platform {
|
||||
PlatformKind::Linux => "Inter",
|
||||
_ => ".SystemUIFont",
|
||||
};
|
||||
|
||||
// Define the theme colors based on the appearance
|
||||
let colors = match mode {
|
||||
ThemeMode::Light => family.light(),
|
||||
ThemeMode::Dark => family.dark(),
|
||||
};
|
||||
|
||||
Theme {
|
||||
font_size: px(15.),
|
||||
font_family: font_family.into(),
|
||||
radius: px(6.),
|
||||
radius_lg: px(10.),
|
||||
shadow: true,
|
||||
scrollbar_mode: ScrollbarMode::default(),
|
||||
notification: NotificationSettings::default(),
|
||||
mode,
|
||||
colors: *colors,
|
||||
theme: Rc::new(family),
|
||||
platform,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
use gpui::{Anchor, Pixels, px};
|
||||
|
||||
use crate::{Edges, TITLEBAR_HEIGHT};
|
||||
|
||||
/// The settings for notifications.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NotificationSettings {
|
||||
/// The placement of the notification, default: [`Anchor::TopRight`]
|
||||
pub placement: Anchor,
|
||||
/// The margins of the notification with respect to the window edges.
|
||||
pub margins: Edges<Pixels>,
|
||||
/// The maximum number of notifications to show at once, default: 10
|
||||
pub max_items: usize,
|
||||
}
|
||||
|
||||
impl Default for NotificationSettings {
|
||||
fn default() -> Self {
|
||||
let offset = px(16.);
|
||||
Self {
|
||||
placement: Anchor::TopRight,
|
||||
margins: Edges {
|
||||
top: TITLEBAR_HEIGHT + offset, // avoid overlap with title bar
|
||||
right: offset,
|
||||
bottom: offset,
|
||||
left: offset,
|
||||
},
|
||||
max_items: 10,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||
pub enum PlatformKind {
|
||||
Mac,
|
||||
Linux,
|
||||
Windows,
|
||||
}
|
||||
|
||||
impl PlatformKind {
|
||||
pub const fn platform() -> Self {
|
||||
if cfg!(any(target_os = "linux", target_os = "freebsd")) {
|
||||
Self::Linux
|
||||
} else if cfg!(target_os = "windows") {
|
||||
Self::Windows
|
||||
} else {
|
||||
Self::Mac
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn is_linux(&self) -> bool {
|
||||
matches!(self, Self::Linux)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn is_windows(&self) -> bool {
|
||||
matches!(self, Self::Windows)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn is_mac(&self) -> bool {
|
||||
matches!(self, Self::Mac)
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context as AnyhowContext, Error};
|
||||
use gpui::{App, AppContext, AssetSource, Context, Entity, Global, SharedString};
|
||||
|
||||
use crate::ThemeFamily;
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
ThemeRegistry::set_global(cx.new(ThemeRegistry::new), cx);
|
||||
}
|
||||
|
||||
struct GlobalThemeRegistry(Entity<ThemeRegistry>);
|
||||
|
||||
impl Global for GlobalThemeRegistry {}
|
||||
|
||||
pub struct ThemeRegistry {
|
||||
/// Map of theme names to theme families
|
||||
themes: HashMap<SharedString, Rc<ThemeFamily>>,
|
||||
}
|
||||
|
||||
impl ThemeRegistry {
|
||||
/// Retrieve the global theme registry state
|
||||
pub fn global(cx: &App) -> Entity<Self> {
|
||||
cx.global::<GlobalThemeRegistry>().0.clone()
|
||||
}
|
||||
|
||||
/// Set the global theme registry instance
|
||||
pub(crate) fn set_global(state: Entity<Self>, cx: &mut App) {
|
||||
cx.set_global(GlobalThemeRegistry(state));
|
||||
}
|
||||
|
||||
/// Create a new theme registry instance
|
||||
pub(crate) fn new(cx: &mut Context<Self>) -> Self {
|
||||
let mut themes = HashMap::new();
|
||||
let asset = cx.asset_source();
|
||||
|
||||
if let Ok(paths) = asset.list("themes") {
|
||||
for path in paths.into_iter() {
|
||||
match Self::load(&path, asset) {
|
||||
Ok(theme) => {
|
||||
themes.insert(path, Rc::new(theme));
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to load theme: {path}. Error: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Self { themes }
|
||||
}
|
||||
|
||||
/// Load a theme from the asset source.
|
||||
fn load(path: &str, asset: &Arc<dyn AssetSource>) -> Result<ThemeFamily, Error> {
|
||||
// Load the theme file from the assets
|
||||
let content = asset.load(path)?.context("Theme not found")?;
|
||||
|
||||
// Parse the JSON content into a Theme Family struct
|
||||
let theme: ThemeFamily = serde_json::from_slice(&content)?;
|
||||
|
||||
Ok(theme)
|
||||
}
|
||||
|
||||
/// Returns a reference to the map of themes.
|
||||
pub fn themes(&self) -> &HashMap<SharedString, Rc<ThemeFamily>> {
|
||||
&self.themes
|
||||
}
|
||||
}
|
||||
@@ -1,281 +0,0 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use gpui::{Hsla, SharedString};
|
||||
|
||||
/// A collection of colors that are used to style the UI.
|
||||
///
|
||||
/// Each step has a semantic meaning, and is used to style different parts of the UI.
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||
pub struct ColorScaleStep(usize);
|
||||
|
||||
impl ColorScaleStep {
|
||||
/// All of the steps in a [`ColorScale`].
|
||||
pub const ALL: [ColorScaleStep; 12] = [
|
||||
Self::ONE,
|
||||
Self::TWO,
|
||||
Self::THREE,
|
||||
Self::FOUR,
|
||||
Self::FIVE,
|
||||
Self::SIX,
|
||||
Self::SEVEN,
|
||||
Self::EIGHT,
|
||||
Self::NINE,
|
||||
Self::TEN,
|
||||
Self::ELEVEN,
|
||||
Self::TWELVE,
|
||||
];
|
||||
pub const EIGHT: Self = Self(8);
|
||||
pub const ELEVEN: Self = Self(11);
|
||||
pub const FIVE: Self = Self(5);
|
||||
pub const FOUR: Self = Self(4);
|
||||
pub const NINE: Self = Self(9);
|
||||
pub const ONE: Self = Self(1);
|
||||
pub const SEVEN: Self = Self(7);
|
||||
pub const SIX: Self = Self(6);
|
||||
pub const TEN: Self = Self(10);
|
||||
pub const THREE: Self = Self(3);
|
||||
pub const TWELVE: Self = Self(12);
|
||||
pub const TWO: Self = Self(2);
|
||||
}
|
||||
|
||||
/// A scale of colors for a given [`ColorScaleSet`].
|
||||
///
|
||||
/// Each [`ColorScale`] contains exactly 12 colors. Refer to
|
||||
/// [`ColorScaleStep`] for a reference of what each step is used for.
|
||||
pub struct ColorScale(Vec<Hsla>);
|
||||
|
||||
impl FromIterator<Hsla> for ColorScale {
|
||||
fn from_iter<T: IntoIterator<Item = Hsla>>(iter: T) -> Self {
|
||||
Self(Vec::from_iter(iter))
|
||||
}
|
||||
}
|
||||
|
||||
impl ColorScale {
|
||||
/// Returns the specified step in the [`ColorScale`].
|
||||
#[inline]
|
||||
pub fn step(&self, step: ColorScaleStep) -> Hsla {
|
||||
// Steps are one-based, so we need convert to the zero-based vec index.
|
||||
self.0[step.0 - 1]
|
||||
}
|
||||
|
||||
/// `Step 1` - Used for main application backgrounds.
|
||||
///
|
||||
/// This step provides a neutral base for any overlaying components, ideal for applications' main backdrop or empty spaces such as canvas areas.
|
||||
///
|
||||
#[inline]
|
||||
pub fn step_1(&self) -> Hsla {
|
||||
self.step(ColorScaleStep::ONE)
|
||||
}
|
||||
|
||||
/// `Step 2` - Used for both main application backgrounds and subtle component backgrounds.
|
||||
///
|
||||
/// Like `Step 1`, this step allows variations in background styles, from striped tables, sidebar backgrounds, to card backgrounds.
|
||||
#[inline]
|
||||
pub fn step_2(&self) -> Hsla {
|
||||
self.step(ColorScaleStep::TWO)
|
||||
}
|
||||
|
||||
/// `Step 3` - Used for UI component backgrounds in their normal states.
|
||||
///
|
||||
/// This step maintains accessibility by guaranteeing a contrast ratio of 4.5:1 with steps 11 and 12 for text. It could also suit hover states for transparent components.
|
||||
#[inline]
|
||||
pub fn step_3(&self) -> Hsla {
|
||||
self.step(ColorScaleStep::THREE)
|
||||
}
|
||||
|
||||
/// `Step 4` - Used for UI component backgrounds in their hover states.
|
||||
///
|
||||
/// Also suited for pressed or selected states of components with a transparent background.
|
||||
#[inline]
|
||||
pub fn step_4(&self) -> Hsla {
|
||||
self.step(ColorScaleStep::FOUR)
|
||||
}
|
||||
|
||||
/// `Step 5` - Used for UI component backgrounds in their pressed or selected states.
|
||||
#[inline]
|
||||
pub fn step_5(&self) -> Hsla {
|
||||
self.step(ColorScaleStep::FIVE)
|
||||
}
|
||||
|
||||
/// `Step 6` - Used for subtle borders on non-interactive components.
|
||||
///
|
||||
/// Its usage spans from sidebars' borders, headers' dividers, cards' outlines, to alerts' edges and separators.
|
||||
#[inline]
|
||||
pub fn step_6(&self) -> Hsla {
|
||||
self.step(ColorScaleStep::SIX)
|
||||
}
|
||||
|
||||
/// `Step 7` - Used for subtle borders on interactive components.
|
||||
///
|
||||
/// This step subtly delineates the boundary of elements users interact with.
|
||||
#[inline]
|
||||
pub fn step_7(&self) -> Hsla {
|
||||
self.step(ColorScaleStep::SEVEN)
|
||||
}
|
||||
|
||||
/// `Step 8` - Used for stronger borders on interactive components and focus rings.
|
||||
///
|
||||
/// It strengthens the visibility and accessibility of active elements and their focus states.
|
||||
#[inline]
|
||||
pub fn step_8(&self) -> Hsla {
|
||||
self.step(ColorScaleStep::EIGHT)
|
||||
}
|
||||
|
||||
/// `Step 9` - Used for solid backgrounds.
|
||||
///
|
||||
/// `Step 9` is the most saturated step, having the least mix of white or black.
|
||||
///
|
||||
/// Due to its high chroma, `Step 9` is versatile and particularly useful for semantic colors such as
|
||||
/// error, warning, and success indicators.
|
||||
#[inline]
|
||||
pub fn step_9(&self) -> Hsla {
|
||||
self.step(ColorScaleStep::NINE)
|
||||
}
|
||||
|
||||
/// `Step 10` - Used for hovered or active solid backgrounds, particularly when `Step 9` is their normal state.
|
||||
///
|
||||
/// May also be used for extremely low contrast text. This should be used sparingly, as it may be difficult to read.
|
||||
#[inline]
|
||||
pub fn step_10(&self) -> Hsla {
|
||||
self.step(ColorScaleStep::TEN)
|
||||
}
|
||||
|
||||
/// `Step 11` - Used for text and icons requiring low contrast or less emphasis.
|
||||
#[inline]
|
||||
pub fn step_11(&self) -> Hsla {
|
||||
self.step(ColorScaleStep::ELEVEN)
|
||||
}
|
||||
|
||||
/// `Step 12` - Used for text and icons requiring high contrast or prominence.
|
||||
#[inline]
|
||||
pub fn step_12(&self) -> Hsla {
|
||||
self.step(ColorScaleStep::TWELVE)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ColorScales {
|
||||
pub gray: ColorScaleSet,
|
||||
pub mauve: ColorScaleSet,
|
||||
pub slate: ColorScaleSet,
|
||||
pub sage: ColorScaleSet,
|
||||
pub olive: ColorScaleSet,
|
||||
pub sand: ColorScaleSet,
|
||||
pub gold: ColorScaleSet,
|
||||
pub bronze: ColorScaleSet,
|
||||
pub brown: ColorScaleSet,
|
||||
pub yellow: ColorScaleSet,
|
||||
pub amber: ColorScaleSet,
|
||||
pub orange: ColorScaleSet,
|
||||
pub tomato: ColorScaleSet,
|
||||
pub red: ColorScaleSet,
|
||||
pub ruby: ColorScaleSet,
|
||||
pub crimson: ColorScaleSet,
|
||||
pub pink: ColorScaleSet,
|
||||
pub plum: ColorScaleSet,
|
||||
pub purple: ColorScaleSet,
|
||||
pub violet: ColorScaleSet,
|
||||
pub iris: ColorScaleSet,
|
||||
pub indigo: ColorScaleSet,
|
||||
pub blue: ColorScaleSet,
|
||||
pub cyan: ColorScaleSet,
|
||||
pub teal: ColorScaleSet,
|
||||
pub jade: ColorScaleSet,
|
||||
pub green: ColorScaleSet,
|
||||
pub grass: ColorScaleSet,
|
||||
pub lime: ColorScaleSet,
|
||||
pub mint: ColorScaleSet,
|
||||
pub sky: ColorScaleSet,
|
||||
pub black: ColorScaleSet,
|
||||
pub white: ColorScaleSet,
|
||||
}
|
||||
|
||||
impl IntoIterator for ColorScales {
|
||||
type IntoIter = std::vec::IntoIter<Self::Item>;
|
||||
type Item = ColorScaleSet;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
vec![
|
||||
self.gray,
|
||||
self.mauve,
|
||||
self.slate,
|
||||
self.sage,
|
||||
self.olive,
|
||||
self.sand,
|
||||
self.gold,
|
||||
self.bronze,
|
||||
self.brown,
|
||||
self.yellow,
|
||||
self.amber,
|
||||
self.orange,
|
||||
self.tomato,
|
||||
self.red,
|
||||
self.ruby,
|
||||
self.crimson,
|
||||
self.pink,
|
||||
self.plum,
|
||||
self.purple,
|
||||
self.violet,
|
||||
self.iris,
|
||||
self.indigo,
|
||||
self.blue,
|
||||
self.cyan,
|
||||
self.teal,
|
||||
self.jade,
|
||||
self.green,
|
||||
self.grass,
|
||||
self.lime,
|
||||
self.mint,
|
||||
self.sky,
|
||||
self.black,
|
||||
self.white,
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides groups of [`ColorScale`]s for light and dark themes, as well as transparent versions of each scale.
|
||||
pub struct ColorScaleSet {
|
||||
name: SharedString,
|
||||
light: ColorScale,
|
||||
dark: ColorScale,
|
||||
light_alpha: ColorScale,
|
||||
dark_alpha: ColorScale,
|
||||
}
|
||||
|
||||
impl ColorScaleSet {
|
||||
pub fn new(
|
||||
name: impl Into<SharedString>,
|
||||
light: ColorScale,
|
||||
light_alpha: ColorScale,
|
||||
dark: ColorScale,
|
||||
dark_alpha: ColorScale,
|
||||
) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
light,
|
||||
light_alpha,
|
||||
dark,
|
||||
dark_alpha,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &SharedString {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub fn light(&self) -> &ColorScale {
|
||||
&self.light
|
||||
}
|
||||
|
||||
pub fn light_alpha(&self) -> &ColorScale {
|
||||
&self.light_alpha
|
||||
}
|
||||
|
||||
pub fn dark(&self) -> &ColorScale {
|
||||
&self.dark
|
||||
}
|
||||
|
||||
pub fn dark_alpha(&self) -> &ColorScale {
|
||||
&self.dark_alpha
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum ScrollbarMode {
|
||||
#[default]
|
||||
Scrolling,
|
||||
Hover,
|
||||
Always,
|
||||
}
|
||||
|
||||
impl ScrollbarMode {
|
||||
pub fn is_scrolling(&self) -> bool {
|
||||
matches!(self, Self::Scrolling)
|
||||
}
|
||||
|
||||
pub fn is_hover(&self) -> bool {
|
||||
matches!(self, Self::Hover)
|
||||
}
|
||||
|
||||
pub fn is_always(&self) -> bool {
|
||||
matches!(self, Self::Always)
|
||||
}
|
||||
}
|
||||
@@ -1,359 +0,0 @@
|
||||
use std::path::Path;
|
||||
|
||||
use gpui::{SharedString, WindowAppearance};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::ThemeColors;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, PartialOrd, Eq, Hash, Deserialize, Serialize)]
|
||||
pub enum ThemeMode {
|
||||
#[default]
|
||||
Light,
|
||||
Dark,
|
||||
}
|
||||
|
||||
impl ThemeMode {
|
||||
pub fn is_dark(&self) -> bool {
|
||||
matches!(self, Self::Dark)
|
||||
}
|
||||
|
||||
/// Return theme name: `light`, `dark`.
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
ThemeMode::Light => "Light",
|
||||
ThemeMode::Dark => "Dark",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WindowAppearance> for ThemeMode {
|
||||
fn from(appearance: WindowAppearance) -> Self {
|
||||
match appearance {
|
||||
WindowAppearance::Dark | WindowAppearance::VibrantDark => Self::Dark,
|
||||
WindowAppearance::Light | WindowAppearance::VibrantLight => Self::Light,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Theme family
|
||||
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct ThemeFamily {
|
||||
/// The unique identifier for the theme.
|
||||
pub id: String,
|
||||
|
||||
/// The name of the theme.
|
||||
pub name: SharedString,
|
||||
|
||||
/// The author of the theme.
|
||||
pub author: SharedString,
|
||||
|
||||
/// The URL of the theme.
|
||||
pub url: String,
|
||||
|
||||
/// The light colors for the theme.
|
||||
pub light: ThemeColors,
|
||||
|
||||
/// The dark colors for the theme.
|
||||
pub dark: ThemeColors,
|
||||
}
|
||||
|
||||
impl Default for ThemeFamily {
|
||||
fn default() -> Self {
|
||||
ThemeFamily {
|
||||
id: "coop".into(),
|
||||
name: "Coop Default Theme".into(),
|
||||
author: "Coop".into(),
|
||||
url: "https://github.com/lumehq/coop".into(),
|
||||
light: ThemeColors::light(),
|
||||
dark: ThemeColors::dark(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ThemeFamily {
|
||||
/// Returns the light colors for the theme.
|
||||
#[inline(always)]
|
||||
pub fn light(&self) -> &ThemeColors {
|
||||
&self.light
|
||||
}
|
||||
|
||||
/// Returns the dark colors for the theme.
|
||||
#[inline(always)]
|
||||
pub fn dark(&self) -> &ThemeColors {
|
||||
&self.dark
|
||||
}
|
||||
|
||||
/// Load a theme family from a JSON file.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `path` - Path to the JSON file containing the theme family. This can be
|
||||
/// an absolute path or a path relative to the current working directory.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns `Ok(ThemeFamily)` if the file was successfully loaded and parsed,
|
||||
/// or `Err(anyhow::Error)` if there was an error reading or parsing the file.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if:
|
||||
/// - The file cannot be read (permission issues, file doesn't exist, etc.)
|
||||
/// - The file contains invalid JSON
|
||||
/// - The JSON structure doesn't match the `ThemeFamily` schema
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// use theme::ThemeFamily;
|
||||
///
|
||||
/// # fn main() -> anyhow::Result<()> {
|
||||
/// // Load from a relative path
|
||||
/// let theme = ThemeFamily::from_file("assets/themes/my-theme.json")?;
|
||||
///
|
||||
/// // Load from an absolute path
|
||||
/// let theme = ThemeFamily::from_file("/path/to/themes/my-theme.json")?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn from_file<P: AsRef<Path>>(path: P) -> anyhow::Result<Self> {
|
||||
let json_data = std::fs::read(path)?;
|
||||
let theme_family = serde_json::from_slice(&json_data)?;
|
||||
|
||||
Ok(theme_family)
|
||||
}
|
||||
|
||||
/// Load a theme family from a JSON file in the assets/themes directory.
|
||||
///
|
||||
/// This function looks for the file at `assets/themes/{name}.json` relative
|
||||
/// to the current working directory. This is useful for loading themes
|
||||
/// from the standard theme directory in the project structure.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - Name of the theme file (without the .json extension)
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns `Ok(ThemeFamily)` if the file was successfully loaded and parsed,
|
||||
/// or `Err(anyhow::Error)` if there was an error reading or parsing the file.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if:
|
||||
/// - The file cannot be read (permission issues, file doesn't exist, etc.)
|
||||
/// - The file contains invalid JSON
|
||||
/// - The JSON structure doesn't match the `ThemeFamily` schema
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// use theme::ThemeFamily;
|
||||
///
|
||||
/// # fn main() -> anyhow::Result<()> {
|
||||
/// // Assuming the file exists at `assets/themes/my-theme.json`
|
||||
/// let theme = ThemeFamily::from_assets("themes/my-theme.json")?;
|
||||
///
|
||||
/// println!("Loaded theme: {}", theme.name);
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn from_assets(target: &str) -> anyhow::Result<Self> {
|
||||
let path = format!("assets/{target}");
|
||||
Self::from_file(path)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::fs;
|
||||
|
||||
use tempfile::tempdir;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_from_file() {
|
||||
// Create a temporary directory for our test
|
||||
let dir = tempdir().unwrap();
|
||||
let file_path = dir.path().join("test-theme.json");
|
||||
|
||||
// Create a minimal valid theme JSON with hex colors
|
||||
// Using simple hex colors that Hsla can parse
|
||||
// Note: We need to escape the # characters in the raw string
|
||||
let json_data = r##"{
|
||||
"id": "test-theme",
|
||||
"name": "Test Theme",
|
||||
"author": "Coop",
|
||||
"url": "https://github.com/lumehq/coop",
|
||||
"light": {
|
||||
"background": "#ffffff",
|
||||
"surface_background": "#fafafa",
|
||||
"elevated_surface_background": "#f5f5f5",
|
||||
"panel_background": "#ffffff",
|
||||
"overlay": "#0000001a",
|
||||
"title_bar": "#00000000",
|
||||
"title_bar_inactive": "#ffffff",
|
||||
"window_border": "#c7c7cf",
|
||||
"border": "#dbdbdb",
|
||||
"border_variant": "#d1d1d1",
|
||||
"border_focused": "#3366cc",
|
||||
"border_selected": "#3366cc",
|
||||
"border_transparent": "#00000000",
|
||||
"border_disabled": "#e6e6e6",
|
||||
"ring": "#4d79d6",
|
||||
"text": "#1a1a1a",
|
||||
"text_muted": "#4d4d4d",
|
||||
"text_placeholder": "#808080",
|
||||
"text_accent": "#3366cc",
|
||||
"icon": "#4d4d4d",
|
||||
"icon_muted": "#808080",
|
||||
"icon_accent": "#3366cc",
|
||||
"element_foreground": "#ffffff",
|
||||
"element_background": "#3366cc",
|
||||
"element_hover": "#3366cce6",
|
||||
"element_active": "#2e5cb8",
|
||||
"element_selected": "#2952a3",
|
||||
"element_disabled": "#3366cc4d",
|
||||
"secondary_foreground": "#2952a3",
|
||||
"secondary_background": "#e6ecf5",
|
||||
"secondary_hover": "#3366cc1a",
|
||||
"secondary_active": "#d9e2f0",
|
||||
"secondary_selected": "#d9e2f0",
|
||||
"secondary_disabled": "#3366cc4d",
|
||||
"danger_foreground": "#ffffff",
|
||||
"danger_background": "#f5e6e6",
|
||||
"danger_hover": "#cc33331a",
|
||||
"danger_active": "#f0d9d9",
|
||||
"danger_selected": "#f0d9d9",
|
||||
"danger_disabled": "#cc33334d",
|
||||
"warning_foreground": "#1a1a1a",
|
||||
"warning_background": "#f5f0e6",
|
||||
"warning_hover": "#cc99331a",
|
||||
"warning_active": "#f0ead9",
|
||||
"warning_selected": "#f0ead9",
|
||||
"warning_disabled": "#cc99334d",
|
||||
"ghost_element_background": "#00000000",
|
||||
"ghost_element_background_alt": "#e6e6e6",
|
||||
"ghost_element_hover": "#0000001a",
|
||||
"ghost_element_active": "#d9d9d9",
|
||||
"ghost_element_selected": "#d9d9d9",
|
||||
"ghost_element_disabled": "#0000000d",
|
||||
"tab_inactive_background": "#e6e6e6",
|
||||
"tab_hover_background": "#e0e0e0",
|
||||
"tab_active_background": "#d9d9d9",
|
||||
"scrollbar_thumb_background": "#00000033",
|
||||
"scrollbar_thumb_hover_background": "#0000004d",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#d9d9d9",
|
||||
"drop_target_background": "#3366cc1a",
|
||||
"cursor": "#3399ff",
|
||||
"selection": "#3399ff40"
|
||||
},
|
||||
"dark": {
|
||||
"background": "#1a1a1a",
|
||||
"surface_background": "#1f1f1f",
|
||||
"elevated_surface_background": "#242424",
|
||||
"panel_background": "#262626",
|
||||
"overlay": "#ffffff1a",
|
||||
"title_bar": "#00000000",
|
||||
"title_bar_inactive": "#1a1a1a",
|
||||
"window_border": "#404046",
|
||||
"border": "#404040",
|
||||
"border_variant": "#383838",
|
||||
"border_focused": "#4d79d6",
|
||||
"border_selected": "#4d79d6",
|
||||
"border_transparent": "#00000000",
|
||||
"border_disabled": "#2e2e2e",
|
||||
"ring": "#668cdf",
|
||||
"text": "#f2f2f2",
|
||||
"text_muted": "#b3b3b3",
|
||||
"text_placeholder": "#808080",
|
||||
"text_accent": "#668cdf",
|
||||
"icon": "#b3b3b3",
|
||||
"icon_muted": "#808080",
|
||||
"icon_accent": "#668cdf",
|
||||
"element_foreground": "#ffffff",
|
||||
"element_background": "#4d79d6",
|
||||
"element_hover": "#4d79d6e6",
|
||||
"element_active": "#456dc1",
|
||||
"element_selected": "#3e62ac",
|
||||
"element_disabled": "#4d79d64d",
|
||||
"secondary_foreground": "#3e62ac",
|
||||
"secondary_background": "#2a3652",
|
||||
"secondary_hover": "#4d79d61a",
|
||||
"secondary_active": "#303d5c",
|
||||
"secondary_selected": "#303d5c",
|
||||
"secondary_disabled": "#4d79d64d",
|
||||
"danger_foreground": "#ffffff",
|
||||
"danger_background": "#522a2a",
|
||||
"danger_hover": "#d64d4d1a",
|
||||
"danger_active": "#5c3030",
|
||||
"danger_selected": "#5c3030",
|
||||
"danger_disabled": "#d64d4d4d",
|
||||
"warning_foreground": "#f2f2f2",
|
||||
"warning_background": "#52482a",
|
||||
"warning_hover": "#d6b34d1a",
|
||||
"warning_active": "#5c5430",
|
||||
"warning_selected": "#5c5430",
|
||||
"warning_disabled": "#d6b34d4d",
|
||||
"ghost_element_background": "#00000000",
|
||||
"ghost_element_background_alt": "#2e2e2e",
|
||||
"ghost_element_hover": "#ffffff1a",
|
||||
"ghost_element_active": "#383838",
|
||||
"ghost_element_selected": "#383838",
|
||||
"ghost_element_disabled": "#ffffff0d",
|
||||
"tab_inactive_background": "#2e2e2e",
|
||||
"tab_hover_background": "#333333",
|
||||
"tab_active_background": "#383838",
|
||||
"scrollbar_thumb_background": "#ffffff33",
|
||||
"scrollbar_thumb_hover_background": "#ffffff4d",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#383838",
|
||||
"drop_target_background": "#4d79d61a",
|
||||
"cursor": "#4db3ff",
|
||||
"selection": "#4db3ff40"
|
||||
}
|
||||
}"##;
|
||||
|
||||
// Write the JSON to the file
|
||||
fs::write(&file_path, json_data).unwrap();
|
||||
|
||||
// Test loading the theme from file
|
||||
let theme = ThemeFamily::from_file(&file_path).unwrap();
|
||||
|
||||
// Verify the loaded theme
|
||||
assert_eq!(theme.id, "test-theme");
|
||||
assert_eq!(theme.name, "Test Theme");
|
||||
|
||||
// Clean up
|
||||
dir.close().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_file_nonexistent() {
|
||||
// Test that loading a non-existent file returns an error
|
||||
let result = ThemeFamily::from_file("non-existent-file.json");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_file_invalid_json() {
|
||||
// Create a temporary directory for our test
|
||||
let dir = tempdir().unwrap();
|
||||
let file_path = dir.path().join("invalid-theme.json");
|
||||
|
||||
// Write invalid JSON
|
||||
fs::write(&file_path, "invalid json").unwrap();
|
||||
|
||||
// Test that loading invalid JSON returns an error
|
||||
let result = ThemeFamily::from_file(&file_path);
|
||||
assert!(result.is_err());
|
||||
|
||||
// Clean up
|
||||
dir.close().unwrap();
|
||||
}
|
||||
}
|
||||
@@ -6,11 +6,11 @@ publish.workspace = true
|
||||
|
||||
[dependencies]
|
||||
common = { path = "../common" }
|
||||
theme = { path = "../theme" }
|
||||
ui = { path = "../ui" }
|
||||
|
||||
gpui.workspace = true
|
||||
gpui-component.workspace = true
|
||||
|
||||
nostr-sdk.workspace = true
|
||||
gpui.workspace = true
|
||||
smol.workspace = true
|
||||
smallvec.workspace = true
|
||||
anyhow.workspace = true
|
||||
|
||||
@@ -5,10 +5,10 @@ use gpui::{
|
||||
AnyElement, Context, Decorations, Hsla, InteractiveElement as _, IntoElement, ParentElement,
|
||||
Pixels, Render, StatefulInteractiveElement as _, Styled, Window, WindowControlArea, px,
|
||||
};
|
||||
use gpui_component::{ActiveTheme, h_flex};
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use theme::{ActiveTheme, CLIENT_SIDE_DECORATION_ROUNDING, PlatformKind};
|
||||
use ui::h_flex;
|
||||
|
||||
use crate::platforms::PlatformKind;
|
||||
#[cfg(target_os = "linux")]
|
||||
use crate::platforms::linux::LinuxWindowControls;
|
||||
use crate::platforms::mac::TRAFFIC_LIGHT_PADDING;
|
||||
@@ -16,6 +16,18 @@ use crate::platforms::windows::WindowsWindowControls;
|
||||
|
||||
mod platforms;
|
||||
|
||||
/// Defines window border radius for platforms that use client side decorations.
|
||||
pub const CLIENT_SIDE_DECORATION_ROUNDING: Pixels = px(10.0);
|
||||
|
||||
/// Defines window shadow size for platforms that use client side decorations.
|
||||
pub const CLIENT_SIDE_DECORATION_SHADOW: Pixels = px(10.0);
|
||||
|
||||
/// Defines window border size for platforms that use client side decorations.
|
||||
pub const CLIENT_SIDE_DECORATION_BORDER: Pixels = px(1.0);
|
||||
|
||||
/// Defines window titlebar height
|
||||
pub const TITLEBAR_HEIGHT: Pixels = px(36.0);
|
||||
|
||||
/// Titlebar
|
||||
pub struct TitleBar {
|
||||
/// Children elements of the title bar.
|
||||
@@ -23,6 +35,9 @@ pub struct TitleBar {
|
||||
|
||||
/// Whether the title bar is currently being moved.
|
||||
should_move: bool,
|
||||
|
||||
/// Current user platform.
|
||||
platform: PlatformKind,
|
||||
}
|
||||
|
||||
impl TitleBar {
|
||||
@@ -30,6 +45,7 @@ impl TitleBar {
|
||||
Self {
|
||||
children: smallvec![],
|
||||
should_move: false,
|
||||
platform: PlatformKind::platform(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +64,7 @@ impl TitleBar {
|
||||
if window.is_window_active() && !self.should_move {
|
||||
cx.theme().title_bar
|
||||
} else {
|
||||
cx.theme().title_bar_inactive
|
||||
cx.theme().title_bar.alpha(0.75)
|
||||
}
|
||||
} else {
|
||||
cx.theme().title_bar
|
||||
@@ -86,7 +102,7 @@ impl Render for TitleBar {
|
||||
.map(|this| {
|
||||
if window.is_fullscreen() {
|
||||
this.px_2()
|
||||
} else if cx.theme().platform.is_mac() {
|
||||
} else if self.platform.is_mac() {
|
||||
this.pr_2().pl(px(TRAFFIC_LIGHT_PADDING))
|
||||
} else {
|
||||
this.px_2()
|
||||
@@ -111,24 +127,24 @@ impl Render for TitleBar {
|
||||
.id("title-bar")
|
||||
.justify_between()
|
||||
.w_full()
|
||||
.when(cx.theme().platform.is_mac(), |this| {
|
||||
.when(self.platform.is_mac(), |this| {
|
||||
this.on_click(|event, window, _| {
|
||||
if event.click_count() == 2 {
|
||||
window.titlebar_double_click();
|
||||
}
|
||||
})
|
||||
})
|
||||
.when(cx.theme().platform.is_linux(), |this| {
|
||||
.when(self.platform.is_linux(), |this| {
|
||||
this.on_click(|event, window, _| {
|
||||
if event.click_count() == 2 {
|
||||
window.zoom_window();
|
||||
}
|
||||
})
|
||||
})
|
||||
.when(!cx.theme().platform.is_mac(), |this| this.pr_2())
|
||||
.when(!self.platform.is_mac(), |this| this.pr_2())
|
||||
.children(children),
|
||||
)
|
||||
.when(!window.is_fullscreen(), |this| match cx.theme().platform {
|
||||
.when(!window.is_fullscreen(), |this| match self.platform {
|
||||
PlatformKind::Linux => {
|
||||
#[cfg(target_os = "linux")]
|
||||
if matches!(decorations, Decorations::Client { .. }) {
|
||||
|
||||
@@ -3,12 +3,12 @@ use std::sync::OnceLock;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
svg, Action, App, InteractiveElement, IntoElement, MouseButton, ParentElement, RenderOnce,
|
||||
SharedString, StatefulInteractiveElement, Styled, Window,
|
||||
Action, App, InteractiveElement, IntoElement, MouseButton, ParentElement, RenderOnce,
|
||||
SharedString, StatefulInteractiveElement, Styled, Window, svg,
|
||||
};
|
||||
use linicon::{lookup_icon, IconType};
|
||||
use linicon::{IconType, lookup_icon};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{h_flex, Icon, IconName, Sizable};
|
||||
use ui::{Icon, IconName, Sizable, h_flex};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct LinuxWindowControls {
|
||||
@@ -34,20 +34,20 @@ impl RenderOnce for LinuxWindowControls {
|
||||
.when(supported_controls.minimize, |this| {
|
||||
this.child(WindowControl::new(
|
||||
LinuxControl::Minimize,
|
||||
IconName::WindowMinimize,
|
||||
CoopIcon::WindowMinimize,
|
||||
))
|
||||
})
|
||||
.when(supported_controls.maximize, |this| {
|
||||
this.child({
|
||||
if window.is_maximized() {
|
||||
WindowControl::new(LinuxControl::Restore, IconName::WindowRestore)
|
||||
WindowControl::new(LinuxControl::Restore, CoopIcon::WindowRestore)
|
||||
} else {
|
||||
WindowControl::new(LinuxControl::Maximize, IconName::WindowMaximize)
|
||||
WindowControl::new(LinuxControl::Maximize, CoopIcon::WindowMaximize)
|
||||
}
|
||||
})
|
||||
})
|
||||
.child(
|
||||
WindowControl::new(LinuxControl::Close, IconName::WindowClose)
|
||||
WindowControl::new(LinuxControl::Close, CoopIcon::WindowClose)
|
||||
.when_some(self.close_window_action, |this, close_action| {
|
||||
this.close_action(close_action)
|
||||
}),
|
||||
|
||||
@@ -2,3 +2,37 @@
|
||||
pub mod linux;
|
||||
pub mod mac;
|
||||
pub mod windows;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||
pub enum PlatformKind {
|
||||
Mac,
|
||||
Linux,
|
||||
Windows,
|
||||
}
|
||||
|
||||
impl PlatformKind {
|
||||
pub const fn platform() -> Self {
|
||||
if cfg!(any(target_os = "linux", target_os = "freebsd")) {
|
||||
Self::Linux
|
||||
} else if cfg!(target_os = "windows") {
|
||||
Self::Windows
|
||||
} else {
|
||||
Self::Mac
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn is_linux(&self) -> bool {
|
||||
matches!(self, Self::Linux)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn is_windows(&self) -> bool {
|
||||
matches!(self, Self::Windows)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn is_mac(&self) -> bool {
|
||||
matches!(self, Self::Mac)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, px, App, ElementId, Hsla, InteractiveElement, IntoElement, ParentElement, Pixels,
|
||||
RenderOnce, Rgba, StatefulInteractiveElement, Styled, Window, WindowControlArea,
|
||||
App, ElementId, Hsla, InteractiveElement, IntoElement, ParentElement, Pixels, RenderOnce, Rgba,
|
||||
StatefulInteractiveElement, Styled, Window, WindowControlArea, div, px,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use ui::h_flex;
|
||||
use gpui_component::{ActiveTheme, h_flex};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct WindowsWindowControls {
|
||||
@@ -45,8 +44,8 @@ impl RenderOnce for WindowsWindowControls {
|
||||
a: 1.0,
|
||||
};
|
||||
|
||||
let button_hover_color = cx.theme().ghost_element_hover;
|
||||
let button_active_color = cx.theme().ghost_element_active;
|
||||
let button_hover_color = cx.theme().transparent;
|
||||
let button_active_color = cx.theme().secondary_active;
|
||||
|
||||
div()
|
||||
.id("windows-window-controls")
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
[package]
|
||||
name = "ui"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[dependencies]
|
||||
common = { path = "../common" }
|
||||
theme = { path = "../theme" }
|
||||
|
||||
gpui.workspace = true
|
||||
smol.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
smallvec.workspace = true
|
||||
anyhow.workspace = true
|
||||
itertools.workspace = true
|
||||
log.workspace = true
|
||||
|
||||
unicode-segmentation = "1.12.0"
|
||||
uuid = "1.10"
|
||||
regex = "1"
|
||||
image = "0.25.1"
|
||||
lsp-types = "0.97.0"
|
||||
rope = { git = "https://github.com/zed-industries/zed" }
|
||||
sum_tree = { git = "https://github.com/zed-industries/zed" }
|
||||
@@ -1,191 +0,0 @@
|
||||
Copyright 2024 Longbridge <https://longbridge.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
@@ -1,12 +0,0 @@
|
||||
use gpui::{actions, Action};
|
||||
use serde::Deserialize;
|
||||
|
||||
/// Define a custom confirm action
|
||||
#[derive(Clone, Action, PartialEq, Eq, Deserialize)]
|
||||
#[action(namespace = list, no_json)]
|
||||
pub struct Confirm {
|
||||
/// Is confirm with secondary.
|
||||
pub secondary: bool,
|
||||
}
|
||||
|
||||
actions!(ui, [Cancel, SelectUp, SelectDown, SelectLeft, SelectRight]);
|
||||
@@ -1,18 +0,0 @@
|
||||
/// A cubic bezier function like CSS `cubic-bezier`.
|
||||
///
|
||||
/// Builder:
|
||||
///
|
||||
/// https://cubic-bezier.com
|
||||
pub fn cubic_bezier(x1: f32, y1: f32, x2: f32, y2: f32) -> impl Fn(f32) -> f32 {
|
||||
move |t: f32| {
|
||||
let one_t = 1.0 - t;
|
||||
let one_t2 = one_t * one_t;
|
||||
let t2 = t * t;
|
||||
let t3 = t2 * t;
|
||||
|
||||
// The Bezier curve function for x and y, where x0 = 0, y0 = 0, x3 = 1, y3 = 1
|
||||
let _x = 3.0 * x1 * one_t2 * t + 3.0 * x2 * one_t * t2 + t3;
|
||||
|
||||
3.0 * y1 * one_t2 * t + 3.0 * y2 * one_t * t2 + t3
|
||||
}
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
AbsoluteLength, App, Div, Hsla, ImageSource, Img, InteractiveElement, Interactivity,
|
||||
IntoElement, ParentElement, RenderOnce, StyleRefinement, Styled, StyledImage, Window, div, img,
|
||||
px,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use crate::{Selectable, Sizable, Size};
|
||||
|
||||
/// Returns the size of the avatar based on the given [`Size`].
|
||||
pub(super) fn avatar_size(size: Size) -> AbsoluteLength {
|
||||
match size {
|
||||
Size::Large => px(64.).into(),
|
||||
Size::Medium => px(32.).into(),
|
||||
Size::Small => px(24.).into(),
|
||||
Size::XSmall => px(20.).into(),
|
||||
Size::Size(size) => size.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// An element that renders a user avatar with customizable appearance options.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ui::{Avatar};
|
||||
///
|
||||
/// Avatar::new("path/to/image.png")
|
||||
/// .grayscale(true)
|
||||
/// .border_color(gpui::red());
|
||||
/// ```
|
||||
#[derive(IntoElement)]
|
||||
pub struct Avatar {
|
||||
base: Div,
|
||||
image: Img,
|
||||
style: StyleRefinement,
|
||||
size: Size,
|
||||
border_color: Option<Hsla>,
|
||||
selected: bool,
|
||||
}
|
||||
|
||||
impl Avatar {
|
||||
/// Creates a new avatar element with the specified image source.
|
||||
pub fn new(src: impl Into<ImageSource>) -> Self {
|
||||
Avatar {
|
||||
base: div(),
|
||||
image: img(src),
|
||||
style: StyleRefinement::default(),
|
||||
size: Size::Medium,
|
||||
border_color: None,
|
||||
selected: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies a grayscale filter to the avatar image.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ui::{Avatar, AvatarShape};
|
||||
///
|
||||
/// let avatar = Avatar::new("path/to/image.png").grayscale(true);
|
||||
/// ```
|
||||
pub fn grayscale(mut self, grayscale: bool) -> Self {
|
||||
self.image = self.image.grayscale(grayscale);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the border color of the avatar.
|
||||
///
|
||||
/// This might be used to match the border to the background color of
|
||||
/// the parent element to create the illusion of cropping another
|
||||
/// shape underneath (for example in face piles.)
|
||||
pub fn border_color(mut self, color: impl Into<Hsla>) -> Self {
|
||||
self.border_color = Some(color.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Sizable for Avatar {
|
||||
fn with_size(mut self, size: impl Into<Size>) -> Self {
|
||||
self.size = size.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for Avatar {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
&mut self.style
|
||||
}
|
||||
}
|
||||
|
||||
impl Selectable for Avatar {
|
||||
fn is_selected(&self) -> bool {
|
||||
self.selected
|
||||
}
|
||||
|
||||
fn selected(mut self, selected: bool) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl InteractiveElement for Avatar {
|
||||
fn interactivity(&mut self) -> &mut Interactivity {
|
||||
self.base.interactivity()
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Avatar {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let border_width = if self.border_color.is_some() {
|
||||
px(2.)
|
||||
} else {
|
||||
px(0.)
|
||||
};
|
||||
let image_size = avatar_size(self.size);
|
||||
let container_size = image_size.to_pixels(window.rem_size()) + border_width * 2.;
|
||||
|
||||
div()
|
||||
.flex_shrink_0()
|
||||
.size(container_size)
|
||||
.rounded_full()
|
||||
.overflow_hidden()
|
||||
.when_some(self.border_color, |this, color| {
|
||||
this.border(border_width).border_color(color)
|
||||
})
|
||||
.child(
|
||||
self.image
|
||||
.size(image_size)
|
||||
.rounded_full()
|
||||
.object_fit(gpui::ObjectFit::Fill)
|
||||
.bg(cx.theme().ghost_element_background)
|
||||
.with_fallback(move || {
|
||||
img("brand/avatar.png")
|
||||
.size(image_size)
|
||||
.rounded_full()
|
||||
.into_any_element()
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,626 +0,0 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
div, relative, AnyElement, App, ClickEvent, Div, ElementId, Hsla, InteractiveElement,
|
||||
IntoElement, ParentElement, RenderOnce, SharedString, Stateful,
|
||||
StatefulInteractiveElement as _, StyleRefinement, Styled, Window,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use crate::indicator::Indicator;
|
||||
use crate::tooltip::Tooltip;
|
||||
use crate::{h_flex, Disableable, Icon, IconName, Selectable, Sizable, Size, StyledExt};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ButtonCustomVariant {
|
||||
color: Hsla,
|
||||
foreground: Hsla,
|
||||
hover: Hsla,
|
||||
active: Hsla,
|
||||
}
|
||||
|
||||
impl ButtonCustomVariant {
|
||||
pub fn new(_window: &Window, cx: &App) -> Self {
|
||||
Self {
|
||||
color: cx.theme().element_background,
|
||||
foreground: cx.theme().element_foreground,
|
||||
hover: cx.theme().element_hover,
|
||||
active: cx.theme().element_active,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn color(mut self, color: Hsla) -> Self {
|
||||
self.color = color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn foreground(mut self, color: Hsla) -> Self {
|
||||
self.foreground = color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn hover(mut self, color: Hsla) -> Self {
|
||||
self.hover = color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn active(mut self, color: Hsla) -> Self {
|
||||
self.active = color;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// The variant of the Button.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum ButtonVariant {
|
||||
#[default]
|
||||
Primary,
|
||||
Secondary,
|
||||
Danger,
|
||||
Warning,
|
||||
Ghost {
|
||||
alt: bool,
|
||||
},
|
||||
Transparent,
|
||||
Custom(ButtonCustomVariant),
|
||||
}
|
||||
|
||||
pub trait ButtonVariants: Sized {
|
||||
fn with_variant(self, variant: ButtonVariant) -> Self;
|
||||
|
||||
/// With the primary style for the Button.
|
||||
fn primary(self) -> Self {
|
||||
self.with_variant(ButtonVariant::Primary)
|
||||
}
|
||||
|
||||
/// With the secondary style for the Button.
|
||||
fn secondary(self) -> Self {
|
||||
self.with_variant(ButtonVariant::Secondary)
|
||||
}
|
||||
|
||||
/// With the danger style for the Button.
|
||||
fn danger(self) -> Self {
|
||||
self.with_variant(ButtonVariant::Danger)
|
||||
}
|
||||
|
||||
/// With the warning style for the Button.
|
||||
fn warning(self) -> Self {
|
||||
self.with_variant(ButtonVariant::Warning)
|
||||
}
|
||||
|
||||
/// With the ghost style for the Button.
|
||||
fn ghost(self) -> Self {
|
||||
self.with_variant(ButtonVariant::Ghost { alt: false })
|
||||
}
|
||||
|
||||
/// With the ghost style for the Button.
|
||||
fn ghost_alt(self) -> Self {
|
||||
self.with_variant(ButtonVariant::Ghost { alt: true })
|
||||
}
|
||||
|
||||
/// With the transparent style for the Button.
|
||||
fn transparent(self) -> Self {
|
||||
self.with_variant(ButtonVariant::Transparent)
|
||||
}
|
||||
|
||||
/// With the custom style for the Button.
|
||||
fn custom(self, style: ButtonCustomVariant) -> Self {
|
||||
self.with_variant(ButtonVariant::Custom(style))
|
||||
}
|
||||
}
|
||||
|
||||
/// A Button element.
|
||||
#[derive(IntoElement)]
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub struct Button {
|
||||
id: ElementId,
|
||||
base: Stateful<Div>,
|
||||
style: StyleRefinement,
|
||||
|
||||
icon: Option<Icon>,
|
||||
label: Option<SharedString>,
|
||||
tooltip: Option<SharedString>,
|
||||
children: Vec<AnyElement>,
|
||||
|
||||
variant: ButtonVariant,
|
||||
size: Size,
|
||||
|
||||
disabled: bool,
|
||||
loading: bool,
|
||||
|
||||
rounded: bool,
|
||||
compact: bool,
|
||||
caret: bool,
|
||||
indicator: bool,
|
||||
|
||||
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
|
||||
on_hover: Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>,
|
||||
|
||||
tab_index: isize,
|
||||
tab_stop: bool,
|
||||
|
||||
pub(crate) selected: bool,
|
||||
}
|
||||
|
||||
impl From<Button> for AnyElement {
|
||||
fn from(button: Button) -> Self {
|
||||
button.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl Button {
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
let id = id.into();
|
||||
|
||||
Self {
|
||||
id: id.clone(),
|
||||
base: div().flex_shrink_0().id(id),
|
||||
style: StyleRefinement::default(),
|
||||
icon: None,
|
||||
label: None,
|
||||
variant: ButtonVariant::default(),
|
||||
disabled: false,
|
||||
selected: false,
|
||||
indicator: false,
|
||||
compact: false,
|
||||
caret: false,
|
||||
rounded: false,
|
||||
size: Size::Medium,
|
||||
tooltip: None,
|
||||
on_click: None,
|
||||
on_hover: None,
|
||||
loading: false,
|
||||
children: Vec::new(),
|
||||
tab_index: 0,
|
||||
tab_stop: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Make the button rounded.
|
||||
pub fn rounded(mut self) -> Self {
|
||||
self.rounded = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set label to the Button, if no label is set, the button will be in Icon Button mode.
|
||||
pub fn label(mut self, label: impl Into<SharedString>) -> Self {
|
||||
self.label = Some(label.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the icon of the button, if the Button have no label, the button well in Icon Button mode.
|
||||
pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
|
||||
self.icon = Some(icon.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the tooltip of the button.
|
||||
pub fn tooltip(mut self, tooltip: impl Into<SharedString>) -> Self {
|
||||
self.tooltip = Some(tooltip.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set true to show the loading indicator.
|
||||
pub fn loading(mut self, loading: bool) -> Self {
|
||||
self.loading = loading;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set true to make the button compact (no padding).
|
||||
pub fn compact(mut self) -> Self {
|
||||
self.compact = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set true to show the caret indicator.
|
||||
pub fn caret(mut self) -> Self {
|
||||
self.caret = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set true to show the indicator.
|
||||
pub fn indicator(mut self) -> Self {
|
||||
self.indicator = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Add click handler.
|
||||
pub fn on_click(
|
||||
mut self,
|
||||
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_click = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add hover handler, the bool parameter indicates whether the mouse is hovering.
|
||||
pub fn on_hover(mut self, handler: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self {
|
||||
self.on_hover = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the tab index of the button, it will be used to focus the button by tab key.
|
||||
///
|
||||
/// Default is 0.
|
||||
pub fn tab_index(mut self, tab_index: isize) -> Self {
|
||||
self.tab_index = tab_index;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the tab stop of the button, if true, the button will be focusable by tab key.
|
||||
///
|
||||
/// Default is true.
|
||||
pub fn tab_stop(mut self, tab_stop: bool) -> Self {
|
||||
self.tab_stop = tab_stop;
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn clickable(&self) -> bool {
|
||||
!(self.disabled || self.loading) && self.on_click.is_some()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn hoverable(&self) -> bool {
|
||||
!(self.disabled || self.loading) && self.on_hover.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
impl Disableable for Button {
|
||||
fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Selectable for Button {
|
||||
fn selected(mut self, selected: bool) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
|
||||
fn is_selected(&self) -> bool {
|
||||
self.selected
|
||||
}
|
||||
}
|
||||
|
||||
impl Sizable for Button {
|
||||
fn with_size(mut self, size: impl Into<Size>) -> Self {
|
||||
self.size = size.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ButtonVariants for Button {
|
||||
fn with_variant(mut self, variant: ButtonVariant) -> Self {
|
||||
self.variant = variant;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for Button {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
&mut self.style
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for Button {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = gpui::AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
}
|
||||
}
|
||||
|
||||
impl InteractiveElement for Button {
|
||||
fn interactivity(&mut self) -> &mut gpui::Interactivity {
|
||||
self.base.interactivity()
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Button {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let style: ButtonVariant = self.variant;
|
||||
let clickable = self.clickable();
|
||||
let hoverable = self.hoverable();
|
||||
let normal_style = style.normal(cx);
|
||||
let icon_size = match self.size {
|
||||
Size::Size(v) => Size::Size(v * 0.75),
|
||||
Size::Large => Size::Medium,
|
||||
_ => self.size,
|
||||
};
|
||||
|
||||
let focus_handle = window
|
||||
.use_keyed_state(self.id.clone(), cx, |_window, cx| cx.focus_handle())
|
||||
.read(cx)
|
||||
.clone();
|
||||
|
||||
self.base
|
||||
.when(!self.disabled, |this| {
|
||||
this.track_focus(
|
||||
&focus_handle
|
||||
.tab_index(self.tab_index)
|
||||
.tab_stop(self.tab_stop),
|
||||
)
|
||||
})
|
||||
.relative()
|
||||
.flex_shrink_0()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.cursor_default()
|
||||
.overflow_hidden()
|
||||
.refine_style(&self.style)
|
||||
.map(|this| match self.rounded {
|
||||
false => this.rounded(cx.theme().radius),
|
||||
true => this.rounded_full(),
|
||||
})
|
||||
.when(!self.compact, |this| {
|
||||
if self.label.is_none() && self.children.is_empty() {
|
||||
// Icon Button
|
||||
match self.size {
|
||||
Size::Size(px) => this.size(px),
|
||||
Size::XSmall => this.size_5(),
|
||||
Size::Small => this.size_6(),
|
||||
Size::Medium => this.size_7(),
|
||||
_ => this.size_9(),
|
||||
}
|
||||
} else {
|
||||
// Normal Button
|
||||
match self.size {
|
||||
Size::Size(size) => this.px(size * 0.2),
|
||||
Size::XSmall => {
|
||||
if self.icon.is_some() {
|
||||
this.h_6().pl_2().pr_2p5()
|
||||
} else {
|
||||
this.h_6().px_2()
|
||||
}
|
||||
}
|
||||
Size::Small => {
|
||||
if self.icon.is_some() {
|
||||
this.h_7().pl_2().pr_2p5()
|
||||
} else {
|
||||
this.h_7().px_2()
|
||||
}
|
||||
}
|
||||
Size::Medium => {
|
||||
if self.icon.is_some() {
|
||||
this.h_8().pl_3().pr_3p5()
|
||||
} else {
|
||||
this.h_8().px_3()
|
||||
}
|
||||
}
|
||||
Size::Large => {
|
||||
if self.icon.is_some() {
|
||||
this.h_10().px_3().pr_3p5()
|
||||
} else {
|
||||
this.h_10().px_3()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.refine_style(&self.style)
|
||||
.on_mouse_down(gpui::MouseButton::Left, move |_, window, cx| {
|
||||
// Stop handle any click event when disabled.
|
||||
// To avoid handle dropdown menu open when button is disabled.
|
||||
if self.disabled {
|
||||
cx.stop_propagation();
|
||||
return;
|
||||
}
|
||||
// Avoid focus on mouse down.
|
||||
window.prevent_default();
|
||||
})
|
||||
.when_some(self.on_click, |this, on_click| {
|
||||
this.on_click(move |event, window, cx| {
|
||||
// Stop handle any click event when disabled.
|
||||
// To avoid handle dropdown menu open when button is disabled.
|
||||
if !clickable {
|
||||
cx.stop_propagation();
|
||||
return;
|
||||
}
|
||||
|
||||
on_click(event, window, cx);
|
||||
})
|
||||
})
|
||||
.when_some(self.on_hover.filter(|_| hoverable), |this, on_hover| {
|
||||
this.on_hover(move |hovered, window, cx| {
|
||||
(on_hover)(hovered, window, cx);
|
||||
})
|
||||
})
|
||||
.child({
|
||||
h_flex()
|
||||
.id("label")
|
||||
.justify_center()
|
||||
.map(|this| match self.size {
|
||||
Size::XSmall => this.text_xs().gap_1(),
|
||||
Size::Small => this.text_sm().gap_1p5(),
|
||||
_ => this.text_sm().gap_2(),
|
||||
})
|
||||
.when(!self.loading, |this| {
|
||||
this.when_some(self.icon, |this, icon| {
|
||||
this.child(icon.with_size(icon_size))
|
||||
})
|
||||
})
|
||||
.when(self.loading, |this| this.child(Indicator::new()))
|
||||
.when_some(self.label, |this, label| {
|
||||
this.child(div().flex_none().line_height(relative(1.)).child(label))
|
||||
})
|
||||
.children(self.children)
|
||||
.when(self.caret, |this| {
|
||||
this.justify_between().gap_0p5().child(
|
||||
Icon::new(IconName::ChevronDown)
|
||||
.small()
|
||||
.text_color(cx.theme().text_muted),
|
||||
)
|
||||
})
|
||||
})
|
||||
.text_color(normal_style.fg)
|
||||
.when(self.indicator && !self.disabled, |this| {
|
||||
this.child(
|
||||
div()
|
||||
.absolute()
|
||||
.bottom_px()
|
||||
.right_px()
|
||||
.size_1()
|
||||
.rounded_full()
|
||||
.bg(gpui::green()),
|
||||
)
|
||||
})
|
||||
.when(!self.disabled && !self.selected, |this| {
|
||||
this.bg(normal_style.bg)
|
||||
.hover(|this| {
|
||||
let hover_style = style.hovered(cx);
|
||||
this.bg(hover_style.bg).text_color(hover_style.fg)
|
||||
})
|
||||
.active(|this| {
|
||||
let active_style = style.active(cx);
|
||||
this.bg(active_style.bg).text_color(active_style.fg)
|
||||
})
|
||||
})
|
||||
.when(self.selected, |this| {
|
||||
let selected_style = style.selected(cx);
|
||||
this.bg(selected_style.bg).text_color(selected_style.fg)
|
||||
})
|
||||
.when(self.disabled, |this| {
|
||||
let disabled_style = style.disabled(cx);
|
||||
this.cursor_not_allowed()
|
||||
.bg(disabled_style.bg)
|
||||
.text_color(disabled_style.fg)
|
||||
})
|
||||
.when(self.loading && !self.disabled, |this| {
|
||||
this.bg(normal_style.bg.opacity(0.8))
|
||||
.text_color(normal_style.fg.opacity(0.8))
|
||||
})
|
||||
.when_some(self.tooltip.clone(), |this, tooltip| {
|
||||
this.tooltip(move |window, cx| Tooltip::new(tooltip.clone(), window, cx).into())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct ButtonVariantStyle {
|
||||
bg: Hsla,
|
||||
fg: Hsla,
|
||||
}
|
||||
|
||||
impl ButtonVariant {
|
||||
fn normal(&self, cx: &App) -> ButtonVariantStyle {
|
||||
let bg = self.bg_color(cx);
|
||||
let fg = self.text_color(cx);
|
||||
|
||||
ButtonVariantStyle { bg, fg }
|
||||
}
|
||||
|
||||
fn bg_color(&self, cx: &App) -> Hsla {
|
||||
match self {
|
||||
ButtonVariant::Primary => cx.theme().element_background,
|
||||
ButtonVariant::Secondary => cx.theme().secondary_background,
|
||||
ButtonVariant::Danger => cx.theme().danger_background,
|
||||
ButtonVariant::Warning => cx.theme().warning_background,
|
||||
ButtonVariant::Ghost { alt } => {
|
||||
if *alt {
|
||||
cx.theme().ghost_element_background_alt
|
||||
} else {
|
||||
cx.theme().ghost_element_background
|
||||
}
|
||||
}
|
||||
ButtonVariant::Custom(colors) => colors.color,
|
||||
_ => gpui::transparent_black(),
|
||||
}
|
||||
}
|
||||
|
||||
fn text_color(&self, cx: &App) -> Hsla {
|
||||
match self {
|
||||
ButtonVariant::Primary => cx.theme().element_foreground,
|
||||
ButtonVariant::Secondary => cx.theme().secondary_foreground,
|
||||
ButtonVariant::Danger => cx.theme().danger_foreground,
|
||||
ButtonVariant::Warning => cx.theme().warning_foreground,
|
||||
ButtonVariant::Transparent => cx.theme().text_placeholder,
|
||||
ButtonVariant::Ghost { alt } => {
|
||||
if *alt {
|
||||
cx.theme().text
|
||||
} else {
|
||||
cx.theme().text_muted
|
||||
}
|
||||
}
|
||||
ButtonVariant::Custom(colors) => colors.foreground,
|
||||
}
|
||||
}
|
||||
|
||||
fn hovered(&self, cx: &App) -> ButtonVariantStyle {
|
||||
let bg = match self {
|
||||
ButtonVariant::Primary => cx.theme().element_hover,
|
||||
ButtonVariant::Secondary => cx.theme().secondary_hover,
|
||||
ButtonVariant::Danger => cx.theme().danger_hover,
|
||||
ButtonVariant::Warning => cx.theme().warning_hover,
|
||||
ButtonVariant::Ghost { .. } => cx.theme().ghost_element_hover,
|
||||
ButtonVariant::Transparent => gpui::transparent_black(),
|
||||
ButtonVariant::Custom(colors) => colors.hover,
|
||||
};
|
||||
|
||||
let fg = match self {
|
||||
ButtonVariant::Secondary => cx.theme().secondary_foreground,
|
||||
ButtonVariant::Ghost { .. } => cx.theme().text,
|
||||
ButtonVariant::Transparent => cx.theme().text_placeholder,
|
||||
_ => self.text_color(cx),
|
||||
};
|
||||
|
||||
ButtonVariantStyle { bg, fg }
|
||||
}
|
||||
|
||||
fn active(&self, cx: &App) -> ButtonVariantStyle {
|
||||
let bg = match self {
|
||||
ButtonVariant::Primary => cx.theme().element_active,
|
||||
ButtonVariant::Secondary => cx.theme().secondary_active,
|
||||
ButtonVariant::Danger => cx.theme().danger_active,
|
||||
ButtonVariant::Warning => cx.theme().warning_active,
|
||||
ButtonVariant::Ghost { .. } => cx.theme().ghost_element_active,
|
||||
ButtonVariant::Transparent => gpui::transparent_black(),
|
||||
ButtonVariant::Custom(colors) => colors.active,
|
||||
};
|
||||
|
||||
let fg = match self {
|
||||
ButtonVariant::Secondary => cx.theme().secondary_foreground,
|
||||
ButtonVariant::Transparent => cx.theme().text_placeholder,
|
||||
_ => self.text_color(cx),
|
||||
};
|
||||
|
||||
ButtonVariantStyle { bg, fg }
|
||||
}
|
||||
|
||||
fn selected(&self, cx: &App) -> ButtonVariantStyle {
|
||||
let bg = match self {
|
||||
ButtonVariant::Primary => cx.theme().element_selected,
|
||||
ButtonVariant::Secondary => cx.theme().secondary_selected,
|
||||
ButtonVariant::Danger => cx.theme().danger_selected,
|
||||
ButtonVariant::Warning => cx.theme().warning_selected,
|
||||
ButtonVariant::Ghost { .. } => cx.theme().ghost_element_selected,
|
||||
ButtonVariant::Transparent => gpui::transparent_black(),
|
||||
ButtonVariant::Custom(colors) => colors.active,
|
||||
};
|
||||
|
||||
let fg = match self {
|
||||
ButtonVariant::Secondary => cx.theme().secondary_foreground,
|
||||
ButtonVariant::Transparent => cx.theme().text_placeholder,
|
||||
_ => self.text_color(cx),
|
||||
};
|
||||
|
||||
ButtonVariantStyle { bg, fg }
|
||||
}
|
||||
|
||||
fn disabled(&self, cx: &App) -> ButtonVariantStyle {
|
||||
let bg = match self {
|
||||
ButtonVariant::Danger => cx.theme().danger_disabled,
|
||||
ButtonVariant::Warning => cx.theme().warning_disabled,
|
||||
ButtonVariant::Ghost { .. } => cx.theme().ghost_element_disabled,
|
||||
ButtonVariant::Secondary => cx.theme().secondary_disabled,
|
||||
_ => cx.theme().element_disabled,
|
||||
};
|
||||
|
||||
let fg = match self {
|
||||
ButtonVariant::Primary => cx.theme().text_muted, // TODO: use a different color?
|
||||
_ => cx.theme().text_muted,
|
||||
};
|
||||
|
||||
ButtonVariantStyle { bg, fg }
|
||||
}
|
||||
}
|
||||
@@ -1,312 +0,0 @@
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
div, px, relative, rems, svg, Animation, AnimationExt, AnyElement, App, Div, ElementId,
|
||||
InteractiveElement, IntoElement, ParentElement, RenderOnce, SharedString,
|
||||
StatefulInteractiveElement, StyleRefinement, Styled, Window,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use crate::icon::IconNamed;
|
||||
use crate::{v_flex, Disableable, IconName, Selectable, Sizable, Size, StyledExt as _};
|
||||
|
||||
/// A Checkbox element.
|
||||
#[allow(clippy::type_complexity)]
|
||||
#[derive(IntoElement)]
|
||||
pub struct Checkbox {
|
||||
id: ElementId,
|
||||
base: Div,
|
||||
style: StyleRefinement,
|
||||
label: Option<SharedString>,
|
||||
children: Vec<AnyElement>,
|
||||
checked: bool,
|
||||
disabled: bool,
|
||||
size: Size,
|
||||
tab_stop: bool,
|
||||
tab_index: isize,
|
||||
on_click: Option<Rc<dyn Fn(&bool, &mut Window, &mut App) + 'static>>,
|
||||
}
|
||||
|
||||
impl Checkbox {
|
||||
/// Create a new Checkbox with the given id.
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
base: div(),
|
||||
style: StyleRefinement::default(),
|
||||
label: None,
|
||||
children: Vec::new(),
|
||||
checked: false,
|
||||
disabled: false,
|
||||
size: Size::default(),
|
||||
on_click: None,
|
||||
tab_stop: true,
|
||||
tab_index: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the label for the checkbox.
|
||||
pub fn label(mut self, label: impl Into<SharedString>) -> Self {
|
||||
self.label = Some(label.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the checked state for the checkbox.
|
||||
pub fn checked(mut self, checked: bool) -> Self {
|
||||
self.checked = checked;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the click handler for the checkbox.
|
||||
///
|
||||
/// The `&bool` parameter indicates the new checked state after the click.
|
||||
pub fn on_click(mut self, handler: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self {
|
||||
self.on_click = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the tab stop for the checkbox, default is true.
|
||||
pub fn tab_stop(mut self, tab_stop: bool) -> Self {
|
||||
self.tab_stop = tab_stop;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the tab index for the checkbox, default is 0.
|
||||
pub fn tab_index(mut self, tab_index: isize) -> Self {
|
||||
self.tab_index = tab_index;
|
||||
self
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn handle_click(
|
||||
on_click: &Option<Rc<dyn Fn(&bool, &mut Window, &mut App) + 'static>>,
|
||||
checked: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let new_checked = !checked;
|
||||
if let Some(f) = on_click {
|
||||
(f)(&new_checked, window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl InteractiveElement for Checkbox {
|
||||
fn interactivity(&mut self) -> &mut gpui::Interactivity {
|
||||
self.base.interactivity()
|
||||
}
|
||||
}
|
||||
impl StatefulInteractiveElement for Checkbox {}
|
||||
|
||||
impl Styled for Checkbox {
|
||||
fn style(&mut self) -> &mut gpui::StyleRefinement {
|
||||
&mut self.style
|
||||
}
|
||||
}
|
||||
|
||||
impl Disableable for Checkbox {
|
||||
fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Selectable for Checkbox {
|
||||
fn selected(self, selected: bool) -> Self {
|
||||
self.checked(selected)
|
||||
}
|
||||
|
||||
fn is_selected(&self) -> bool {
|
||||
self.checked
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for Checkbox {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements);
|
||||
}
|
||||
}
|
||||
|
||||
impl Sizable for Checkbox {
|
||||
fn with_size(mut self, size: impl Into<Size>) -> Self {
|
||||
self.size = size.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn checkbox_check_icon(
|
||||
id: ElementId,
|
||||
size: Size,
|
||||
checked: bool,
|
||||
disabled: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> impl IntoElement {
|
||||
let toggle_state = window.use_keyed_state(id, cx, |_, _| checked);
|
||||
|
||||
let color = if disabled {
|
||||
cx.theme().text.opacity(0.5)
|
||||
} else {
|
||||
cx.theme().text
|
||||
};
|
||||
|
||||
svg()
|
||||
.absolute()
|
||||
.top_px()
|
||||
.left_px()
|
||||
.map(|this| match size {
|
||||
Size::XSmall => this.size_2(),
|
||||
Size::Small => this.size_2p5(),
|
||||
Size::Medium => this.size_3(),
|
||||
Size::Large => this.size_3p5(),
|
||||
_ => this.size_3(),
|
||||
})
|
||||
.text_color(color)
|
||||
.map(|this| match checked {
|
||||
true => this.path(IconName::Check.path()),
|
||||
_ => this,
|
||||
})
|
||||
.map(|this| {
|
||||
if !disabled && checked != *toggle_state.read(cx) {
|
||||
let duration = Duration::from_secs_f64(0.25);
|
||||
cx.spawn({
|
||||
let toggle_state = toggle_state.clone();
|
||||
async move |cx| {
|
||||
cx.background_executor().timer(duration).await;
|
||||
toggle_state.update(cx, |this, _| *this = checked);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
this.with_animation(
|
||||
ElementId::NamedInteger("toggle".into(), checked as u64),
|
||||
Animation::new(Duration::from_secs_f64(0.25)),
|
||||
move |this, delta| {
|
||||
this.opacity(if checked { 1.0 * delta } else { 1.0 - delta })
|
||||
},
|
||||
)
|
||||
.into_any_element()
|
||||
} else {
|
||||
this.into_any_element()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
impl RenderOnce for Checkbox {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let focus_handle = window
|
||||
.use_keyed_state(self.id.clone(), cx, |_, cx| cx.focus_handle())
|
||||
.read(cx)
|
||||
.clone();
|
||||
|
||||
let checked = self.checked;
|
||||
let radius = cx.theme().radius.min(px(4.));
|
||||
|
||||
let border_color = if checked {
|
||||
cx.theme().border_focused
|
||||
} else {
|
||||
cx.theme().border
|
||||
};
|
||||
|
||||
let color = if self.disabled {
|
||||
border_color.opacity(0.5)
|
||||
} else {
|
||||
border_color
|
||||
};
|
||||
|
||||
div().child(
|
||||
self.base
|
||||
.id(self.id.clone())
|
||||
.when(!self.disabled, |this| {
|
||||
this.track_focus(
|
||||
&focus_handle
|
||||
.tab_stop(self.tab_stop)
|
||||
.tab_index(self.tab_index),
|
||||
)
|
||||
})
|
||||
.h_flex()
|
||||
.gap_2()
|
||||
.items_start()
|
||||
.line_height(relative(1.))
|
||||
.text_color(cx.theme().text)
|
||||
.map(|this| match self.size {
|
||||
Size::XSmall => this.text_xs(),
|
||||
Size::Small => this.text_sm(),
|
||||
Size::Medium => this.text_base(),
|
||||
Size::Large => this.text_lg(),
|
||||
_ => this,
|
||||
})
|
||||
.when(self.disabled, |this| this.text_color(cx.theme().text_muted))
|
||||
.rounded(cx.theme().radius * 0.5)
|
||||
.refine_style(&self.style)
|
||||
.child(
|
||||
div()
|
||||
.relative()
|
||||
.map(|this| match self.size {
|
||||
Size::XSmall => this.size_3(),
|
||||
Size::Small => this.size_3p5(),
|
||||
Size::Medium => this.size_4(),
|
||||
Size::Large => this.size(rems(1.125)),
|
||||
_ => this.size_4(),
|
||||
})
|
||||
.flex_shrink_0()
|
||||
.border_1()
|
||||
.border_color(color)
|
||||
.rounded(radius)
|
||||
.when(cx.theme().shadow && !self.disabled, |this| this.shadow_xs())
|
||||
.map(|this| match checked {
|
||||
false => this.bg(cx.theme().background),
|
||||
_ => this.bg(color),
|
||||
})
|
||||
.child(checkbox_check_icon(
|
||||
self.id,
|
||||
self.size,
|
||||
checked,
|
||||
self.disabled,
|
||||
window,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.when(self.label.is_some() || !self.children.is_empty(), |this| {
|
||||
this.child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.line_height(relative(1.2))
|
||||
.gap_1()
|
||||
.map(|this| {
|
||||
if let Some(label) = self.label {
|
||||
this.child(
|
||||
div()
|
||||
.size_full()
|
||||
.text_color(cx.theme().text)
|
||||
.when(self.disabled, |this| {
|
||||
this.text_color(cx.theme().text_muted)
|
||||
})
|
||||
.line_height(relative(1.))
|
||||
.child(label),
|
||||
)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
})
|
||||
.children(self.children),
|
||||
)
|
||||
})
|
||||
.on_mouse_down(gpui::MouseButton::Left, |_, window, _| {
|
||||
// Avoid focus on mouse down.
|
||||
window.prevent_default();
|
||||
})
|
||||
.when(!self.disabled, |this| {
|
||||
this.on_click({
|
||||
let on_click = self.on_click.clone();
|
||||
move |_, window, cx| {
|
||||
window.prevent_default();
|
||||
Self::handle_click(&on_click, checked, window, cx);
|
||||
}
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
div, px, Axis, Div, Hsla, IntoElement, ParentElement, RenderOnce, SharedString, Styled,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
/// A divider that can be either vertical or horizontal.
|
||||
#[derive(IntoElement)]
|
||||
pub struct Divider {
|
||||
base: Div,
|
||||
label: Option<SharedString>,
|
||||
axis: Axis,
|
||||
color: Option<Hsla>,
|
||||
}
|
||||
|
||||
impl Divider {
|
||||
pub fn vertical() -> Self {
|
||||
Self {
|
||||
base: div().h_full(),
|
||||
axis: Axis::Vertical,
|
||||
label: None,
|
||||
color: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn horizontal() -> Self {
|
||||
Self {
|
||||
base: div().w_full(),
|
||||
axis: Axis::Horizontal,
|
||||
label: None,
|
||||
color: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn label(mut self, label: impl Into<SharedString>) -> Self {
|
||||
self.label = Some(label.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn color(mut self, color: impl Into<Hsla>) -> Self {
|
||||
self.color = Some(color.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for Divider {
|
||||
fn style(&mut self) -> &mut gpui::StyleRefinement {
|
||||
self.base.style()
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Divider {
|
||||
fn render(self, _window: &mut gpui::Window, cx: &mut gpui::App) -> impl IntoElement {
|
||||
self.base
|
||||
.flex()
|
||||
.flex_shrink_0()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.rounded_full()
|
||||
.map(|this| match self.axis {
|
||||
Axis::Vertical => this.w(px(1.)).h_full(),
|
||||
Axis::Horizontal => this.h(px(1.)).w_full(),
|
||||
})
|
||||
.bg(self.color.unwrap_or(cx.theme().border_variant)),
|
||||
)
|
||||
.when_some(self.label, |this, label| {
|
||||
this.child(
|
||||
div()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.mx_auto()
|
||||
.text_xs()
|
||||
.bg(cx.theme().background)
|
||||
.child(label),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,436 +0,0 @@
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
App, AppContext, Axis, Context, Element, Empty, Entity, IntoElement, MouseMoveEvent,
|
||||
MouseUpEvent, ParentElement as _, Pixels, Point, Render, Style, StyleRefinement, Styled as _,
|
||||
WeakEntity, Window, div, px,
|
||||
};
|
||||
|
||||
use super::{DockArea, DockItem};
|
||||
use crate::StyledExt;
|
||||
use crate::dock::panel::PanelView;
|
||||
use crate::dock::tab_panel::TabPanel;
|
||||
use crate::resizable::{PANEL_MIN_SIZE, resize_handle};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ResizePanel;
|
||||
|
||||
impl Render for ResizePanel {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
Empty
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum DockPlacement {
|
||||
Center,
|
||||
Left,
|
||||
Bottom,
|
||||
Right,
|
||||
}
|
||||
|
||||
impl DockPlacement {
|
||||
fn axis(&self) -> Axis {
|
||||
match self {
|
||||
Self::Left | Self::Right => Axis::Horizontal,
|
||||
Self::Bottom => Axis::Vertical,
|
||||
Self::Center => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_left(&self) -> bool {
|
||||
matches!(self, Self::Left)
|
||||
}
|
||||
|
||||
pub fn is_bottom(&self) -> bool {
|
||||
matches!(self, Self::Bottom)
|
||||
}
|
||||
|
||||
pub fn is_right(&self) -> bool {
|
||||
matches!(self, Self::Right)
|
||||
}
|
||||
}
|
||||
|
||||
/// The Dock is a fixed container that places at left, bottom, right of the Windows.
|
||||
///
|
||||
/// This is unlike Panel, it can't be move or add any other panel.
|
||||
pub struct Dock {
|
||||
pub(super) placement: DockPlacement,
|
||||
dock_area: WeakEntity<DockArea>,
|
||||
|
||||
/// Dock layout
|
||||
pub(crate) panel: DockItem,
|
||||
|
||||
/// The size is means the width or height of the Dock, if the placement is left or right, the size is width, otherwise the size is height.
|
||||
pub(super) size: Pixels,
|
||||
|
||||
/// Whether the Dock is open
|
||||
pub(super) open: bool,
|
||||
|
||||
/// Whether the Dock is collapsible, default: true
|
||||
pub(super) collapsible: bool,
|
||||
|
||||
/// Whether the Dock is resizing
|
||||
resizing: bool,
|
||||
}
|
||||
|
||||
impl Dock {
|
||||
pub(crate) fn new(
|
||||
dock_area: WeakEntity<DockArea>,
|
||||
placement: DockPlacement,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let panel = cx.new(|cx| {
|
||||
let mut tab = TabPanel::new(None, dock_area.clone(), window, cx);
|
||||
tab.closable = true;
|
||||
tab
|
||||
});
|
||||
|
||||
let panel = DockItem::Tabs {
|
||||
items: Vec::new(),
|
||||
active_ix: 0,
|
||||
view: panel.clone(),
|
||||
};
|
||||
|
||||
Self::subscribe_panel_events(dock_area.clone(), &panel, window, cx);
|
||||
|
||||
Self {
|
||||
placement,
|
||||
dock_area,
|
||||
panel,
|
||||
open: true,
|
||||
collapsible: true,
|
||||
size: px(200.0),
|
||||
resizing: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn left(
|
||||
dock_area: WeakEntity<DockArea>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
Self::new(dock_area, DockPlacement::Left, window, cx)
|
||||
}
|
||||
|
||||
pub fn bottom(
|
||||
dock_area: WeakEntity<DockArea>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
Self::new(dock_area, DockPlacement::Bottom, window, cx)
|
||||
}
|
||||
|
||||
pub fn right(
|
||||
dock_area: WeakEntity<DockArea>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
Self::new(dock_area, DockPlacement::Right, window, cx)
|
||||
}
|
||||
|
||||
/// Update the Dock to be collapsible or not.
|
||||
///
|
||||
/// And if the Dock is not collapsible, it will be open.
|
||||
pub fn set_collapsible(
|
||||
&mut self,
|
||||
collapsible: bool,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.collapsible = collapsible;
|
||||
if !collapsible {
|
||||
self.open = true
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn subscribe_panel_events(
|
||||
dock_area: WeakEntity<DockArea>,
|
||||
panel: &DockItem,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
match panel {
|
||||
DockItem::Tabs { view, .. } => {
|
||||
window.defer(cx, {
|
||||
let view = view.clone();
|
||||
move |window, cx| {
|
||||
_ = dock_area.update(cx, |this, cx| {
|
||||
this.subscribe_panel(&view, window, cx);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
DockItem::Split { items, view, .. } => {
|
||||
for item in items {
|
||||
Self::subscribe_panel_events(dock_area.clone(), item, window, cx);
|
||||
}
|
||||
window.defer(cx, {
|
||||
let view = view.clone();
|
||||
move |window, cx| {
|
||||
_ = dock_area.update(cx, |this, cx| {
|
||||
this.subscribe_panel(&view, window, cx);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
DockItem::Panel { .. } => {
|
||||
// Not supported
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_panel(&mut self, panel: DockItem, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.panel = panel;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn is_open(&self) -> bool {
|
||||
self.open
|
||||
}
|
||||
|
||||
pub fn toggle_open(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.set_open(!self.open, window, cx);
|
||||
}
|
||||
|
||||
/// Returns the size of the Dock, the size is means the width or height of
|
||||
/// the Dock, if the placement is left or right, the size is width,
|
||||
/// otherwise the size is height.
|
||||
pub fn size(&self) -> Pixels {
|
||||
self.size
|
||||
}
|
||||
|
||||
/// Set the size of the Dock.
|
||||
pub fn set_size(&mut self, size: Pixels, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.size = size.max(PANEL_MIN_SIZE);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Set the open state of the Dock.
|
||||
pub fn set_open(&mut self, open: bool, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.open = open;
|
||||
let item = self.panel.clone();
|
||||
cx.defer_in(window, move |_, window, cx| {
|
||||
item.set_collapsed(!open, window, cx);
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Add item to the Dock.
|
||||
pub fn add_panel(
|
||||
&mut self,
|
||||
panel: Arc<dyn PanelView>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.panel.add_panel(panel, &self.dock_area, window, cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn render_resize_handle(
|
||||
&mut self,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let axis = self.placement.axis();
|
||||
let view = cx.entity().clone();
|
||||
|
||||
resize_handle("resize-handle", axis)
|
||||
.placement(self.placement)
|
||||
.on_drag(ResizePanel {}, move |info, _, _, cx| {
|
||||
cx.stop_propagation();
|
||||
view.update(cx, |view, _cx| {
|
||||
view.resizing = true;
|
||||
});
|
||||
cx.new(|_| info.deref().clone())
|
||||
})
|
||||
}
|
||||
|
||||
fn resize(
|
||||
&mut self,
|
||||
mouse_position: Point<Pixels>,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if !self.resizing {
|
||||
return;
|
||||
}
|
||||
|
||||
let dock_area = self
|
||||
.dock_area
|
||||
.upgrade()
|
||||
.expect("DockArea is missing")
|
||||
.read(cx);
|
||||
|
||||
let area_bounds = dock_area.bounds;
|
||||
let mut left_dock_size = px(0.0);
|
||||
let mut right_dock_size = px(0.0);
|
||||
|
||||
// Get the size of the left dock if it's open and not the current dock
|
||||
if let Some(left_dock) = &dock_area.left_dock
|
||||
&& left_dock.entity_id() != cx.entity().entity_id()
|
||||
{
|
||||
let left_dock_read = left_dock.read(cx);
|
||||
if left_dock_read.is_open() {
|
||||
left_dock_size = left_dock_read.size;
|
||||
}
|
||||
}
|
||||
|
||||
// Get the size of the right dock if it's open and not the current dock
|
||||
if let Some(right_dock) = &dock_area.right_dock
|
||||
&& right_dock.entity_id() != cx.entity().entity_id()
|
||||
{
|
||||
let right_dock_read = right_dock.read(cx);
|
||||
if right_dock_read.is_open() {
|
||||
right_dock_size = right_dock_read.size;
|
||||
}
|
||||
}
|
||||
|
||||
let size = match self.placement {
|
||||
DockPlacement::Left => mouse_position.x - area_bounds.left(),
|
||||
DockPlacement::Right => area_bounds.right() - mouse_position.x,
|
||||
DockPlacement::Bottom => area_bounds.bottom() - mouse_position.y,
|
||||
DockPlacement::Center => unreachable!(),
|
||||
};
|
||||
|
||||
match self.placement {
|
||||
DockPlacement::Left => {
|
||||
let max_size = area_bounds.size.width - PANEL_MIN_SIZE - right_dock_size;
|
||||
self.size = size.clamp(PANEL_MIN_SIZE, max_size);
|
||||
}
|
||||
DockPlacement::Right => {
|
||||
let max_size = area_bounds.size.width - PANEL_MIN_SIZE - left_dock_size;
|
||||
self.size = size.clamp(PANEL_MIN_SIZE, max_size);
|
||||
}
|
||||
DockPlacement::Bottom => {
|
||||
let max_size = area_bounds.size.height - PANEL_MIN_SIZE;
|
||||
self.size = size.clamp(PANEL_MIN_SIZE, max_size);
|
||||
}
|
||||
DockPlacement::Center => unreachable!(),
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn done_resizing(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {
|
||||
self.resizing = false;
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Dock {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl gpui::IntoElement {
|
||||
if !self.open && !self.placement.is_bottom() {
|
||||
return div();
|
||||
}
|
||||
|
||||
let cache_style = StyleRefinement::default().absolute().size_full();
|
||||
|
||||
div()
|
||||
.relative()
|
||||
.overflow_hidden()
|
||||
.map(|this| match self.placement {
|
||||
DockPlacement::Left | DockPlacement::Right => this.h_flex().h_full().w(self.size),
|
||||
DockPlacement::Bottom => this.w_full().h(self.size),
|
||||
DockPlacement::Center => unreachable!(),
|
||||
})
|
||||
// Bottom Dock should keep the title bar, then user can click the Toggle button
|
||||
.when(!self.open && self.placement.is_bottom(), |this| {
|
||||
this.h(px(29.))
|
||||
})
|
||||
.map(|this| match &self.panel {
|
||||
DockItem::Split { view, .. } => this.child(view.clone()),
|
||||
DockItem::Tabs { view, .. } => this.child(view.clone()),
|
||||
DockItem::Panel { view, .. } => this.child(view.clone().view().cached(cache_style)),
|
||||
})
|
||||
.child(self.render_resize_handle(window, cx))
|
||||
.child(DockElement {
|
||||
view: cx.entity().clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct DockElement {
|
||||
view: Entity<Dock>,
|
||||
}
|
||||
|
||||
impl IntoElement for DockElement {
|
||||
type Element = Self;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for DockElement {
|
||||
type PrepaintState = ();
|
||||
type RequestLayoutState = ();
|
||||
|
||||
fn id(&self) -> Option<gpui::ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
_: Option<&gpui::GlobalElementId>,
|
||||
_: Option<&gpui::InspectorElementId>,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut App,
|
||||
) -> (gpui::LayoutId, Self::RequestLayoutState) {
|
||||
(window.request_layout(Style::default(), None, cx), ())
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_: Option<&gpui::GlobalElementId>,
|
||||
_: Option<&gpui::InspectorElementId>,
|
||||
_: gpui::Bounds<Pixels>,
|
||||
_: &mut Self::RequestLayoutState,
|
||||
_window: &mut Window,
|
||||
_cx: &mut App,
|
||||
) -> Self::PrepaintState {
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_: Option<&gpui::GlobalElementId>,
|
||||
_: Option<&gpui::InspectorElementId>,
|
||||
_: gpui::Bounds<Pixels>,
|
||||
_: &mut Self::RequestLayoutState,
|
||||
_: &mut Self::PrepaintState,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
window.on_mouse_event({
|
||||
let view = self.view.clone();
|
||||
let is_resizing = view.read(cx).resizing;
|
||||
move |e: &MouseMoveEvent, phase, window, cx| {
|
||||
if !is_resizing {
|
||||
return;
|
||||
}
|
||||
if !phase.bubble() {
|
||||
return;
|
||||
}
|
||||
|
||||
view.update(cx, |view, cx| view.resize(e.position, window, cx))
|
||||
}
|
||||
});
|
||||
|
||||
// When any mouse up, stop dragging
|
||||
window.on_mouse_event({
|
||||
let view = self.view.clone();
|
||||
move |_: &MouseUpEvent, phase, window, cx| {
|
||||
if phase.bubble() {
|
||||
view.update(cx, |view, cx| view.done_resizing(window, cx));
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,807 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
AnyElement, AnyView, App, AppContext, Axis, Bounds, Context, Decorations, Edges, Entity,
|
||||
EntityId, EventEmitter, Focusable, InteractiveElement as _, IntoElement, ParentElement as _,
|
||||
Pixels, Render, SharedString, Styled, Subscription, WeakEntity, Window, actions, div, px,
|
||||
};
|
||||
use theme::CLIENT_SIDE_DECORATION_ROUNDING;
|
||||
|
||||
use crate::ElementExt;
|
||||
|
||||
#[allow(clippy::module_inception)]
|
||||
mod dock;
|
||||
mod panel;
|
||||
mod stack_panel;
|
||||
mod tab_panel;
|
||||
|
||||
pub use dock::*;
|
||||
pub use panel::*;
|
||||
pub use stack_panel::*;
|
||||
pub use tab_panel::*;
|
||||
|
||||
actions!(dock, [ToggleZoom, ClosePanel]);
|
||||
|
||||
pub enum DockEvent {
|
||||
/// The layout of the dock has changed, subscribers this to save the layout.
|
||||
///
|
||||
/// This event is emitted when every time the layout of the dock has changed,
|
||||
/// So it emits may be too frequently, you may want to debounce the event.
|
||||
LayoutChanged,
|
||||
}
|
||||
|
||||
/// The main area of the dock.
|
||||
pub struct DockArea {
|
||||
pub(crate) bounds: Bounds<Pixels>,
|
||||
|
||||
/// The center view of the dockarea.
|
||||
pub items: DockItem,
|
||||
|
||||
/// The left dock of the dock_area.
|
||||
left_dock: Option<Entity<Dock>>,
|
||||
|
||||
/// The bottom dock of the dock_area.
|
||||
bottom_dock: Option<Entity<Dock>>,
|
||||
|
||||
/// The right dock of the dock_area.
|
||||
right_dock: Option<Entity<Dock>>,
|
||||
|
||||
/// The entity_id of the [`TabPanel`](TabPanel) where each toggle button should be displayed,
|
||||
toggle_button_panels: Edges<Option<EntityId>>,
|
||||
|
||||
/// Whether to show the toggle button.
|
||||
toggle_button_visible: bool,
|
||||
|
||||
/// The top zoom view of the dock_area, if any.
|
||||
zoom_view: Option<AnyView>,
|
||||
|
||||
/// Lock panels layout, but allow to resize.
|
||||
is_locked: bool,
|
||||
|
||||
/// The panel style, default is [`PanelStyle::Default`](PanelStyle::Default).
|
||||
pub(crate) panel_style: PanelStyle,
|
||||
subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
/// DockItem is a tree structure that represents the layout of the dock.
|
||||
#[derive(Clone)]
|
||||
pub enum DockItem {
|
||||
/// Split layout
|
||||
Split {
|
||||
axis: Axis,
|
||||
items: Vec<DockItem>,
|
||||
sizes: Vec<Option<Pixels>>,
|
||||
view: Entity<StackPanel>,
|
||||
},
|
||||
/// Tab layout
|
||||
Tabs {
|
||||
items: Vec<Arc<dyn PanelView>>,
|
||||
active_ix: usize,
|
||||
view: Entity<TabPanel>,
|
||||
},
|
||||
/// Single panel layout
|
||||
Panel { view: Arc<dyn PanelView> },
|
||||
}
|
||||
|
||||
impl DockItem {
|
||||
/// Create DockItem with split layout, each item of panel have equal size.
|
||||
pub fn split(
|
||||
axis: Axis,
|
||||
items: Vec<DockItem>,
|
||||
dock_area: &WeakEntity<DockArea>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Self {
|
||||
let sizes = vec![None; items.len()];
|
||||
|
||||
Self::split_with_sizes(axis, items, sizes, dock_area, window, cx)
|
||||
}
|
||||
|
||||
/// Create DockItem with split layout, each item of panel have specified size.
|
||||
///
|
||||
/// Please note that the `items` and `sizes` must have the same length.
|
||||
/// Set `None` in `sizes` to make the index of panel have auto size.
|
||||
pub fn split_with_sizes(
|
||||
axis: Axis,
|
||||
items: Vec<DockItem>,
|
||||
sizes: Vec<Option<Pixels>>,
|
||||
dock_area: &WeakEntity<DockArea>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Self {
|
||||
let mut items = items;
|
||||
|
||||
let stack_panel = cx.new(|cx| {
|
||||
let mut stack_panel = StackPanel::new(axis, window, cx);
|
||||
for (i, item) in items.iter_mut().enumerate() {
|
||||
let view = item.view();
|
||||
let size = sizes.get(i).copied().flatten();
|
||||
stack_panel.add_panel(view.clone(), size, dock_area.clone(), window, cx)
|
||||
}
|
||||
|
||||
for (i, item) in items.iter().enumerate() {
|
||||
let view = item.view();
|
||||
let size = sizes.get(i).copied().flatten();
|
||||
stack_panel.add_panel(view.clone(), size, dock_area.clone(), window, cx)
|
||||
}
|
||||
|
||||
stack_panel
|
||||
});
|
||||
|
||||
window.defer(cx, {
|
||||
let stack_panel = stack_panel.clone();
|
||||
let dock_area = dock_area.clone();
|
||||
move |window, cx| {
|
||||
_ = dock_area.update(cx, |this, cx| {
|
||||
this.subscribe_panel(&stack_panel, window, cx);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Self::Split {
|
||||
axis,
|
||||
items,
|
||||
sizes,
|
||||
view: stack_panel,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create DockItem with panel layout
|
||||
pub fn panel(panel: Arc<dyn PanelView>) -> Self {
|
||||
Self::Panel { view: panel }
|
||||
}
|
||||
|
||||
/// Create DockItem with tabs layout, items are displayed as tabs.
|
||||
///
|
||||
/// The `active_ix` is the index of the active tab, if `None` the first tab is active.
|
||||
pub fn tabs(
|
||||
items: Vec<Arc<dyn PanelView>>,
|
||||
active_ix: Option<usize>,
|
||||
dock_area: &WeakEntity<DockArea>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Self {
|
||||
let mut new_items: Vec<Arc<dyn PanelView>> = vec![];
|
||||
for item in items.into_iter() {
|
||||
new_items.push(item)
|
||||
}
|
||||
Self::new_tabs(new_items, active_ix, dock_area, window, cx)
|
||||
}
|
||||
|
||||
pub fn tab<P: Panel>(
|
||||
item: Entity<P>,
|
||||
dock_area: &WeakEntity<DockArea>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Self {
|
||||
Self::new_tabs(vec![Arc::new(item.clone())], None, dock_area, window, cx)
|
||||
}
|
||||
|
||||
fn new_tabs(
|
||||
items: Vec<Arc<dyn PanelView>>,
|
||||
active_ix: Option<usize>,
|
||||
dock_area: &WeakEntity<DockArea>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Self {
|
||||
let active_ix = active_ix.unwrap_or(0);
|
||||
let tab_panel = cx.new(|cx| {
|
||||
let mut tab_panel = TabPanel::new(None, dock_area.clone(), window, cx);
|
||||
for item in items.iter() {
|
||||
tab_panel.add_panel(item.clone(), window, cx)
|
||||
}
|
||||
tab_panel.active_ix = active_ix;
|
||||
tab_panel
|
||||
});
|
||||
|
||||
Self::Tabs {
|
||||
items,
|
||||
active_ix,
|
||||
view: tab_panel,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns all panel ids
|
||||
pub fn panel_ids(&self, cx: &App) -> Vec<SharedString> {
|
||||
match self {
|
||||
Self::Panel { .. } => vec![],
|
||||
Self::Tabs { view, .. } => view.read(cx).panel_ids(cx),
|
||||
Self::Split { items, .. } => items
|
||||
.iter()
|
||||
.filter_map(|item| match item {
|
||||
DockItem::Tabs { view, .. } => Some(view.read(cx).panel_ids(cx)),
|
||||
_ => None,
|
||||
})
|
||||
.flatten()
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the views of the dock item.
|
||||
pub fn view(&self) -> Arc<dyn PanelView> {
|
||||
match self {
|
||||
Self::Split { view, .. } => Arc::new(view.clone()),
|
||||
Self::Tabs { view, .. } => Arc::new(view.clone()),
|
||||
Self::Panel { view, .. } => view.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Find existing panel in the dock item.
|
||||
pub fn find_panel(&self, panel: Arc<dyn PanelView>) -> Option<Arc<dyn PanelView>> {
|
||||
match self {
|
||||
Self::Split { items, .. } => {
|
||||
items.iter().find_map(|item| item.find_panel(panel.clone()))
|
||||
}
|
||||
Self::Tabs { items, .. } => items.iter().find(|item| *item == &panel).cloned(),
|
||||
Self::Panel { view } => Some(view.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a panel to the dock item.
|
||||
pub fn add_panel(
|
||||
&mut self,
|
||||
panel: Arc<dyn PanelView>,
|
||||
dock_area: &WeakEntity<DockArea>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
match self {
|
||||
Self::Tabs { view, items, .. } => {
|
||||
items.push(panel.clone());
|
||||
view.update(cx, |tab_panel, cx| {
|
||||
tab_panel.add_panel(panel, window, cx);
|
||||
});
|
||||
}
|
||||
Self::Split { view, items, .. } => {
|
||||
// Iter items to add panel to the first tabs
|
||||
for item in items.iter_mut() {
|
||||
if let DockItem::Tabs { view, .. } = item {
|
||||
view.update(cx, |tab_panel, cx| {
|
||||
tab_panel.add_panel(panel.clone(), window, cx);
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Unable to find tabs, create new tabs
|
||||
let new_item = Self::tabs(vec![panel.clone()], None, dock_area, window, cx);
|
||||
items.push(new_item.clone());
|
||||
view.update(cx, |stack_panel, cx| {
|
||||
stack_panel.add_panel(new_item.view(), None, dock_area.clone(), window, cx);
|
||||
});
|
||||
}
|
||||
Self::Panel { .. } => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the collapsed state of the dock area
|
||||
pub fn set_collapsed(&self, collapsed: bool, window: &mut Window, cx: &mut App) {
|
||||
match self {
|
||||
DockItem::Tabs { view, .. } => {
|
||||
view.update(cx, |tab_panel, cx| {
|
||||
tab_panel.set_collapsed(collapsed, window, cx);
|
||||
});
|
||||
}
|
||||
DockItem::Split { items, .. } => {
|
||||
// For each child item, set collapsed state
|
||||
for item in items {
|
||||
item.set_collapsed(collapsed, window, cx);
|
||||
}
|
||||
}
|
||||
DockItem::Panel { .. } => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursively traverses to find the left-most and top-most TabPanel.
|
||||
pub(crate) fn left_top_tab_panel(&self, cx: &App) -> Option<Entity<TabPanel>> {
|
||||
match self {
|
||||
DockItem::Tabs { view, .. } => Some(view.clone()),
|
||||
DockItem::Split { view, .. } => view.read(cx).left_top_tab_panel(true, cx),
|
||||
DockItem::Panel { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursively traverses to find the right-most and top-most TabPanel.
|
||||
pub(crate) fn right_top_tab_panel(&self, cx: &App) -> Option<Entity<TabPanel>> {
|
||||
match self {
|
||||
DockItem::Tabs { view, .. } => Some(view.clone()),
|
||||
DockItem::Split { view, .. } => view.read(cx).right_top_tab_panel(true, cx),
|
||||
DockItem::Panel { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn focus_tab_panel(&self, window: &mut Window, cx: &mut App) {
|
||||
if let DockItem::Tabs { view, .. } = self {
|
||||
window.focus(&view.read(cx).focus_handle(cx), cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DockArea {
|
||||
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let stack_panel = cx.new(|cx| StackPanel::new(Axis::Horizontal, window, cx));
|
||||
|
||||
let dock_item = DockItem::Split {
|
||||
axis: Axis::Horizontal,
|
||||
items: vec![],
|
||||
sizes: vec![],
|
||||
view: stack_panel.clone(),
|
||||
};
|
||||
|
||||
let mut this = Self {
|
||||
bounds: Bounds::default(),
|
||||
items: dock_item,
|
||||
zoom_view: None,
|
||||
toggle_button_panels: Edges::default(),
|
||||
toggle_button_visible: true,
|
||||
left_dock: None,
|
||||
right_dock: None,
|
||||
bottom_dock: None,
|
||||
is_locked: false,
|
||||
panel_style: PanelStyle::Default,
|
||||
subscriptions: vec![],
|
||||
};
|
||||
|
||||
this.subscribe_panel(&stack_panel, window, cx);
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
/// Set the panel style of the dock area.
|
||||
pub fn style(mut self, style: PanelStyle) -> Self {
|
||||
self.panel_style = style;
|
||||
self
|
||||
}
|
||||
|
||||
/// The DockItem as the center of the dock area.
|
||||
///
|
||||
/// This is used to render at the Center of the DockArea.
|
||||
pub fn set_center(&mut self, item: DockItem, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.subscribe_item(&item, window, cx);
|
||||
self.items = item;
|
||||
self.update_toggle_button_tab_panels(window, cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_left_dock(
|
||||
&mut self,
|
||||
panel: DockItem,
|
||||
size: Option<Pixels>,
|
||||
open: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.subscribe_item(&panel, window, cx);
|
||||
let weak_self = cx.entity().downgrade();
|
||||
self.left_dock = Some(cx.new(|cx| {
|
||||
let mut dock = Dock::left(weak_self.clone(), window, cx);
|
||||
if let Some(size) = size {
|
||||
dock.set_size(size, window, cx);
|
||||
}
|
||||
dock.set_panel(panel, window, cx);
|
||||
dock.set_open(open, window, cx);
|
||||
dock
|
||||
}));
|
||||
self.update_toggle_button_tab_panels(window, cx);
|
||||
}
|
||||
|
||||
pub fn set_bottom_dock(
|
||||
&mut self,
|
||||
panel: DockItem,
|
||||
size: Option<Pixels>,
|
||||
open: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.subscribe_item(&panel, window, cx);
|
||||
let weak_self = cx.entity().downgrade();
|
||||
self.bottom_dock = Some(cx.new(|cx| {
|
||||
let mut dock = Dock::bottom(weak_self.clone(), window, cx);
|
||||
if let Some(size) = size {
|
||||
dock.set_size(size, window, cx);
|
||||
}
|
||||
dock.set_panel(panel, window, cx);
|
||||
dock.set_open(open, window, cx);
|
||||
dock
|
||||
}));
|
||||
self.update_toggle_button_tab_panels(window, cx);
|
||||
}
|
||||
|
||||
pub fn set_right_dock(
|
||||
&mut self,
|
||||
panel: DockItem,
|
||||
size: Option<Pixels>,
|
||||
open: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.subscribe_item(&panel, window, cx);
|
||||
let weak_self = cx.entity().downgrade();
|
||||
self.right_dock = Some(cx.new(|cx| {
|
||||
let mut dock = Dock::right(weak_self.clone(), window, cx);
|
||||
if let Some(size) = size {
|
||||
dock.set_size(size, window, cx);
|
||||
}
|
||||
dock.set_panel(panel, window, cx);
|
||||
dock.set_open(open, window, cx);
|
||||
dock
|
||||
}));
|
||||
self.update_toggle_button_tab_panels(window, cx);
|
||||
}
|
||||
|
||||
/// Reset all docks
|
||||
pub fn reset(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.left_dock = None;
|
||||
self.right_dock = None;
|
||||
self.bottom_dock = None;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Set locked state of the dock area, if locked, the dock area cannot be split or move, but allows to resize panels.
|
||||
pub fn set_locked(&mut self, locked: bool, _window: &mut Window, _cx: &mut App) {
|
||||
self.is_locked = locked;
|
||||
}
|
||||
|
||||
/// Determine if the dock area is locked.
|
||||
pub fn is_locked(&self) -> bool {
|
||||
self.is_locked
|
||||
}
|
||||
|
||||
/// Determine if the dock area has a dock at the given placement.
|
||||
pub fn has_dock(&self, placement: DockPlacement) -> bool {
|
||||
match placement {
|
||||
DockPlacement::Left => self.left_dock.is_some(),
|
||||
DockPlacement::Bottom => self.bottom_dock.is_some(),
|
||||
DockPlacement::Right => self.right_dock.is_some(),
|
||||
DockPlacement::Center => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine if the dock at the given placement is open.
|
||||
pub fn is_dock_open(&self, placement: DockPlacement, cx: &App) -> bool {
|
||||
match placement {
|
||||
DockPlacement::Left => self
|
||||
.left_dock
|
||||
.as_ref()
|
||||
.map(|dock| dock.read(cx).is_open())
|
||||
.unwrap_or(false),
|
||||
DockPlacement::Bottom => self
|
||||
.bottom_dock
|
||||
.as_ref()
|
||||
.map(|dock| dock.read(cx).is_open())
|
||||
.unwrap_or(false),
|
||||
DockPlacement::Right => self
|
||||
.right_dock
|
||||
.as_ref()
|
||||
.map(|dock| dock.read(cx).is_open())
|
||||
.unwrap_or(false),
|
||||
DockPlacement::Center => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the dock at the given placement to be open or closed.
|
||||
///
|
||||
/// Only the left, bottom, right dock can be toggled.
|
||||
pub fn set_dock_collapsible(
|
||||
&mut self,
|
||||
collapsible_edges: Edges<bool>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(left_dock) = self.left_dock.as_ref() {
|
||||
left_dock.update(cx, |dock, cx| {
|
||||
dock.set_collapsible(collapsible_edges.left, window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(bottom_dock) = self.bottom_dock.as_ref() {
|
||||
bottom_dock.update(cx, |dock, cx| {
|
||||
dock.set_collapsible(collapsible_edges.bottom, window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(right_dock) = self.right_dock.as_ref() {
|
||||
right_dock.update(cx, |dock, cx| {
|
||||
dock.set_collapsible(collapsible_edges.right, window, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine if the dock at the given placement is collapsible.
|
||||
pub fn is_dock_collapsible(&self, placement: DockPlacement, cx: &App) -> bool {
|
||||
match placement {
|
||||
DockPlacement::Left => self
|
||||
.left_dock
|
||||
.as_ref()
|
||||
.map(|dock| dock.read(cx).collapsible)
|
||||
.unwrap_or(false),
|
||||
DockPlacement::Bottom => self
|
||||
.bottom_dock
|
||||
.as_ref()
|
||||
.map(|dock| dock.read(cx).collapsible)
|
||||
.unwrap_or(false),
|
||||
DockPlacement::Right => self
|
||||
.right_dock
|
||||
.as_ref()
|
||||
.map(|dock| dock.read(cx).collapsible)
|
||||
.unwrap_or(false),
|
||||
DockPlacement::Center => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_dock(
|
||||
&self,
|
||||
placement: DockPlacement,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let dock = match placement {
|
||||
DockPlacement::Left => &self.left_dock,
|
||||
DockPlacement::Bottom => &self.bottom_dock,
|
||||
DockPlacement::Right => &self.right_dock,
|
||||
DockPlacement::Center => return,
|
||||
};
|
||||
|
||||
if let Some(dock) = dock {
|
||||
dock.update(cx, |view, cx| {
|
||||
view.toggle_open(window, cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a panel item to the dock area at the given placement.
|
||||
///
|
||||
/// If the left, bottom, right dock is not present, it will set the dock at the placement.
|
||||
pub fn add_panel(
|
||||
&mut self,
|
||||
panel: Arc<dyn PanelView>,
|
||||
placement: DockPlacement,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let weak_self = cx.entity().downgrade();
|
||||
|
||||
match placement {
|
||||
DockPlacement::Left => {
|
||||
if let Some(dock) = self.left_dock.as_ref() {
|
||||
dock.update(cx, |dock, cx| dock.add_panel(panel, window, cx))
|
||||
} else {
|
||||
self.set_left_dock(
|
||||
DockItem::tabs(vec![panel], None, &weak_self, window, cx),
|
||||
Some(px(320.)),
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
DockPlacement::Bottom => {
|
||||
if let Some(dock) = self.bottom_dock.as_ref() {
|
||||
dock.update(cx, |dock, cx| dock.add_panel(panel, window, cx))
|
||||
} else {
|
||||
self.set_bottom_dock(
|
||||
DockItem::tabs(vec![panel], None, &weak_self, window, cx),
|
||||
None,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
DockPlacement::Right => {
|
||||
self.set_right_dock(
|
||||
DockItem::tabs(vec![panel], None, &weak_self, window, cx),
|
||||
Some(px(320.)),
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
DockPlacement::Center => {
|
||||
self.items
|
||||
.add_panel(panel, &cx.entity().downgrade(), window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Subscribe event on the panels
|
||||
fn subscribe_item(&mut self, item: &DockItem, window: &mut Window, cx: &mut Context<Self>) {
|
||||
match item {
|
||||
DockItem::Split { items, view, .. } => {
|
||||
for item in items {
|
||||
self.subscribe_item(item, window, cx);
|
||||
}
|
||||
|
||||
self.subscriptions.push(cx.subscribe_in(
|
||||
view,
|
||||
window,
|
||||
move |_, _, event, window, cx| {
|
||||
if let PanelEvent::LayoutChanged = event {
|
||||
cx.spawn_in(window, async move |view, window| {
|
||||
_ = view.update_in(window, |view, window, cx| {
|
||||
view.update_toggle_button_tab_panels(window, cx)
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.emit(DockEvent::LayoutChanged);
|
||||
}
|
||||
},
|
||||
));
|
||||
}
|
||||
DockItem::Tabs { .. } => {
|
||||
// We subscribe to the tab panel event in StackPanel's insert_panel
|
||||
}
|
||||
DockItem::Panel { .. } => {
|
||||
// Not supported
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Subscribe zoom event on the panel
|
||||
pub(crate) fn subscribe_panel<P: Panel>(
|
||||
&mut self,
|
||||
view: &Entity<P>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<DockArea>,
|
||||
) {
|
||||
let subscription =
|
||||
cx.subscribe_in(
|
||||
view,
|
||||
window,
|
||||
move |_this, panel, event, window, cx| match event {
|
||||
PanelEvent::ZoomIn => {
|
||||
let panel = panel.clone();
|
||||
cx.spawn_in(window, async move |view, window| {
|
||||
view.update_in(window, |view, window, cx| {
|
||||
view.set_zoomed_in(panel, window, cx);
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
PanelEvent::ZoomOut => {
|
||||
cx.spawn_in(window, async move |view, window| {
|
||||
_ = view.update_in(window, |view, window, cx| {
|
||||
view.set_zoomed_out(window, cx);
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
PanelEvent::LayoutChanged => {
|
||||
cx.spawn_in(window, async move |view, window| {
|
||||
view.update_in(window, |view, window, cx| {
|
||||
view.update_toggle_button_tab_panels(window, cx)
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
// Emit layout changed event for dock
|
||||
cx.emit(DockEvent::LayoutChanged);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
self.subscriptions.push(subscription);
|
||||
}
|
||||
|
||||
pub fn set_zoomed_in<P: Panel>(
|
||||
&mut self,
|
||||
panel: Entity<P>,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.zoom_view = Some(panel.into());
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_zoomed_out(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.zoom_view = None;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn render_items(&self, _window: &mut Window, _cx: &mut Context<Self>) -> AnyElement {
|
||||
match &self.items {
|
||||
DockItem::Split { view, .. } => view.clone().into_any_element(),
|
||||
DockItem::Tabs { view, .. } => view.clone().into_any_element(),
|
||||
DockItem::Panel { view, .. } => view.clone().view().into_any_element(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_toggle_button_tab_panels(
|
||||
&mut self,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
// Left toggle button
|
||||
self.toggle_button_panels.left = self
|
||||
.items
|
||||
.left_top_tab_panel(cx)
|
||||
.map(|view| view.entity_id());
|
||||
|
||||
// Right toggle button
|
||||
self.toggle_button_panels.right = self
|
||||
.items
|
||||
.right_top_tab_panel(cx)
|
||||
.map(|view| view.entity_id());
|
||||
|
||||
// Bottom toggle button
|
||||
self.toggle_button_panels.bottom = self
|
||||
.bottom_dock
|
||||
.as_ref()
|
||||
.and_then(|dock| dock.read(cx).panel.left_top_tab_panel(cx))
|
||||
.map(|view| view.entity_id());
|
||||
}
|
||||
|
||||
pub fn focus_tab_panel(&mut self, window: &mut Window, cx: &mut App) {
|
||||
self.items.focus_tab_panel(window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DockEvent> for DockArea {}
|
||||
|
||||
impl Render for DockArea {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let view = cx.entity().clone();
|
||||
let decorations = window.window_decorations();
|
||||
|
||||
div()
|
||||
.id("dock-area")
|
||||
.relative()
|
||||
.size_full()
|
||||
.overflow_hidden()
|
||||
.on_prepaint(move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds))
|
||||
.map(|this| {
|
||||
if let Some(zoom_view) = self.zoom_view.clone() {
|
||||
this.map(|this| match decorations {
|
||||
Decorations::Server => this,
|
||||
Decorations::Client { tiling } => this
|
||||
.when(!(tiling.top || tiling.right), |div| {
|
||||
div.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
})
|
||||
.when(!(tiling.top || tiling.left), |div| {
|
||||
div.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
}),
|
||||
})
|
||||
.child(zoom_view)
|
||||
} else {
|
||||
// render dock
|
||||
this.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.h_full()
|
||||
// Left dock
|
||||
.when_some(self.left_dock.clone(), |this, dock| {
|
||||
this.child(div().flex().flex_none().child(dock))
|
||||
})
|
||||
// Center
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_1()
|
||||
.flex_col()
|
||||
.overflow_hidden()
|
||||
// Top center
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.overflow_hidden()
|
||||
.child(self.render_items(window, cx)),
|
||||
)
|
||||
// Bottom Dock
|
||||
.when_some(self.bottom_dock.clone(), |this, dock| {
|
||||
this.child(dock)
|
||||
}),
|
||||
)
|
||||
// Right Dock
|
||||
.when_some(self.right_dock.clone(), |this, dock| {
|
||||
this.child(div().flex().flex_none().child(dock))
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
use gpui::{
|
||||
AnyElement, AnyView, App, Element, Entity, EventEmitter, FocusHandle, Focusable, Render,
|
||||
SharedString, Window,
|
||||
};
|
||||
|
||||
use crate::button::Button;
|
||||
use crate::menu::PopupMenu;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PanelEvent {
|
||||
ZoomIn,
|
||||
ZoomOut,
|
||||
LayoutChanged,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PanelStyle {
|
||||
/// Display the TabBar when there are multiple tabs, otherwise display the simple title.
|
||||
Default,
|
||||
/// Always display the tab bar.
|
||||
TabBar,
|
||||
}
|
||||
|
||||
pub trait Panel: EventEmitter<PanelEvent> + Render + Focusable {
|
||||
/// The name of the panel used to serialize, deserialize and identify the panel.
|
||||
///
|
||||
/// This is used to identify the panel when deserializing the panel.
|
||||
/// Once you have defined a panel id, this must not be changed.
|
||||
fn panel_id(&self) -> SharedString;
|
||||
|
||||
/// The title of the panel
|
||||
fn title(&self, _cx: &App) -> AnyElement {
|
||||
SharedString::from("Unnamed").into_any()
|
||||
}
|
||||
|
||||
/// Whether the panel can be closed, default is `true`.
|
||||
fn closable(&self, _cx: &App) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Return true if the panel is zoomable, default is `false`.
|
||||
fn zoomable(&self, _cx: &App) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Return false to hide panel, true to show panel, default is `true`.
|
||||
///
|
||||
/// This method called in Panel render, we should make sure it is fast.
|
||||
fn visible(&self, _cx: &App) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Set active state of the panel.
|
||||
///
|
||||
/// This method will be called when the panel is active or inactive.
|
||||
///
|
||||
/// The last_active_panel and current_active_panel will be touched when the panel is active.
|
||||
fn set_active(&self, _active: bool, _cx: &mut App) {}
|
||||
|
||||
/// Set zoomed state of the panel.
|
||||
///
|
||||
/// This method will be called when the panel is zoomed or unzoomed.
|
||||
///
|
||||
/// Only current Panel will touch this method.
|
||||
fn set_zoomed(&self, _zoomed: bool, _cx: &mut App) {}
|
||||
|
||||
/// The addition popup menu of the panel, default is `None`.
|
||||
fn popup_menu(&self, this: PopupMenu, _cx: &App) -> PopupMenu {
|
||||
this
|
||||
}
|
||||
|
||||
/// The addition toolbar buttons of the panel used to show in the right of the title bar, default is `None`.
|
||||
fn toolbar_buttons(&self, _window: &Window, _cx: &App) -> Vec<Button> {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
pub trait PanelView: 'static + Send + Sync {
|
||||
fn panel_id(&self, cx: &App) -> SharedString;
|
||||
fn title(&self, cx: &App) -> AnyElement;
|
||||
fn closable(&self, cx: &App) -> bool;
|
||||
fn zoomable(&self, cx: &App) -> bool;
|
||||
fn visible(&self, cx: &App) -> bool;
|
||||
fn set_active(&self, active: bool, cx: &mut App);
|
||||
fn set_zoomed(&self, zoomed: bool, cx: &mut App);
|
||||
fn popup_menu(&self, menu: PopupMenu, cx: &App) -> PopupMenu;
|
||||
fn toolbar_buttons(&self, window: &Window, cx: &App) -> Vec<Button>;
|
||||
fn view(&self) -> AnyView;
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle;
|
||||
}
|
||||
|
||||
impl<T: Panel> PanelView for Entity<T> {
|
||||
fn panel_id(&self, cx: &App) -> SharedString {
|
||||
self.read(cx).panel_id()
|
||||
}
|
||||
|
||||
fn title(&self, cx: &App) -> AnyElement {
|
||||
self.read(cx).title(cx)
|
||||
}
|
||||
|
||||
fn closable(&self, cx: &App) -> bool {
|
||||
self.read(cx).closable(cx)
|
||||
}
|
||||
|
||||
fn zoomable(&self, cx: &App) -> bool {
|
||||
self.read(cx).zoomable(cx)
|
||||
}
|
||||
|
||||
fn visible(&self, cx: &App) -> bool {
|
||||
self.read(cx).visible(cx)
|
||||
}
|
||||
|
||||
fn set_active(&self, active: bool, cx: &mut App) {
|
||||
self.update(cx, |this, cx| {
|
||||
this.set_active(active, cx);
|
||||
})
|
||||
}
|
||||
|
||||
fn set_zoomed(&self, zoomed: bool, cx: &mut App) {
|
||||
self.update(cx, |this, cx| {
|
||||
this.set_zoomed(zoomed, cx);
|
||||
})
|
||||
}
|
||||
|
||||
fn popup_menu(&self, menu: PopupMenu, cx: &App) -> PopupMenu {
|
||||
self.read(cx).popup_menu(menu, cx)
|
||||
}
|
||||
|
||||
fn toolbar_buttons(&self, window: &Window, cx: &App) -> Vec<Button> {
|
||||
self.read(cx).toolbar_buttons(window, cx)
|
||||
}
|
||||
|
||||
fn view(&self) -> AnyView {
|
||||
self.clone().into()
|
||||
}
|
||||
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
self.read(cx).focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&dyn PanelView> for AnyView {
|
||||
fn from(handle: &dyn PanelView) -> Self {
|
||||
handle.view()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Panel> From<&dyn PanelView> for Entity<T> {
|
||||
fn from(value: &dyn PanelView) -> Self {
|
||||
value.view().downcast::<T>().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for dyn PanelView {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.view() == other.view()
|
||||
}
|
||||
}
|
||||
@@ -1,394 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
App, AppContext, Axis, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
IntoElement, ParentElement, Pixels, Render, SharedString, Styled, Subscription, WeakEntity,
|
||||
Window,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use theme::{ActiveTheme, AxisExt as _, CLIENT_SIDE_DECORATION_ROUNDING, Placement};
|
||||
|
||||
use super::{DockArea, PanelEvent};
|
||||
use crate::dock::panel::{Panel, PanelView};
|
||||
use crate::dock::tab_panel::TabPanel;
|
||||
use crate::h_flex;
|
||||
use crate::resizable::{
|
||||
PANEL_MIN_SIZE, ResizablePanelEvent, ResizablePanelGroup, ResizablePanelState, ResizableState,
|
||||
resizable_panel,
|
||||
};
|
||||
|
||||
pub struct StackPanel {
|
||||
pub(super) parent: Option<WeakEntity<StackPanel>>,
|
||||
pub(super) axis: Axis,
|
||||
focus_handle: FocusHandle,
|
||||
pub(crate) panels: SmallVec<[Arc<dyn PanelView>; 2]>,
|
||||
state: Entity<ResizableState>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl Panel for StackPanel {
|
||||
fn panel_id(&self) -> SharedString {
|
||||
"StackPanel".into()
|
||||
}
|
||||
|
||||
fn title(&self, _cx: &App) -> gpui::AnyElement {
|
||||
"StackPanel".into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl StackPanel {
|
||||
pub fn new(axis: Axis, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let state = cx.new(|_| ResizableState::default());
|
||||
|
||||
// Bubble up the resize event.
|
||||
let subscriptions =
|
||||
vec![
|
||||
cx.subscribe_in(&state, window, |_, _, _: &ResizablePanelEvent, _, cx| {
|
||||
cx.emit(PanelEvent::LayoutChanged)
|
||||
}),
|
||||
];
|
||||
|
||||
Self {
|
||||
axis,
|
||||
parent: None,
|
||||
focus_handle: cx.focus_handle(),
|
||||
panels: SmallVec::new(),
|
||||
state,
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
|
||||
/// The first level of the stack panel is root, will not have a parent.
|
||||
fn is_root(&self) -> bool {
|
||||
self.parent.is_none()
|
||||
}
|
||||
|
||||
/// Return true if self or parent only have last panel.
|
||||
pub fn is_last_panel(&self, cx: &App) -> bool {
|
||||
if self.panels.len() > 1 {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(parent) = &self.parent
|
||||
&& let Some(parent) = parent.upgrade()
|
||||
{
|
||||
return parent.read(cx).is_last_panel(cx);
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub fn panels_len(&self) -> usize {
|
||||
self.panels.len()
|
||||
}
|
||||
|
||||
/// Return the index of the panel.
|
||||
pub fn index_of_panel(&self, panel: Arc<dyn PanelView>) -> Option<usize> {
|
||||
self.panels.iter().position(|p| p == &panel)
|
||||
}
|
||||
|
||||
/// Add a panel at the end of the stack.
|
||||
pub fn add_panel(
|
||||
&mut self,
|
||||
panel: Arc<dyn PanelView>,
|
||||
size: Option<Pixels>,
|
||||
dock_area: WeakEntity<DockArea>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.insert_panel(panel, self.panels.len(), size, dock_area, window, cx);
|
||||
}
|
||||
|
||||
pub fn add_panel_at(
|
||||
&mut self,
|
||||
panel: Arc<dyn PanelView>,
|
||||
placement: Placement,
|
||||
size: Option<Pixels>,
|
||||
dock_area: WeakEntity<DockArea>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.insert_panel_at(
|
||||
panel,
|
||||
self.panels_len(),
|
||||
placement,
|
||||
size,
|
||||
dock_area,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn insert_panel_at(
|
||||
&mut self,
|
||||
panel: Arc<dyn PanelView>,
|
||||
ix: usize,
|
||||
placement: Placement,
|
||||
size: Option<Pixels>,
|
||||
dock_area: WeakEntity<DockArea>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match placement {
|
||||
Placement::Top | Placement::Left => {
|
||||
self.insert_panel_before(panel, ix, size, dock_area, window, cx)
|
||||
}
|
||||
Placement::Right | Placement::Bottom => {
|
||||
self.insert_panel_after(panel, ix, size, dock_area, window, cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert a panel at the index.
|
||||
pub fn insert_panel_before(
|
||||
&mut self,
|
||||
panel: Arc<dyn PanelView>,
|
||||
ix: usize,
|
||||
size: Option<Pixels>,
|
||||
dock_area: WeakEntity<DockArea>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.insert_panel(panel, ix, size, dock_area, window, cx);
|
||||
}
|
||||
|
||||
/// Insert a panel after the index.
|
||||
pub fn insert_panel_after(
|
||||
&mut self,
|
||||
panel: Arc<dyn PanelView>,
|
||||
ix: usize,
|
||||
size: Option<Pixels>,
|
||||
dock_area: WeakEntity<DockArea>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.insert_panel(panel, ix + 1, size, dock_area, window, cx);
|
||||
}
|
||||
|
||||
fn insert_panel(
|
||||
&mut self,
|
||||
panel: Arc<dyn PanelView>,
|
||||
ix: usize,
|
||||
size: Option<Pixels>,
|
||||
dock_area: WeakEntity<DockArea>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
// If the panel is already in the stack, return.
|
||||
if self.index_of_panel(panel.clone()).is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
let view = cx.entity().clone();
|
||||
|
||||
window.defer(cx, {
|
||||
let panel = panel.clone();
|
||||
|
||||
move |window, cx| {
|
||||
// If the panel is a TabPanel, set its parent to this.
|
||||
if let Ok(tab_panel) = panel.view().downcast::<TabPanel>() {
|
||||
tab_panel.update(cx, |tab_panel, _| tab_panel.set_parent(view.downgrade()));
|
||||
} else if let Ok(stack_panel) = panel.view().downcast::<Self>() {
|
||||
stack_panel.update(cx, |stack_panel, _| {
|
||||
stack_panel.parent = Some(view.downgrade())
|
||||
});
|
||||
}
|
||||
|
||||
// Subscribe to the panel's layout change event.
|
||||
_ = dock_area.update(cx, |this, cx| {
|
||||
if let Ok(tab_panel) = panel.view().downcast::<TabPanel>() {
|
||||
this.subscribe_panel(&tab_panel, window, cx);
|
||||
} else if let Ok(stack_panel) = panel.view().downcast::<Self>() {
|
||||
this.subscribe_panel(&stack_panel, window, cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let ix = if ix > self.panels.len() {
|
||||
self.panels.len()
|
||||
} else {
|
||||
ix
|
||||
};
|
||||
|
||||
// Get avg size of all panels to insert new panel, if size is None.
|
||||
let size = match size {
|
||||
Some(size) => size,
|
||||
None => {
|
||||
let state = self.state.read(cx);
|
||||
(state.container_size() / (state.sizes().len() + 1) as f32).max(PANEL_MIN_SIZE)
|
||||
}
|
||||
};
|
||||
|
||||
// Insert panel
|
||||
self.panels.insert(ix, panel.clone());
|
||||
|
||||
// Update resizable state
|
||||
self.state.update(cx, |state, cx| {
|
||||
state.insert_panel(Some(size), Some(ix), cx);
|
||||
});
|
||||
|
||||
cx.emit(PanelEvent::LayoutChanged);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Remove panel from the stack.
|
||||
///
|
||||
/// If `ix` is not found, do nothing.
|
||||
pub fn remove_panel(
|
||||
&mut self,
|
||||
panel: Arc<dyn PanelView>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(ix) = self.index_of_panel(panel.clone()) else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.panels.remove(ix);
|
||||
self.state.update(cx, |state, cx| {
|
||||
state.remove_panel(ix, cx);
|
||||
});
|
||||
|
||||
cx.emit(PanelEvent::LayoutChanged);
|
||||
|
||||
self.remove_self_if_empty(window, cx);
|
||||
}
|
||||
|
||||
/// Replace the old panel with the new panel at same index.
|
||||
pub fn replace_panel(
|
||||
&mut self,
|
||||
old_panel: Arc<dyn PanelView>,
|
||||
new_panel: Entity<StackPanel>,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(ix) = self.index_of_panel(old_panel.clone()) {
|
||||
self.panels[ix] = Arc::new(new_panel.clone());
|
||||
self.state.update(cx, |state, cx| {
|
||||
state.replace_panel(ix, ResizablePanelState::default(), cx);
|
||||
});
|
||||
cx.emit(PanelEvent::LayoutChanged);
|
||||
}
|
||||
}
|
||||
|
||||
/// If children is empty, remove self from parent view.
|
||||
pub fn remove_self_if_empty(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.is_root() {
|
||||
return;
|
||||
}
|
||||
|
||||
if !self.panels.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let view = cx.entity().clone();
|
||||
if let Some(parent) = self.parent.as_ref() {
|
||||
_ = parent.update(cx, |parent, cx| {
|
||||
parent.remove_panel(Arc::new(view.clone()), window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
cx.emit(PanelEvent::LayoutChanged);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Find the first top left in the stack.
|
||||
pub fn left_top_tab_panel(&self, check_parent: bool, cx: &App) -> Option<Entity<TabPanel>> {
|
||||
if check_parent
|
||||
&& let Some(parent) = self.parent.as_ref().and_then(|parent| parent.upgrade())
|
||||
&& let Some(panel) = parent.read(cx).left_top_tab_panel(true, cx)
|
||||
{
|
||||
return Some(panel);
|
||||
}
|
||||
|
||||
let first_panel = self.panels.first();
|
||||
if let Some(view) = first_panel {
|
||||
if let Ok(tab_panel) = view.view().downcast::<TabPanel>() {
|
||||
Some(tab_panel)
|
||||
} else if let Ok(stack_panel) = view.view().downcast::<StackPanel>() {
|
||||
stack_panel.read(cx).left_top_tab_panel(false, cx)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the first top right in the stack.
|
||||
pub fn right_top_tab_panel(&self, check_parent: bool, cx: &App) -> Option<Entity<TabPanel>> {
|
||||
if check_parent
|
||||
&& let Some(parent) = self.parent.as_ref().and_then(|parent| parent.upgrade())
|
||||
&& let Some(panel) = parent.read(cx).right_top_tab_panel(true, cx)
|
||||
{
|
||||
return Some(panel);
|
||||
}
|
||||
|
||||
let panel = if self.axis.is_vertical() {
|
||||
self.panels.first()
|
||||
} else {
|
||||
self.panels.last()
|
||||
};
|
||||
|
||||
if let Some(view) = panel {
|
||||
if let Ok(tab_panel) = view.view().downcast::<TabPanel>() {
|
||||
Some(tab_panel)
|
||||
} else if let Ok(stack_panel) = view.view().downcast::<StackPanel>() {
|
||||
stack_panel.read(cx).right_top_tab_panel(false, cx)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove all panels from the stack.
|
||||
pub fn remove_all_panels(&mut self, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.panels.clear();
|
||||
self.state.update(cx, |state, cx| {
|
||||
state.clear();
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
|
||||
/// Change the axis of the stack panel.
|
||||
pub fn set_axis(&mut self, axis: Axis, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.axis = axis;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for StackPanel {
|
||||
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<PanelEvent> for StackPanel {}
|
||||
impl EventEmitter<DismissEvent> for StackPanel {}
|
||||
|
||||
impl Render for StackPanel {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
h_flex()
|
||||
.size_full()
|
||||
.overflow_hidden()
|
||||
.bg(cx.theme().panel_background)
|
||||
.when(cx.theme().platform.is_linux(), |this| {
|
||||
this.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
})
|
||||
.child(
|
||||
ResizablePanelGroup::new("stack-panel-group")
|
||||
.with_state(&self.state)
|
||||
.axis(self.axis)
|
||||
.children(self.panels.clone().into_iter().map(|panel| {
|
||||
resizable_panel()
|
||||
.child(panel.view())
|
||||
.visible(panel.visible(cx))
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,27 +0,0 @@
|
||||
use gpui::{canvas, App, Bounds, ParentElement, Pixels, Styled as _, Window};
|
||||
|
||||
/// A trait to extend [`gpui::Element`] with additional functionality.
|
||||
pub trait ElementExt: ParentElement + Sized {
|
||||
/// Add a prepaint callback to the element.
|
||||
///
|
||||
/// This is a helper method to get the bounds of the element after paint.
|
||||
///
|
||||
/// The first argument is the bounds of the element in pixels.
|
||||
///
|
||||
/// See also [`gpui::canvas`].
|
||||
fn on_prepaint<F>(self, f: F) -> Self
|
||||
where
|
||||
F: FnOnce(Bounds<Pixels>, &mut Window, &mut App) + 'static,
|
||||
{
|
||||
self.child(
|
||||
canvas(
|
||||
move |bounds, window, cx| f(bounds, window, cx),
|
||||
|_, _, _, _| {},
|
||||
)
|
||||
.absolute()
|
||||
.size_full(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ParentElement> ElementExt for T {}
|
||||
@@ -1,21 +0,0 @@
|
||||
use gpui::{App, ClickEvent, InteractiveElement, Stateful, Window};
|
||||
|
||||
pub trait InteractiveElementExt: InteractiveElement {
|
||||
/// Set the listener for a double click event.
|
||||
fn on_double_click(
|
||||
mut self,
|
||||
listener: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||
) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.interactivity().on_click(move |event, window, cx| {
|
||||
if event.click_count() == 2 {
|
||||
listener(event, window, cx);
|
||||
}
|
||||
});
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: InteractiveElement> InteractiveElementExt for Stateful<E> {}
|
||||
@@ -1,39 +0,0 @@
|
||||
use gpui::{Context, FocusHandle, Window};
|
||||
|
||||
/// A trait for views that can cycle focus between its children.
|
||||
///
|
||||
/// This will provide a default implementation for the `cycle_focus` method that will cycle focus.
|
||||
///
|
||||
/// You should implement the `cycle_focus_handles` method to return a list of focus handles that
|
||||
/// should be cycled, and the cycle will follow the order of the list.
|
||||
pub trait FocusableCycle {
|
||||
/// Returns a list of focus handles that should be cycled.
|
||||
fn cycle_focus_handles(&self, window: &mut Window, cx: &mut Context<Self>) -> Vec<FocusHandle>
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
/// Cycles focus between the focus handles returned by `cycle_focus_handles`.
|
||||
/// If `is_next` is `true`, it will cycle to the next focus handle, otherwise it will cycle to prev.
|
||||
fn cycle_focus(&self, is_next: bool, window: &mut Window, cx: &mut Context<Self>)
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let focused_handle = window.focused(cx);
|
||||
let handles = self.cycle_focus_handles(window, cx);
|
||||
let handles = if is_next {
|
||||
handles
|
||||
} else {
|
||||
handles.into_iter().rev().collect()
|
||||
};
|
||||
|
||||
let fallback_handle = handles[0].clone();
|
||||
let target_focus_handle = handles
|
||||
.into_iter()
|
||||
.skip_while(|handle| Some(handle) != focused_handle.as_ref())
|
||||
.nth(1)
|
||||
.unwrap_or(fallback_handle);
|
||||
|
||||
target_focus_handle.focus(window, cx);
|
||||
cx.stop_propagation();
|
||||
}
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, relative, AnyElement, App, ElementId, InteractiveElement as _, IntoElement, ParentElement,
|
||||
RenderOnce, StyleRefinement, Styled, Window,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use crate::{v_flex, StyledExt as _};
|
||||
|
||||
/// The variant of the GroupBox.
|
||||
#[derive(Debug, Clone, Default, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum GroupBoxVariant {
|
||||
#[default]
|
||||
Normal,
|
||||
Fill,
|
||||
}
|
||||
|
||||
/// Trait to add GroupBox variant methods to elements.
|
||||
pub trait GroupBoxVariants: Sized {
|
||||
/// Set the variant of the [`GroupBox`].
|
||||
#[must_use]
|
||||
fn with_variant(self, variant: GroupBoxVariant) -> Self;
|
||||
|
||||
/// Set to use [`GroupBoxVariant::Normal`] to GroupBox.
|
||||
#[must_use]
|
||||
fn normal(self) -> Self {
|
||||
self.with_variant(GroupBoxVariant::Normal)
|
||||
}
|
||||
|
||||
/// Set to use [`GroupBoxVariant::Fill`] to GroupBox.
|
||||
#[must_use]
|
||||
fn fill(self) -> Self {
|
||||
self.with_variant(GroupBoxVariant::Fill)
|
||||
}
|
||||
}
|
||||
|
||||
impl GroupBoxVariant {
|
||||
/// Convert the GroupBoxVariant to a string.
|
||||
pub const fn as_str(&self) -> &str {
|
||||
match self {
|
||||
GroupBoxVariant::Normal => "normal",
|
||||
GroupBoxVariant::Fill => "fill",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for GroupBoxVariant {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"fill" => Ok(GroupBoxVariant::Fill),
|
||||
_ => Ok(GroupBoxVariant::Normal),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// GroupBox is a styled container element that with
|
||||
/// an optional title to groups related content together.
|
||||
#[derive(IntoElement)]
|
||||
pub struct GroupBox {
|
||||
id: Option<ElementId>,
|
||||
variant: GroupBoxVariant,
|
||||
style: StyleRefinement,
|
||||
title_style: StyleRefinement,
|
||||
title: Option<AnyElement>,
|
||||
content_style: StyleRefinement,
|
||||
children: SmallVec<[AnyElement; 1]>,
|
||||
}
|
||||
|
||||
impl GroupBox {
|
||||
/// Create a new GroupBox.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
id: None,
|
||||
variant: GroupBoxVariant::default(),
|
||||
style: StyleRefinement::default(),
|
||||
title_style: StyleRefinement::default(),
|
||||
content_style: StyleRefinement::default(),
|
||||
title: None,
|
||||
children: SmallVec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the id of the group box, default is None.
|
||||
#[must_use]
|
||||
pub fn id(mut self, id: impl Into<ElementId>) -> Self {
|
||||
self.id = Some(id.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the title of the group box, default is None.
|
||||
#[must_use]
|
||||
pub fn title(mut self, title: impl IntoElement) -> Self {
|
||||
self.title = Some(title.into_any_element());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the style of the title of the group box to override the default style, default is None.
|
||||
#[must_use]
|
||||
pub fn title_style(mut self, style: StyleRefinement) -> Self {
|
||||
self.title_style = style;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the style of the content of the group box to override the default style, default is None.
|
||||
#[must_use]
|
||||
pub fn content_style(mut self, style: StyleRefinement) -> Self {
|
||||
self.content_style = style;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for GroupBox {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for GroupBox {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements);
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for GroupBox {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
&mut self.style
|
||||
}
|
||||
}
|
||||
|
||||
impl GroupBoxVariants for GroupBox {
|
||||
fn with_variant(mut self, variant: GroupBoxVariant) -> Self {
|
||||
self.variant = variant;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for GroupBox {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let (bg, has_paddings) = match self.variant {
|
||||
GroupBoxVariant::Normal => (None, false),
|
||||
GroupBoxVariant::Fill => (Some(cx.theme().surface_background), true),
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.id(self.id.unwrap_or("group-box".into()))
|
||||
.w_full()
|
||||
.when(has_paddings, |this| this.gap_3())
|
||||
.when(!has_paddings, |this| this.gap_4())
|
||||
.refine_style(&self.style)
|
||||
.when_some(self.title, |this, title| {
|
||||
this.child(
|
||||
div()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.line_height(relative(1.))
|
||||
.refine_style(&self.title_style)
|
||||
.text_sm()
|
||||
.font_semibold()
|
||||
.child(title),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
v_flex()
|
||||
.when_some(bg, |this, bg| this.bg(bg))
|
||||
.text_color(cx.theme().text)
|
||||
.when(has_paddings, |this| this.p_2())
|
||||
.gap_4()
|
||||
.rounded(cx.theme().radius_lg)
|
||||
.refine_style(&self.content_style)
|
||||
.children(self.children),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
use std::fmt::Debug;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
pub trait HistoryItem: Clone + PartialEq {
|
||||
fn version(&self) -> usize;
|
||||
fn set_version(&mut self, version: usize);
|
||||
}
|
||||
|
||||
/// The History is used to keep track of changes to a model and to allow undo and redo operations.
|
||||
///
|
||||
/// This is now used in Input for undo/redo operations. You can also use this in
|
||||
/// your own models to keep track of changes, for example to track the tab
|
||||
/// history for prev/next features.
|
||||
///
|
||||
/// ## Use cases
|
||||
///
|
||||
/// - Undo/redo operations in Input
|
||||
/// - Tracking tab history for prev/next features
|
||||
#[derive(Debug)]
|
||||
pub struct History<I: HistoryItem> {
|
||||
undos: Vec<I>,
|
||||
redos: Vec<I>,
|
||||
last_changed_at: Instant,
|
||||
version: usize,
|
||||
max_undo: usize,
|
||||
group_interval: Option<Duration>,
|
||||
unique: bool,
|
||||
pub ignore: bool,
|
||||
}
|
||||
|
||||
impl<I> History<I>
|
||||
where
|
||||
I: HistoryItem,
|
||||
{
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
undos: Default::default(),
|
||||
redos: Default::default(),
|
||||
ignore: false,
|
||||
last_changed_at: Instant::now(),
|
||||
version: 0,
|
||||
max_undo: 1000,
|
||||
group_interval: None,
|
||||
unique: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the maximum number of undo steps to keep, defaults to 1000.
|
||||
pub fn max_undo(mut self, max_undo: usize) -> Self {
|
||||
self.max_undo = max_undo;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the history to be unique, defaults to false.
|
||||
/// If set to true, the history will only keep unique changes.
|
||||
pub fn unique(mut self) -> Self {
|
||||
self.unique = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the interval in milliseconds to group changes, defaults to None.
|
||||
pub fn group_interval(mut self, group_interval: Duration) -> Self {
|
||||
self.group_interval = Some(group_interval);
|
||||
self
|
||||
}
|
||||
|
||||
/// Increment the version number if the last change was made more than `GROUP_INTERVAL` milliseconds ago.
|
||||
fn inc_version(&mut self) -> usize {
|
||||
let t = Instant::now();
|
||||
if Some(self.last_changed_at.elapsed()) > self.group_interval {
|
||||
self.version += 1;
|
||||
}
|
||||
|
||||
self.last_changed_at = t;
|
||||
self.version
|
||||
}
|
||||
|
||||
/// Get the current version number.
|
||||
pub fn version(&self) -> usize {
|
||||
self.version
|
||||
}
|
||||
|
||||
pub fn push(&mut self, item: I) {
|
||||
let version = self.inc_version();
|
||||
|
||||
if self.undos.len() >= self.max_undo {
|
||||
self.undos.remove(0);
|
||||
}
|
||||
|
||||
if self.unique {
|
||||
self.undos.retain(|c| *c != item);
|
||||
self.redos.retain(|c| *c != item);
|
||||
}
|
||||
|
||||
let mut item = item;
|
||||
item.set_version(version);
|
||||
self.undos.push(item);
|
||||
}
|
||||
|
||||
/// Get the undo stack.
|
||||
pub fn undos(&self) -> &Vec<I> {
|
||||
&self.undos
|
||||
}
|
||||
|
||||
/// Get the redo stack.
|
||||
pub fn redos(&self) -> &Vec<I> {
|
||||
&self.redos
|
||||
}
|
||||
|
||||
/// Clear the undo and redo stacks.
|
||||
pub fn clear(&mut self) {
|
||||
self.undos.clear();
|
||||
self.redos.clear();
|
||||
}
|
||||
|
||||
pub fn undo(&mut self) -> Option<Vec<I>> {
|
||||
if let Some(first_change) = self.undos.pop() {
|
||||
let mut changes = vec![first_change.clone()];
|
||||
// pick the next all changes with the same version
|
||||
while self
|
||||
.undos
|
||||
.iter()
|
||||
.filter(|c| c.version() == first_change.version())
|
||||
.count()
|
||||
> 0
|
||||
{
|
||||
let change = self.undos.pop().unwrap();
|
||||
changes.push(change);
|
||||
}
|
||||
|
||||
self.redos.extend(changes.clone());
|
||||
Some(changes)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn redo(&mut self) -> Option<Vec<I>> {
|
||||
if let Some(first_change) = self.redos.pop() {
|
||||
let mut changes = vec![first_change.clone()];
|
||||
// pick the next all changes with the same version
|
||||
while self
|
||||
.redos
|
||||
.iter()
|
||||
.filter(|c| c.version() == first_change.version())
|
||||
.count()
|
||||
> 0
|
||||
{
|
||||
let change = self.redos.pop().unwrap();
|
||||
changes.push(change);
|
||||
}
|
||||
self.undos.extend(changes.clone());
|
||||
Some(changes)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<I> Default for History<I>
|
||||
where
|
||||
I: HistoryItem,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@@ -1,317 +0,0 @@
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
AnyElement, App, AppContext, Context, Entity, Hsla, IntoElement, Radians, Render, RenderOnce,
|
||||
SharedString, StyleRefinement, Styled, Svg, Transformation, Window, svg,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use crate::{Sizable, Size};
|
||||
|
||||
pub trait IconNamed {
|
||||
/// Returns the embedded path of the icon.
|
||||
fn path(self) -> SharedString;
|
||||
}
|
||||
|
||||
impl<T: IconNamed> From<T> for Icon {
|
||||
fn from(value: T) -> Self {
|
||||
Icon::build(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement, Clone)]
|
||||
pub enum IconName {
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
Boom,
|
||||
Book,
|
||||
ChevronDown,
|
||||
CaretDown,
|
||||
CaretRight,
|
||||
CaretUp,
|
||||
Check,
|
||||
CheckCircle,
|
||||
Close,
|
||||
CloseCircle,
|
||||
CloseCircleFill,
|
||||
Copy,
|
||||
Device,
|
||||
Door,
|
||||
Ellipsis,
|
||||
Emoji,
|
||||
Eye,
|
||||
Input,
|
||||
Info,
|
||||
Invite,
|
||||
Inbox,
|
||||
InboxFill,
|
||||
Link,
|
||||
Loader,
|
||||
Moon,
|
||||
Plus,
|
||||
PlusCircle,
|
||||
Profile,
|
||||
Reset,
|
||||
Relay,
|
||||
Reply,
|
||||
Refresh,
|
||||
Scan,
|
||||
Search,
|
||||
Settings,
|
||||
Settings2,
|
||||
Sun,
|
||||
Ship,
|
||||
Shield,
|
||||
Group,
|
||||
UserKey,
|
||||
Upload,
|
||||
Usb,
|
||||
PanelLeft,
|
||||
PanelLeftOpen,
|
||||
PanelRight,
|
||||
PanelRightOpen,
|
||||
PanelBottom,
|
||||
PanelBottomOpen,
|
||||
PaperPlaneFill,
|
||||
Warning,
|
||||
WindowClose,
|
||||
WindowMaximize,
|
||||
WindowMinimize,
|
||||
WindowRestore,
|
||||
Fistbump,
|
||||
FistbumpFill,
|
||||
Zoom,
|
||||
}
|
||||
|
||||
impl IconName {
|
||||
/// Return the icon as a Entity<Icon>
|
||||
pub fn view(self, cx: &mut App) -> Entity<Icon> {
|
||||
Icon::build(self).view(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl IconNamed for IconName {
|
||||
fn path(self) -> SharedString {
|
||||
match self {
|
||||
Self::ArrowLeft => "icons/arrow-left.svg",
|
||||
Self::ArrowRight => "icons/arrow-right.svg",
|
||||
Self::Boom => "icons/boom.svg",
|
||||
Self::Book => "icons/book.svg",
|
||||
Self::ChevronDown => "icons/chevron-down.svg",
|
||||
Self::CaretDown => "icons/caret-down.svg",
|
||||
Self::CaretRight => "icons/caret-right.svg",
|
||||
Self::CaretUp => "icons/caret-up.svg",
|
||||
Self::Check => "icons/check.svg",
|
||||
Self::CheckCircle => "icons/check-circle.svg",
|
||||
Self::Close => "icons/close.svg",
|
||||
Self::CloseCircle => "icons/close-circle.svg",
|
||||
Self::CloseCircleFill => "icons/close-circle-fill.svg",
|
||||
Self::Copy => "icons/copy.svg",
|
||||
Self::Device => "icons/device.svg",
|
||||
Self::Door => "icons/door.svg",
|
||||
Self::Ellipsis => "icons/ellipsis.svg",
|
||||
Self::Emoji => "icons/emoji.svg",
|
||||
Self::Eye => "icons/eye.svg",
|
||||
Self::Input => "icons/input.svg",
|
||||
Self::Info => "icons/info.svg",
|
||||
Self::Invite => "icons/invite.svg",
|
||||
Self::Inbox => "icons/inbox.svg",
|
||||
Self::InboxFill => "icons/inbox-fill.svg",
|
||||
Self::Link => "icons/link.svg",
|
||||
Self::Loader => "icons/loader.svg",
|
||||
Self::Moon => "icons/moon.svg",
|
||||
Self::Plus => "icons/plus.svg",
|
||||
Self::PlusCircle => "icons/plus-circle.svg",
|
||||
Self::Profile => "icons/profile.svg",
|
||||
Self::Reset => "icons/reset.svg",
|
||||
Self::Relay => "icons/relay.svg",
|
||||
Self::Reply => "icons/reply.svg",
|
||||
Self::Refresh => "icons/refresh.svg",
|
||||
Self::Scan => "icons/scan.svg",
|
||||
Self::Search => "icons/search.svg",
|
||||
Self::Settings => "icons/settings.svg",
|
||||
Self::Settings2 => "icons/settings2.svg",
|
||||
Self::Sun => "icons/sun.svg",
|
||||
Self::Ship => "icons/ship.svg",
|
||||
Self::Shield => "icons/shield.svg",
|
||||
Self::UserKey => "icons/user-key.svg",
|
||||
Self::Upload => "icons/upload.svg",
|
||||
Self::Usb => "icons/usb.svg",
|
||||
Self::Group => "icons/group.svg",
|
||||
Self::PanelLeft => "icons/panel-left.svg",
|
||||
Self::PanelLeftOpen => "icons/panel-left-open.svg",
|
||||
Self::PanelRight => "icons/panel-right.svg",
|
||||
Self::PanelRightOpen => "icons/panel-right-open.svg",
|
||||
Self::PanelBottom => "icons/panel-bottom.svg",
|
||||
Self::PanelBottomOpen => "icons/panel-bottom-open.svg",
|
||||
Self::PaperPlaneFill => "icons/paper-plane-fill.svg",
|
||||
Self::Warning => "icons/warning.svg",
|
||||
Self::WindowClose => "icons/window-close.svg",
|
||||
Self::WindowMaximize => "icons/window-maximize.svg",
|
||||
Self::WindowMinimize => "icons/window-minimize.svg",
|
||||
Self::WindowRestore => "icons/window-restore.svg",
|
||||
Self::Fistbump => "icons/fistbump.svg",
|
||||
Self::FistbumpFill => "icons/fistbump-fill.svg",
|
||||
Self::Zoom => "icons/zoom.svg",
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IconName> for AnyElement {
|
||||
fn from(val: IconName) -> Self {
|
||||
Icon::build(val).into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for IconName {
|
||||
fn render(self, _: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
Icon::build(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct Icon {
|
||||
base: Svg,
|
||||
style: StyleRefinement,
|
||||
path: SharedString,
|
||||
text_color: Option<Hsla>,
|
||||
size: Option<Size>,
|
||||
rotation: Option<Radians>,
|
||||
}
|
||||
|
||||
impl Default for Icon {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
base: svg().flex_none().size_4(),
|
||||
style: StyleRefinement::default(),
|
||||
path: "".into(),
|
||||
text_color: None,
|
||||
size: None,
|
||||
rotation: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for Icon {
|
||||
fn clone(&self) -> Self {
|
||||
let mut this = Self::default().path(self.path.clone());
|
||||
this.style = self.style.clone();
|
||||
this.rotation = self.rotation;
|
||||
this.size = self.size;
|
||||
this.text_color = self.text_color;
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
impl Icon {
|
||||
pub fn new(icon: impl Into<Icon>) -> Self {
|
||||
icon.into()
|
||||
}
|
||||
|
||||
fn build(name: impl IconNamed) -> Self {
|
||||
Self::default().path(name.path())
|
||||
}
|
||||
|
||||
/// Set the icon path of the Assets bundle
|
||||
///
|
||||
/// For example: `icons/foo.svg`
|
||||
pub fn path(mut self, path: impl Into<SharedString>) -> Self {
|
||||
self.path = path.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Create a new view for the icon
|
||||
pub fn view(self, cx: &mut App) -> Entity<Icon> {
|
||||
cx.new(|_| self)
|
||||
}
|
||||
|
||||
pub fn transform(mut self, transformation: gpui::Transformation) -> Self {
|
||||
self.base = self.base.with_transformation(transformation);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn empty() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Rotate the icon by the given angle
|
||||
pub fn rotate(mut self, radians: impl Into<Radians>) -> Self {
|
||||
self.base = self
|
||||
.base
|
||||
.with_transformation(Transformation::rotate(radians));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for Icon {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
&mut self.style
|
||||
}
|
||||
|
||||
fn text_color(mut self, color: impl Into<Hsla>) -> Self {
|
||||
self.text_color = Some(color.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Sizable for Icon {
|
||||
fn with_size(mut self, size: impl Into<Size>) -> Self {
|
||||
self.size = Some(size.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Icon {
|
||||
fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
let text_color = self.text_color.unwrap_or_else(|| window.text_style().color);
|
||||
let text_size = window.text_style().font_size.to_pixels(window.rem_size());
|
||||
let has_base_size = self.style.size.width.is_some() || self.style.size.height.is_some();
|
||||
|
||||
let mut base = self.base;
|
||||
*base.style() = self.style;
|
||||
|
||||
base.flex_shrink_0()
|
||||
.text_color(text_color)
|
||||
.when(!has_base_size, |this| this.size(text_size))
|
||||
.when_some(self.size, |this, size| match size {
|
||||
Size::Size(px) => this.size(px),
|
||||
Size::XSmall => this.size_3(),
|
||||
Size::Small => this.size_4(),
|
||||
Size::Medium => this.size_5(),
|
||||
Size::Large => this.size_6(),
|
||||
})
|
||||
.path(self.path)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Icon> for AnyElement {
|
||||
fn from(val: Icon) -> Self {
|
||||
val.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Icon {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let text_color = self.text_color.unwrap_or_else(|| cx.theme().icon);
|
||||
let text_size = window.text_style().font_size.to_pixels(window.rem_size());
|
||||
let has_base_size = self.style.size.width.is_some() || self.style.size.height.is_some();
|
||||
|
||||
let mut base = svg().flex_none();
|
||||
*base.style() = self.style.clone();
|
||||
|
||||
base.flex_shrink_0()
|
||||
.text_color(text_color)
|
||||
.when(!has_base_size, |this| this.size(text_size))
|
||||
.when_some(self.size, |this, size| match size {
|
||||
Size::Size(px) => this.size(px),
|
||||
Size::XSmall => this.size_3(),
|
||||
Size::Small => this.size_4(),
|
||||
Size::Medium => this.size_5(),
|
||||
Size::Large => this.size_6(),
|
||||
})
|
||||
.path(self.path.clone())
|
||||
.when_some(self.rotation, |this, rotation| {
|
||||
this.with_transformation(Transformation::rotate(rotation))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
use std::fmt::{Debug, Display};
|
||||
|
||||
use gpui::ElementId;
|
||||
|
||||
/// Represents an index path in a list, which consists of a section index,
|
||||
///
|
||||
/// The default values for section, row, and column are all set to 0.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub struct IndexPath {
|
||||
/// The section index.
|
||||
pub section: usize,
|
||||
/// The item index in the section.
|
||||
pub row: usize,
|
||||
/// The column index.
|
||||
pub column: usize,
|
||||
}
|
||||
|
||||
impl From<IndexPath> for ElementId {
|
||||
fn from(path: IndexPath) -> Self {
|
||||
ElementId::Name(format!("index-path({},{},{})", path.section, path.row, path.column).into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for IndexPath {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"IndexPath(section: {}, row: {}, column: {})",
|
||||
self.section, self.row, self.column
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl IndexPath {
|
||||
/// Create a new index path with the specified section and row.
|
||||
///
|
||||
/// The `section` is set to 0 by default.
|
||||
/// The `column` is set to 0 by default.
|
||||
pub fn new(row: usize) -> Self {
|
||||
IndexPath {
|
||||
section: 0,
|
||||
row,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the section for the index path.
|
||||
pub fn section(mut self, section: usize) -> Self {
|
||||
self.section = section;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the row for the index path.
|
||||
pub fn row(mut self, row: usize) -> Self {
|
||||
self.row = row;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the column for the index path.
|
||||
pub fn column(mut self, column: usize) -> Self {
|
||||
self.column = column;
|
||||
self
|
||||
}
|
||||
|
||||
/// Check if the self is equal to the given index path (Same section and row).
|
||||
pub fn eq_row(&self, index: IndexPath) -> bool {
|
||||
self.section == index.section && self.row == index.row
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
div, ease_in_out, percentage, Animation, AnimationExt as _, App, Hsla, IntoElement,
|
||||
ParentElement, RenderOnce, Styled as _, Transformation, Window,
|
||||
};
|
||||
|
||||
use crate::{Icon, IconName, Sizable, Size};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct Indicator {
|
||||
size: Size,
|
||||
icon: Icon,
|
||||
speed: Duration,
|
||||
color: Option<Hsla>,
|
||||
}
|
||||
|
||||
impl Indicator {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
size: Size::Small,
|
||||
speed: Duration::from_secs(1),
|
||||
icon: Icon::new(IconName::Loader),
|
||||
color: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
|
||||
self.icon = icon.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn color(mut self, color: Hsla) -> Self {
|
||||
self.color = Some(color);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Indicator {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Sizable for Indicator {
|
||||
fn with_size(mut self, size: impl Into<Size>) -> Self {
|
||||
self.size = size.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Indicator {
|
||||
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
div().child(
|
||||
self.icon
|
||||
.with_size(self.size)
|
||||
.when_some(self.color, |this, color| this.text_color(color))
|
||||
.with_animation(
|
||||
"circle",
|
||||
Animation::new(self.speed).repeat().with_easing(ease_in_out),
|
||||
|this, delta| this.transform(Transformation::rotate(percentage(delta))),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use gpui::{px, Context, Pixels};
|
||||
|
||||
static INTERVAL: Duration = Duration::from_millis(500);
|
||||
static PAUSE_DELAY: Duration = Duration::from_millis(300);
|
||||
pub(super) const CURSOR_WIDTH: Pixels = px(1.5);
|
||||
|
||||
/// To manage the Input cursor blinking.
|
||||
///
|
||||
/// It will start blinking with a interval of 500ms.
|
||||
/// Every loop will notify the view to update the `visible`, and Input will observe this update to touch repaint.
|
||||
///
|
||||
/// The input painter will check if this in visible state, then it will draw the cursor.
|
||||
pub struct BlinkCursor {
|
||||
visible: bool,
|
||||
paused: bool,
|
||||
epoch: usize,
|
||||
}
|
||||
|
||||
impl BlinkCursor {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
visible: false,
|
||||
paused: false,
|
||||
epoch: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Start the blinking
|
||||
pub fn start(&mut self, cx: &mut Context<Self>) {
|
||||
self.blink(self.epoch, cx);
|
||||
}
|
||||
|
||||
pub fn stop(&mut self, cx: &mut Context<Self>) {
|
||||
self.epoch = 0;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn next_epoch(&mut self) -> usize {
|
||||
self.epoch += 1;
|
||||
self.epoch
|
||||
}
|
||||
|
||||
fn blink(&mut self, epoch: usize, cx: &mut Context<Self>) {
|
||||
if self.paused || epoch != self.epoch {
|
||||
self.visible = true;
|
||||
return;
|
||||
}
|
||||
|
||||
self.visible = !self.visible;
|
||||
cx.notify();
|
||||
|
||||
// Schedule the next blink
|
||||
let epoch = self.next_epoch();
|
||||
cx.spawn(async move |this, cx| {
|
||||
cx.background_executor().timer(INTERVAL).await;
|
||||
|
||||
if let Some(this) = this.upgrade() {
|
||||
this.update(cx, |this, cx| this.blink(epoch, cx));
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn visible(&self) -> bool {
|
||||
// Keep showing the cursor if paused
|
||||
self.paused || self.visible
|
||||
}
|
||||
|
||||
/// Pause the blinking, and delay 500ms to resume the blinking.
|
||||
pub fn pause(&mut self, cx: &mut Context<Self>) {
|
||||
self.paused = true;
|
||||
self.visible = true;
|
||||
cx.notify();
|
||||
|
||||
// delay 500ms to start the blinking
|
||||
let epoch = self.next_epoch();
|
||||
cx.spawn(async move |this, cx| {
|
||||
cx.background_executor().timer(PAUSE_DELAY).await;
|
||||
|
||||
if let Some(this) = this.upgrade() {
|
||||
this.update(cx, |this, cx| {
|
||||
this.paused = false;
|
||||
this.blink(epoch, cx);
|
||||
});
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for BlinkCursor {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
use std::fmt::Debug;
|
||||
|
||||
use crate::history::HistoryItem;
|
||||
use crate::input::cursor::Selection;
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct Change {
|
||||
pub(crate) old_range: Selection,
|
||||
pub(crate) old_text: String,
|
||||
pub(crate) new_range: Selection,
|
||||
pub(crate) new_text: String,
|
||||
version: usize,
|
||||
}
|
||||
|
||||
impl Change {
|
||||
pub fn new(
|
||||
old_range: impl Into<Selection>,
|
||||
old_text: &str,
|
||||
new_range: impl Into<Selection>,
|
||||
new_text: &str,
|
||||
) -> Self {
|
||||
Self {
|
||||
old_range: old_range.into(),
|
||||
old_text: old_text.to_string(),
|
||||
new_range: new_range.into(),
|
||||
new_text: new_text.to_string(),
|
||||
version: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HistoryItem for Change {
|
||||
fn version(&self) -> usize {
|
||||
self.version
|
||||
}
|
||||
|
||||
fn set_version(&mut self, version: usize) {
|
||||
self.version = version;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
use gpui::{App, Styled};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use crate::button::{Button, ButtonVariants};
|
||||
use crate::{Icon, IconName, Sizable};
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn clear_button(cx: &App) -> Button {
|
||||
Button::new("clean")
|
||||
.icon(Icon::new(IconName::CloseCircle))
|
||||
.tooltip("Clear")
|
||||
.small()
|
||||
.transparent()
|
||||
.text_color(cx.theme().text_muted)
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
use std::ops::Range;
|
||||
|
||||
/// A selection in the text, represented by start and end byte indices.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
|
||||
pub struct Selection {
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
}
|
||||
|
||||
impl Selection {
|
||||
pub fn new(start: usize, end: usize) -> Self {
|
||||
Self { start, end }
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.end.saturating_sub(self.start)
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.start == self.end
|
||||
}
|
||||
|
||||
/// Clears the selection, setting start and end to 0.
|
||||
pub fn clear(&mut self) {
|
||||
self.start = 0;
|
||||
self.end = 0;
|
||||
}
|
||||
|
||||
/// Checks if the given offset is within the selection range.
|
||||
pub fn contains(&self, offset: usize) -> bool {
|
||||
offset >= self.start && offset < self.end
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Range<usize>> for Selection {
|
||||
fn from(value: Range<usize>) -> Self {
|
||||
Self::new(value.start, value.end)
|
||||
}
|
||||
}
|
||||
impl From<Selection> for Range<usize> {
|
||||
fn from(value: Selection) -> Self {
|
||||
value.start..value.end
|
||||
}
|
||||
}
|
||||
|
||||
pub type Position = lsp_types::Position;
|
||||
@@ -1,888 +0,0 @@
|
||||
use std::ops::Range;
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, Corners, Element, ElementId, ElementInputHandler, Entity, GlobalElementId, Hitbox,
|
||||
IntoElement, LayoutId, MouseButton, MouseMoveEvent, Path, Pixels, Point, ShapedLine,
|
||||
SharedString, Size, Style, TextAlign, TextRun, UnderlineStyle, Window, fill, point, px,
|
||||
relative, size,
|
||||
};
|
||||
use rope::Rope;
|
||||
use smallvec::SmallVec;
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use super::blink_cursor::CURSOR_WIDTH;
|
||||
use super::rope_ext::RopeExt;
|
||||
use super::state::{InputState, LastLayout};
|
||||
use crate::Root;
|
||||
|
||||
const BOTTOM_MARGIN_ROWS: usize = 3;
|
||||
pub(super) const RIGHT_MARGIN: Pixels = px(10.);
|
||||
pub(super) const LINE_NUMBER_RIGHT_MARGIN: Pixels = px(10.);
|
||||
|
||||
pub(super) struct TextElement {
|
||||
pub(crate) state: Entity<InputState>,
|
||||
placeholder: SharedString,
|
||||
}
|
||||
|
||||
impl TextElement {
|
||||
pub(super) fn new(state: Entity<InputState>) -> Self {
|
||||
Self {
|
||||
state,
|
||||
placeholder: SharedString::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the placeholder text of the input field.
|
||||
pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
|
||||
self.placeholder = placeholder.into();
|
||||
self
|
||||
}
|
||||
|
||||
fn paint_mouse_listeners(&mut self, window: &mut Window, _: &mut App) {
|
||||
window.on_mouse_event({
|
||||
let state = self.state.clone();
|
||||
|
||||
move |event: &MouseMoveEvent, _, window, cx| {
|
||||
if event.pressed_button == Some(MouseButton::Left) {
|
||||
state.update(cx, |state, cx| {
|
||||
state.on_drag_move(event, window, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns the:
|
||||
///
|
||||
/// - cursor bounds
|
||||
/// - scroll offset
|
||||
/// - current row index (No only the visible lines, but all lines)
|
||||
///
|
||||
/// This method also will update for track scroll to cursor.
|
||||
fn layout_cursor(
|
||||
&self,
|
||||
last_layout: &LastLayout,
|
||||
bounds: &mut Bounds<Pixels>,
|
||||
_: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (Option<Bounds<Pixels>>, Point<Pixels>, Option<usize>) {
|
||||
let state = self.state.read(cx);
|
||||
|
||||
let line_height = last_layout.line_height;
|
||||
let visible_range = &last_layout.visible_range;
|
||||
let lines = &last_layout.lines;
|
||||
let text_wrapper = &state.text_wrapper;
|
||||
let line_number_width = last_layout.line_number_width;
|
||||
|
||||
let mut selected_range = state.selected_range;
|
||||
if let Some(ime_marked_range) = &state.ime_marked_range {
|
||||
selected_range = (ime_marked_range.end..ime_marked_range.end).into();
|
||||
}
|
||||
|
||||
let cursor = state.cursor();
|
||||
let mut current_row = None;
|
||||
let mut scroll_offset = state.scroll_handle.offset();
|
||||
let mut cursor_bounds = None;
|
||||
|
||||
// If the input has a fixed height (Otherwise is auto-grow), we need to add a bottom margin to the input.
|
||||
let top_bottom_margin = if state.mode.is_auto_grow() {
|
||||
#[allow(clippy::if_same_then_else)]
|
||||
line_height
|
||||
} else if visible_range.len() < BOTTOM_MARGIN_ROWS * 8 {
|
||||
line_height
|
||||
} else {
|
||||
BOTTOM_MARGIN_ROWS * line_height
|
||||
};
|
||||
|
||||
// The cursor corresponds to the current cursor position in the text no only the line.
|
||||
let mut cursor_pos = None;
|
||||
let mut cursor_start = None;
|
||||
let mut cursor_end = None;
|
||||
|
||||
let mut prev_lines_offset = 0;
|
||||
let mut offset_y = px(0.);
|
||||
|
||||
for (ix, wrap_line) in text_wrapper.lines.iter().enumerate() {
|
||||
let row = ix;
|
||||
let line_origin = point(px(0.), offset_y);
|
||||
|
||||
// break loop if all cursor positions are found
|
||||
if cursor_pos.is_some() && cursor_start.is_some() && cursor_end.is_some() {
|
||||
break;
|
||||
}
|
||||
|
||||
let in_visible_range = ix >= visible_range.start;
|
||||
if let Some(line) = in_visible_range
|
||||
.then(|| lines.get(ix.saturating_sub(visible_range.start)))
|
||||
.flatten()
|
||||
{
|
||||
// If in visible range lines
|
||||
if cursor_pos.is_none() {
|
||||
let offset = cursor.saturating_sub(prev_lines_offset);
|
||||
if let Some(pos) = line.position_for_index(offset, line_height) {
|
||||
current_row = Some(row);
|
||||
cursor_pos = Some(line_origin + pos);
|
||||
}
|
||||
}
|
||||
if cursor_start.is_none() {
|
||||
let offset = selected_range.start.saturating_sub(prev_lines_offset);
|
||||
if let Some(pos) = line.position_for_index(offset, line_height) {
|
||||
cursor_start = Some(line_origin + pos);
|
||||
}
|
||||
}
|
||||
if cursor_end.is_none() {
|
||||
let offset = selected_range.end.saturating_sub(prev_lines_offset);
|
||||
if let Some(pos) = line.position_for_index(offset, line_height) {
|
||||
cursor_end = Some(line_origin + pos);
|
||||
}
|
||||
}
|
||||
|
||||
offset_y += line.size(line_height).height;
|
||||
// +1 for the last `\n`
|
||||
prev_lines_offset += line.len() + 1;
|
||||
} else {
|
||||
// If not in the visible range.
|
||||
|
||||
// Just increase the offset_y and prev_lines_offset.
|
||||
// This will let the scroll_offset to track the cursor position correctly.
|
||||
if prev_lines_offset >= cursor && cursor_pos.is_none() {
|
||||
current_row = Some(row);
|
||||
cursor_pos = Some(line_origin);
|
||||
}
|
||||
if prev_lines_offset >= selected_range.start && cursor_start.is_none() {
|
||||
cursor_start = Some(line_origin);
|
||||
}
|
||||
if prev_lines_offset >= selected_range.end && cursor_end.is_none() {
|
||||
cursor_end = Some(line_origin);
|
||||
}
|
||||
|
||||
offset_y += wrap_line.height(line_height);
|
||||
// +1 for the last `\n`
|
||||
prev_lines_offset += wrap_line.len() + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if let (Some(cursor_pos), Some(cursor_start), Some(cursor_end)) =
|
||||
(cursor_pos, cursor_start, cursor_end)
|
||||
{
|
||||
let selection_changed = state.last_selected_range != Some(selected_range);
|
||||
if selection_changed {
|
||||
scroll_offset.x = if scroll_offset.x + cursor_pos.x
|
||||
> (bounds.size.width - line_number_width - RIGHT_MARGIN)
|
||||
{
|
||||
// cursor is out of right
|
||||
bounds.size.width - line_number_width - RIGHT_MARGIN - cursor_pos.x
|
||||
} else if scroll_offset.x + cursor_pos.x < px(0.) {
|
||||
// cursor is out of left
|
||||
scroll_offset.x - cursor_pos.x
|
||||
} else {
|
||||
scroll_offset.x
|
||||
};
|
||||
|
||||
// If we change the scroll_offset.y, GPUI will render and trigger the next run loop.
|
||||
// So, here we just adjust offset by `line_height` for move smooth.
|
||||
scroll_offset.y =
|
||||
if scroll_offset.y + cursor_pos.y > bounds.size.height - top_bottom_margin {
|
||||
// cursor is out of bottom
|
||||
scroll_offset.y - line_height
|
||||
} else if scroll_offset.y + cursor_pos.y < top_bottom_margin {
|
||||
// cursor is out of top
|
||||
(scroll_offset.y + line_height).min(px(0.))
|
||||
} else {
|
||||
scroll_offset.y
|
||||
};
|
||||
|
||||
if state.selection_reversed {
|
||||
if scroll_offset.x + cursor_start.x < px(0.) {
|
||||
// selection start is out of left
|
||||
scroll_offset.x = -cursor_start.x;
|
||||
}
|
||||
if scroll_offset.y + cursor_start.y < px(0.) {
|
||||
// selection start is out of top
|
||||
scroll_offset.y = -cursor_start.y;
|
||||
}
|
||||
} else {
|
||||
if scroll_offset.x + cursor_end.x <= px(0.) {
|
||||
// selection end is out of left
|
||||
scroll_offset.x = -cursor_end.x;
|
||||
}
|
||||
if scroll_offset.y + cursor_end.y <= px(0.) {
|
||||
// selection end is out of top
|
||||
scroll_offset.y = -cursor_end.y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cursor bounds
|
||||
let cursor_height = line_height;
|
||||
cursor_bounds = Some(Bounds::new(
|
||||
point(
|
||||
bounds.left() + cursor_pos.x + line_number_width + scroll_offset.x,
|
||||
bounds.top() + cursor_pos.y + ((line_height - cursor_height) / 2.),
|
||||
),
|
||||
size(CURSOR_WIDTH, cursor_height),
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(deferred_scroll_offset) = state.deferred_scroll_offset {
|
||||
scroll_offset = deferred_scroll_offset;
|
||||
}
|
||||
|
||||
bounds.origin += scroll_offset;
|
||||
|
||||
(cursor_bounds, scroll_offset, current_row)
|
||||
}
|
||||
|
||||
/// Layout the match range to a Path.
|
||||
pub(crate) fn layout_match_range(
|
||||
range: Range<usize>,
|
||||
last_layout: &LastLayout,
|
||||
bounds: &mut Bounds<Pixels>,
|
||||
) -> Option<Path<Pixels>> {
|
||||
if range.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if range.start < last_layout.visible_range_offset.start
|
||||
|| range.end > last_layout.visible_range_offset.end
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let line_height = last_layout.line_height;
|
||||
let visible_top = last_layout.visible_top;
|
||||
let visible_start_offset = last_layout.visible_range_offset.start;
|
||||
let lines = &last_layout.lines;
|
||||
let line_number_width = last_layout.line_number_width;
|
||||
|
||||
let start_ix = range.start;
|
||||
let end_ix = range.end;
|
||||
|
||||
let mut prev_lines_offset = visible_start_offset;
|
||||
let mut offset_y = visible_top;
|
||||
let mut line_corners = vec![];
|
||||
|
||||
for line in lines.iter() {
|
||||
let line_size = line.size(line_height);
|
||||
let line_wrap_width = line_size.width;
|
||||
|
||||
let line_origin = point(px(0.), offset_y);
|
||||
|
||||
let line_cursor_start =
|
||||
line.position_for_index(start_ix.saturating_sub(prev_lines_offset), line_height);
|
||||
let line_cursor_end =
|
||||
line.position_for_index(end_ix.saturating_sub(prev_lines_offset), line_height);
|
||||
|
||||
if line_cursor_start.is_some() || line_cursor_end.is_some() {
|
||||
let start = line_cursor_start
|
||||
.unwrap_or_else(|| line.position_for_index(0, line_height).unwrap());
|
||||
|
||||
let end = line_cursor_end
|
||||
.unwrap_or_else(|| line.position_for_index(line.len(), line_height).unwrap());
|
||||
|
||||
// Split the selection into multiple items
|
||||
let wrapped_lines =
|
||||
(end.y / line_height).ceil() as usize - (start.y / line_height).ceil() as usize;
|
||||
|
||||
let mut end_x = end.x;
|
||||
if wrapped_lines > 0 {
|
||||
end_x = line_wrap_width;
|
||||
}
|
||||
|
||||
// Ensure at least 6px width for the selection for empty lines.
|
||||
end_x = end_x.max(start.x + px(6.));
|
||||
|
||||
line_corners.push(Corners {
|
||||
top_left: line_origin + point(start.x, start.y),
|
||||
top_right: line_origin + point(end_x, start.y),
|
||||
bottom_left: line_origin + point(start.x, start.y + line_height),
|
||||
bottom_right: line_origin + point(end_x, start.y + line_height),
|
||||
});
|
||||
|
||||
// wrapped lines
|
||||
for i in 1..=wrapped_lines {
|
||||
let start = point(px(0.), start.y + i as f32 * line_height);
|
||||
let mut end = point(end.x, end.y + i as f32 * line_height);
|
||||
if i < wrapped_lines {
|
||||
end.x = line_size.width;
|
||||
}
|
||||
|
||||
line_corners.push(Corners {
|
||||
top_left: line_origin + point(start.x, start.y),
|
||||
top_right: line_origin + point(end.x, start.y),
|
||||
bottom_left: line_origin + point(start.x, start.y + line_height),
|
||||
bottom_right: line_origin + point(end.x, start.y + line_height),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if line_cursor_start.is_some() && line_cursor_end.is_some() {
|
||||
break;
|
||||
}
|
||||
|
||||
offset_y += line_size.height;
|
||||
// +1 for skip the last `\n`
|
||||
prev_lines_offset += line.len() + 1;
|
||||
}
|
||||
|
||||
let mut points = vec![];
|
||||
if line_corners.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Fix corners to make sure the left to right direction
|
||||
for corners in &mut line_corners {
|
||||
if corners.top_left.x > corners.top_right.x {
|
||||
std::mem::swap(&mut corners.top_left, &mut corners.top_right);
|
||||
std::mem::swap(&mut corners.bottom_left, &mut corners.bottom_right);
|
||||
}
|
||||
}
|
||||
|
||||
for corners in &line_corners {
|
||||
points.push(corners.top_right);
|
||||
points.push(corners.bottom_right);
|
||||
points.push(corners.bottom_left);
|
||||
}
|
||||
|
||||
let mut rev_line_corners = line_corners.iter().rev().peekable();
|
||||
while let Some(corners) = rev_line_corners.next() {
|
||||
points.push(corners.top_left);
|
||||
if let Some(next) = rev_line_corners.peek()
|
||||
&& next.top_left.x > corners.top_left.x
|
||||
{
|
||||
points.push(point(next.top_left.x, corners.top_left.y));
|
||||
}
|
||||
}
|
||||
|
||||
// print_points_as_svg_path(&line_corners, &points);
|
||||
|
||||
let path_origin = bounds.origin + point(line_number_width, px(0.));
|
||||
let first_p = *points.first().unwrap();
|
||||
let mut builder = gpui::PathBuilder::fill();
|
||||
builder.move_to(path_origin + first_p);
|
||||
for p in points.iter().skip(1) {
|
||||
builder.line_to(path_origin + *p);
|
||||
}
|
||||
|
||||
builder.build().ok()
|
||||
}
|
||||
|
||||
fn layout_selections(
|
||||
&self,
|
||||
last_layout: &LastLayout,
|
||||
bounds: &mut Bounds<Pixels>,
|
||||
cx: &mut App,
|
||||
) -> Option<Path<Pixels>> {
|
||||
let state = self.state.read(cx);
|
||||
let mut selected_range = state.selected_range;
|
||||
if let Some(ime_marked_range) = &state.ime_marked_range
|
||||
&& !ime_marked_range.is_empty()
|
||||
{
|
||||
selected_range = (ime_marked_range.end..ime_marked_range.end).into();
|
||||
}
|
||||
if selected_range.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (start_ix, end_ix) = if selected_range.start < selected_range.end {
|
||||
(selected_range.start, selected_range.end)
|
||||
} else {
|
||||
(selected_range.end, selected_range.start)
|
||||
};
|
||||
|
||||
let range = start_ix.max(last_layout.visible_range_offset.start)
|
||||
..end_ix.min(last_layout.visible_range_offset.end);
|
||||
|
||||
Self::layout_match_range(range, last_layout, bounds)
|
||||
}
|
||||
|
||||
/// Calculate the visible range of lines in the viewport.
|
||||
///
|
||||
/// Returns
|
||||
///
|
||||
/// - visible_range: The visible range is based on unwrapped lines (Zero based).
|
||||
/// - visible_top: The top position of the first visible line in the scroll viewport.
|
||||
fn calculate_visible_range(
|
||||
&self,
|
||||
state: &InputState,
|
||||
line_height: Pixels,
|
||||
input_height: Pixels,
|
||||
) -> (Range<usize>, Pixels) {
|
||||
// Add extra rows to avoid showing empty space when scroll to bottom.
|
||||
let extra_rows = 1;
|
||||
let mut visible_top = px(0.);
|
||||
if state.mode.is_single_line() {
|
||||
return (0..1, visible_top);
|
||||
}
|
||||
|
||||
let total_lines = state.text_wrapper.len();
|
||||
let scroll_top = if let Some(deferred_scroll_offset) = state.deferred_scroll_offset {
|
||||
deferred_scroll_offset.y
|
||||
} else {
|
||||
state.scroll_handle.offset().y
|
||||
};
|
||||
|
||||
let mut visible_range = 0..total_lines;
|
||||
let mut line_bottom = px(0.);
|
||||
for (ix, line) in state.text_wrapper.lines.iter().enumerate() {
|
||||
let wrapped_height = line.height(line_height);
|
||||
line_bottom += wrapped_height;
|
||||
|
||||
if line_bottom < -scroll_top {
|
||||
visible_top = line_bottom - wrapped_height;
|
||||
visible_range.start = ix;
|
||||
}
|
||||
|
||||
if line_bottom + scroll_top >= input_height {
|
||||
visible_range.end = (ix + extra_rows).min(total_lines);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
(visible_range, visible_top)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct PrepaintState {
|
||||
/// The lines of entire lines.
|
||||
last_layout: LastLayout,
|
||||
/// The lines only contains the visible lines in the viewport, based on `visible_range`.
|
||||
///
|
||||
/// The child is the soft lines.
|
||||
line_numbers: Option<Vec<SmallVec<[ShapedLine; 1]>>>,
|
||||
/// Size of the scrollable area by entire lines.
|
||||
scroll_size: Size<Pixels>,
|
||||
cursor_bounds: Option<Bounds<Pixels>>,
|
||||
cursor_scroll_offset: Point<Pixels>,
|
||||
selection_path: Option<Path<Pixels>>,
|
||||
hover_highlight_path: Option<Path<Pixels>>,
|
||||
search_match_paths: Vec<(Path<Pixels>, bool)>,
|
||||
hover_definition_hitbox: Option<Hitbox>,
|
||||
bounds: Bounds<Pixels>,
|
||||
}
|
||||
|
||||
impl IntoElement for TextElement {
|
||||
type Element = Self;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for TextElement {
|
||||
type PrepaintState = PrepaintState;
|
||||
type RequestLayoutState = ();
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_: Option<&gpui::InspectorElementId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let state = self.state.read(cx);
|
||||
let line_height = window.line_height();
|
||||
|
||||
let mut style = Style::default();
|
||||
style.size.width = relative(1.).into();
|
||||
if state.mode.is_multi_line() {
|
||||
style.flex_grow = 1.0;
|
||||
style.size.height = relative(1.).into();
|
||||
if state.mode.is_auto_grow() {
|
||||
// Auto grow to let height match to rows, but not exceed max rows.
|
||||
let rows = state.mode.max_rows().min(state.mode.rows());
|
||||
style.min_size.height = (rows * line_height).into();
|
||||
} else {
|
||||
style.min_size.height = line_height.into();
|
||||
}
|
||||
} else {
|
||||
// For single-line inputs, the minimum height should be the line height
|
||||
style.size.height = line_height.into();
|
||||
};
|
||||
|
||||
(window.request_layout(style, [], cx), ())
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_: Option<&gpui::InspectorElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
_request_layout: &mut Self::RequestLayoutState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Self::PrepaintState {
|
||||
let state = self.state.read(cx);
|
||||
let line_height = window.line_height();
|
||||
|
||||
let (visible_range, visible_top) =
|
||||
self.calculate_visible_range(state, line_height, bounds.size.height);
|
||||
let visible_start_offset = state.text.line_start_offset(visible_range.start);
|
||||
let visible_end_offset = state
|
||||
.text
|
||||
.line_end_offset(visible_range.end.saturating_sub(1));
|
||||
|
||||
let state = self.state.read(cx);
|
||||
let multi_line = state.mode.is_multi_line();
|
||||
let text = state.text.clone();
|
||||
let is_empty = text.is_empty();
|
||||
let placeholder = self.placeholder.clone();
|
||||
let style = window.text_style();
|
||||
let font_size = style.font_size.to_pixels(window.rem_size());
|
||||
let mut bounds = bounds;
|
||||
|
||||
let (display_text, text_color) = if is_empty {
|
||||
(Rope::from(placeholder.as_str()), cx.theme().text_muted)
|
||||
} else if state.masked {
|
||||
(
|
||||
Rope::from("*".repeat(text.chars_count()).as_str()),
|
||||
cx.theme().text,
|
||||
)
|
||||
} else {
|
||||
(text.clone(), cx.theme().text)
|
||||
};
|
||||
|
||||
let line_number_width = px(0.);
|
||||
|
||||
let run = TextRun {
|
||||
len: display_text.len(),
|
||||
font: style.font(),
|
||||
color: text_color,
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
};
|
||||
|
||||
let marked_run = TextRun {
|
||||
len: 0,
|
||||
font: style.font(),
|
||||
color: text_color,
|
||||
background_color: None,
|
||||
underline: Some(UnderlineStyle {
|
||||
thickness: px(1.),
|
||||
color: Some(text_color),
|
||||
wavy: false,
|
||||
}),
|
||||
strikethrough: None,
|
||||
};
|
||||
|
||||
let runs = if !is_empty {
|
||||
vec![run]
|
||||
} else if let Some(ime_marked_range) = &state.ime_marked_range {
|
||||
// IME marked text
|
||||
vec![
|
||||
TextRun {
|
||||
len: ime_marked_range.start,
|
||||
..run.clone()
|
||||
},
|
||||
TextRun {
|
||||
len: ime_marked_range.end - ime_marked_range.start,
|
||||
underline: marked_run.underline,
|
||||
..run.clone()
|
||||
},
|
||||
TextRun {
|
||||
len: display_text.len() - ime_marked_range.end,
|
||||
..run.clone()
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
.filter(|run| run.len > 0)
|
||||
.collect()
|
||||
} else {
|
||||
vec![run]
|
||||
};
|
||||
|
||||
let wrap_width = if multi_line && state.soft_wrap {
|
||||
Some(bounds.size.width - line_number_width - RIGHT_MARGIN)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// NOTE: Here 50 lines about 150µs
|
||||
// let measure = crate::Measure::new("shape_text");
|
||||
let visible_text = display_text
|
||||
.slice_rows(visible_range.start as u32..visible_range.end as u32)
|
||||
.to_string();
|
||||
|
||||
let lines = window
|
||||
.text_system()
|
||||
.shape_text(visible_text.into(), font_size, &runs, wrap_width, None)
|
||||
.expect("failed to shape text");
|
||||
// measure.end();
|
||||
|
||||
let mut longest_line_width = wrap_width.unwrap_or(px(0.));
|
||||
if state.mode.is_multi_line() && !state.soft_wrap && lines.len() > 1 {
|
||||
let longtest_line: SharedString = state
|
||||
.text
|
||||
.line(state.text.summary().longest_row as usize)
|
||||
.to_string()
|
||||
.into();
|
||||
longest_line_width = window
|
||||
.text_system()
|
||||
.shape_line(
|
||||
longtest_line.clone(),
|
||||
font_size,
|
||||
&[TextRun {
|
||||
len: longtest_line.len(),
|
||||
font: style.font(),
|
||||
color: gpui::black(),
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
}],
|
||||
wrap_width,
|
||||
)
|
||||
.width;
|
||||
}
|
||||
|
||||
let total_wrapped_lines = state.text_wrapper.len();
|
||||
let empty_bottom_height = px(0.);
|
||||
|
||||
let scroll_size = size(
|
||||
if longest_line_width + line_number_width + RIGHT_MARGIN > bounds.size.width {
|
||||
longest_line_width + line_number_width + RIGHT_MARGIN
|
||||
} else {
|
||||
longest_line_width
|
||||
},
|
||||
(total_wrapped_lines as f32 * line_height + empty_bottom_height)
|
||||
.max(bounds.size.height),
|
||||
);
|
||||
|
||||
let mut last_layout = LastLayout {
|
||||
visible_range,
|
||||
visible_top,
|
||||
visible_range_offset: visible_start_offset..visible_end_offset,
|
||||
line_height,
|
||||
wrap_width,
|
||||
line_number_width,
|
||||
lines: Rc::new(lines),
|
||||
cursor_bounds: None,
|
||||
};
|
||||
|
||||
// `position_for_index` for example
|
||||
//
|
||||
// #### text
|
||||
//
|
||||
// Hello 世界,this is GPUI component.
|
||||
// The GPUI Component is a collection of UI components for
|
||||
// GPUI framework, including Button, Input, Checkbox, Radio,
|
||||
// Dropdown, Tab, and more...
|
||||
//
|
||||
// wrap_width: 444px, line_height: 20px
|
||||
//
|
||||
// #### lines[0]
|
||||
//
|
||||
// | index | pos | line |
|
||||
// |-------|------------------|------|
|
||||
// | 5 | (37 px, 0.0) | 0 |
|
||||
// | 38 | (261.7 px, 20.0) | 0 |
|
||||
// | 40 | None | - |
|
||||
//
|
||||
// #### lines[1]
|
||||
//
|
||||
// | index | position | line |
|
||||
// |-------|-----------------------|------|
|
||||
// | 5 | (43.578125 px, 0.0) | 0 |
|
||||
// | 56 | (422.21094 px, 0.0) | 0 |
|
||||
// | 57 | (11.6328125 px, 20.0) | 1 |
|
||||
// | 114 | (429.85938 px, 20.0) | 1 |
|
||||
// | 115 | (11.3125 px, 40.0) | 2 |
|
||||
|
||||
// Calculate the scroll offset to keep the cursor in view
|
||||
|
||||
let (cursor_bounds, cursor_scroll_offset, _) =
|
||||
self.layout_cursor(&last_layout, &mut bounds, window, cx);
|
||||
last_layout.cursor_bounds = cursor_bounds;
|
||||
|
||||
let selection_path = self.layout_selections(&last_layout, &mut bounds, cx);
|
||||
let search_match_paths = vec![];
|
||||
let hover_highlight_path = None;
|
||||
let line_numbers = None;
|
||||
let hover_definition_hitbox = None;
|
||||
|
||||
PrepaintState {
|
||||
bounds,
|
||||
last_layout,
|
||||
scroll_size,
|
||||
line_numbers,
|
||||
cursor_bounds,
|
||||
cursor_scroll_offset,
|
||||
selection_path,
|
||||
search_match_paths,
|
||||
hover_highlight_path,
|
||||
hover_definition_hitbox,
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_: Option<&gpui::InspectorElementId>,
|
||||
input_bounds: Bounds<Pixels>,
|
||||
_request_layout: &mut Self::RequestLayoutState,
|
||||
prepaint: &mut Self::PrepaintState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let focus_handle = self.state.read(cx).focus_handle.clone();
|
||||
let show_cursor = self.state.read(cx).show_cursor(window, cx);
|
||||
let focused = focus_handle.is_focused(window);
|
||||
let bounds = prepaint.bounds;
|
||||
let selected_range = self.state.read(cx).selected_range;
|
||||
|
||||
window.handle_input(
|
||||
&focus_handle,
|
||||
ElementInputHandler::new(bounds, self.state.clone()),
|
||||
cx,
|
||||
);
|
||||
|
||||
// Set Root focused_input when self is focused
|
||||
if focused {
|
||||
let state = self.state.clone();
|
||||
if Root::read(window, cx).focused_input.as_ref() != Some(&state) {
|
||||
Root::update(window, cx, |root, _, cx| {
|
||||
root.focused_input = Some(state);
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// And reset focused_input when next_frame start
|
||||
window.on_next_frame({
|
||||
let state = self.state.clone();
|
||||
move |window, cx| {
|
||||
if !focused && Root::read(window, cx).focused_input.as_ref() == Some(&state) {
|
||||
Root::update(window, cx, |root, _, cx| {
|
||||
root.focused_input = None;
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Paint multi line text
|
||||
let line_height = window.line_height();
|
||||
let origin = bounds.origin;
|
||||
|
||||
let invisible_top_padding = prepaint.last_layout.visible_top;
|
||||
|
||||
let mut mask_offset_y = px(0.);
|
||||
if self.state.read(cx).masked {
|
||||
// Move down offset for vertical centering the *****
|
||||
if cfg!(target_os = "macos") {
|
||||
mask_offset_y = px(3.);
|
||||
} else {
|
||||
mask_offset_y = px(2.5);
|
||||
}
|
||||
}
|
||||
|
||||
// Paint active line
|
||||
let mut offset_y = px(0.);
|
||||
if let Some(line_numbers) = prepaint.line_numbers.as_ref() {
|
||||
offset_y += invisible_top_padding;
|
||||
|
||||
// Each item is the normal lines.
|
||||
for lines in line_numbers.iter() {
|
||||
let height = line_height * lines.len() as f32;
|
||||
offset_y += height;
|
||||
}
|
||||
}
|
||||
|
||||
// Paint selections
|
||||
if window.is_window_active() {
|
||||
let secondary_selection = cx.theme().selection;
|
||||
for (path, is_active) in prepaint.search_match_paths.iter() {
|
||||
window.paint_path(path.clone(), secondary_selection);
|
||||
|
||||
if *is_active {
|
||||
window.paint_path(path.clone(), cx.theme().selection);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(path) = prepaint.selection_path.take() {
|
||||
window.paint_path(path, cx.theme().selection);
|
||||
}
|
||||
|
||||
// Paint hover highlight
|
||||
if let Some(path) = prepaint.hover_highlight_path.take() {
|
||||
window.paint_path(path, secondary_selection);
|
||||
}
|
||||
}
|
||||
|
||||
// Paint text
|
||||
let mut offset_y = mask_offset_y + invisible_top_padding;
|
||||
for line in prepaint.last_layout.lines.iter() {
|
||||
let p = point(
|
||||
origin.x + prepaint.last_layout.line_number_width,
|
||||
origin.y + offset_y,
|
||||
);
|
||||
_ = line.paint(p, line_height, TextAlign::Left, None, window, cx);
|
||||
offset_y += line.size(line_height).height;
|
||||
}
|
||||
|
||||
// Paint blinking cursor
|
||||
if focused
|
||||
&& show_cursor
|
||||
&& let Some(mut cursor_bounds) = prepaint.cursor_bounds.take()
|
||||
{
|
||||
cursor_bounds.origin.y += prepaint.cursor_scroll_offset.y;
|
||||
window.paint_quad(fill(cursor_bounds, cx.theme().cursor));
|
||||
}
|
||||
|
||||
// Paint line numbers
|
||||
let mut offset_y = px(0.);
|
||||
if let Some(line_numbers) = prepaint.line_numbers.as_ref() {
|
||||
offset_y += invisible_top_padding;
|
||||
|
||||
// Paint line number background
|
||||
window.paint_quad(fill(
|
||||
Bounds {
|
||||
origin: input_bounds.origin,
|
||||
size: size(
|
||||
prepaint.last_layout.line_number_width - LINE_NUMBER_RIGHT_MARGIN,
|
||||
input_bounds.size.height,
|
||||
),
|
||||
},
|
||||
cx.theme().background,
|
||||
));
|
||||
|
||||
// Each item is the normal lines.
|
||||
for lines in line_numbers.iter() {
|
||||
let p = point(input_bounds.origin.x, origin.y + offset_y);
|
||||
|
||||
for line in lines {
|
||||
_ = line.paint(p, line_height, TextAlign::Left, None, window, cx);
|
||||
offset_y += line_height;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.state.update(cx, |state, cx| {
|
||||
state.last_layout = Some(prepaint.last_layout.clone());
|
||||
state.last_bounds = Some(bounds);
|
||||
state.last_cursor = Some(state.cursor());
|
||||
state.set_input_bounds(input_bounds, cx);
|
||||
state.last_selected_range = Some(selected_range);
|
||||
state.scroll_size = prepaint.scroll_size;
|
||||
state.update_scroll_offset(Some(prepaint.cursor_scroll_offset), cx);
|
||||
state.deferred_scroll_offset = None;
|
||||
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
if let Some(hitbox) = prepaint.hover_definition_hitbox.as_ref() {
|
||||
window.set_cursor_style(gpui::CursorStyle::PointingHand, hitbox);
|
||||
}
|
||||
|
||||
self.paint_mouse_listeners(window, cx);
|
||||
}
|
||||
}
|
||||
@@ -1,409 +0,0 @@
|
||||
use gpui::SharedString;
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub enum MaskToken {
|
||||
/// 0 Digit, equivalent to `[0]`
|
||||
// Digit0,
|
||||
/// Digit, equivalent to `[0-9]`
|
||||
Digit,
|
||||
/// Letter, equivalent to `[a-zA-Z]`
|
||||
Letter,
|
||||
/// Letter or digit, equivalent to `[a-zA-Z0-9]`
|
||||
LetterOrDigit,
|
||||
/// Separator
|
||||
Sep(char),
|
||||
/// Any character
|
||||
Any,
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
impl MaskToken {
|
||||
/// Check if the token is any character.
|
||||
pub fn is_any(&self) -> bool {
|
||||
matches!(self, MaskToken::Any)
|
||||
}
|
||||
|
||||
/// Check if the token is a match for the given character.
|
||||
///
|
||||
/// The separator is always a match any input character.
|
||||
fn is_match(&self, ch: char) -> bool {
|
||||
match self {
|
||||
MaskToken::Digit => ch.is_ascii_digit(),
|
||||
MaskToken::Letter => ch.is_ascii_alphabetic(),
|
||||
MaskToken::LetterOrDigit => ch.is_ascii_alphanumeric(),
|
||||
MaskToken::Any => true,
|
||||
MaskToken::Sep(c) => *c == ch,
|
||||
}
|
||||
}
|
||||
|
||||
/// Is the token a separator (Can be ignored)
|
||||
fn is_sep(&self) -> bool {
|
||||
matches!(self, MaskToken::Sep(_))
|
||||
}
|
||||
|
||||
/// Check if the token is a number.
|
||||
pub fn is_number(&self) -> bool {
|
||||
matches!(self, MaskToken::Digit)
|
||||
}
|
||||
|
||||
pub fn placeholder(&self) -> char {
|
||||
match self {
|
||||
MaskToken::Sep(c) => *c,
|
||||
_ => '_',
|
||||
}
|
||||
}
|
||||
|
||||
fn mask_char(&self, ch: char) -> char {
|
||||
match self {
|
||||
MaskToken::Digit | MaskToken::LetterOrDigit | MaskToken::Letter => ch,
|
||||
MaskToken::Sep(c) => *c,
|
||||
MaskToken::Any => ch,
|
||||
}
|
||||
}
|
||||
|
||||
fn unmask_char(&self, ch: char) -> Option<char> {
|
||||
match self {
|
||||
MaskToken::Digit => Some(ch),
|
||||
MaskToken::Letter => Some(ch),
|
||||
MaskToken::LetterOrDigit => Some(ch),
|
||||
MaskToken::Any => Some(ch),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub enum MaskPattern {
|
||||
#[default]
|
||||
None,
|
||||
Pattern {
|
||||
pattern: SharedString,
|
||||
tokens: Vec<MaskToken>,
|
||||
},
|
||||
Number {
|
||||
/// Group separator, e.g. "," or " "
|
||||
separator: Option<char>,
|
||||
/// Number of fraction digits, e.g. 2 for 123.45
|
||||
fraction: Option<usize>,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<&str> for MaskPattern {
|
||||
fn from(pattern: &str) -> Self {
|
||||
Self::new(pattern)
|
||||
}
|
||||
}
|
||||
|
||||
impl MaskPattern {
|
||||
/// Create a new mask pattern
|
||||
///
|
||||
/// - `9` - Digit
|
||||
/// - `A` - Letter
|
||||
/// - `#` - Letter or Digit
|
||||
/// - `*` - Any character
|
||||
/// - other characters - Separator
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// - `(999)999-9999` - US phone number: (123)456-7890
|
||||
/// - `99999-9999` - ZIP code: 12345-6789
|
||||
/// - `AAAA-99-####` - Custom pattern: ABCD-12-3AB4
|
||||
/// - `*999*` - Custom pattern: (123) or [123]
|
||||
pub fn new(pattern: &str) -> Self {
|
||||
let tokens = pattern
|
||||
.chars()
|
||||
.map(|ch| match ch {
|
||||
// '0' => MaskToken::Digit0,
|
||||
'9' => MaskToken::Digit,
|
||||
'A' => MaskToken::Letter,
|
||||
'#' => MaskToken::LetterOrDigit,
|
||||
'*' => MaskToken::Any,
|
||||
_ => MaskToken::Sep(ch),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Self::Pattern {
|
||||
pattern: pattern.to_owned().into(),
|
||||
tokens,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
fn tokens(&self) -> Option<&Vec<MaskToken>> {
|
||||
match self {
|
||||
Self::Pattern { tokens, .. } => Some(tokens),
|
||||
Self::Number { .. } => None,
|
||||
Self::None => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new mask pattern with group separator, e.g. "," or " "
|
||||
pub fn number(sep: Option<char>) -> Self {
|
||||
Self::Number {
|
||||
separator: sep,
|
||||
fraction: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn placeholder(&self) -> Option<String> {
|
||||
match self {
|
||||
Self::Pattern { tokens, .. } => {
|
||||
Some(tokens.iter().map(|token| token.placeholder()).collect())
|
||||
}
|
||||
Self::Number { .. } => None,
|
||||
Self::None => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return true if the mask pattern is None or no any pattern.
|
||||
pub fn is_none(&self) -> bool {
|
||||
match self {
|
||||
Self::Pattern { tokens, .. } => tokens.is_empty(),
|
||||
Self::Number { .. } => false,
|
||||
Self::None => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check is the mask text is valid.
|
||||
///
|
||||
/// If the mask pattern is None, always return true.
|
||||
pub fn is_valid(&self, mask_text: &str) -> bool {
|
||||
if self.is_none() {
|
||||
return true;
|
||||
}
|
||||
|
||||
let mut text_index = 0;
|
||||
let mask_text_chars: Vec<char> = mask_text.chars().collect();
|
||||
match self {
|
||||
Self::Pattern { tokens, .. } => {
|
||||
for token in tokens {
|
||||
if text_index >= mask_text_chars.len() {
|
||||
break;
|
||||
}
|
||||
|
||||
let ch = mask_text_chars[text_index];
|
||||
if token.is_match(ch) {
|
||||
text_index += 1;
|
||||
}
|
||||
}
|
||||
text_index == mask_text.len()
|
||||
}
|
||||
Self::Number { separator, .. } => {
|
||||
if mask_text.is_empty() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// check if the text is valid number
|
||||
let mut parts = mask_text.split('.');
|
||||
let int_part = parts.next().unwrap_or("");
|
||||
let frac_part = parts.next();
|
||||
|
||||
if int_part.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let sign_positions: Vec<usize> = int_part
|
||||
.chars()
|
||||
.enumerate()
|
||||
.filter_map(|(i, ch)| match is_sign(&ch) {
|
||||
true => Some(i),
|
||||
false => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
// only one sign is valid
|
||||
// sign is only valid at the beginning of the string
|
||||
if sign_positions.len() > 1 || sign_positions.first() > Some(&0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// check if the integer part is valid
|
||||
if !int_part.chars().enumerate().all(|(i, ch)| {
|
||||
ch.is_ascii_digit() || is_sign(&ch) && i == 0 || Some(ch) == *separator
|
||||
}) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// check if the fraction part is valid
|
||||
if let Some(frac) = frac_part
|
||||
&& !frac
|
||||
.chars()
|
||||
.all(|ch| ch.is_ascii_digit() || Some(ch) == *separator)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
Self::None => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if valid input char at the given position.
|
||||
pub fn is_valid_at(&self, ch: char, pos: usize) -> bool {
|
||||
if self.is_none() {
|
||||
return true;
|
||||
}
|
||||
|
||||
match self {
|
||||
Self::Pattern { tokens, .. } => {
|
||||
if let Some(token) = tokens.get(pos) {
|
||||
if token.is_match(ch) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if token.is_sep() {
|
||||
// If next token is match, it's valid
|
||||
if let Some(next_token) = tokens.get(pos + 1)
|
||||
&& next_token.is_match(ch)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
Self::Number { .. } => true,
|
||||
Self::None => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Format the text according to the mask pattern
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// - pattern: (999)999-999
|
||||
/// - text: 123456789
|
||||
/// - mask_text: (123)456-789
|
||||
pub fn mask(&self, text: &str) -> SharedString {
|
||||
if self.is_none() {
|
||||
return text.to_owned().into();
|
||||
}
|
||||
|
||||
match self {
|
||||
Self::Number {
|
||||
separator,
|
||||
fraction,
|
||||
} => {
|
||||
if let Some(sep) = *separator {
|
||||
// Remove the existing group separator
|
||||
let text = text.replace(sep, "");
|
||||
|
||||
let mut parts = text.split('.');
|
||||
let int_part = parts.next().unwrap_or("");
|
||||
|
||||
// Limit the fraction part to the given range, if not enough, pad with 0
|
||||
let frac_part = parts.next().map(|part| {
|
||||
part.chars()
|
||||
.take(fraction.unwrap_or(usize::MAX))
|
||||
.collect::<String>()
|
||||
});
|
||||
|
||||
// Reverse the integer part for easier grouping
|
||||
let mut chars: Vec<char> = int_part.chars().rev().collect();
|
||||
|
||||
// Removing the sign from formatting to avoid cases such as: -,123
|
||||
let maybe_signed = chars.iter().position(is_sign).map(|pos| chars.remove(pos));
|
||||
|
||||
let mut result = String::new();
|
||||
for (i, ch) in chars.iter().enumerate() {
|
||||
if i > 0 && i % 3 == 0 {
|
||||
result.push(sep);
|
||||
}
|
||||
result.push(*ch);
|
||||
}
|
||||
let int_with_sep: String = result.chars().rev().collect();
|
||||
|
||||
let final_str = if let Some(frac) = frac_part {
|
||||
if fraction == &Some(0) {
|
||||
int_with_sep
|
||||
} else {
|
||||
format!("{int_with_sep}.{frac}")
|
||||
}
|
||||
} else {
|
||||
int_with_sep
|
||||
};
|
||||
|
||||
let final_str = if let Some(sign) = maybe_signed {
|
||||
format!("{sign}{final_str}")
|
||||
} else {
|
||||
final_str
|
||||
};
|
||||
|
||||
return final_str.into();
|
||||
}
|
||||
|
||||
text.to_owned().into()
|
||||
}
|
||||
Self::Pattern { tokens, .. } => {
|
||||
let mut result = String::new();
|
||||
let mut text_index = 0;
|
||||
let text_chars: Vec<char> = text.chars().collect();
|
||||
for (pos, token) in tokens.iter().enumerate() {
|
||||
if text_index >= text_chars.len() {
|
||||
break;
|
||||
}
|
||||
let ch = text_chars[text_index];
|
||||
// Break if expected char is not match
|
||||
if !token.is_sep() && !self.is_valid_at(ch, pos) {
|
||||
break;
|
||||
}
|
||||
let mask_ch = token.mask_char(ch);
|
||||
result.push(mask_ch);
|
||||
if ch == mask_ch {
|
||||
text_index += 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
result.into()
|
||||
}
|
||||
Self::None => text.to_owned().into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract original text from masked text
|
||||
pub fn unmask(&self, mask_text: &str) -> String {
|
||||
match self {
|
||||
Self::Number { separator, .. } => {
|
||||
if let Some(sep) = *separator {
|
||||
let mut result = String::new();
|
||||
for ch in mask_text.chars() {
|
||||
if ch == sep {
|
||||
continue;
|
||||
}
|
||||
result.push(ch);
|
||||
}
|
||||
|
||||
if result.contains('.') {
|
||||
result = result.trim_end_matches('0').to_string();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
mask_text.to_owned()
|
||||
}
|
||||
Self::Pattern { tokens, .. } => {
|
||||
let mut result = String::new();
|
||||
let mask_text_chars: Vec<char> = mask_text.chars().collect();
|
||||
for (text_index, token) in tokens.iter().enumerate() {
|
||||
if text_index >= mask_text_chars.len() {
|
||||
break;
|
||||
}
|
||||
let ch = mask_text_chars[text_index];
|
||||
let unmask_ch = token.unmask_char(ch);
|
||||
if let Some(ch) = unmask_ch {
|
||||
result.push(ch);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
Self::None => mask_text.to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_sign(ch: &char) -> bool {
|
||||
matches!(ch, '+' | '-')
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
mod blink_cursor;
|
||||
mod change;
|
||||
mod cursor;
|
||||
mod element;
|
||||
mod mask_pattern;
|
||||
mod mode;
|
||||
mod rope_ext;
|
||||
mod state;
|
||||
mod text_input;
|
||||
mod text_wrapper;
|
||||
|
||||
pub(crate) mod clear_button;
|
||||
|
||||
pub use state::*;
|
||||
pub use text_input::*;
|
||||
@@ -1,129 +0,0 @@
|
||||
use gpui::SharedString;
|
||||
|
||||
use super::text_wrapper::TextWrapper;
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct TabSize {
|
||||
/// Default is 2
|
||||
pub tab_size: usize,
|
||||
/// Set true to use `\t` as tab indent, default is false
|
||||
pub hard_tabs: bool,
|
||||
}
|
||||
|
||||
impl Default for TabSize {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
tab_size: 2,
|
||||
hard_tabs: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TabSize {
|
||||
pub(super) fn to_string(self) -> SharedString {
|
||||
if self.hard_tabs {
|
||||
"\t".into()
|
||||
} else {
|
||||
" ".repeat(self.tab_size).into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub enum InputMode {
|
||||
#[default]
|
||||
SingleLine,
|
||||
MultiLine {
|
||||
tab: TabSize,
|
||||
rows: usize,
|
||||
},
|
||||
AutoGrow {
|
||||
rows: usize,
|
||||
min_rows: usize,
|
||||
max_rows: usize,
|
||||
},
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
impl InputMode {
|
||||
#[inline]
|
||||
pub(super) fn is_single_line(&self) -> bool {
|
||||
matches!(self, InputMode::SingleLine)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(super) fn is_auto_grow(&self) -> bool {
|
||||
matches!(self, InputMode::AutoGrow { .. })
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(super) fn is_multi_line(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
InputMode::MultiLine { .. } | InputMode::AutoGrow { .. }
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn set_rows(&mut self, new_rows: usize) {
|
||||
match self {
|
||||
InputMode::MultiLine { rows, .. } => {
|
||||
*rows = new_rows;
|
||||
}
|
||||
InputMode::AutoGrow {
|
||||
rows,
|
||||
min_rows,
|
||||
max_rows,
|
||||
} => {
|
||||
*rows = new_rows.clamp(*min_rows, *max_rows);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn update_auto_grow(&mut self, text_wrapper: &TextWrapper) {
|
||||
if self.is_single_line() {
|
||||
return;
|
||||
}
|
||||
|
||||
let wrapped_lines = text_wrapper.len();
|
||||
self.set_rows(wrapped_lines);
|
||||
}
|
||||
|
||||
/// At least 1 row be return.
|
||||
pub(super) fn rows(&self) -> usize {
|
||||
match self {
|
||||
InputMode::MultiLine { rows, .. } => *rows,
|
||||
InputMode::AutoGrow { rows, .. } => *rows,
|
||||
_ => 1,
|
||||
}
|
||||
.max(1)
|
||||
}
|
||||
|
||||
/// At least 1 row be return.
|
||||
#[allow(unused)]
|
||||
pub(super) fn min_rows(&self) -> usize {
|
||||
match self {
|
||||
InputMode::MultiLine { .. } => 1,
|
||||
InputMode::AutoGrow { min_rows, .. } => *min_rows,
|
||||
_ => 1,
|
||||
}
|
||||
.max(1)
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub(super) fn max_rows(&self) -> usize {
|
||||
match self {
|
||||
InputMode::MultiLine { .. } => usize::MAX,
|
||||
InputMode::AutoGrow { max_rows, .. } => *max_rows,
|
||||
_ => 1,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(super) fn tab_size(&self) -> Option<&TabSize> {
|
||||
match self {
|
||||
InputMode::MultiLine { tab, .. } => Some(tab),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,208 +0,0 @@
|
||||
use std::ops::Range;
|
||||
|
||||
use rope::{Point, Rope};
|
||||
|
||||
use super::cursor::Position;
|
||||
|
||||
/// An extension trait for `Rope` to provide additional utility methods.
|
||||
pub trait RopeExt {
|
||||
/// Get the line at the given row (0-based) index, including the `\r` at the end, but not `\n`.
|
||||
///
|
||||
/// Return empty rope if the row (0-based) is out of bounds.
|
||||
fn line(&self, row: usize) -> Rope;
|
||||
|
||||
/// Start offset of the line at the given row (0-based) index.
|
||||
fn line_start_offset(&self, row: usize) -> usize;
|
||||
|
||||
/// Line the end offset (including `\n`) of the line at the given row (0-based) index.
|
||||
///
|
||||
/// Return the end of the rope if the row is out of bounds.
|
||||
fn line_end_offset(&self, row: usize) -> usize;
|
||||
|
||||
/// Return the number of lines in the rope.
|
||||
fn lines_len(&self) -> usize;
|
||||
|
||||
/// Return the lines iterator.
|
||||
///
|
||||
/// Each line is including the `\r` at the end, but not `\n`.
|
||||
fn lines(&self) -> RopeLines;
|
||||
|
||||
/// Check is equal to another rope.
|
||||
fn eq(&self, other: &Rope) -> bool;
|
||||
|
||||
/// Total number of characters in the rope.
|
||||
fn chars_count(&self) -> usize;
|
||||
|
||||
/// Get char at the given offset (byte).
|
||||
///
|
||||
/// If the offset is in the middle of a multi-byte character will panic.
|
||||
///
|
||||
/// If the offset is out of bounds, return None.
|
||||
fn char_at(&self, offset: usize) -> Option<char>;
|
||||
|
||||
/// Get the byte offset from the given line, column [`Position`] (0-based).
|
||||
fn position_to_offset(&self, line_col: &Position) -> usize;
|
||||
|
||||
/// Get the line, column [`Position`] (0-based) from the given byte offset.
|
||||
fn offset_to_position(&self, offset: usize) -> Position;
|
||||
|
||||
/// Get the word byte range at the given offset (byte).
|
||||
#[allow(dead_code)]
|
||||
fn word_range(&self, offset: usize) -> Option<Range<usize>>;
|
||||
|
||||
/// Get word at the given offset (byte).
|
||||
#[allow(dead_code)]
|
||||
fn word_at(&self, offset: usize) -> String;
|
||||
}
|
||||
|
||||
/// An iterator over the lines of a `Rope`.
|
||||
pub struct RopeLines {
|
||||
row: usize,
|
||||
end_row: usize,
|
||||
rope: Rope,
|
||||
}
|
||||
|
||||
impl RopeLines {
|
||||
/// Create a new `RopeLines` iterator.
|
||||
pub fn new(rope: Rope) -> Self {
|
||||
let end_row = rope.lines_len();
|
||||
Self {
|
||||
row: 0,
|
||||
end_row,
|
||||
rope,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for RopeLines {
|
||||
type Item = Rope;
|
||||
|
||||
#[inline]
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.row >= self.end_row {
|
||||
return None;
|
||||
}
|
||||
|
||||
let line = self.rope.line(self.row);
|
||||
self.row += 1;
|
||||
Some(line)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn nth(&mut self, n: usize) -> Option<Self::Item> {
|
||||
self.row = self.row.saturating_add(n);
|
||||
self.next()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
let len = self.end_row - self.row;
|
||||
(len, Some(len))
|
||||
}
|
||||
}
|
||||
|
||||
impl std::iter::ExactSizeIterator for RopeLines {}
|
||||
impl std::iter::FusedIterator for RopeLines {}
|
||||
|
||||
impl RopeExt for Rope {
|
||||
fn line(&self, row: usize) -> Rope {
|
||||
let start = self.line_start_offset(row);
|
||||
let end = start + self.line_len(row as u32) as usize;
|
||||
self.slice(start..end)
|
||||
}
|
||||
|
||||
fn line_start_offset(&self, row: usize) -> usize {
|
||||
let row = row as u32;
|
||||
self.point_to_offset(Point::new(row, 0))
|
||||
}
|
||||
|
||||
fn position_to_offset(&self, pos: &Position) -> usize {
|
||||
let line = self.line(pos.line as usize);
|
||||
self.line_start_offset(pos.line as usize)
|
||||
+ line
|
||||
.chars()
|
||||
.take(pos.character as usize)
|
||||
.map(|c| c.len_utf8())
|
||||
.sum::<usize>()
|
||||
}
|
||||
|
||||
fn offset_to_position(&self, offset: usize) -> Position {
|
||||
let point = self.offset_to_point(offset);
|
||||
let line = self.line(point.row as usize);
|
||||
let column = line.clip_offset(point.column as usize, sum_tree::Bias::Left);
|
||||
let character = line.slice(0..column).chars().count();
|
||||
Position::new(point.row, character as u32)
|
||||
}
|
||||
|
||||
fn line_end_offset(&self, row: usize) -> usize {
|
||||
if row > self.max_point().row as usize {
|
||||
return self.len();
|
||||
}
|
||||
|
||||
self.line_start_offset(row) + self.line_len(row as u32) as usize
|
||||
}
|
||||
|
||||
fn lines_len(&self) -> usize {
|
||||
self.max_point().row as usize + 1
|
||||
}
|
||||
|
||||
fn lines(&self) -> RopeLines {
|
||||
RopeLines::new(self.clone())
|
||||
}
|
||||
|
||||
fn eq(&self, other: &Rope) -> bool {
|
||||
self.summary() == other.summary()
|
||||
}
|
||||
|
||||
fn chars_count(&self) -> usize {
|
||||
self.chars().count()
|
||||
}
|
||||
|
||||
fn char_at(&self, offset: usize) -> Option<char> {
|
||||
if offset > self.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let offset = self.clip_offset(offset, sum_tree::Bias::Left);
|
||||
self.slice(offset..self.len()).chars().next()
|
||||
}
|
||||
|
||||
fn word_range(&self, offset: usize) -> Option<Range<usize>> {
|
||||
if offset >= self.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let offset = self.clip_offset(offset, sum_tree::Bias::Left);
|
||||
|
||||
let mut left = String::new();
|
||||
for c in self.reversed_chars_at(offset) {
|
||||
if c.is_alphanumeric() || c == '_' {
|
||||
left.insert(0, c);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let start = offset.saturating_sub(left.len());
|
||||
|
||||
let right = self
|
||||
.chars_at(offset)
|
||||
.take_while(|c| c.is_alphanumeric() || *c == '_')
|
||||
.collect::<String>();
|
||||
|
||||
let end = offset + right.len();
|
||||
|
||||
if start == end {
|
||||
None
|
||||
} else {
|
||||
Some(start..end)
|
||||
}
|
||||
}
|
||||
|
||||
fn word_at(&self, offset: usize) -> String {
|
||||
if let Some(range) = self.word_range(offset) {
|
||||
self.slice(range).to_string()
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,314 +0,0 @@
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
div, px, relative, AnyElement, App, DefiniteLength, Entity, InteractiveElement as _,
|
||||
IntoElement, MouseButton, ParentElement as _, Rems, RenderOnce, StyleRefinement, Styled,
|
||||
Window,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use super::clear_button::clear_button;
|
||||
use super::state::{InputState, CONTEXT};
|
||||
use crate::button::{Button, ButtonVariants};
|
||||
use crate::indicator::Indicator;
|
||||
use crate::{h_flex, IconName, Sizable, Size, StyleSized, StyledExt};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct TextInput {
|
||||
state: Entity<InputState>,
|
||||
style: StyleRefinement,
|
||||
size: Size,
|
||||
prefix: Option<AnyElement>,
|
||||
suffix: Option<AnyElement>,
|
||||
height: Option<DefiniteLength>,
|
||||
appearance: bool,
|
||||
cleanable: bool,
|
||||
mask_toggle: bool,
|
||||
disabled: bool,
|
||||
bordered: bool,
|
||||
focus_bordered: bool,
|
||||
}
|
||||
|
||||
impl Sizable for TextInput {
|
||||
fn with_size(mut self, size: impl Into<Size>) -> Self {
|
||||
self.size = size.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl TextInput {
|
||||
/// Create a new [`TextInput`] element bind to the [`InputState`].
|
||||
pub fn new(state: &Entity<InputState>) -> Self {
|
||||
Self {
|
||||
state: state.clone(),
|
||||
size: Size::default(),
|
||||
style: StyleRefinement::default(),
|
||||
prefix: None,
|
||||
suffix: None,
|
||||
height: None,
|
||||
appearance: true,
|
||||
cleanable: false,
|
||||
mask_toggle: false,
|
||||
disabled: false,
|
||||
bordered: true,
|
||||
focus_bordered: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prefix(mut self, prefix: impl IntoElement) -> Self {
|
||||
self.prefix = Some(prefix.into_any_element());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn suffix(mut self, suffix: impl IntoElement) -> Self {
|
||||
self.suffix = Some(suffix.into_any_element());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set full height of the input (Multi-line only).
|
||||
pub fn h_full(mut self) -> Self {
|
||||
self.height = Some(relative(1.));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set height of the input (Multi-line only).
|
||||
pub fn h(mut self, height: impl Into<DefiniteLength>) -> Self {
|
||||
self.height = Some(height.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the appearance of the input field, if false the input field will no border, background.
|
||||
pub fn appearance(mut self, appearance: bool) -> Self {
|
||||
self.appearance = appearance;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the bordered for the input, default: true
|
||||
pub fn bordered(mut self, bordered: bool) -> Self {
|
||||
self.bordered = bordered;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set focus border for the input, default is true.
|
||||
pub fn focus_bordered(mut self, bordered: bool) -> Self {
|
||||
self.focus_bordered = bordered;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set true to show the clear button when the input field is not empty.
|
||||
pub fn cleanable(mut self) -> Self {
|
||||
self.cleanable = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set to enable toggle button for password mask state.
|
||||
pub fn mask_toggle(mut self) -> Self {
|
||||
self.mask_toggle = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set to disable the input field.
|
||||
pub fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
|
||||
fn render_toggle_mask_button(state: Entity<InputState>) -> impl IntoElement {
|
||||
Button::new("toggle-mask")
|
||||
.icon(IconName::Eye)
|
||||
.xsmall()
|
||||
.ghost()
|
||||
.on_mouse_down(MouseButton::Left, {
|
||||
let state = state.clone();
|
||||
move |_, window, cx| {
|
||||
state.update(cx, |state, cx| {
|
||||
state.set_masked(false, window, cx);
|
||||
})
|
||||
}
|
||||
})
|
||||
.on_mouse_up(MouseButton::Left, {
|
||||
let state = state.clone();
|
||||
move |_, window, cx| {
|
||||
state.update(cx, |state, cx| {
|
||||
state.set_masked(true, window, cx);
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for TextInput {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
&mut self.style
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for TextInput {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
const LINE_HEIGHT: Rems = Rems(1.25);
|
||||
|
||||
let font = window.text_style().font();
|
||||
let font_size = window.text_style().font_size.to_pixels(window.rem_size());
|
||||
|
||||
self.state.update(cx, |state, cx| {
|
||||
state.text_wrapper.set_font(font, font_size, cx);
|
||||
state.text_wrapper.prepare_if_need(&state.text, cx);
|
||||
state.disabled = self.disabled;
|
||||
});
|
||||
|
||||
let state = self.state.read(cx);
|
||||
let focused = state.focus_handle.is_focused(window) && !state.disabled;
|
||||
|
||||
let gap_x = match self.size {
|
||||
Size::Small => px(4.),
|
||||
Size::Large => px(8.),
|
||||
_ => px(4.),
|
||||
};
|
||||
|
||||
let bg = if state.disabled {
|
||||
cx.theme().surface_background
|
||||
} else {
|
||||
cx.theme().elevated_surface_background
|
||||
};
|
||||
|
||||
let prefix = self.prefix;
|
||||
let suffix = self.suffix;
|
||||
|
||||
let show_clear_button = self.cleanable
|
||||
&& !state.loading
|
||||
&& !state.text.is_empty()
|
||||
&& state.mode.is_single_line();
|
||||
|
||||
let has_suffix = suffix.is_some() || state.loading || self.mask_toggle || show_clear_button;
|
||||
|
||||
div()
|
||||
.id(("input", self.state.entity_id()))
|
||||
.flex()
|
||||
.key_context(CONTEXT)
|
||||
.track_focus(&state.focus_handle)
|
||||
.when(!state.disabled, |this| {
|
||||
this.on_action(window.listener_for(&self.state, InputState::backspace))
|
||||
.on_action(window.listener_for(&self.state, InputState::delete))
|
||||
.on_action(
|
||||
window.listener_for(&self.state, InputState::delete_to_beginning_of_line),
|
||||
)
|
||||
.on_action(window.listener_for(&self.state, InputState::delete_to_end_of_line))
|
||||
.on_action(window.listener_for(&self.state, InputState::delete_previous_word))
|
||||
.on_action(window.listener_for(&self.state, InputState::delete_next_word))
|
||||
.on_action(window.listener_for(&self.state, InputState::enter))
|
||||
.on_action(window.listener_for(&self.state, InputState::escape))
|
||||
.on_action(window.listener_for(&self.state, InputState::paste))
|
||||
.on_action(window.listener_for(&self.state, InputState::cut))
|
||||
.on_action(window.listener_for(&self.state, InputState::undo))
|
||||
.on_action(window.listener_for(&self.state, InputState::redo))
|
||||
.when(state.mode.is_multi_line(), |this| {
|
||||
this.on_action(window.listener_for(&self.state, InputState::indent_inline))
|
||||
.on_action(window.listener_for(&self.state, InputState::outdent_inline))
|
||||
.on_action(window.listener_for(&self.state, InputState::indent_block))
|
||||
.on_action(window.listener_for(&self.state, InputState::outdent_block))
|
||||
.on_action(
|
||||
window.listener_for(&self.state, InputState::shift_to_new_line),
|
||||
)
|
||||
})
|
||||
})
|
||||
.on_action(window.listener_for(&self.state, InputState::left))
|
||||
.on_action(window.listener_for(&self.state, InputState::right))
|
||||
.on_action(window.listener_for(&self.state, InputState::select_left))
|
||||
.on_action(window.listener_for(&self.state, InputState::select_right))
|
||||
.when(state.mode.is_multi_line(), |this| {
|
||||
this.on_action(window.listener_for(&self.state, InputState::up))
|
||||
.on_action(window.listener_for(&self.state, InputState::down))
|
||||
.on_action(window.listener_for(&self.state, InputState::select_up))
|
||||
.on_action(window.listener_for(&self.state, InputState::select_down))
|
||||
.on_action(window.listener_for(&self.state, InputState::page_up))
|
||||
.on_action(window.listener_for(&self.state, InputState::page_down))
|
||||
})
|
||||
.on_action(window.listener_for(&self.state, InputState::select_all))
|
||||
.on_action(window.listener_for(&self.state, InputState::select_to_start_of_line))
|
||||
.on_action(window.listener_for(&self.state, InputState::select_to_end_of_line))
|
||||
.on_action(window.listener_for(&self.state, InputState::select_to_previous_word))
|
||||
.on_action(window.listener_for(&self.state, InputState::select_to_next_word))
|
||||
.on_action(window.listener_for(&self.state, InputState::home))
|
||||
.on_action(window.listener_for(&self.state, InputState::end))
|
||||
.on_action(window.listener_for(&self.state, InputState::move_to_start))
|
||||
.on_action(window.listener_for(&self.state, InputState::move_to_end))
|
||||
.on_action(window.listener_for(&self.state, InputState::move_to_previous_word))
|
||||
.on_action(window.listener_for(&self.state, InputState::move_to_next_word))
|
||||
.on_action(window.listener_for(&self.state, InputState::select_to_start))
|
||||
.on_action(window.listener_for(&self.state, InputState::select_to_end))
|
||||
.on_action(window.listener_for(&self.state, InputState::show_character_palette))
|
||||
.on_action(window.listener_for(&self.state, InputState::copy))
|
||||
.on_key_down(window.listener_for(&self.state, InputState::on_key_down))
|
||||
.on_mouse_down(
|
||||
MouseButton::Left,
|
||||
window.listener_for(&self.state, InputState::on_mouse_down),
|
||||
)
|
||||
.on_mouse_down(
|
||||
MouseButton::Right,
|
||||
window.listener_for(&self.state, InputState::on_mouse_down),
|
||||
)
|
||||
.on_mouse_up(
|
||||
MouseButton::Left,
|
||||
window.listener_for(&self.state, InputState::on_mouse_up),
|
||||
)
|
||||
.on_mouse_up(
|
||||
MouseButton::Right,
|
||||
window.listener_for(&self.state, InputState::on_mouse_up),
|
||||
)
|
||||
.on_scroll_wheel(window.listener_for(&self.state, InputState::on_scroll_wheel))
|
||||
.size_full()
|
||||
.line_height(LINE_HEIGHT)
|
||||
.input_px(self.size)
|
||||
.input_py(self.size)
|
||||
.input_h(self.size)
|
||||
.cursor_text()
|
||||
.text_size(font_size)
|
||||
.items_center()
|
||||
.when(state.mode.is_multi_line(), |this| {
|
||||
this.h_auto()
|
||||
.when_some(self.height, |this, height| this.h(height))
|
||||
})
|
||||
.when(self.appearance, |this| {
|
||||
this.bg(bg)
|
||||
.rounded(cx.theme().radius)
|
||||
.when(self.bordered, |this| {
|
||||
this.border_color(cx.theme().border)
|
||||
.border_1()
|
||||
.when(cx.theme().shadow, |this| this.shadow_xs())
|
||||
.when(focused && self.focus_bordered, |this| {
|
||||
this.border_color(cx.theme().border_focused)
|
||||
})
|
||||
})
|
||||
})
|
||||
.items_center()
|
||||
.gap(gap_x)
|
||||
.refine_style(&self.style)
|
||||
.children(prefix)
|
||||
.child(self.state.clone())
|
||||
.when(has_suffix, |this| {
|
||||
this.pr_2().child(
|
||||
h_flex()
|
||||
.id("suffix")
|
||||
.gap(gap_x)
|
||||
.when(self.appearance, |this| this.bg(bg))
|
||||
.items_center()
|
||||
.when(state.loading, |this| {
|
||||
this.child(Indicator::new().color(cx.theme().text_muted))
|
||||
})
|
||||
.when(self.mask_toggle, |this| {
|
||||
this.child(Self::render_toggle_mask_button(self.state.clone()))
|
||||
})
|
||||
.when(show_clear_button, |this| {
|
||||
this.child(clear_button(cx).on_click({
|
||||
let state = self.state.clone();
|
||||
move |_, window, cx| {
|
||||
state.update(cx, |state, cx| {
|
||||
state.clean(window, cx);
|
||||
})
|
||||
}
|
||||
}))
|
||||
})
|
||||
.children(suffix),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
use std::ops::Range;
|
||||
|
||||
use gpui::{App, Font, LineFragment, Pixels};
|
||||
use rope::Rope;
|
||||
|
||||
use super::rope_ext::RopeExt;
|
||||
|
||||
/// A line with soft wrapped lines info.
|
||||
#[derive(Clone)]
|
||||
pub(super) struct LineItem {
|
||||
/// The original line text.
|
||||
line: Rope,
|
||||
/// The soft wrapped lines relative byte range (0..line.len) of this line (Include first line).
|
||||
///
|
||||
/// FIXME: Here in somecase, the `line_wrapper.wrap_line` has returned different
|
||||
/// like the `window.text_system().shape_text`. So, this value may not equal
|
||||
/// the actual rendered lines.
|
||||
wrapped_lines: Vec<Range<usize>>,
|
||||
}
|
||||
|
||||
impl LineItem {
|
||||
/// Get the bytes length of this line.
|
||||
#[inline]
|
||||
pub(super) fn len(&self) -> usize {
|
||||
self.line.len()
|
||||
}
|
||||
|
||||
/// Get number of soft wrapped lines of this line (include the first line).
|
||||
#[inline]
|
||||
pub(super) fn lines_len(&self) -> usize {
|
||||
self.wrapped_lines.len()
|
||||
}
|
||||
|
||||
/// Get the height of this line item with given line height.
|
||||
pub(super) fn height(&self, line_height: Pixels) -> Pixels {
|
||||
self.lines_len() as f32 * line_height
|
||||
}
|
||||
}
|
||||
|
||||
/// Used to prepare the text with soft wrap to be get lines to displayed in the Editor.
|
||||
///
|
||||
/// After use lines to calculate the scroll size of the Editor.
|
||||
pub(super) struct TextWrapper {
|
||||
text: Rope,
|
||||
|
||||
/// Total wrapped lines (Inlucde the first line), value is start and end index of the line.
|
||||
soft_lines: usize,
|
||||
font: Font,
|
||||
font_size: Pixels,
|
||||
|
||||
/// If is none, it means the text is not wrapped
|
||||
wrap_width: Option<Pixels>,
|
||||
|
||||
/// The lines by split \n
|
||||
pub(super) lines: Vec<LineItem>,
|
||||
|
||||
_initialized: bool,
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
impl TextWrapper {
|
||||
pub(super) fn new(font: Font, font_size: Pixels, wrap_width: Option<Pixels>) -> Self {
|
||||
Self {
|
||||
text: Rope::new(),
|
||||
font,
|
||||
font_size,
|
||||
wrap_width,
|
||||
soft_lines: 0,
|
||||
lines: Vec::new(),
|
||||
_initialized: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(super) fn set_default_text(&mut self, text: &Rope) {
|
||||
self.text = text.clone();
|
||||
}
|
||||
|
||||
/// Get the total number of lines including wrapped lines.
|
||||
#[inline]
|
||||
pub(super) fn len(&self) -> usize {
|
||||
self.soft_lines
|
||||
}
|
||||
|
||||
/// Get the line item by row index.
|
||||
#[inline]
|
||||
pub(super) fn line(&self, row: usize) -> Option<&LineItem> {
|
||||
self.lines.get(row)
|
||||
}
|
||||
|
||||
pub(super) fn set_wrap_width(&mut self, wrap_width: Option<Pixels>, cx: &mut App) {
|
||||
if wrap_width == self.wrap_width {
|
||||
return;
|
||||
}
|
||||
self.wrap_width = wrap_width;
|
||||
self.update_all(&self.text.clone(), true, cx);
|
||||
}
|
||||
|
||||
pub(super) fn set_font(&mut self, font: Font, font_size: Pixels, cx: &mut App) {
|
||||
if self.font.eq(&font) && self.font_size == font_size {
|
||||
return;
|
||||
}
|
||||
self.font = font;
|
||||
self.font_size = font_size;
|
||||
self.update_all(&self.text.clone(), true, cx);
|
||||
}
|
||||
|
||||
pub(super) fn prepare_if_need(&mut self, text: &Rope, cx: &mut App) {
|
||||
if self._initialized {
|
||||
return;
|
||||
}
|
||||
self._initialized = true;
|
||||
self.update_all(text, true, cx);
|
||||
}
|
||||
|
||||
/// Update the text wrapper and recalculate the wrapped lines.
|
||||
///
|
||||
/// If the `text` is the same as the current text, do nothing.
|
||||
///
|
||||
/// - `changed_text`: The text [`Rope`] that has changed.
|
||||
/// - `range`: The `selected_range` before change.
|
||||
/// - `new_text`: The inserted text.
|
||||
/// - `force`: Whether to force the update, if false, the update will be skipped if the text is the same.
|
||||
/// - `cx`: The application context.
|
||||
pub(super) fn update(
|
||||
&mut self,
|
||||
changed_text: &Rope,
|
||||
range: &Range<usize>,
|
||||
new_text: &Rope,
|
||||
force: bool,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let mut line_wrapper = cx
|
||||
.text_system()
|
||||
.line_wrapper(self.font.clone(), self.font_size);
|
||||
self._update(
|
||||
changed_text,
|
||||
range,
|
||||
new_text,
|
||||
force,
|
||||
&mut |line_str, wrap_width| {
|
||||
line_wrapper
|
||||
.wrap_line(&[LineFragment::text(line_str)], wrap_width)
|
||||
.collect()
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn _update<F>(
|
||||
&mut self,
|
||||
changed_text: &Rope,
|
||||
range: &Range<usize>,
|
||||
new_text: &Rope,
|
||||
force: bool,
|
||||
wrap_line: &mut F,
|
||||
) where
|
||||
F: FnMut(&str, Pixels) -> Vec<gpui::Boundary>,
|
||||
{
|
||||
if self.text.eq(changed_text) && !force {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the old changed lines.
|
||||
let start_row = self.text.offset_to_point(range.start).row as usize;
|
||||
let start_row = start_row.min(self.lines.len().saturating_sub(1));
|
||||
let end_row = self.text.offset_to_point(range.end).row as usize;
|
||||
let end_row = end_row.min(self.lines.len().saturating_sub(1));
|
||||
let rows_range = start_row..=end_row;
|
||||
|
||||
// To add the new lines.
|
||||
let new_start_row = changed_text.offset_to_point(range.start).row as usize;
|
||||
let new_start_offset = changed_text.line_start_offset(new_start_row);
|
||||
let new_end_row = changed_text
|
||||
.offset_to_point(range.start + new_text.len())
|
||||
.row as usize;
|
||||
let new_end_offset = changed_text.line_end_offset(new_end_row);
|
||||
let new_range = new_start_offset..new_end_offset;
|
||||
|
||||
let mut new_lines = vec![];
|
||||
|
||||
let wrap_width = self.wrap_width;
|
||||
|
||||
for line in changed_text.slice(new_range).lines() {
|
||||
let line_str = line.to_string();
|
||||
let mut wrapped_lines = vec![];
|
||||
let mut prev_boundary_ix = 0;
|
||||
|
||||
// If wrap_width is Pixels::MAX, skip wrapping to disable word wrap
|
||||
if let Some(wrap_width) = wrap_width {
|
||||
// Here only have wrapped line, if there is no wrap meet, the `line_wraps` result will empty.
|
||||
for boundary in wrap_line(&line_str, wrap_width) {
|
||||
wrapped_lines.push(prev_boundary_ix..boundary.ix);
|
||||
prev_boundary_ix = boundary.ix;
|
||||
}
|
||||
}
|
||||
|
||||
// Reset of the line
|
||||
if !line_str[prev_boundary_ix..].is_empty() || prev_boundary_ix == 0 {
|
||||
wrapped_lines.push(prev_boundary_ix..line.len());
|
||||
}
|
||||
|
||||
new_lines.push(LineItem {
|
||||
line: line.clone(),
|
||||
wrapped_lines,
|
||||
});
|
||||
}
|
||||
|
||||
// dbg!(&new_lines.len());
|
||||
// dbg!(self.lines.len());
|
||||
if self.lines.is_empty() {
|
||||
self.lines = new_lines;
|
||||
} else {
|
||||
self.lines.splice(rows_range, new_lines);
|
||||
}
|
||||
|
||||
// dbg!(self.lines.len());
|
||||
self.text = changed_text.clone();
|
||||
self.soft_lines = self.lines.iter().map(|l| l.lines_len()).sum();
|
||||
}
|
||||
|
||||
/// Update the text wrapper and recalculate the wrapped lines.
|
||||
///
|
||||
/// If the `text` is the same as the current text, do nothing.
|
||||
pub(crate) fn update_all(&mut self, text: &Rope, force: bool, cx: &mut App) {
|
||||
self.update(text, &(0..text.len()), text, force, cx);
|
||||
}
|
||||
}
|
||||
@@ -1,312 +0,0 @@
|
||||
use gpui::{
|
||||
div, relative, Action, AsKeystroke, FocusHandle, IntoElement, KeyContext, Keystroke,
|
||||
ParentElement as _, RenderOnce, StyleRefinement, Styled, Window,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use crate::StyledExt;
|
||||
|
||||
/// A key binding tag
|
||||
#[derive(IntoElement, Clone, Debug)]
|
||||
pub struct Kbd {
|
||||
style: StyleRefinement,
|
||||
stroke: Keystroke,
|
||||
appearance: bool,
|
||||
}
|
||||
|
||||
impl From<Keystroke> for Kbd {
|
||||
fn from(stroke: Keystroke) -> Self {
|
||||
Self {
|
||||
style: StyleRefinement::default(),
|
||||
stroke,
|
||||
appearance: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Kbd {
|
||||
pub fn new(stroke: Keystroke) -> Self {
|
||||
Self {
|
||||
style: StyleRefinement::default(),
|
||||
stroke,
|
||||
appearance: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the appearance of the keybinding.
|
||||
pub fn appearance(mut self, appearance: bool) -> Self {
|
||||
self.appearance = appearance;
|
||||
self
|
||||
}
|
||||
|
||||
/// Return the first keybinding for the given action and context.
|
||||
pub fn binding_for_action(
|
||||
action: &dyn Action,
|
||||
context: Option<&str>,
|
||||
window: &Window,
|
||||
) -> Option<Self> {
|
||||
let key_context = context.and_then(|context| KeyContext::parse(context).ok());
|
||||
let binding = match key_context {
|
||||
Some(context) => {
|
||||
window.highest_precedence_binding_for_action_in_context(action, context)
|
||||
}
|
||||
None => window.highest_precedence_binding_for_action(action),
|
||||
}?;
|
||||
|
||||
binding
|
||||
.keystrokes()
|
||||
.first()
|
||||
.map(|key| Self::new(key.as_keystroke().clone()))
|
||||
}
|
||||
|
||||
/// Return the first keybinding for the given action and focus handle.
|
||||
pub fn binding_for_action_in(
|
||||
action: &dyn Action,
|
||||
focus_handle: &FocusHandle,
|
||||
window: &Window,
|
||||
) -> Option<Self> {
|
||||
let binding = window.highest_precedence_binding_for_action_in(action, focus_handle)?;
|
||||
|
||||
binding
|
||||
.keystrokes()
|
||||
.first()
|
||||
.map(|key| Self::new(key.as_keystroke().clone()))
|
||||
}
|
||||
|
||||
/// Return the Platform specific keybinding string by KeyStroke
|
||||
///
|
||||
/// macOS: https://support.apple.com/en-us/HT201236
|
||||
/// Windows: https://support.microsoft.com/en-us/windows/keyboard-shortcuts-in-windows-dcc61a57-8ff0-cffe-9796-cb9706c75eec
|
||||
pub fn format(key: &Keystroke) -> String {
|
||||
#[cfg(target_os = "macos")]
|
||||
const DIVIDER: &str = "";
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
const DIVIDER: &str = "+";
|
||||
|
||||
let mut parts = vec![];
|
||||
|
||||
// The key map order in macOS is: ⌃⌥⇧⌘
|
||||
// And in Windows is: Ctrl+Alt+Shift+Win
|
||||
|
||||
if key.modifiers.control {
|
||||
#[cfg(target_os = "macos")]
|
||||
parts.push("⌃");
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
parts.push("Ctrl");
|
||||
}
|
||||
|
||||
if key.modifiers.alt {
|
||||
#[cfg(target_os = "macos")]
|
||||
parts.push("⌥");
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
parts.push("Alt");
|
||||
}
|
||||
|
||||
if key.modifiers.shift {
|
||||
#[cfg(target_os = "macos")]
|
||||
parts.push("⇧");
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
parts.push("Shift");
|
||||
}
|
||||
|
||||
if key.modifiers.platform {
|
||||
#[cfg(target_os = "macos")]
|
||||
parts.push("⌘");
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
parts.push("Win");
|
||||
}
|
||||
|
||||
let mut keys = String::new();
|
||||
let key_str = key.key.as_str();
|
||||
match key_str {
|
||||
#[cfg(target_os = "macos")]
|
||||
"ctrl" => keys.push('⌃'),
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
"ctrl" => keys.push_str("Ctrl"),
|
||||
#[cfg(target_os = "macos")]
|
||||
"alt" => keys.push('⌥'),
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
"alt" => keys.push_str("Alt"),
|
||||
#[cfg(target_os = "macos")]
|
||||
"shift" => keys.push('⇧'),
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
"shift" => keys.push_str("Shift"),
|
||||
#[cfg(target_os = "macos")]
|
||||
"cmd" => keys.push('⌘'),
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
"cmd" => keys.push_str("Win"),
|
||||
#[cfg(target_os = "macos")]
|
||||
"space" => keys.push_str("Space"),
|
||||
#[cfg(target_os = "macos")]
|
||||
"backspace" => keys.push('⌫'),
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
"backspace" => keys.push_str("Backspace"),
|
||||
#[cfg(target_os = "macos")]
|
||||
"delete" => keys.push('⌫'),
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
"delete" => keys.push_str("Delete"),
|
||||
#[cfg(target_os = "macos")]
|
||||
"escape" => keys.push('⎋'),
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
"escape" => keys.push_str("Esc"),
|
||||
#[cfg(target_os = "macos")]
|
||||
"enter" => keys.push('⏎'),
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
"enter" => keys.push_str("Enter"),
|
||||
"pagedown" => keys.push_str("Page Down"),
|
||||
"pageup" => keys.push_str("Page Up"),
|
||||
#[cfg(target_os = "macos")]
|
||||
"left" => keys.push('←'),
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
"left" => keys.push_str("Left"),
|
||||
#[cfg(target_os = "macos")]
|
||||
"right" => keys.push('→'),
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
"right" => keys.push_str("Right"),
|
||||
#[cfg(target_os = "macos")]
|
||||
"up" => keys.push('↑'),
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
"up" => keys.push_str("Up"),
|
||||
#[cfg(target_os = "macos")]
|
||||
"down" => keys.push('↓'),
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
"down" => keys.push_str("Down"),
|
||||
_ => {
|
||||
if key_str.len() == 1 {
|
||||
keys.push_str(&key_str.to_uppercase());
|
||||
} else {
|
||||
let mut chars = key_str.chars();
|
||||
if let Some(first_char) = chars.next() {
|
||||
keys.push_str(&format!(
|
||||
"{}{}",
|
||||
first_char.to_uppercase(),
|
||||
chars.collect::<String>()
|
||||
));
|
||||
} else {
|
||||
keys.push_str(key_str);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parts.push(&keys);
|
||||
parts.join(DIVIDER)
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for Kbd {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
&mut self.style
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Kbd {
|
||||
fn render(self, _: &mut gpui::Window, cx: &mut gpui::App) -> impl gpui::IntoElement {
|
||||
if !self.appearance {
|
||||
return Self::format(&self.stroke).into_any_element();
|
||||
}
|
||||
|
||||
div()
|
||||
.border_1()
|
||||
.border_color(cx.theme().border)
|
||||
.text_color(cx.theme().text_muted)
|
||||
.bg(cx.theme().surface_background)
|
||||
.py_0p5()
|
||||
.px_1()
|
||||
.min_w_5()
|
||||
.text_center()
|
||||
.rounded_sm()
|
||||
.line_height(relative(1.))
|
||||
.text_xs()
|
||||
.whitespace_normal()
|
||||
.flex_shrink_0()
|
||||
.refine_style(&self.style)
|
||||
.child(Self::format(&self.stroke))
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn test_format() {
|
||||
use gpui::Keystroke;
|
||||
|
||||
use super::Kbd;
|
||||
|
||||
if cfg!(target_os = "macos") {
|
||||
assert_eq!(Kbd::format(&Keystroke::parse("cmd-a").unwrap()), "⌘A");
|
||||
assert_eq!(Kbd::format(&Keystroke::parse("cmd--").unwrap()), "⌘-");
|
||||
assert_eq!(Kbd::format(&Keystroke::parse("cmd-+").unwrap()), "⌘+");
|
||||
assert_eq!(Kbd::format(&Keystroke::parse("cmd-enter").unwrap()), "⌘⏎");
|
||||
assert_eq!(
|
||||
Kbd::format(&Keystroke::parse("secondary-f12").unwrap()),
|
||||
"⌘F12"
|
||||
);
|
||||
assert_eq!(
|
||||
Kbd::format(&Keystroke::parse("shift-pagedown").unwrap()),
|
||||
"⇧Page Down"
|
||||
);
|
||||
assert_eq!(
|
||||
Kbd::format(&Keystroke::parse("shift-pageup").unwrap()),
|
||||
"⇧Page Up"
|
||||
);
|
||||
assert_eq!(
|
||||
Kbd::format(&Keystroke::parse("shift-space").unwrap()),
|
||||
"⇧Space"
|
||||
);
|
||||
assert_eq!(Kbd::format(&Keystroke::parse("cmd-ctrl-a").unwrap()), "⌃⌘A");
|
||||
assert_eq!(
|
||||
Kbd::format(&Keystroke::parse("cmd-alt-backspace").unwrap()),
|
||||
"⌥⌘⌫"
|
||||
);
|
||||
assert_eq!(
|
||||
Kbd::format(&Keystroke::parse("shift-delete").unwrap()),
|
||||
"⇧⌫"
|
||||
);
|
||||
assert_eq!(
|
||||
Kbd::format(&Keystroke::parse("cmd-ctrl-shift-a").unwrap()),
|
||||
"⌃⇧⌘A"
|
||||
);
|
||||
assert_eq!(
|
||||
Kbd::format(&Keystroke::parse("cmd-ctrl-shift-alt-a").unwrap()),
|
||||
"⌃⌥⇧⌘A"
|
||||
);
|
||||
} else {
|
||||
assert_eq!(Kbd::format(&Keystroke::parse("a").unwrap()), "A");
|
||||
assert_eq!(Kbd::format(&Keystroke::parse("ctrl-a").unwrap()), "Ctrl+A");
|
||||
assert_eq!(
|
||||
Kbd::format(&Keystroke::parse("shift-space").unwrap()),
|
||||
"Shift+Space"
|
||||
);
|
||||
assert_eq!(
|
||||
Kbd::format(&Keystroke::parse("ctrl-alt-a").unwrap()),
|
||||
"Ctrl+Alt+A"
|
||||
);
|
||||
assert_eq!(
|
||||
Kbd::format(&Keystroke::parse("ctrl-alt-shift-a").unwrap()),
|
||||
"Ctrl+Alt+Shift+A"
|
||||
);
|
||||
assert_eq!(
|
||||
Kbd::format(&Keystroke::parse("ctrl-alt-shift-win-a").unwrap()),
|
||||
"Ctrl+Alt+Shift+Win+A"
|
||||
);
|
||||
assert_eq!(
|
||||
Kbd::format(&Keystroke::parse("ctrl-shift-backspace").unwrap()),
|
||||
"Ctrl+Shift+Backspace"
|
||||
);
|
||||
assert_eq!(
|
||||
Kbd::format(&Keystroke::parse("alt-delete").unwrap()),
|
||||
"Alt+Delete"
|
||||
);
|
||||
assert_eq!(
|
||||
Kbd::format(&Keystroke::parse("alt-tab").unwrap()),
|
||||
"Alt+Tab"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
pub use element_ext::ElementExt;
|
||||
pub use event::InteractiveElementExt;
|
||||
pub use focusable::FocusableCycle;
|
||||
pub use icon::*;
|
||||
pub use index_path::IndexPath;
|
||||
pub use kbd::*;
|
||||
pub use root::{Root, window_paddings};
|
||||
pub use styled::*;
|
||||
pub use window_ext::*;
|
||||
|
||||
pub use crate::Disableable;
|
||||
|
||||
pub mod actions;
|
||||
pub mod animation;
|
||||
pub mod avatar;
|
||||
pub mod button;
|
||||
pub mod checkbox;
|
||||
pub mod divider;
|
||||
pub mod dock;
|
||||
pub mod group_box;
|
||||
pub mod history;
|
||||
pub mod indicator;
|
||||
pub mod input;
|
||||
pub mod list;
|
||||
pub mod menu;
|
||||
pub mod modal;
|
||||
pub mod notification;
|
||||
pub mod popover;
|
||||
pub mod resizable;
|
||||
pub mod scroll;
|
||||
pub mod skeleton;
|
||||
pub mod switch;
|
||||
pub mod tab;
|
||||
pub mod tooltip;
|
||||
|
||||
mod element_ext;
|
||||
mod event;
|
||||
mod focusable;
|
||||
mod icon;
|
||||
mod index_path;
|
||||
mod kbd;
|
||||
mod root;
|
||||
mod styled;
|
||||
mod window_ext;
|
||||
|
||||
/// Initialize the UI module.
|
||||
///
|
||||
/// This must be called before using any of the UI components.
|
||||
/// You can initialize the UI module at your application's entry point.
|
||||
pub fn init(cx: &mut gpui::App) {
|
||||
input::init(cx);
|
||||
list::init(cx);
|
||||
modal::init(cx);
|
||||
popover::init(cx);
|
||||
menu::init(cx);
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{App, Pixels, Size};
|
||||
|
||||
use crate::IndexPath;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum RowEntry {
|
||||
Entry(IndexPath),
|
||||
SectionHeader(usize),
|
||||
SectionFooter(usize),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub(crate) struct MeasuredEntrySize {
|
||||
pub(crate) item_size: Size<Pixels>,
|
||||
pub(crate) section_header_size: Size<Pixels>,
|
||||
pub(crate) section_footer_size: Size<Pixels>,
|
||||
}
|
||||
|
||||
impl RowEntry {
|
||||
#[inline]
|
||||
#[allow(unused)]
|
||||
pub(crate) fn is_section_header(&self) -> bool {
|
||||
matches!(self, RowEntry::SectionHeader(_))
|
||||
}
|
||||
|
||||
pub(crate) fn eq_index_path(&self, path: &IndexPath) -> bool {
|
||||
match self {
|
||||
RowEntry::Entry(index_path) => index_path == path,
|
||||
RowEntry::SectionHeader(_) | RowEntry::SectionFooter(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub(crate) fn index(&self) -> IndexPath {
|
||||
match self {
|
||||
RowEntry::Entry(index_path) => *index_path,
|
||||
RowEntry::SectionHeader(ix) => IndexPath::default().section(*ix),
|
||||
RowEntry::SectionFooter(ix) => IndexPath::default().section(*ix),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[allow(unused)]
|
||||
pub(crate) fn is_section_footer(&self) -> bool {
|
||||
matches!(self, RowEntry::SectionFooter(_))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn is_entry(&self) -> bool {
|
||||
matches!(self, RowEntry::Entry(_))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[allow(unused)]
|
||||
pub(crate) fn section_ix(&self) -> Option<usize> {
|
||||
match self {
|
||||
RowEntry::SectionHeader(ix) | RowEntry::SectionFooter(ix) => Some(*ix),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub(crate) struct RowsCache {
|
||||
/// Only have section's that have rows.
|
||||
pub(crate) entities: Rc<Vec<RowEntry>>,
|
||||
pub(crate) items_count: usize,
|
||||
/// The sections, the item is number of rows in each section.
|
||||
pub(crate) sections: Rc<Vec<usize>>,
|
||||
pub(crate) entries_sizes: Rc<Vec<Size<Pixels>>>,
|
||||
measured_size: MeasuredEntrySize,
|
||||
}
|
||||
|
||||
impl RowsCache {
|
||||
pub(crate) fn get(&self, flatten_ix: usize) -> Option<RowEntry> {
|
||||
self.entities.get(flatten_ix).cloned()
|
||||
}
|
||||
|
||||
/// Returns the number of flattened rows (Includes header, item, footer).
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
self.entities.len()
|
||||
}
|
||||
|
||||
/// Return the number of items in the cache.
|
||||
pub(crate) fn items_count(&self) -> usize {
|
||||
self.items_count
|
||||
}
|
||||
|
||||
/// Returns the index of the Entry with given path in the flattened rows.
|
||||
pub(crate) fn position_of(&self, path: &IndexPath) -> Option<usize> {
|
||||
self.entities
|
||||
.iter()
|
||||
.position(|p| p.is_entry() && p.eq_index_path(path))
|
||||
}
|
||||
|
||||
/// Return prev row, if the row is the first in the first section, goes to the last row.
|
||||
///
|
||||
/// Empty rows section are skipped.
|
||||
pub(crate) fn prev(&self, path: Option<IndexPath>) -> IndexPath {
|
||||
let path = path.unwrap_or_default();
|
||||
let Some(pos) = self.position_of(&path) else {
|
||||
return self
|
||||
.entities
|
||||
.iter()
|
||||
.rfind(|entry| entry.is_entry())
|
||||
.map(|entry| entry.index())
|
||||
.unwrap_or_default();
|
||||
};
|
||||
|
||||
if let Some(path) = self
|
||||
.entities
|
||||
.iter()
|
||||
.take(pos)
|
||||
.rev()
|
||||
.find(|entry| entry.is_entry())
|
||||
.map(|entry| entry.index())
|
||||
{
|
||||
path
|
||||
} else {
|
||||
self.entities
|
||||
.iter()
|
||||
.rfind(|entry| entry.is_entry())
|
||||
.map(|entry| entry.index())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the next row, if the row is the last in the last section, goes to the first row.
|
||||
///
|
||||
/// Empty rows section are skipped.
|
||||
pub(crate) fn next(&self, path: Option<IndexPath>) -> IndexPath {
|
||||
let Some(mut path) = path else {
|
||||
return IndexPath::default();
|
||||
};
|
||||
|
||||
let Some(pos) = self.position_of(&path) else {
|
||||
return self
|
||||
.entities
|
||||
.iter()
|
||||
.find(|entry| entry.is_entry())
|
||||
.map(|entry| entry.index())
|
||||
.unwrap_or_default();
|
||||
};
|
||||
|
||||
if let Some(next_path) = self
|
||||
.entities
|
||||
.iter()
|
||||
.skip(pos + 1)
|
||||
.find(|entry| entry.is_entry())
|
||||
.map(|entry| entry.index())
|
||||
{
|
||||
path = next_path;
|
||||
} else {
|
||||
path = self
|
||||
.entities
|
||||
.iter()
|
||||
.find(|entry| entry.is_entry())
|
||||
.map(|entry| entry.index())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
path
|
||||
}
|
||||
|
||||
pub(crate) fn prepare_if_needed<F>(
|
||||
&mut self,
|
||||
sections_count: usize,
|
||||
measured_size: MeasuredEntrySize,
|
||||
cx: &App,
|
||||
rows_count_f: F,
|
||||
) where
|
||||
F: Fn(usize, &App) -> usize,
|
||||
{
|
||||
let mut new_sections = vec![];
|
||||
for section_ix in 0..sections_count {
|
||||
new_sections.push(rows_count_f(section_ix, cx));
|
||||
}
|
||||
|
||||
let need_update = new_sections != *self.sections || self.measured_size != measured_size;
|
||||
|
||||
if !need_update {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut entries_sizes = vec![];
|
||||
let mut total_items_count = 0;
|
||||
self.measured_size = measured_size;
|
||||
self.sections = Rc::new(new_sections);
|
||||
self.entities = Rc::new(
|
||||
self.sections
|
||||
.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(section, items_count)| {
|
||||
total_items_count += items_count;
|
||||
let mut children = vec![];
|
||||
if *items_count == 0 {
|
||||
return children;
|
||||
}
|
||||
|
||||
children.push(RowEntry::SectionHeader(section));
|
||||
entries_sizes.push(measured_size.section_header_size);
|
||||
for row in 0..*items_count {
|
||||
children.push(RowEntry::Entry(IndexPath {
|
||||
section,
|
||||
row,
|
||||
..Default::default()
|
||||
}));
|
||||
entries_sizes.push(measured_size.item_size);
|
||||
}
|
||||
children.push(RowEntry::SectionFooter(section));
|
||||
entries_sizes.push(measured_size.section_footer_size);
|
||||
children
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
self.entries_sizes = Rc::new(entries_sizes);
|
||||
self.items_count = total_items_count;
|
||||
}
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
use gpui::{AnyElement, App, Context, IntoElement, ParentElement as _, Styled as _, Task, Window};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use crate::list::loading::Loading;
|
||||
use crate::list::ListState;
|
||||
use crate::{h_flex, Icon, IconName, IndexPath, Selectable};
|
||||
|
||||
/// A delegate for the List.
|
||||
#[allow(unused)]
|
||||
pub trait ListDelegate: Sized + 'static {
|
||||
type Item: Selectable + IntoElement;
|
||||
|
||||
/// When Query Input change, this method will be called.
|
||||
/// You can perform search here.
|
||||
fn perform_search(
|
||||
&mut self,
|
||||
query: &str,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<ListState<Self>>,
|
||||
) -> Task<()> {
|
||||
Task::ready(())
|
||||
}
|
||||
|
||||
/// Return the number of sections in the list, default is 1.
|
||||
///
|
||||
/// Min value is 1.
|
||||
fn sections_count(&self, cx: &App) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
/// Return the number of items in the section at the given index.
|
||||
///
|
||||
/// NOTE: Only the sections with items_count > 0 will be rendered. If the section has 0 items,
|
||||
/// the section header and footer will also be skipped.
|
||||
fn items_count(&self, section: usize, cx: &App) -> usize;
|
||||
|
||||
/// Render the item at the given index.
|
||||
///
|
||||
/// Return None will skip the item.
|
||||
///
|
||||
/// NOTE: Every item should have same height.
|
||||
fn render_item(
|
||||
&mut self,
|
||||
ix: IndexPath,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<ListState<Self>>,
|
||||
) -> Option<Self::Item>;
|
||||
|
||||
/// Render the section header at the given index, default is None.
|
||||
///
|
||||
/// NOTE: Every header should have same height.
|
||||
fn render_section_header(
|
||||
&mut self,
|
||||
section: usize,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<ListState<Self>>,
|
||||
) -> Option<impl IntoElement> {
|
||||
None::<AnyElement>
|
||||
}
|
||||
|
||||
/// Render the section footer at the given index, default is None.
|
||||
///
|
||||
/// NOTE: Every footer should have same height.
|
||||
fn render_section_footer(
|
||||
&mut self,
|
||||
section: usize,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<ListState<Self>>,
|
||||
) -> Option<impl IntoElement> {
|
||||
None::<AnyElement>
|
||||
}
|
||||
|
||||
/// Return a Element to show when list is empty.
|
||||
fn render_empty(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<ListState<Self>>,
|
||||
) -> impl IntoElement {
|
||||
h_flex()
|
||||
.size_full()
|
||||
.justify_center()
|
||||
.text_color(cx.theme().text_muted.opacity(0.6))
|
||||
.child(Icon::new(IconName::Inbox).size_12())
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// Returns Some(AnyElement) to render the initial state of the list.
|
||||
///
|
||||
/// This can be used to show a view for the list before the user has
|
||||
/// interacted with it.
|
||||
///
|
||||
/// For example: The last search results, or the last selected item.
|
||||
///
|
||||
/// Default is None, that means no initial state.
|
||||
fn render_initial(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<ListState<Self>>,
|
||||
) -> Option<AnyElement> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns the loading state to show the loading view.
|
||||
fn loading(&self, cx: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Returns a Element to show when loading, default is built-in Skeleton
|
||||
/// loading view.
|
||||
fn render_loading(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<ListState<Self>>,
|
||||
) -> impl IntoElement {
|
||||
Loading
|
||||
}
|
||||
|
||||
/// Set the selected index, just store the ix, don't confirm.
|
||||
fn set_selected_index(
|
||||
&mut self,
|
||||
ix: Option<IndexPath>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<ListState<Self>>,
|
||||
);
|
||||
|
||||
/// Set the index of the item that has been right clicked.
|
||||
fn set_right_clicked_index(
|
||||
&mut self,
|
||||
ix: Option<IndexPath>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<ListState<Self>>,
|
||||
) {
|
||||
}
|
||||
|
||||
/// Set the confirm and give the selected index,
|
||||
/// this is means user have clicked the item or pressed Enter.
|
||||
///
|
||||
/// This will always to `set_selected_index` before confirm.
|
||||
fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<ListState<Self>>) {
|
||||
}
|
||||
|
||||
/// Cancel the selection, e.g.: Pressed ESC.
|
||||
fn cancel(&mut self, window: &mut Window, cx: &mut Context<ListState<Self>>) {}
|
||||
|
||||
/// Return true to enable load more data when scrolling to the bottom.
|
||||
///
|
||||
/// Default: false
|
||||
fn has_more(&self, cx: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Returns a threshold value (n entities), of course,
|
||||
/// when scrolling to the bottom, the remaining number of rows
|
||||
/// triggers `load_more`.
|
||||
///
|
||||
/// This should smaller than the total number of first load rows.
|
||||
///
|
||||
/// Default: 20 entities (section header, footer and row)
|
||||
fn load_more_threshold(&self) -> usize {
|
||||
20
|
||||
}
|
||||
|
||||
/// Load more data when the table is scrolled to the bottom.
|
||||
///
|
||||
/// This will performed in a background task.
|
||||
///
|
||||
/// This is always called when the table is near the bottom,
|
||||
/// so you must check if there is more data to load or lock
|
||||
/// the loading state.
|
||||
fn load_more(&mut self, window: &mut Window, cx: &mut Context<ListState<Self>>) {}
|
||||
}
|
||||
@@ -1,745 +0,0 @@
|
||||
use std::ops::Range;
|
||||
use std::time::Duration;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
App, AppContext, AvailableSpace, ClickEvent, Context, DefiniteLength, EdgesRefinement, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, KeyBinding, Length,
|
||||
ListSizingBehavior, MouseButton, ParentElement, Render, RenderOnce, ScrollStrategy,
|
||||
SharedString, StatefulInteractiveElement, StyleRefinement, Styled, Subscription, Task,
|
||||
UniformListScrollHandle, Window, div, px, size, uniform_list,
|
||||
};
|
||||
use smol::Timer;
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use crate::actions::{Cancel, Confirm, SelectDown, SelectUp};
|
||||
use crate::input::{InputEvent, InputState, TextInput};
|
||||
use crate::list::ListDelegate;
|
||||
use crate::list::cache::{MeasuredEntrySize, RowEntry, RowsCache};
|
||||
use crate::scroll::{Scrollbar, ScrollbarHandle};
|
||||
use crate::{Icon, IconName, IndexPath, Selectable, Sizable, Size, StyledExt, v_flex};
|
||||
|
||||
pub(crate) fn init(cx: &mut App) {
|
||||
let context: Option<&str> = Some("List");
|
||||
cx.bind_keys([
|
||||
KeyBinding::new("escape", Cancel, context),
|
||||
KeyBinding::new("enter", Confirm { secondary: false }, context),
|
||||
KeyBinding::new("secondary-enter", Confirm { secondary: true }, context),
|
||||
KeyBinding::new("up", SelectUp, context),
|
||||
KeyBinding::new("down", SelectDown, context),
|
||||
]);
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum ListEvent {
|
||||
/// Move to select item.
|
||||
Select(IndexPath),
|
||||
/// Click on item or pressed Enter.
|
||||
Confirm(IndexPath),
|
||||
/// Pressed ESC to deselect the item.
|
||||
Cancel,
|
||||
}
|
||||
|
||||
struct ListOptions {
|
||||
size: Size,
|
||||
scrollbar_visible: bool,
|
||||
search_placeholder: Option<SharedString>,
|
||||
max_height: Option<Length>,
|
||||
paddings: EdgesRefinement<DefiniteLength>,
|
||||
}
|
||||
|
||||
impl Default for ListOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
size: Size::default(),
|
||||
scrollbar_visible: true,
|
||||
max_height: None,
|
||||
search_placeholder: None,
|
||||
paddings: EdgesRefinement::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The state for List.
|
||||
///
|
||||
/// List required all items has the same height.
|
||||
pub struct ListState<D: ListDelegate> {
|
||||
pub(crate) focus_handle: FocusHandle,
|
||||
pub(crate) query_input: Entity<InputState>,
|
||||
options: ListOptions,
|
||||
delegate: D,
|
||||
last_query: Option<String>,
|
||||
scroll_handle: UniformListScrollHandle,
|
||||
rows_cache: RowsCache,
|
||||
selected_index: Option<IndexPath>,
|
||||
item_to_measure_index: IndexPath,
|
||||
deferred_scroll_to_index: Option<(IndexPath, ScrollStrategy)>,
|
||||
mouse_right_clicked_index: Option<IndexPath>,
|
||||
reset_on_cancel: bool,
|
||||
searchable: bool,
|
||||
selectable: bool,
|
||||
_search_task: Task<()>,
|
||||
_load_more_task: Task<()>,
|
||||
_query_input_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl<D> ListState<D>
|
||||
where
|
||||
D: ListDelegate,
|
||||
{
|
||||
pub fn new(delegate: D, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let query_input = cx.new(|cx| InputState::new(window, cx).placeholder("Search..."));
|
||||
let _query_input_subscription =
|
||||
cx.subscribe_in(&query_input, window, Self::on_query_input_event);
|
||||
|
||||
Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
options: ListOptions::default(),
|
||||
delegate,
|
||||
rows_cache: RowsCache::default(),
|
||||
query_input,
|
||||
last_query: None,
|
||||
selected_index: None,
|
||||
selectable: true,
|
||||
searchable: false,
|
||||
item_to_measure_index: IndexPath::default(),
|
||||
deferred_scroll_to_index: None,
|
||||
mouse_right_clicked_index: None,
|
||||
scroll_handle: UniformListScrollHandle::new(),
|
||||
reset_on_cancel: true,
|
||||
_search_task: Task::ready(()),
|
||||
_load_more_task: Task::ready(()),
|
||||
_query_input_subscription,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets whether the list is searchable, default is `false`.
|
||||
///
|
||||
/// When `true`, there will be a search input at the top of the list.
|
||||
pub fn searchable(mut self, searchable: bool) -> Self {
|
||||
self.searchable = searchable;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_searchable(&mut self, searchable: bool, cx: &mut Context<Self>) {
|
||||
self.searchable = searchable;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Sets whether the list is selectable, default is true.
|
||||
pub fn selectable(mut self, selectable: bool) -> Self {
|
||||
self.selectable = selectable;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets whether the list is selectable, default is true.
|
||||
pub fn set_selectable(&mut self, selectable: bool, cx: &mut Context<Self>) {
|
||||
self.selectable = selectable;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn delegate(&self) -> &D {
|
||||
&self.delegate
|
||||
}
|
||||
|
||||
pub fn delegate_mut(&mut self) -> &mut D {
|
||||
&mut self.delegate
|
||||
}
|
||||
|
||||
/// Focus the list, if the list is searchable, focus the search input.
|
||||
pub fn focus(&mut self, window: &mut Window, cx: &mut App) {
|
||||
self.focus_handle(cx).focus(window, cx);
|
||||
}
|
||||
|
||||
/// Return true if either the list or the search input is focused.
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn is_focused(&self, window: &Window, cx: &App) -> bool {
|
||||
self.focus_handle.is_focused(window) || self.query_input.focus_handle(cx).is_focused(window)
|
||||
}
|
||||
|
||||
/// Set the selected index of the list,
|
||||
/// this will also scroll to the selected item.
|
||||
pub(crate) fn _set_selected_index(
|
||||
&mut self,
|
||||
ix: Option<IndexPath>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if !self.selectable {
|
||||
return;
|
||||
}
|
||||
|
||||
self.selected_index = ix;
|
||||
self.delegate.set_selected_index(ix, window, cx);
|
||||
self.scroll_to_selected_item(window, cx);
|
||||
}
|
||||
|
||||
/// Set the selected index of the list,
|
||||
/// this method will not scroll to the selected item.
|
||||
pub fn set_selected_index(
|
||||
&mut self,
|
||||
ix: Option<IndexPath>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.selected_index = ix;
|
||||
self.delegate.set_selected_index(ix, window, cx);
|
||||
}
|
||||
|
||||
pub fn selected_index(&self) -> Option<IndexPath> {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
/// Set the index of the item that has been right clicked.
|
||||
pub fn set_right_clicked_index(
|
||||
&mut self,
|
||||
ix: Option<IndexPath>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.mouse_right_clicked_index = ix;
|
||||
self.delegate.set_right_clicked_index(ix, window, cx);
|
||||
}
|
||||
|
||||
/// Returns the index of the item that has been right clicked.
|
||||
pub fn right_clicked_index(&self) -> Option<IndexPath> {
|
||||
self.mouse_right_clicked_index
|
||||
}
|
||||
|
||||
/// Set a specific list item for measurement.
|
||||
pub fn set_item_to_measure_index(
|
||||
&mut self,
|
||||
ix: IndexPath,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.item_to_measure_index = ix;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Scroll to the item at the given index.
|
||||
pub fn scroll_to_item(
|
||||
&mut self,
|
||||
ix: IndexPath,
|
||||
strategy: ScrollStrategy,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if ix.section == 0 && ix.row == 0 {
|
||||
// If the item is the first item, scroll to the top.
|
||||
let mut offset = self.scroll_handle.offset();
|
||||
offset.y = px(0.);
|
||||
self.scroll_handle.set_offset(offset);
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
self.deferred_scroll_to_index = Some((ix, strategy));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Get scroll handle
|
||||
pub fn scroll_handle(&self) -> &UniformListScrollHandle {
|
||||
&self.scroll_handle
|
||||
}
|
||||
|
||||
pub fn scroll_to_selected_item(&mut self, _: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(ix) = self.selected_index {
|
||||
self.deferred_scroll_to_index = Some((ix, ScrollStrategy::Top));
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn on_query_input_event(
|
||||
&mut self,
|
||||
state: &Entity<InputState>,
|
||||
event: &InputEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
InputEvent::Change => {
|
||||
let text = state.read(cx).value();
|
||||
let text = text.trim().to_string();
|
||||
if Some(&text) == self.last_query.as_ref() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.set_searching(true, window, cx);
|
||||
let search = self.delegate.perform_search(&text, window, cx);
|
||||
|
||||
if self.rows_cache.len() > 0 {
|
||||
self._set_selected_index(Some(IndexPath::default()), window, cx);
|
||||
} else {
|
||||
self._set_selected_index(None, window, cx);
|
||||
}
|
||||
|
||||
self._search_task = cx.spawn_in(window, async move |this, window| {
|
||||
search.await;
|
||||
|
||||
_ = this.update_in(window, |this, _, _| {
|
||||
this.scroll_handle.scroll_to_item(0, ScrollStrategy::Top);
|
||||
this.last_query = Some(text);
|
||||
});
|
||||
|
||||
// Always wait 100ms to avoid flicker
|
||||
Timer::after(Duration::from_millis(100)).await;
|
||||
_ = this.update_in(window, |this, window, cx| {
|
||||
this.set_searching(false, window, cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
InputEvent::PressEnter { secondary } => self.on_action_confirm(
|
||||
&Confirm {
|
||||
secondary: *secondary,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_searching(&mut self, searching: bool, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.query_input
|
||||
.update(cx, |input, cx| input.set_loading(searching, cx));
|
||||
}
|
||||
|
||||
/// Dispatch delegate's `load_more` method when the
|
||||
/// visible range is near the end.
|
||||
fn load_more_if_need(
|
||||
&mut self,
|
||||
entities_count: usize,
|
||||
visible_end: usize,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
// FIXME: Here need void sections items count.
|
||||
|
||||
let threshold = self.delegate.load_more_threshold();
|
||||
// Securely handle subtract logic to prevent attempt
|
||||
// to subtract with overflow
|
||||
if visible_end >= entities_count.saturating_sub(threshold) {
|
||||
if !self.delegate.has_more(cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
self._load_more_task = cx.spawn_in(window, async move |view, cx| {
|
||||
_ = view.update_in(cx, |view, window, cx| {
|
||||
view.delegate.load_more(window, cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn reset_on_cancel(mut self, reset: bool) -> Self {
|
||||
self.reset_on_cancel = reset;
|
||||
self
|
||||
}
|
||||
|
||||
fn on_action_cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.propagate();
|
||||
if self.reset_on_cancel {
|
||||
self._set_selected_index(None, window, cx);
|
||||
}
|
||||
|
||||
self.delegate.cancel(window, cx);
|
||||
cx.emit(ListEvent::Cancel);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn on_action_confirm(
|
||||
&mut self,
|
||||
confirm: &Confirm,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.rows_cache.len() == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(ix) = self.selected_index else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.delegate
|
||||
.set_selected_index(self.selected_index, window, cx);
|
||||
self.delegate.confirm(confirm.secondary, window, cx);
|
||||
cx.emit(ListEvent::Confirm(ix));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn select_item(&mut self, ix: IndexPath, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if !self.selectable {
|
||||
return;
|
||||
}
|
||||
|
||||
self.selected_index = Some(ix);
|
||||
self.delegate.set_selected_index(Some(ix), window, cx);
|
||||
self.scroll_to_selected_item(window, cx);
|
||||
cx.emit(ListEvent::Select(ix));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub(crate) fn on_action_select_prev(
|
||||
&mut self,
|
||||
_: &SelectUp,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.rows_cache.len() == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let prev_ix = self.rows_cache.prev(self.selected_index);
|
||||
self.select_item(prev_ix, window, cx);
|
||||
}
|
||||
|
||||
pub(crate) fn on_action_select_next(
|
||||
&mut self,
|
||||
_: &SelectDown,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.rows_cache.len() == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let next_ix = self.rows_cache.next(self.selected_index);
|
||||
self.select_item(next_ix, window, cx);
|
||||
}
|
||||
|
||||
fn prepare_items_if_needed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let sections_count = self.delegate.sections_count(cx).max(1);
|
||||
let mut measured_size = MeasuredEntrySize::default();
|
||||
|
||||
// Measure the item_height and section header/footer height.
|
||||
let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
|
||||
measured_size.item_size = self
|
||||
.render_list_item(self.item_to_measure_index, window, cx)
|
||||
.into_any_element()
|
||||
.layout_as_root(available_space, window, cx);
|
||||
|
||||
if let Some(mut el) = self
|
||||
.delegate
|
||||
.render_section_header(0, window, cx)
|
||||
.map(|r| r.into_any_element())
|
||||
{
|
||||
measured_size.section_header_size = el.layout_as_root(available_space, window, cx);
|
||||
}
|
||||
if let Some(mut el) = self
|
||||
.delegate
|
||||
.render_section_footer(0, window, cx)
|
||||
.map(|r| r.into_any_element())
|
||||
{
|
||||
measured_size.section_footer_size = el.layout_as_root(available_space, window, cx);
|
||||
}
|
||||
|
||||
self.rows_cache
|
||||
.prepare_if_needed(sections_count, measured_size, cx, |section_ix, cx| {
|
||||
self.delegate.items_count(section_ix, cx)
|
||||
});
|
||||
}
|
||||
|
||||
fn render_list_item(
|
||||
&mut self,
|
||||
ix: IndexPath,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let selectable = self.selectable;
|
||||
let selected = self.selected_index.map(|s| s.eq_row(ix)).unwrap_or(false);
|
||||
let mouse_right_clicked = self
|
||||
.mouse_right_clicked_index
|
||||
.map(|s| s.eq_row(ix))
|
||||
.unwrap_or(false);
|
||||
let id = SharedString::from(format!("list-item-{}", ix));
|
||||
|
||||
div()
|
||||
.id(id)
|
||||
.w_full()
|
||||
.relative()
|
||||
.overflow_hidden()
|
||||
.children(self.delegate.render_item(ix, window, cx).map(|item| {
|
||||
item.selected(selected)
|
||||
.secondary_selected(mouse_right_clicked)
|
||||
}))
|
||||
.when(selectable, |this| {
|
||||
this.on_click(cx.listener(move |this, e: &ClickEvent, window, cx| {
|
||||
this.set_right_clicked_index(None, window, cx);
|
||||
this.selected_index = Some(ix);
|
||||
this.on_action_confirm(
|
||||
&Confirm {
|
||||
secondary: e.modifiers().secondary(),
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}))
|
||||
.on_mouse_down(
|
||||
MouseButton::Right,
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
this.set_right_clicked_index(Some(ix), window, cx);
|
||||
cx.notify();
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn render_items(
|
||||
&mut self,
|
||||
items_count: usize,
|
||||
entities_count: usize,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let rows_cache = self.rows_cache.clone();
|
||||
let scrollbar_visible = self.options.scrollbar_visible;
|
||||
let scroll_handle = self.scroll_handle.clone();
|
||||
|
||||
v_flex()
|
||||
.flex_grow_1()
|
||||
.relative()
|
||||
.size_full()
|
||||
.when_some(self.options.max_height, |this, h| this.max_h(h))
|
||||
.overflow_hidden()
|
||||
.when(items_count == 0, |this| {
|
||||
this.child(self.delegate.render_empty(window, cx))
|
||||
})
|
||||
.when(items_count > 0, {
|
||||
|this| {
|
||||
this.child(
|
||||
uniform_list(
|
||||
"virtual-list",
|
||||
rows_cache.items_count(),
|
||||
cx.processor(move |this, range: Range<usize>, window, cx| {
|
||||
this.load_more_if_need(entities_count, range.end, window, cx);
|
||||
|
||||
// NOTE: Here the v_virtual_list would not able to have gap_y,
|
||||
// because the section header, footer is always have rendered as a empty child item,
|
||||
// even the delegate give a None result.
|
||||
|
||||
range
|
||||
.map(|ix| {
|
||||
let Some(entry) = rows_cache.get(ix) else {
|
||||
return div();
|
||||
};
|
||||
|
||||
div().children(match entry {
|
||||
RowEntry::Entry(index) => Some(
|
||||
this.render_list_item(index, window, cx)
|
||||
.into_any_element(),
|
||||
),
|
||||
RowEntry::SectionHeader(section_ix) => this
|
||||
.delegate_mut()
|
||||
.render_section_header(section_ix, window, cx)
|
||||
.map(|r| r.into_any_element()),
|
||||
RowEntry::SectionFooter(section_ix) => this
|
||||
.delegate_mut()
|
||||
.render_section_footer(section_ix, window, cx)
|
||||
.map(|r| r.into_any_element()),
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}),
|
||||
)
|
||||
.when(self.options.max_height.is_some(), |this| {
|
||||
this.with_sizing_behavior(ListSizingBehavior::Infer)
|
||||
})
|
||||
.track_scroll(&scroll_handle)
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
})
|
||||
.when(scrollbar_visible, |this| {
|
||||
this.child(Scrollbar::vertical(&scroll_handle))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> Focusable for ListState<D>
|
||||
where
|
||||
D: ListDelegate,
|
||||
{
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
if self.searchable {
|
||||
self.query_input.focus_handle(cx)
|
||||
} else {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<D> EventEmitter<ListEvent> for ListState<D> where D: ListDelegate {}
|
||||
impl<D> Render for ListState<D>
|
||||
where
|
||||
D: ListDelegate,
|
||||
{
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
self.prepare_items_if_needed(window, cx);
|
||||
|
||||
// Scroll to the selected item if it is set.
|
||||
if let Some((ix, strategy)) = self.deferred_scroll_to_index.take()
|
||||
&& let Some(item_ix) = self.rows_cache.position_of(&ix)
|
||||
{
|
||||
self.scroll_handle.scroll_to_item(item_ix, strategy);
|
||||
}
|
||||
|
||||
let loading = self.delegate().loading(cx);
|
||||
let query_input = if self.searchable {
|
||||
// sync placeholder
|
||||
if let Some(placeholder) = &self.options.search_placeholder {
|
||||
self.query_input.update(cx, |input, cx| {
|
||||
input.set_placeholder(placeholder.clone(), window, cx);
|
||||
});
|
||||
}
|
||||
Some(self.query_input.clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let loading_view = if loading {
|
||||
Some(self.delegate.render_loading(window, cx).into_any_element())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let initial_view = if let Some(input) = &query_input {
|
||||
if input.read(cx).value().is_empty() {
|
||||
self.delegate.render_initial(window, cx)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let items_count = self.rows_cache.items_count();
|
||||
let entities_count = self.rows_cache.len();
|
||||
let mouse_right_clicked_index = self.mouse_right_clicked_index;
|
||||
|
||||
v_flex()
|
||||
.key_context("List")
|
||||
.id("list-state")
|
||||
.track_focus(&self.focus_handle)
|
||||
.size_full()
|
||||
.relative()
|
||||
.overflow_hidden()
|
||||
.when_some(query_input, |this, input| {
|
||||
this.child(
|
||||
div()
|
||||
.map(|this| match self.options.size {
|
||||
Size::Small => this.px_1p5(),
|
||||
_ => this.px_2(),
|
||||
})
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().border)
|
||||
.child(
|
||||
TextInput::new(&input)
|
||||
.with_size(self.options.size)
|
||||
.appearance(false)
|
||||
.cleanable()
|
||||
.p_0()
|
||||
.prefix(
|
||||
Icon::new(IconName::Search).text_color(cx.theme().text_muted),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
.when(!loading, |this| {
|
||||
this.on_action(cx.listener(Self::on_action_cancel))
|
||||
.on_action(cx.listener(Self::on_action_confirm))
|
||||
.on_action(cx.listener(Self::on_action_select_next))
|
||||
.on_action(cx.listener(Self::on_action_select_prev))
|
||||
.map(|this| {
|
||||
if let Some(view) = initial_view {
|
||||
this.child(view)
|
||||
} else {
|
||||
this.child(self.render_items(items_count, entities_count, window, cx))
|
||||
}
|
||||
})
|
||||
// Click out to cancel right clicked row
|
||||
.when(mouse_right_clicked_index.is_some(), |this| {
|
||||
this.on_mouse_down_out(cx.listener(|this, _, window, cx| {
|
||||
this.set_right_clicked_index(None, window, cx);
|
||||
cx.notify();
|
||||
}))
|
||||
})
|
||||
})
|
||||
.children(loading_view)
|
||||
}
|
||||
}
|
||||
|
||||
/// The List element.
|
||||
#[derive(IntoElement)]
|
||||
pub struct List<D: ListDelegate + 'static> {
|
||||
state: Entity<ListState<D>>,
|
||||
style: StyleRefinement,
|
||||
options: ListOptions,
|
||||
}
|
||||
|
||||
impl<D> List<D>
|
||||
where
|
||||
D: ListDelegate + 'static,
|
||||
{
|
||||
/// Create a new List element with the given ListState entity.
|
||||
pub fn new(state: &Entity<ListState<D>>) -> Self {
|
||||
Self {
|
||||
state: state.clone(),
|
||||
style: StyleRefinement::default(),
|
||||
options: ListOptions::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set whether the scrollbar is visible, default is `true`.
|
||||
pub fn scrollbar_visible(mut self, visible: bool) -> Self {
|
||||
self.options.scrollbar_visible = visible;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the placeholder text for the search input.
|
||||
pub fn search_placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
|
||||
self.options.search_placeholder = Some(placeholder.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> Styled for List<D>
|
||||
where
|
||||
D: ListDelegate + 'static,
|
||||
{
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
&mut self.style
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> Sizable for List<D>
|
||||
where
|
||||
D: ListDelegate + 'static,
|
||||
{
|
||||
fn with_size(mut self, size: impl Into<Size>) -> Self {
|
||||
self.options.size = size.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> RenderOnce for List<D>
|
||||
where
|
||||
D: ListDelegate + 'static,
|
||||
{
|
||||
fn render(mut self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
// Take paddings, max_height to options, and clear them from style,
|
||||
// because they would be applied to the inner virtual list.
|
||||
self.options.paddings = self.style.padding.clone();
|
||||
self.options.max_height = self.style.max_size.height;
|
||||
self.style.padding = EdgesRefinement::default();
|
||||
self.style.max_size.height = None;
|
||||
|
||||
self.state.update(cx, |state, _| {
|
||||
state.options = self.options;
|
||||
});
|
||||
|
||||
div()
|
||||
.id("list")
|
||||
.size_full()
|
||||
.refine_style(&self.style)
|
||||
.child(self.state.clone())
|
||||
}
|
||||
}
|
||||
@@ -1,226 +0,0 @@
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
div, AnyElement, App, ClickEvent, Div, ElementId, InteractiveElement, IntoElement,
|
||||
MouseMoveEvent, ParentElement, RenderOnce, Stateful, StatefulInteractiveElement as _,
|
||||
StyleRefinement, Styled, Window,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use crate::{h_flex, Disableable, Icon, Selectable, Sizable as _, StyledExt};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
enum ListItemMode {
|
||||
#[default]
|
||||
Entry,
|
||||
Separator,
|
||||
}
|
||||
|
||||
impl ListItemMode {
|
||||
#[inline]
|
||||
fn is_separator(&self) -> bool {
|
||||
matches!(self, ListItemMode::Separator)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ListItem {
|
||||
base: Stateful<Div>,
|
||||
mode: ListItemMode,
|
||||
style: StyleRefinement,
|
||||
disabled: bool,
|
||||
selected: bool,
|
||||
secondary_selected: bool,
|
||||
confirmed: bool,
|
||||
check_icon: Option<Icon>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
on_mouse_enter: Option<Box<dyn Fn(&MouseMoveEvent, &mut Window, &mut App) + 'static>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
suffix: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyElement + 'static>>,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
}
|
||||
|
||||
impl ListItem {
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
let id: ElementId = id.into();
|
||||
Self {
|
||||
mode: ListItemMode::Entry,
|
||||
base: h_flex().id(id),
|
||||
style: StyleRefinement::default(),
|
||||
disabled: false,
|
||||
selected: false,
|
||||
secondary_selected: false,
|
||||
confirmed: false,
|
||||
on_click: None,
|
||||
on_mouse_enter: None,
|
||||
check_icon: None,
|
||||
suffix: None,
|
||||
children: SmallVec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set this list item to as a separator, it not able to be selected.
|
||||
pub fn separator(mut self) -> Self {
|
||||
self.mode = ListItemMode::Separator;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set to show check icon, default is None.
|
||||
pub fn check_icon(mut self, icon: impl Into<Icon>) -> Self {
|
||||
self.check_icon = Some(icon.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set ListItem as the selected item style.
|
||||
pub fn selected(mut self, selected: bool) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set ListItem as the confirmed item style, it will show a check icon.
|
||||
pub fn confirmed(mut self, confirmed: bool) -> Self {
|
||||
self.confirmed = confirmed;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the suffix element of the input field, for example a clear button.
|
||||
pub fn suffix<F, E>(mut self, builder: F) -> Self
|
||||
where
|
||||
F: Fn(&mut Window, &mut App) -> E + 'static,
|
||||
E: IntoElement,
|
||||
{
|
||||
self.suffix = Some(Box::new(move |window, cx| {
|
||||
builder(window, cx).into_any_element()
|
||||
}));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_click(
|
||||
mut self,
|
||||
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_click = Some(Box::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_mouse_enter(
|
||||
mut self,
|
||||
handler: impl Fn(&MouseMoveEvent, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_mouse_enter = Some(Box::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Disableable for ListItem {
|
||||
fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Selectable for ListItem {
|
||||
fn selected(mut self, selected: bool) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
|
||||
fn is_selected(&self) -> bool {
|
||||
self.selected
|
||||
}
|
||||
|
||||
fn secondary_selected(mut self, selected: bool) -> Self {
|
||||
self.secondary_selected = selected;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for ListItem {
|
||||
fn style(&mut self) -> &mut gpui::StyleRefinement {
|
||||
&mut self.style
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for ListItem {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = gpui::AnyElement>) {
|
||||
self.children.extend(elements);
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ListItem {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let is_active = self.confirmed || self.selected;
|
||||
|
||||
let corner_radii = self.style.corner_radii.clone();
|
||||
|
||||
let _selected_style = StyleRefinement {
|
||||
corner_radii,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let is_selectable = !(self.disabled || self.mode.is_separator());
|
||||
|
||||
self.base
|
||||
.relative()
|
||||
.gap_x_1()
|
||||
.py_1()
|
||||
.px_3()
|
||||
.text_base()
|
||||
.text_color(cx.theme().text)
|
||||
.relative()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.refine_style(&self.style)
|
||||
.when(is_selectable, |this| {
|
||||
this.when_some(self.on_click, |this, on_click| this.on_click(on_click))
|
||||
.when_some(self.on_mouse_enter, |this, on_mouse_enter| {
|
||||
this.on_mouse_move(move |ev, window, cx| (on_mouse_enter)(ev, window, cx))
|
||||
})
|
||||
.when(!is_active, |this| {
|
||||
this.hover(|this| this.bg(cx.theme().ghost_element_hover))
|
||||
})
|
||||
})
|
||||
.when(!is_selectable, |this| {
|
||||
this.text_color(cx.theme().text_muted)
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.gap_x_1()
|
||||
.child(div().w_full().children(self.children))
|
||||
.when_some(self.check_icon, |this, icon| {
|
||||
this.child(
|
||||
div()
|
||||
.w_5()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.when(self.confirmed, |this| {
|
||||
this.child(icon.small().text_color(cx.theme().text_muted))
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.when_some(self.suffix, |this, suffix| this.child(suffix(window, cx)))
|
||||
.map(|this| {
|
||||
if is_selectable && (self.selected || self.secondary_selected) {
|
||||
let bg = if self.selected {
|
||||
cx.theme().ghost_element_active
|
||||
} else {
|
||||
cx.theme().ghost_element_background
|
||||
};
|
||||
this.bg(bg)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
use gpui::{IntoElement, ParentElement as _, RenderOnce, Styled};
|
||||
|
||||
use super::ListItem;
|
||||
use crate::skeleton::Skeleton;
|
||||
use crate::v_flex;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct Loading;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
struct LoadingItem;
|
||||
|
||||
impl RenderOnce for LoadingItem {
|
||||
fn render(self, _window: &mut gpui::Window, _cx: &mut gpui::App) -> impl IntoElement {
|
||||
ListItem::new("skeleton").disabled(true).child(
|
||||
v_flex()
|
||||
.gap_1p5()
|
||||
.overflow_hidden()
|
||||
.child(Skeleton::new().h_5().w_48().max_w_full())
|
||||
.child(Skeleton::new().secondary().h_3().w_64().max_w_full()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Loading {
|
||||
fn render(self, _window: &mut gpui::Window, _cx: &mut gpui::App) -> impl IntoElement {
|
||||
v_flex()
|
||||
.py_2p5()
|
||||
.gap_3()
|
||||
.child(LoadingItem)
|
||||
.child(LoadingItem)
|
||||
.child(LoadingItem)
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
pub(crate) mod cache;
|
||||
mod delegate;
|
||||
#[allow(clippy::module_inception)]
|
||||
mod list;
|
||||
mod list_item;
|
||||
mod loading;
|
||||
mod separator_item;
|
||||
|
||||
pub use delegate::*;
|
||||
pub use list::*;
|
||||
pub use list_item::*;
|
||||
pub use separator_item::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Settings for List.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ListSettings {
|
||||
/// Whether to use active highlight style on ListItem, default
|
||||
pub active_highlight: bool,
|
||||
}
|
||||
|
||||
impl Default for ListSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
active_highlight: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
use gpui::{AnyElement, ParentElement, RenderOnce, StyleRefinement};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::list::ListItem;
|
||||
use crate::{Selectable, StyledExt};
|
||||
|
||||
pub struct ListSeparatorItem {
|
||||
style: StyleRefinement,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
}
|
||||
|
||||
impl ListSeparatorItem {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
style: StyleRefinement::default(),
|
||||
children: SmallVec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ListSeparatorItem {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for ListSeparatorItem {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements);
|
||||
}
|
||||
}
|
||||
|
||||
impl Selectable for ListSeparatorItem {
|
||||
fn selected(self, _: bool) -> Self {
|
||||
self
|
||||
}
|
||||
|
||||
fn is_selected(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ListSeparatorItem {
|
||||
fn render(self, _: &mut gpui::Window, _: &mut gpui::App) -> impl gpui::IntoElement {
|
||||
ListItem::new("separator")
|
||||
.refine_style(&self.style)
|
||||
.children(self.children)
|
||||
.disabled(true)
|
||||
}
|
||||
}
|
||||
@@ -1,257 +0,0 @@
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
App, AppContext as _, ClickEvent, Context, DismissEvent, Entity, Focusable,
|
||||
InteractiveElement as _, IntoElement, KeyBinding, MouseButton, OwnedMenu, ParentElement,
|
||||
Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Window, anchored,
|
||||
deferred, div, px,
|
||||
};
|
||||
|
||||
use crate::actions::{Cancel, SelectLeft, SelectRight};
|
||||
use crate::button::{Button, ButtonVariants};
|
||||
use crate::menu::PopupMenu;
|
||||
use crate::{Selectable, Sizable, h_flex};
|
||||
|
||||
const CONTEXT: &str = "AppMenuBar";
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
cx.bind_keys([
|
||||
KeyBinding::new("escape", Cancel, Some(CONTEXT)),
|
||||
KeyBinding::new("left", SelectLeft, Some(CONTEXT)),
|
||||
KeyBinding::new("right", SelectRight, Some(CONTEXT)),
|
||||
]);
|
||||
}
|
||||
|
||||
/// The application menu bar, for Windows and Linux.
|
||||
pub struct AppMenuBar {
|
||||
menus: Vec<Entity<AppMenu>>,
|
||||
selected_index: Option<usize>,
|
||||
}
|
||||
|
||||
impl AppMenuBar {
|
||||
/// Create a new app menu bar.
|
||||
pub fn new(cx: &mut App) -> Entity<Self> {
|
||||
cx.new(|cx| {
|
||||
let mut this = Self {
|
||||
selected_index: None,
|
||||
menus: Vec::new(),
|
||||
};
|
||||
this.reload(cx);
|
||||
this
|
||||
})
|
||||
}
|
||||
|
||||
/// Reload the menus from the app.
|
||||
pub fn reload(&mut self, cx: &mut Context<Self>) {
|
||||
let menu_bar = cx.entity();
|
||||
self.menus = cx
|
||||
.get_menus()
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, menu)| AppMenu::new(ix, menu, menu_bar.clone(), cx))
|
||||
.collect();
|
||||
self.selected_index = None;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn on_move_left(&mut self, _: &SelectLeft, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(selected_index) = self.selected_index else {
|
||||
return;
|
||||
};
|
||||
|
||||
let new_ix = if selected_index == 0 {
|
||||
self.menus.len().saturating_sub(1)
|
||||
} else {
|
||||
selected_index.saturating_sub(1)
|
||||
};
|
||||
self.set_selected_index(Some(new_ix), window, cx);
|
||||
}
|
||||
|
||||
fn on_move_right(&mut self, _: &SelectRight, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(selected_index) = self.selected_index else {
|
||||
return;
|
||||
};
|
||||
|
||||
let new_ix = if selected_index + 1 >= self.menus.len() {
|
||||
0
|
||||
} else {
|
||||
selected_index + 1
|
||||
};
|
||||
self.set_selected_index(Some(new_ix), window, cx);
|
||||
}
|
||||
|
||||
fn on_cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.set_selected_index(None, window, cx);
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: Option<usize>, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.selected_index = ix;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn has_activated_menu(&self) -> bool {
|
||||
self.selected_index.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AppMenuBar {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
h_flex()
|
||||
.id("app-menu-bar")
|
||||
.key_context(CONTEXT)
|
||||
.on_action(cx.listener(Self::on_move_left))
|
||||
.on_action(cx.listener(Self::on_move_right))
|
||||
.on_action(cx.listener(Self::on_cancel))
|
||||
.size_full()
|
||||
.gap_x_1()
|
||||
.overflow_x_scroll()
|
||||
.children(self.menus.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// A menu in the menu bar.
|
||||
pub(super) struct AppMenu {
|
||||
menu_bar: Entity<AppMenuBar>,
|
||||
ix: usize,
|
||||
name: SharedString,
|
||||
menu: OwnedMenu,
|
||||
popup_menu: Option<Entity<PopupMenu>>,
|
||||
|
||||
_subscription: Option<Subscription>,
|
||||
}
|
||||
|
||||
impl AppMenu {
|
||||
pub(super) fn new(
|
||||
ix: usize,
|
||||
menu: &OwnedMenu,
|
||||
menu_bar: Entity<AppMenuBar>,
|
||||
cx: &mut App,
|
||||
) -> Entity<Self> {
|
||||
let name = menu.name.clone();
|
||||
cx.new(|_| Self {
|
||||
ix,
|
||||
menu_bar,
|
||||
name,
|
||||
menu: menu.clone(),
|
||||
popup_menu: None,
|
||||
_subscription: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn build_popup_menu(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Entity<PopupMenu> {
|
||||
let popup_menu = match self.popup_menu.as_ref() {
|
||||
None => {
|
||||
let items = self.menu.items.clone();
|
||||
let popup_menu = PopupMenu::build(window, cx, |menu, window, cx| {
|
||||
menu.when_some(window.focused(cx), |this, handle| {
|
||||
this.action_context(handle)
|
||||
})
|
||||
.with_menu_items(items, window, cx)
|
||||
});
|
||||
popup_menu.read(cx).focus_handle(cx).focus(window, cx);
|
||||
self._subscription =
|
||||
Some(cx.subscribe_in(&popup_menu, window, Self::handle_dismiss));
|
||||
self.popup_menu = Some(popup_menu.clone());
|
||||
|
||||
popup_menu
|
||||
}
|
||||
Some(menu) => menu.clone(),
|
||||
};
|
||||
|
||||
let focus_handle = popup_menu.read(cx).focus_handle(cx);
|
||||
if !focus_handle.contains_focused(window, cx) {
|
||||
focus_handle.focus(window, cx);
|
||||
}
|
||||
|
||||
popup_menu
|
||||
}
|
||||
|
||||
fn handle_dismiss(
|
||||
&mut self,
|
||||
_: &Entity<PopupMenu>,
|
||||
_: &DismissEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self._subscription.take();
|
||||
self.popup_menu.take();
|
||||
self.menu_bar.update(cx, |state, cx| {
|
||||
state.on_cancel(&Cancel, window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_trigger_click(
|
||||
&mut self,
|
||||
_: &ClickEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let is_selected = self.menu_bar.read(cx).selected_index == Some(self.ix);
|
||||
|
||||
self.menu_bar.update(cx, |state, cx| {
|
||||
let new_ix = if is_selected { None } else { Some(self.ix) };
|
||||
state.set_selected_index(new_ix, window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_hover(&mut self, hovered: &bool, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if !*hovered {
|
||||
return;
|
||||
}
|
||||
|
||||
let has_activated_menu = self.menu_bar.read(cx).has_activated_menu();
|
||||
if !has_activated_menu {
|
||||
return;
|
||||
}
|
||||
|
||||
self.menu_bar.update(cx, |state, cx| {
|
||||
state.set_selected_index(Some(self.ix), window, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AppMenu {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let menu_bar = self.menu_bar.read(cx);
|
||||
let is_selected = menu_bar.selected_index == Some(self.ix);
|
||||
|
||||
div()
|
||||
.id(self.ix)
|
||||
.relative()
|
||||
.child(
|
||||
Button::new("menu")
|
||||
.small()
|
||||
.py_0p5()
|
||||
.compact()
|
||||
.ghost()
|
||||
.label(self.name.clone())
|
||||
.selected(is_selected)
|
||||
.on_mouse_down(MouseButton::Left, |_, window, cx| {
|
||||
// Stop propagation to avoid dragging the window.
|
||||
window.prevent_default();
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_click(cx.listener(Self::handle_trigger_click)),
|
||||
)
|
||||
.on_hover(cx.listener(Self::handle_hover))
|
||||
.when(is_selected, |this| {
|
||||
this.child(deferred(
|
||||
anchored()
|
||||
.anchor(gpui::Anchor::TopLeft)
|
||||
.snap_to_window_with_margin(px(8.))
|
||||
.child(
|
||||
div()
|
||||
.size_full()
|
||||
.occlude()
|
||||
.top_1()
|
||||
.child(self.build_popup_menu(window, cx)),
|
||||
),
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,323 +0,0 @@
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
Anchor, AnyElement, App, Context, DismissEvent, Element, ElementId, Entity, Focusable,
|
||||
GlobalElementId, Hitbox, HitboxBehavior, InspectorElementId, InteractiveElement, IntoElement,
|
||||
MouseButton, MouseDownEvent, ParentElement, Pixels, Point, StyleRefinement, Styled,
|
||||
Subscription, Window, anchored, deferred, div, px,
|
||||
};
|
||||
|
||||
use crate::menu::PopupMenu;
|
||||
|
||||
/// A extension trait for adding a context menu to an element.
|
||||
pub trait ContextMenuExt: ParentElement + Styled {
|
||||
/// Add a context menu to the element.
|
||||
///
|
||||
/// This will changed the element to be `relative` positioned, and add a child `ContextMenu` element.
|
||||
/// Because the `ContextMenu` element is positioned `absolute`, it will not affect the layout of the parent element.
|
||||
fn context_menu(
|
||||
self,
|
||||
f: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
|
||||
) -> ContextMenu<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
// Generate a unique ID based on the element's memory address to ensure
|
||||
// each context menu has its own state and doesn't share with others
|
||||
let id = format!("context-menu-{:p}", &self as *const _);
|
||||
ContextMenu::new(id, self).menu(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: ParentElement + Styled> ContextMenuExt for E {}
|
||||
|
||||
/// A context menu that can be shown on right-click.
|
||||
pub struct ContextMenu<E: ParentElement + Styled + Sized> {
|
||||
id: ElementId,
|
||||
element: Option<E>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
menu: Option<Rc<dyn Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu>>,
|
||||
// This is not in use, just for style refinement forwarding.
|
||||
_ignore_style: StyleRefinement,
|
||||
anchor: Anchor,
|
||||
}
|
||||
|
||||
impl<E: ParentElement + Styled> ContextMenu<E> {
|
||||
/// Create a new context menu with the given ID.
|
||||
pub fn new(id: impl Into<ElementId>, element: E) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
element: Some(element),
|
||||
menu: None,
|
||||
anchor: Anchor::TopLeft,
|
||||
_ignore_style: StyleRefinement::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the context menu using the given builder function.
|
||||
#[must_use]
|
||||
fn menu<F>(mut self, builder: F) -> Self
|
||||
where
|
||||
F: Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
|
||||
{
|
||||
self.menu = Some(Rc::new(builder));
|
||||
self
|
||||
}
|
||||
|
||||
fn with_element_state<R>(
|
||||
&mut self,
|
||||
id: &GlobalElementId,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
f: impl FnOnce(&mut Self, &mut ContextMenuState, &mut Window, &mut App) -> R,
|
||||
) -> R {
|
||||
window.with_optional_element_state::<ContextMenuState, _>(
|
||||
Some(id),
|
||||
|element_state, window| {
|
||||
let mut element_state = element_state.unwrap().unwrap_or_default();
|
||||
let result = f(self, &mut element_state, window, cx);
|
||||
(result, Some(element_state))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: ParentElement + Styled> ParentElement for ContextMenu<E> {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
if let Some(element) = &mut self.element {
|
||||
element.extend(elements);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: ParentElement + Styled> Styled for ContextMenu<E> {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
if let Some(element) = &mut self.element {
|
||||
element.style()
|
||||
} else {
|
||||
&mut self._ignore_style
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: ParentElement + Styled + IntoElement + 'static> IntoElement for ContextMenu<E> {
|
||||
type Element = Self;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
struct ContextMenuSharedState {
|
||||
menu_view: Option<Entity<PopupMenu>>,
|
||||
open: bool,
|
||||
position: Point<Pixels>,
|
||||
_subscription: Option<Subscription>,
|
||||
}
|
||||
|
||||
pub struct ContextMenuState {
|
||||
element: Option<AnyElement>,
|
||||
shared_state: Rc<RefCell<ContextMenuSharedState>>,
|
||||
}
|
||||
|
||||
impl Default for ContextMenuState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
element: None,
|
||||
shared_state: Rc::new(RefCell::new(ContextMenuSharedState {
|
||||
menu_view: None,
|
||||
open: false,
|
||||
position: Default::default(),
|
||||
_subscription: None,
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: ParentElement + Styled + IntoElement + 'static> Element for ContextMenu<E> {
|
||||
type PrepaintState = Hitbox;
|
||||
type RequestLayoutState = ContextMenuState;
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
Some(self.id.clone())
|
||||
}
|
||||
|
||||
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
id: Option<&gpui::GlobalElementId>,
|
||||
_: Option<&gpui::InspectorElementId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (gpui::LayoutId, Self::RequestLayoutState) {
|
||||
let anchor = self.anchor;
|
||||
|
||||
self.with_element_state(
|
||||
id.unwrap(),
|
||||
window,
|
||||
cx,
|
||||
|this, state: &mut ContextMenuState, window, cx| {
|
||||
let (position, open) = {
|
||||
let shared_state = state.shared_state.borrow();
|
||||
(shared_state.position, shared_state.open)
|
||||
};
|
||||
let menu_view = state.shared_state.borrow().menu_view.clone();
|
||||
let mut menu_element = None;
|
||||
if open {
|
||||
let has_menu_item = menu_view
|
||||
.as_ref()
|
||||
.map(|menu| !menu.read(cx).is_empty())
|
||||
.unwrap_or(false);
|
||||
|
||||
if has_menu_item {
|
||||
menu_element = Some(
|
||||
deferred(
|
||||
anchored().child(
|
||||
div()
|
||||
.w(window.bounds().size.width)
|
||||
.h(window.bounds().size.height)
|
||||
.on_scroll_wheel(|_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.child(
|
||||
anchored()
|
||||
.position(position)
|
||||
.snap_to_window_with_margin(px(8.))
|
||||
.anchor(anchor)
|
||||
.when_some(menu_view, |this, menu| {
|
||||
// Focus the menu, so that can be handle the action.
|
||||
if !menu
|
||||
.focus_handle(cx)
|
||||
.contains_focused(window, cx)
|
||||
{
|
||||
menu.focus_handle(cx).focus(window, cx);
|
||||
}
|
||||
|
||||
this.child(menu.clone())
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
.with_priority(1)
|
||||
.into_any(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mut element = this
|
||||
.element
|
||||
.take()
|
||||
.expect("Element should exists.")
|
||||
.children(menu_element)
|
||||
.into_any_element();
|
||||
|
||||
let layout_id = element.request_layout(window, cx);
|
||||
|
||||
(
|
||||
layout_id,
|
||||
ContextMenuState {
|
||||
element: Some(element),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_: Option<&gpui::GlobalElementId>,
|
||||
_: Option<&InspectorElementId>,
|
||||
bounds: gpui::Bounds<gpui::Pixels>,
|
||||
request_layout: &mut Self::RequestLayoutState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Self::PrepaintState {
|
||||
if let Some(element) = &mut request_layout.element {
|
||||
element.prepaint(window, cx);
|
||||
}
|
||||
window.insert_hitbox(bounds, HitboxBehavior::Normal)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
id: Option<&gpui::GlobalElementId>,
|
||||
_: Option<&InspectorElementId>,
|
||||
_: gpui::Bounds<gpui::Pixels>,
|
||||
request_layout: &mut Self::RequestLayoutState,
|
||||
hitbox: &mut Self::PrepaintState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
if let Some(element) = &mut request_layout.element {
|
||||
element.paint(window, cx);
|
||||
}
|
||||
|
||||
// Take the builder before setting up element state to avoid borrow issues
|
||||
let builder = self.menu.clone();
|
||||
|
||||
self.with_element_state(
|
||||
id.unwrap(),
|
||||
window,
|
||||
cx,
|
||||
|_view, state: &mut ContextMenuState, window, _| {
|
||||
let shared_state = state.shared_state.clone();
|
||||
|
||||
let hitbox = hitbox.clone();
|
||||
// When right mouse click, to build content menu, and show it at the mouse position.
|
||||
window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| {
|
||||
if phase.bubble()
|
||||
&& event.button == MouseButton::Right
|
||||
&& hitbox.is_hovered(window)
|
||||
{
|
||||
{
|
||||
let mut shared_state = shared_state.borrow_mut();
|
||||
// Clear any existing menu view to allow immediate replacement
|
||||
// Set the new position and open the menu
|
||||
shared_state.menu_view = None;
|
||||
shared_state._subscription = None;
|
||||
shared_state.position = event.position;
|
||||
shared_state.open = true;
|
||||
}
|
||||
|
||||
// Use defer to build the menu in the next frame, avoiding race conditions
|
||||
window.defer(cx, {
|
||||
let shared_state = shared_state.clone();
|
||||
let builder = builder.clone();
|
||||
move |window, cx| {
|
||||
let menu = PopupMenu::build(window, cx, move |menu, window, cx| {
|
||||
let Some(build) = &builder else {
|
||||
return menu;
|
||||
};
|
||||
build(menu, window, cx)
|
||||
});
|
||||
|
||||
// Set up the subscription for dismiss handling
|
||||
let _subscription = window.subscribe(&menu, cx, {
|
||||
let shared_state = shared_state.clone();
|
||||
move |_, _: &DismissEvent, window, _cx| {
|
||||
shared_state.borrow_mut().open = false;
|
||||
window.refresh();
|
||||
}
|
||||
});
|
||||
|
||||
// Update the shared state with the built menu and subscription
|
||||
{
|
||||
let mut state = shared_state.borrow_mut();
|
||||
state.menu_view = Some(menu.clone());
|
||||
state._subscription = Some(_subscription);
|
||||
window.refresh();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
Anchor, Context, DismissEvent, ElementId, Entity, Focusable, InteractiveElement, IntoElement,
|
||||
RenderOnce, SharedString, StyleRefinement, Styled, Window,
|
||||
};
|
||||
|
||||
use crate::Selectable;
|
||||
use crate::avatar::Avatar;
|
||||
use crate::button::Button;
|
||||
use crate::menu::PopupMenu;
|
||||
use crate::popover::Popover;
|
||||
|
||||
/// A dropdown menu trait for buttons and other interactive elements
|
||||
pub trait DropdownMenu: Styled + Selectable + InteractiveElement + IntoElement + 'static {
|
||||
/// Create a dropdown menu with the given items, anchored to the TopLeft corner
|
||||
fn dropdown_menu(
|
||||
self,
|
||||
f: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
|
||||
) -> DropdownMenuPopover<Self> {
|
||||
self.dropdown_menu_with_anchor(Anchor::TopLeft, f)
|
||||
}
|
||||
|
||||
/// Create a dropdown menu with the given items, anchored to the given corner
|
||||
fn dropdown_menu_with_anchor(
|
||||
mut self,
|
||||
anchor: impl Into<Anchor>,
|
||||
f: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
|
||||
) -> DropdownMenuPopover<Self> {
|
||||
let style = self.style().clone();
|
||||
let id = self.interactivity().element_id.clone();
|
||||
|
||||
DropdownMenuPopover::new(id.unwrap_or(0.into()), anchor, self, f).trigger_style(style)
|
||||
}
|
||||
}
|
||||
|
||||
impl DropdownMenu for Button {}
|
||||
|
||||
impl DropdownMenu for Avatar {}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct DropdownMenuPopover<T: Selectable + IntoElement + 'static> {
|
||||
id: ElementId,
|
||||
style: StyleRefinement,
|
||||
anchor: Anchor,
|
||||
trigger: T,
|
||||
#[allow(clippy::type_complexity)]
|
||||
builder: Rc<dyn Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu>,
|
||||
}
|
||||
|
||||
impl<T> DropdownMenuPopover<T>
|
||||
where
|
||||
T: Selectable + IntoElement + 'static,
|
||||
{
|
||||
fn new(
|
||||
id: ElementId,
|
||||
anchor: impl Into<Anchor>,
|
||||
trigger: T,
|
||||
builder: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: SharedString::from(format!("dropdown-menu:{:?}", id)).into(),
|
||||
style: StyleRefinement::default(),
|
||||
anchor: anchor.into(),
|
||||
trigger,
|
||||
builder: Rc::new(builder),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the anchor corner for the dropdown menu popover.
|
||||
pub fn anchor(mut self, anchor: impl Into<Anchor>) -> Self {
|
||||
self.anchor = anchor.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the style refinement for the dropdown menu trigger.
|
||||
fn trigger_style(mut self, style: StyleRefinement) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct DropdownMenuState {
|
||||
menu: Option<Entity<PopupMenu>>,
|
||||
}
|
||||
|
||||
impl<T> RenderOnce for DropdownMenuPopover<T>
|
||||
where
|
||||
T: Selectable + IntoElement + 'static,
|
||||
{
|
||||
fn render(self, window: &mut Window, cx: &mut gpui::App) -> impl IntoElement {
|
||||
let builder = self.builder.clone();
|
||||
let menu_state =
|
||||
window.use_keyed_state(self.id.clone(), cx, |_, _| DropdownMenuState::default());
|
||||
|
||||
Popover::new(SharedString::from(format!("popover:{}", self.id)))
|
||||
.appearance(false)
|
||||
.overlay_closable(false)
|
||||
.trigger(self.trigger)
|
||||
.trigger_style(self.style)
|
||||
.anchor(self.anchor)
|
||||
.content(move |_, window, cx| {
|
||||
// Here is special logic to only create the PopupMenu once and reuse it.
|
||||
// Because this `content` will called in every time render, so we need to store the menu
|
||||
// in state to avoid recreating at every render.
|
||||
//
|
||||
// And we also need to rebuild the menu when it is dismissed, to rebuild menu items
|
||||
// dynamically for support `dropdown_menu` method, so we listen for DismissEvent below.
|
||||
let menu = match menu_state.read(cx).menu.clone() {
|
||||
Some(menu) => menu,
|
||||
None => {
|
||||
let builder = builder.clone();
|
||||
let menu = PopupMenu::build(window, cx, move |menu, window, cx| {
|
||||
builder(menu, window, cx)
|
||||
});
|
||||
menu_state.update(cx, |state, _| {
|
||||
state.menu = Some(menu.clone());
|
||||
});
|
||||
menu.focus_handle(cx).focus(window, cx);
|
||||
|
||||
// Listen for dismiss events from the PopupMenu to close the popover.
|
||||
let popover_state = cx.entity();
|
||||
window
|
||||
.subscribe(&menu, cx, {
|
||||
let menu_state = menu_state.clone();
|
||||
move |_, _: &DismissEvent, window, cx| {
|
||||
popover_state.update(cx, |state, cx| {
|
||||
state.dismiss(window, cx);
|
||||
});
|
||||
menu_state.update(cx, |state, _| {
|
||||
state.menu = None;
|
||||
});
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
menu.clone()
|
||||
}
|
||||
};
|
||||
|
||||
menu.clone()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
AnyElement, App, ClickEvent, ElementId, InteractiveElement, IntoElement, MouseButton,
|
||||
ParentElement, RenderOnce, SharedString, StatefulInteractiveElement as _, StyleRefinement,
|
||||
Styled, Window,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use crate::{h_flex, Disableable, StyledExt};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub(crate) struct MenuItemElement {
|
||||
id: ElementId,
|
||||
group_name: SharedString,
|
||||
style: StyleRefinement,
|
||||
disabled: bool,
|
||||
selected: bool,
|
||||
#[allow(clippy::type_complexity)]
|
||||
on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
on_hover: Option<Box<dyn Fn(&bool, &mut Window, &mut App) + 'static>>,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
}
|
||||
|
||||
impl MenuItemElement {
|
||||
/// Create a new MenuItem with the given ID and group name.
|
||||
pub(crate) fn new(id: impl Into<ElementId>, group_name: impl Into<SharedString>) -> Self {
|
||||
let id: ElementId = id.into();
|
||||
Self {
|
||||
id: id.clone(),
|
||||
group_name: group_name.into(),
|
||||
style: StyleRefinement::default(),
|
||||
disabled: false,
|
||||
selected: false,
|
||||
on_click: None,
|
||||
on_hover: None,
|
||||
children: SmallVec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set ListItem as the selected item style.
|
||||
pub(crate) fn selected(mut self, selected: bool) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the disabled state of the MenuItem.
|
||||
pub(crate) fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set a handler for when the MenuItem is clicked.
|
||||
pub(crate) fn on_click(
|
||||
mut self,
|
||||
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_click = Some(Box::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set a handler for when the mouse enters the MenuItem.
|
||||
#[allow(unused)]
|
||||
pub fn on_hover(mut self, handler: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self {
|
||||
self.on_hover = Some(Box::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Disableable for MenuItemElement {
|
||||
fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for MenuItemElement {
|
||||
fn style(&mut self) -> &mut gpui::StyleRefinement {
|
||||
&mut self.style
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for MenuItemElement {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = gpui::AnyElement>) {
|
||||
self.children.extend(elements);
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for MenuItemElement {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
h_flex()
|
||||
.id(self.id)
|
||||
.group(&self.group_name)
|
||||
.gap_x_1()
|
||||
.p_1()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().text)
|
||||
.relative()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.refine_style(&self.style)
|
||||
.when_some(self.on_hover, |this, on_hover| {
|
||||
this.on_hover(move |hovered, window, cx| (on_hover)(hovered, window, cx))
|
||||
})
|
||||
.when(!self.disabled, |this| {
|
||||
this.group_hover(self.group_name, |this| {
|
||||
this.bg(cx.theme().secondary_background)
|
||||
.text_color(cx.theme().secondary_foreground)
|
||||
})
|
||||
.when(self.selected, |this| {
|
||||
this.bg(cx.theme().secondary_background)
|
||||
.text_color(cx.theme().secondary_foreground)
|
||||
})
|
||||
.when_some(self.on_click, |this, on_click| {
|
||||
this.on_mouse_down(MouseButton::Left, move |_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_click(on_click)
|
||||
})
|
||||
})
|
||||
.when(self.disabled, |this| this.text_color(cx.theme().text_muted))
|
||||
.children(self.children)
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
use gpui::App;
|
||||
|
||||
mod app_menu_bar;
|
||||
mod context_menu;
|
||||
mod dropdown_menu;
|
||||
mod menu_item;
|
||||
mod popup_menu;
|
||||
|
||||
pub use app_menu_bar::AppMenuBar;
|
||||
pub use context_menu::{ContextMenu, ContextMenuExt, ContextMenuState};
|
||||
pub use dropdown_menu::DropdownMenu;
|
||||
pub use popup_menu::{PopupMenu, PopupMenuItem};
|
||||
|
||||
pub(crate) fn init(cx: &mut App) {
|
||||
app_menu_bar::init(cx);
|
||||
popup_menu::init(cx);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,540 +0,0 @@
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
Animation, AnimationExt as _, AnyElement, App, Bounds, BoxShadow, ClickEvent, Div, FocusHandle,
|
||||
InteractiveElement, IntoElement, KeyBinding, MouseButton, ParentElement, Pixels, Point,
|
||||
RenderOnce, SharedString, StyleRefinement, Styled, Window, anchored, div, hsla, point, px,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use crate::actions::{Cancel, Confirm};
|
||||
use crate::animation::cubic_bezier;
|
||||
use crate::button::{Button, ButtonCustomVariant, ButtonVariant, ButtonVariants as _};
|
||||
use crate::scroll::ScrollableElement;
|
||||
use crate::{IconName, Root, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
|
||||
|
||||
const CONTEXT: &str = "Modal";
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
cx.bind_keys([
|
||||
KeyBinding::new("escape", Cancel, Some(CONTEXT)),
|
||||
KeyBinding::new("enter", Confirm { secondary: false }, Some(CONTEXT)),
|
||||
]);
|
||||
}
|
||||
|
||||
type OnClose = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>;
|
||||
type OnOk = Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static>>;
|
||||
type OnCancel = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static>;
|
||||
type RenderButtonFn = Box<dyn FnOnce(&mut Window, &mut App) -> AnyElement>;
|
||||
type FooterFn =
|
||||
Box<dyn Fn(RenderButtonFn, RenderButtonFn, &mut Window, &mut App) -> Vec<AnyElement>>;
|
||||
|
||||
/// Modal button props.
|
||||
pub struct ModalButtonProps {
|
||||
ok_text: Option<SharedString>,
|
||||
ok_variant: ButtonVariant,
|
||||
cancel_text: Option<SharedString>,
|
||||
cancel_variant: ButtonVariant,
|
||||
}
|
||||
|
||||
impl Default for ModalButtonProps {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
ok_text: None,
|
||||
ok_variant: ButtonVariant::Primary,
|
||||
cancel_text: None,
|
||||
cancel_variant: ButtonVariant::Ghost { alt: false },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ModalButtonProps {
|
||||
/// Sets the text of the OK button. Default is `OK`.
|
||||
pub fn ok_text(mut self, ok_text: impl Into<SharedString>) -> Self {
|
||||
self.ok_text = Some(ok_text.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the variant of the OK button. Default is `ButtonVariant::Primary`.
|
||||
pub fn ok_variant(mut self, ok_variant: ButtonVariant) -> Self {
|
||||
self.ok_variant = ok_variant;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the text of the Cancel button. Default is `Cancel`.
|
||||
pub fn cancel_text(mut self, cancel_text: impl Into<SharedString>) -> Self {
|
||||
self.cancel_text = Some(cancel_text.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the variant of the Cancel button. Default is `ButtonVariant::default()`.
|
||||
pub fn cancel_variant(mut self, cancel_variant: ButtonVariant) -> Self {
|
||||
self.cancel_variant = cancel_variant;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct Modal {
|
||||
style: StyleRefinement,
|
||||
title: Option<AnyElement>,
|
||||
footer: Option<FooterFn>,
|
||||
content: Div,
|
||||
width: Pixels,
|
||||
max_width: Option<Pixels>,
|
||||
margin_top: Option<Pixels>,
|
||||
|
||||
on_close: OnClose,
|
||||
on_ok: OnOk,
|
||||
on_cancel: OnCancel,
|
||||
|
||||
overlay: bool,
|
||||
overlay_closable: bool,
|
||||
keyboard: bool,
|
||||
show_close: bool,
|
||||
button_props: ModalButtonProps,
|
||||
|
||||
/// This will be change when open the modal, the focus handle is create when open the modal.
|
||||
pub focus_handle: FocusHandle,
|
||||
pub layer_ix: usize,
|
||||
pub overlay_visible: bool,
|
||||
}
|
||||
|
||||
impl Modal {
|
||||
pub fn new(_window: &mut Window, cx: &mut App) -> Self {
|
||||
Self {
|
||||
style: StyleRefinement::default(),
|
||||
focus_handle: cx.focus_handle(),
|
||||
title: None,
|
||||
footer: None,
|
||||
content: v_flex(),
|
||||
margin_top: None,
|
||||
width: px(380.),
|
||||
max_width: None,
|
||||
overlay: true,
|
||||
keyboard: true,
|
||||
layer_ix: 0,
|
||||
overlay_visible: false,
|
||||
on_close: Rc::new(|_, _, _| {}),
|
||||
on_ok: None,
|
||||
on_cancel: Rc::new(|_, _, _| true),
|
||||
button_props: ModalButtonProps::default(),
|
||||
show_close: true,
|
||||
overlay_closable: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the title of the modal.
|
||||
pub fn title(mut self, title: impl IntoElement) -> Self {
|
||||
self.title = Some(title.into_any_element());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the footer of the modal.
|
||||
///
|
||||
/// The `footer` is a function that takes two `RenderButtonFn` and a `WindowContext` and returns a list of `AnyElement`.
|
||||
///
|
||||
/// - First `RenderButtonFn` is the render function for the OK button.
|
||||
/// - Second `RenderButtonFn` is the render function for the CANCEL button.
|
||||
///
|
||||
/// When you set the footer, the footer will be placed default footer buttons.
|
||||
pub fn footer<E, F>(mut self, footer: F) -> Self
|
||||
where
|
||||
E: IntoElement,
|
||||
F: Fn(RenderButtonFn, RenderButtonFn, &mut Window, &mut App) -> Vec<E> + 'static,
|
||||
{
|
||||
self.footer = Some(Box::new(move |ok, cancel, window, cx| {
|
||||
footer(ok, cancel, window, cx)
|
||||
.into_iter()
|
||||
.map(|e| e.into_any_element())
|
||||
.collect()
|
||||
}));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set to use confirm modal, with OK and Cancel buttons.
|
||||
///
|
||||
/// See also [`Self::alert`]
|
||||
pub fn confirm(self) -> Self {
|
||||
self.footer(|ok, cancel, window, cx| vec![cancel(window, cx), ok(window, cx)])
|
||||
.overlay_closable(false)
|
||||
.show_close(false)
|
||||
}
|
||||
|
||||
/// Set to as a alter modal, with OK button.
|
||||
///
|
||||
/// See also [`Self::confirm`]
|
||||
pub fn alert(self) -> Self {
|
||||
self.footer(|ok, _, window, cx| vec![ok(window, cx)])
|
||||
.overlay_closable(false)
|
||||
.show_close(false)
|
||||
}
|
||||
|
||||
/// Set the button props of the modal.
|
||||
pub fn button_props(mut self, button_props: ModalButtonProps) -> Self {
|
||||
self.button_props = button_props;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the callback for when the modal is closed.
|
||||
///
|
||||
/// Called after [`Self::on_ok`] or [`Self::on_cancel`] callback.
|
||||
pub fn on_close(
|
||||
mut self,
|
||||
on_close: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_close = Rc::new(on_close);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the callback for when the modal is has been confirmed.
|
||||
///
|
||||
/// The callback should return `true` to close the modal, if return `false` the modal will not be closed.
|
||||
pub fn on_ok(
|
||||
mut self,
|
||||
on_ok: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static,
|
||||
) -> Self {
|
||||
self.on_ok = Some(Rc::new(on_ok));
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the callback for when the modal is has been canceled.
|
||||
///
|
||||
/// The callback should return `true` to close the modal, if return `false` the modal will not be closed.
|
||||
pub fn on_cancel(
|
||||
mut self,
|
||||
on_cancel: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static,
|
||||
) -> Self {
|
||||
self.on_cancel = Rc::new(on_cancel);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the false to hide close icon, default: true
|
||||
pub fn show_close(mut self, show_close: bool) -> Self {
|
||||
self.show_close = show_close;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the top offset of the modal, defaults to None, will use the 1/10 of the viewport height.
|
||||
pub fn margin_top(mut self, margin_top: Pixels) -> Self {
|
||||
self.margin_top = Some(margin_top);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the width of the modal, defaults to 480px.
|
||||
pub fn width(mut self, width: Pixels) -> Self {
|
||||
self.width = width;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the maximum width of the modal, defaults to `None`.
|
||||
pub fn max_w(mut self, max_width: Pixels) -> Self {
|
||||
self.max_width = Some(max_width);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the overlay of the modal, defaults to `true`.
|
||||
pub fn overlay(mut self, overlay: bool) -> Self {
|
||||
self.overlay = overlay;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the overlay closable of the modal, defaults to `true`.
|
||||
///
|
||||
/// When the overlay is clicked, the modal will be closed.
|
||||
pub fn overlay_closable(mut self, overlay_closable: bool) -> Self {
|
||||
self.overlay_closable = overlay_closable;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set whether to support keyboard esc to close the modal, defaults to `true`.
|
||||
pub fn keyboard(mut self, keyboard: bool) -> Self {
|
||||
self.keyboard = keyboard;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn has_overlay(&self) -> bool {
|
||||
self.overlay
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for Modal {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.content.extend(elements);
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for Modal {
|
||||
fn style(&mut self) -> &mut gpui::StyleRefinement {
|
||||
&mut self.style
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Modal {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl gpui::IntoElement {
|
||||
let layer_ix = self.layer_ix;
|
||||
let on_close = self.on_close.clone();
|
||||
let on_ok = self.on_ok.clone();
|
||||
let on_cancel = self.on_cancel.clone();
|
||||
|
||||
let render_ok: RenderButtonFn = Box::new({
|
||||
let on_ok = on_ok.clone();
|
||||
let on_close = on_close.clone();
|
||||
let ok_variant = self.button_props.ok_variant;
|
||||
let ok_text = self.button_props.ok_text.unwrap_or_else(|| "OK".into());
|
||||
|
||||
move |_, _| {
|
||||
Button::new("ok")
|
||||
.label(ok_text)
|
||||
.with_variant(ok_variant)
|
||||
.small()
|
||||
.flex_1()
|
||||
.on_click({
|
||||
let on_ok = on_ok.clone();
|
||||
let on_close = on_close.clone();
|
||||
|
||||
move |_, window, cx| {
|
||||
if let Some(on_ok) = &on_ok
|
||||
&& !on_ok(&ClickEvent::default(), window, cx)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
on_close(&ClickEvent::default(), window, cx);
|
||||
window.close_modal(cx);
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
});
|
||||
|
||||
let render_cancel: RenderButtonFn = Box::new({
|
||||
let on_cancel = on_cancel.clone();
|
||||
let on_close = on_close.clone();
|
||||
let cancel_variant = self.button_props.cancel_variant;
|
||||
let cancel_text = self
|
||||
.button_props
|
||||
.cancel_text
|
||||
.unwrap_or_else(|| "Cancel".into());
|
||||
|
||||
move |_, _| {
|
||||
Button::new("cancel")
|
||||
.label(cancel_text)
|
||||
.with_variant(cancel_variant)
|
||||
.small()
|
||||
.flex_1()
|
||||
.on_click({
|
||||
let on_cancel = on_cancel.clone();
|
||||
let on_close = on_close.clone();
|
||||
move |_, window, cx| {
|
||||
if !on_cancel(&ClickEvent::default(), window, cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
on_close(&ClickEvent::default(), window, cx);
|
||||
window.close_modal(cx);
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
});
|
||||
|
||||
let window_paddings = crate::root::window_paddings(window, cx);
|
||||
let radius = cx.theme().radius_lg;
|
||||
|
||||
let view_size = window.viewport_size()
|
||||
- gpui::size(
|
||||
window_paddings.left + window_paddings.right,
|
||||
window_paddings.top + window_paddings.bottom,
|
||||
);
|
||||
|
||||
let bounds = Bounds {
|
||||
origin: Point::default(),
|
||||
size: view_size,
|
||||
};
|
||||
|
||||
let offset_top = px(layer_ix as f32 * 16.);
|
||||
let y = self.margin_top.unwrap_or(view_size.height / 10.) + offset_top;
|
||||
let x = bounds.center().x - self.width / 2.;
|
||||
|
||||
let mut padding_right = px(8.);
|
||||
let mut padding_left = px(8.);
|
||||
|
||||
if let Some(pl) = self.style.padding.left {
|
||||
padding_left = pl.to_pixels(self.width.into(), window.rem_size());
|
||||
}
|
||||
|
||||
if let Some(pr) = self.style.padding.right {
|
||||
padding_right = pr.to_pixels(self.width.into(), window.rem_size());
|
||||
}
|
||||
|
||||
let animation = Animation::new(Duration::from_secs_f64(0.25))
|
||||
.with_easing(cubic_bezier(0.32, 0.72, 0., 1.));
|
||||
|
||||
anchored()
|
||||
.position(point(window_paddings.left, window_paddings.top))
|
||||
.snap_to_window()
|
||||
.child(
|
||||
div()
|
||||
.id("modal")
|
||||
.w(view_size.width)
|
||||
.h(view_size.height)
|
||||
.when(self.overlay_visible, |this| {
|
||||
this.occlude().bg(cx.theme().overlay)
|
||||
})
|
||||
.when(self.overlay_closable, |this| {
|
||||
// Only the last modal owns the `mouse down - close modal` event.
|
||||
if (self.layer_ix + 1) != Root::read(window, cx).active_modals.len() {
|
||||
return this;
|
||||
}
|
||||
|
||||
this.on_mouse_down(MouseButton::Left, {
|
||||
let on_cancel = on_cancel.clone();
|
||||
let on_close = on_close.clone();
|
||||
move |_, window, cx| {
|
||||
on_cancel(&ClickEvent::default(), window, cx);
|
||||
on_close(&ClickEvent::default(), window, cx);
|
||||
window.close_modal(cx);
|
||||
}
|
||||
})
|
||||
})
|
||||
.child(
|
||||
v_flex()
|
||||
.id(layer_ix)
|
||||
.bg(cx.theme().background)
|
||||
.border_1()
|
||||
.border_color(cx.theme().border.alpha(0.4))
|
||||
.rounded(radius)
|
||||
.when(cx.theme().shadow, |this| this.shadow_xl())
|
||||
.min_h_24()
|
||||
.key_context(CONTEXT)
|
||||
.track_focus(&self.focus_handle)
|
||||
.refine_style(&self.style)
|
||||
.when(self.keyboard, |this| {
|
||||
this.on_action({
|
||||
let on_cancel = on_cancel.clone();
|
||||
let on_close = on_close.clone();
|
||||
move |_: &Cancel, window, cx| {
|
||||
// FIXME:
|
||||
//
|
||||
// Here some Modal have no focus_handle, so it will not work will Escape key.
|
||||
// But by now, we `cx.close_modal()` going to close the last active model, so the Escape is unexpected to work.
|
||||
on_cancel(&ClickEvent::default(), window, cx);
|
||||
on_close(&ClickEvent::default(), window, cx);
|
||||
window.close_modal(cx);
|
||||
}
|
||||
})
|
||||
.on_action({
|
||||
let on_ok = on_ok.clone();
|
||||
let on_close = on_close.clone();
|
||||
let has_footer = self.footer.is_some();
|
||||
move |_: &Confirm, window, cx| {
|
||||
if let Some(on_ok) = &on_ok {
|
||||
if on_ok(&ClickEvent::default(), window, cx) {
|
||||
on_close(&ClickEvent::default(), window, cx);
|
||||
window.close_modal(cx);
|
||||
}
|
||||
} else if has_footer {
|
||||
window.close_modal(cx);
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
// There style is high priority, can't be overridden.
|
||||
.absolute()
|
||||
.occlude()
|
||||
.relative()
|
||||
.left(x)
|
||||
.top(y)
|
||||
.w(self.width)
|
||||
.when_some(self.max_width, |this, w| this.max_w(w))
|
||||
.child(
|
||||
div()
|
||||
.px_2()
|
||||
.h_4()
|
||||
.w_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.when_some(self.title, |this, title| {
|
||||
this.h_10().font_semibold().text_center().child(title)
|
||||
}),
|
||||
)
|
||||
.when(self.show_close, |this| {
|
||||
this.child(
|
||||
Button::new("close")
|
||||
.icon(IconName::CloseCircleFill)
|
||||
.absolute()
|
||||
.top_1p5()
|
||||
.right_2()
|
||||
.custom(
|
||||
ButtonCustomVariant::new(window, cx)
|
||||
.foreground(cx.theme().icon_muted)
|
||||
.color(cx.theme().ghost_element_background)
|
||||
.hover(cx.theme().ghost_element_background)
|
||||
.active(cx.theme().ghost_element_background),
|
||||
)
|
||||
.on_click(move |_, window, cx| {
|
||||
on_cancel(&ClickEvent::default(), window, cx);
|
||||
on_close(&ClickEvent::default(), window, cx);
|
||||
window.close_modal(cx);
|
||||
}),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.pt_px()
|
||||
.w_full()
|
||||
.h_auto()
|
||||
.flex_1()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
v_flex()
|
||||
.pr(padding_right)
|
||||
.pl(padding_left)
|
||||
.size_full()
|
||||
.overflow_y_scrollbar()
|
||||
.child(self.content),
|
||||
),
|
||||
)
|
||||
.when_none(&self.footer, |this| this.child(div().pt(padding_left)))
|
||||
.when_some(self.footer, |this, footer| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.pt(padding_left)
|
||||
.pr(padding_right)
|
||||
.pb(padding_left)
|
||||
.pl(padding_right)
|
||||
.justify_end()
|
||||
.children(footer(render_ok, render_cancel, window, cx)),
|
||||
)
|
||||
})
|
||||
.with_animation("slide-down", animation.clone(), move |this, delta| {
|
||||
let y_offset = px(0.) + delta * px(30.);
|
||||
// This is equivalent to `shadow_xl` with an extra opacity.
|
||||
let shadow = vec![
|
||||
BoxShadow {
|
||||
color: hsla(0., 0., 0., 0.1 * delta),
|
||||
offset: point(px(0.), px(20.)),
|
||||
blur_radius: px(25.),
|
||||
spread_radius: px(-5.),
|
||||
inset: false,
|
||||
},
|
||||
BoxShadow {
|
||||
color: hsla(0., 0., 0., 0.1 * delta),
|
||||
offset: point(px(0.), px(8.)),
|
||||
blur_radius: px(10.),
|
||||
spread_radius: px(-6.),
|
||||
inset: false,
|
||||
},
|
||||
];
|
||||
this.top(y + y_offset).shadow(shadow)
|
||||
}),
|
||||
)
|
||||
.with_animation("fade-in", animation, move |this, delta| this.opacity(delta)),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,578 +0,0 @@
|
||||
use std::any::TypeId;
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
Anchor, Animation, AnimationExt, AnyElement, App, AppContext, ClickEvent, Context,
|
||||
DismissEvent, ElementId, Entity, EventEmitter, InteractiveElement as _, IntoElement,
|
||||
ParentElement as _, Render, SharedString, StatefulInteractiveElement, StyleRefinement, Styled,
|
||||
Subscription, Window, div, px, relative,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use crate::animation::cubic_bezier;
|
||||
use crate::button::{Button, ButtonVariants as _};
|
||||
use crate::{Icon, IconName, Sizable as _, Size, StyledExt, h_flex, v_flex};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub enum NotificationKind {
|
||||
#[default]
|
||||
Info,
|
||||
Success,
|
||||
Warning,
|
||||
Error,
|
||||
}
|
||||
|
||||
impl NotificationKind {
|
||||
fn icon(&self, cx: &App) -> Icon {
|
||||
match self {
|
||||
Self::Info => Icon::new(IconName::Info)
|
||||
.with_size(Size::Medium)
|
||||
.text_color(cx.theme().icon),
|
||||
Self::Success => Icon::new(IconName::CheckCircle)
|
||||
.with_size(Size::Medium)
|
||||
.text_color(cx.theme().icon_accent),
|
||||
Self::Warning => Icon::new(IconName::Warning)
|
||||
.with_size(Size::Medium)
|
||||
.text_color(cx.theme().text_warning),
|
||||
Self::Error => Icon::new(IconName::CloseCircle)
|
||||
.with_size(Size::Medium)
|
||||
.text_color(cx.theme().danger_foreground),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Hash, Eq)]
|
||||
pub(crate) enum NotificationId {
|
||||
Id(TypeId),
|
||||
IdAndElementId(TypeId, ElementId),
|
||||
}
|
||||
|
||||
impl From<TypeId> for NotificationId {
|
||||
fn from(type_id: TypeId) -> Self {
|
||||
Self::Id(type_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(TypeId, ElementId)> for NotificationId {
|
||||
fn from((type_id, id): (TypeId, ElementId)) -> Self {
|
||||
Self::IdAndElementId(type_id, id)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
/// A notification element.
|
||||
pub struct Notification {
|
||||
/// The id is used make the notification unique.
|
||||
/// Then you push a notification with the same id, the previous notification will be replaced.
|
||||
///
|
||||
/// None means the notification will be added to the end of the list.
|
||||
id: NotificationId,
|
||||
style: StyleRefinement,
|
||||
kind: Option<NotificationKind>,
|
||||
title: Option<SharedString>,
|
||||
message: Option<SharedString>,
|
||||
icon: Option<Icon>,
|
||||
autohide: bool,
|
||||
action_builder: Option<Rc<dyn Fn(&mut Self, &mut Window, &mut Context<Self>) -> Button>>,
|
||||
content_builder: Option<Rc<dyn Fn(&mut Self, &mut Window, &mut Context<Self>) -> AnyElement>>,
|
||||
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
|
||||
closing: bool,
|
||||
}
|
||||
|
||||
impl From<String> for Notification {
|
||||
fn from(s: String) -> Self {
|
||||
Self::new().message(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SharedString> for Notification {
|
||||
fn from(s: SharedString) -> Self {
|
||||
Self::new().message(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for Notification {
|
||||
fn from(s: &'static str) -> Self {
|
||||
Self::new().message(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(NotificationKind, &'static str)> for Notification {
|
||||
fn from((kind, content): (NotificationKind, &'static str)) -> Self {
|
||||
Self::new().message(content).with_kind(kind)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(NotificationKind, SharedString)> for Notification {
|
||||
fn from((kind, content): (NotificationKind, SharedString)) -> Self {
|
||||
Self::new().message(content).with_kind(kind)
|
||||
}
|
||||
}
|
||||
|
||||
struct DefaultIdType;
|
||||
|
||||
impl Notification {
|
||||
/// Create a new notification.
|
||||
///
|
||||
/// The default id is a random UUID.
|
||||
pub fn new() -> Self {
|
||||
let id: SharedString = uuid::Uuid::new_v4().to_string().into();
|
||||
let id = (TypeId::of::<DefaultIdType>(), id.into());
|
||||
|
||||
Self {
|
||||
id: id.into(),
|
||||
style: StyleRefinement::default(),
|
||||
title: None,
|
||||
message: None,
|
||||
kind: None,
|
||||
icon: None,
|
||||
autohide: true,
|
||||
action_builder: None,
|
||||
content_builder: None,
|
||||
on_click: None,
|
||||
closing: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the message of the notification, default is None.
|
||||
pub fn message(mut self, message: impl Into<SharedString>) -> Self {
|
||||
self.message = Some(message.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Create an info notification with the given message.
|
||||
pub fn info(message: impl Into<SharedString>) -> Self {
|
||||
Self::new()
|
||||
.message(message)
|
||||
.with_kind(NotificationKind::Info)
|
||||
}
|
||||
|
||||
/// Create a success notification with the given message.
|
||||
pub fn success(message: impl Into<SharedString>) -> Self {
|
||||
Self::new()
|
||||
.message(message)
|
||||
.with_kind(NotificationKind::Success)
|
||||
}
|
||||
|
||||
/// Create a warning notification with the given message.
|
||||
pub fn warning(message: impl Into<SharedString>) -> Self {
|
||||
Self::new()
|
||||
.message(message)
|
||||
.with_kind(NotificationKind::Warning)
|
||||
}
|
||||
|
||||
/// Create an error notification with the given message.
|
||||
pub fn error(message: impl Into<SharedString>) -> Self {
|
||||
Self::new()
|
||||
.message(message)
|
||||
.with_kind(NotificationKind::Error)
|
||||
}
|
||||
|
||||
/// Set the type for unique identification of the notification.
|
||||
///
|
||||
/// ```rs
|
||||
/// struct MyNotificationKind;
|
||||
/// let notification = Notification::new("Hello").id::<MyNotificationKind>();
|
||||
/// ```
|
||||
pub fn id<T: Sized + 'static>(mut self) -> Self {
|
||||
self.id = TypeId::of::<T>().into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the type and id of the notification, used to uniquely identify the notification.
|
||||
pub fn type_id<T: Sized + 'static>(mut self, key: impl Into<ElementId>) -> Self {
|
||||
self.id = (TypeId::of::<T>(), key.into()).into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the title of the notification, default is None.
|
||||
///
|
||||
/// If title is None, the notification will not have a title.
|
||||
pub fn title(mut self, title: impl Into<SharedString>) -> Self {
|
||||
self.title = Some(title.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the icon of the notification.
|
||||
///
|
||||
/// If icon is None, the notification will use the default icon of the type.
|
||||
pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
|
||||
self.icon = Some(icon.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the type of the notification, default is NotificationType::Info.
|
||||
pub fn with_kind(mut self, kind: NotificationKind) -> Self {
|
||||
self.kind = Some(kind);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the auto hide of the notification, default is true.
|
||||
pub fn autohide(mut self, autohide: bool) -> Self {
|
||||
self.autohide = autohide;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the click callback of the notification.
|
||||
pub fn on_click(
|
||||
mut self,
|
||||
on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_click = Some(Rc::new(on_click));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the action button of the notification.
|
||||
///
|
||||
/// When an action is set, the notification will not autohide.
|
||||
pub fn action<F>(mut self, action: F) -> Self
|
||||
where
|
||||
F: Fn(&mut Self, &mut Window, &mut Context<Self>) -> Button + 'static,
|
||||
{
|
||||
self.action_builder = Some(Rc::new(action));
|
||||
self.autohide = false;
|
||||
self
|
||||
}
|
||||
|
||||
/// Dismiss the notification.
|
||||
pub fn dismiss(&mut self, _: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.closing {
|
||||
return;
|
||||
}
|
||||
self.closing = true;
|
||||
cx.notify();
|
||||
|
||||
// Dismiss the notification after 0.15s to show the animation.
|
||||
cx.spawn(async move |view, cx| {
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_secs_f32(0.15))
|
||||
.await;
|
||||
|
||||
cx.update(|cx| {
|
||||
if let Some(view) = view.upgrade() {
|
||||
view.update(cx, |view, cx| {
|
||||
view.closing = false;
|
||||
cx.emit(DismissEvent);
|
||||
});
|
||||
}
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
/// Set the content of the notification.
|
||||
pub fn content(
|
||||
mut self,
|
||||
content: impl Fn(&mut Self, &mut Window, &mut Context<Self>) -> AnyElement + 'static,
|
||||
) -> Self {
|
||||
self.content_builder = Some(Rc::new(content));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Notification {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for Notification {}
|
||||
|
||||
impl FluentBuilder for Notification {}
|
||||
|
||||
impl Styled for Notification {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
&mut self.style
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Notification {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let content = self
|
||||
.content_builder
|
||||
.clone()
|
||||
.map(|builder| builder(self, window, cx));
|
||||
|
||||
let action = self.action_builder.clone().map(|builder| {
|
||||
builder(self, window, cx)
|
||||
.xsmall()
|
||||
.primary()
|
||||
.px_3()
|
||||
.font_semibold()
|
||||
});
|
||||
|
||||
let icon = match self.kind {
|
||||
None => self.icon.clone(),
|
||||
Some(kind) => Some(kind.icon(cx)),
|
||||
};
|
||||
|
||||
let background = match self.kind {
|
||||
Some(NotificationKind::Error) => cx.theme().danger_background,
|
||||
_ => cx.theme().surface_background,
|
||||
};
|
||||
|
||||
let text_color = match self.kind {
|
||||
Some(NotificationKind::Error) => cx.theme().danger_foreground,
|
||||
_ => cx.theme().text,
|
||||
};
|
||||
|
||||
let closing = self.closing;
|
||||
let has_title = self.title.is_some();
|
||||
let only_message = !has_title && content.is_none() && action.is_none();
|
||||
let placement = cx.theme().notification.placement;
|
||||
|
||||
h_flex()
|
||||
.id("notification")
|
||||
.group("")
|
||||
.occlude()
|
||||
.relative()
|
||||
.w_112()
|
||||
.border_1()
|
||||
.border_color(cx.theme().border)
|
||||
.bg(background)
|
||||
.text_color(text_color)
|
||||
.rounded(cx.theme().radius_lg)
|
||||
.when(cx.theme().shadow, |this| this.shadow_md())
|
||||
.p_2()
|
||||
.gap_2()
|
||||
.justify_start()
|
||||
.items_start()
|
||||
.when(only_message, |this| this.items_center())
|
||||
.refine_style(&self.style)
|
||||
.when_some(icon, |this, icon| {
|
||||
this.child(div().flex_shrink_0().size_5().child(icon))
|
||||
})
|
||||
.child(
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.gap_1()
|
||||
.overflow_hidden()
|
||||
.when_some(self.title.clone(), |this, title| {
|
||||
this.child(h_flex().h_5().text_sm().font_semibold().child(title))
|
||||
})
|
||||
.when_some(self.message.clone(), |this, message| {
|
||||
this.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.when(has_title, |this| this.text_color(cx.theme().text_muted))
|
||||
.line_height(relative(1.3))
|
||||
.child(message),
|
||||
)
|
||||
})
|
||||
.when_some(content, |this, content| this.child(content))
|
||||
.when_some(action, |this, action| {
|
||||
this.gap_2()
|
||||
.child(h_flex().w_full().flex_1().justify_end().child(action))
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.top(px(6.5))
|
||||
.right(px(6.5))
|
||||
.invisible()
|
||||
.group_hover("", |this| this.visible())
|
||||
.child(
|
||||
Button::new("close")
|
||||
.icon(IconName::Close)
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.on_click(cx.listener(move |this, _ev, window, cx| {
|
||||
this.dismiss(window, cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
.when_some(self.on_click.clone(), |this, on_click| {
|
||||
this.on_click(cx.listener(move |view, event, window, cx| {
|
||||
view.dismiss(window, cx);
|
||||
on_click(event, window, cx);
|
||||
}))
|
||||
})
|
||||
.on_aux_click(cx.listener(move |view, event: &ClickEvent, window, cx| {
|
||||
if event.is_middle_click() {
|
||||
view.dismiss(window, cx);
|
||||
}
|
||||
}))
|
||||
.with_animation(
|
||||
ElementId::NamedInteger("slide-down".into(), closing as u64),
|
||||
Animation::new(Duration::from_secs_f64(0.25))
|
||||
.with_easing(cubic_bezier(0.4, 0., 0.2, 1.)),
|
||||
move |this, delta| {
|
||||
if closing {
|
||||
let opacity = 1. - delta;
|
||||
let that = this
|
||||
.shadow_none()
|
||||
.opacity(opacity)
|
||||
.when(opacity < 0.85, |this| this.shadow_none());
|
||||
match placement {
|
||||
Anchor::TopRight | Anchor::BottomRight => {
|
||||
let x_offset = px(0.) + delta * px(45.);
|
||||
that.left(px(0.) + x_offset)
|
||||
}
|
||||
Anchor::TopLeft | Anchor::BottomLeft => {
|
||||
let x_offset = px(0.) - delta * px(45.);
|
||||
that.left(px(0.) + x_offset)
|
||||
}
|
||||
Anchor::TopCenter => {
|
||||
let y_offset = px(0.) - delta * px(45.);
|
||||
that.top(px(0.) + y_offset)
|
||||
}
|
||||
Anchor::BottomCenter => {
|
||||
let y_offset = px(0.) + delta * px(45.);
|
||||
that.top(px(0.) + y_offset)
|
||||
}
|
||||
_ => that,
|
||||
}
|
||||
} else {
|
||||
let opacity = delta;
|
||||
let y_offset = match placement {
|
||||
Anchor::TopLeft | Anchor::TopRight | Anchor::TopCenter => {
|
||||
px(-45.) + delta * px(45.)
|
||||
}
|
||||
Anchor::BottomLeft | Anchor::BottomRight | Anchor::BottomCenter => {
|
||||
px(45.) - delta * px(45.)
|
||||
}
|
||||
_ => px(0.),
|
||||
};
|
||||
this.top(px(0.) + y_offset)
|
||||
.opacity(opacity)
|
||||
.when(opacity < 0.85, |this| this.shadow_none())
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// A list of notifications.
|
||||
pub struct NotificationList {
|
||||
/// Notifications that will be auto hidden.
|
||||
pub(crate) notifications: VecDeque<Entity<Notification>>,
|
||||
|
||||
/// Whether the notification list is expanded.
|
||||
expanded: bool,
|
||||
|
||||
/// Subscriptions
|
||||
_subscriptions: HashMap<NotificationId, Subscription>,
|
||||
}
|
||||
|
||||
impl NotificationList {
|
||||
pub fn new(_window: &mut Window, _cx: &mut Context<Self>) -> Self {
|
||||
Self {
|
||||
notifications: VecDeque::new(),
|
||||
expanded: false,
|
||||
_subscriptions: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(
|
||||
&mut self,
|
||||
notification: impl Into<Notification>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let notification = notification.into();
|
||||
let id = notification.id.clone();
|
||||
let autohide = notification.autohide;
|
||||
|
||||
// Remove the notification by id, for keep unique.
|
||||
self.notifications.retain(|note| note.read(cx).id != id);
|
||||
|
||||
let notification = cx.new(|_| notification);
|
||||
|
||||
self._subscriptions.insert(
|
||||
id.clone(),
|
||||
cx.subscribe(¬ification, move |view, _, _: &DismissEvent, cx| {
|
||||
view.notifications.retain(|note| id != note.read(cx).id);
|
||||
view._subscriptions.remove(&id);
|
||||
}),
|
||||
);
|
||||
|
||||
self.notifications.push_back(notification.clone());
|
||||
|
||||
if autohide {
|
||||
// Sleep for 5 seconds to autohide the notification
|
||||
cx.spawn_in(window, async move |_this, cx| {
|
||||
cx.background_executor().timer(Duration::from_secs(5)).await;
|
||||
|
||||
if let Err(err) =
|
||||
notification.update_in(cx, |note, window, cx| note.dismiss(window, cx))
|
||||
{
|
||||
log::error!("failed to auto hide notification: {:?}", err);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub(crate) fn close(
|
||||
&mut self,
|
||||
id: impl Into<NotificationId>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let id: NotificationId = id.into();
|
||||
if let Some(n) = self.notifications.iter().find(|n| n.read(cx).id == id) {
|
||||
n.update(cx, |note, cx| note.dismiss(window, cx))
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn clear(&mut self, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.notifications.clear();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn notifications(&self) -> Vec<Entity<Notification>> {
|
||||
self.notifications.iter().cloned().collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for NotificationList {
|
||||
fn render(
|
||||
&mut self,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let size = window.viewport_size();
|
||||
let items = self.notifications.iter().rev().take(10).rev().cloned();
|
||||
|
||||
let placement = cx.theme().notification.placement;
|
||||
let margins = &cx.theme().notification.margins;
|
||||
|
||||
v_flex()
|
||||
.id("notification-list")
|
||||
.max_h(size.height)
|
||||
.pt(margins.top)
|
||||
.pb(margins.bottom)
|
||||
.gap_3()
|
||||
.when(
|
||||
matches!(placement, Anchor::TopRight),
|
||||
|this| this.pr(margins.right), // ignore left
|
||||
)
|
||||
.when(
|
||||
matches!(placement, Anchor::TopLeft),
|
||||
|this| this.pl(margins.left), // ignore right
|
||||
)
|
||||
.when(
|
||||
matches!(placement, Anchor::BottomLeft),
|
||||
|this| this.flex_col_reverse().pl(margins.left), // ignore right
|
||||
)
|
||||
.when(
|
||||
matches!(placement, Anchor::BottomRight),
|
||||
|this| this.flex_col_reverse().pr(margins.right), // ignore left
|
||||
)
|
||||
.when(matches!(placement, Anchor::BottomCenter), |this| {
|
||||
this.flex_col_reverse()
|
||||
})
|
||||
.on_hover(cx.listener(|view, hovered, _, cx| {
|
||||
view.expanded = *hovered;
|
||||
cx.notify()
|
||||
}))
|
||||
.children(items)
|
||||
}
|
||||
}
|
||||
@@ -1,432 +0,0 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
Anchor, AnyElement, App, Bounds, Context, Deferred, DismissEvent, Div, ElementId, EventEmitter,
|
||||
FocusHandle, Focusable, InteractiveElement as _, IntoElement, KeyBinding, MouseButton,
|
||||
ParentElement, Pixels, Point, Render, RenderOnce, Stateful, StyleRefinement, Styled,
|
||||
Subscription, Window, anchored, deferred, div, px,
|
||||
};
|
||||
|
||||
use crate::actions::Cancel;
|
||||
use crate::{ElementExt, Selectable, StyledExt as _, v_flex};
|
||||
|
||||
const CONTEXT: &str = "Popover";
|
||||
|
||||
pub(crate) fn init(cx: &mut App) {
|
||||
cx.bind_keys([KeyBinding::new("escape", Cancel, Some(CONTEXT))])
|
||||
}
|
||||
|
||||
/// A popover element that can be triggered by a button or any other element.
|
||||
#[derive(IntoElement)]
|
||||
pub struct Popover {
|
||||
id: ElementId,
|
||||
style: StyleRefinement,
|
||||
anchor: Anchor,
|
||||
default_open: bool,
|
||||
open: Option<bool>,
|
||||
tracked_focus_handle: Option<FocusHandle>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
trigger: Option<Box<dyn FnOnce(bool, &Window, &App) -> AnyElement + 'static>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
content: Option<
|
||||
Rc<
|
||||
dyn Fn(&mut PopoverState, &mut Window, &mut Context<PopoverState>) -> AnyElement
|
||||
+ 'static,
|
||||
>,
|
||||
>,
|
||||
children: Vec<AnyElement>,
|
||||
/// Style for trigger element.
|
||||
/// This is used for hotfix the trigger element style to support w_full.
|
||||
trigger_style: Option<StyleRefinement>,
|
||||
mouse_button: MouseButton,
|
||||
appearance: bool,
|
||||
overlay_closable: bool,
|
||||
#[allow(clippy::type_complexity)]
|
||||
on_open_change: Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>,
|
||||
}
|
||||
|
||||
impl Popover {
|
||||
/// Create a new Popover with `view` mode.
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
style: StyleRefinement::default(),
|
||||
anchor: Anchor::TopLeft,
|
||||
trigger: None,
|
||||
trigger_style: None,
|
||||
content: None,
|
||||
tracked_focus_handle: None,
|
||||
children: vec![],
|
||||
mouse_button: MouseButton::Left,
|
||||
appearance: true,
|
||||
overlay_closable: true,
|
||||
default_open: false,
|
||||
open: None,
|
||||
on_open_change: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the anchor corner of the popover, default is `Corner::TopLeft`.
|
||||
///
|
||||
/// This method is kept for backward compatibility with `Corner` type.
|
||||
/// Internally, it converts `Corner` to `Anchor`.
|
||||
pub fn anchor(mut self, anchor: impl Into<Anchor>) -> Self {
|
||||
self.anchor = anchor.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the mouse button to trigger the popover, default is `MouseButton::Left`.
|
||||
pub fn mouse_button(mut self, mouse_button: MouseButton) -> Self {
|
||||
self.mouse_button = mouse_button;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the trigger element of the popover.
|
||||
pub fn trigger<T>(mut self, trigger: T) -> Self
|
||||
where
|
||||
T: Selectable + IntoElement + 'static,
|
||||
{
|
||||
self.trigger = Some(Box::new(|is_open, _, _| {
|
||||
let selected = trigger.is_selected();
|
||||
trigger.selected(selected || is_open).into_any_element()
|
||||
}));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the default open state of the popover, default is `false`.
|
||||
///
|
||||
/// This is only used to initialize the open state of the popover.
|
||||
///
|
||||
/// And please note that if you use the `open` method, this value will be ignored.
|
||||
pub fn default_open(mut self, open: bool) -> Self {
|
||||
self.default_open = open;
|
||||
self
|
||||
}
|
||||
|
||||
/// Force set the open state of the popover.
|
||||
///
|
||||
/// If this is set, the popover will be controlled by this value.
|
||||
///
|
||||
/// NOTE: You must be used in conjunction with `on_open_change` to handle state changes.
|
||||
pub fn open(mut self, open: bool) -> Self {
|
||||
self.open = Some(open);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a callback to be called when the open state changes.
|
||||
///
|
||||
/// The first `&bool` parameter is the **new open state**.
|
||||
///
|
||||
/// This is useful when using the `open` method to control the popover state.
|
||||
pub fn on_open_change<F>(mut self, callback: F) -> Self
|
||||
where
|
||||
F: Fn(&bool, &mut Window, &mut App) + 'static,
|
||||
{
|
||||
self.on_open_change = Some(Rc::new(callback));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the style for the trigger element.
|
||||
pub fn trigger_style(mut self, style: StyleRefinement) -> Self {
|
||||
self.trigger_style = Some(style);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set whether clicking outside the popover will dismiss it, default is `true`.
|
||||
pub fn overlay_closable(mut self, closable: bool) -> Self {
|
||||
self.overlay_closable = closable;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the content builder for content of the Popover.
|
||||
///
|
||||
/// This callback will called every time on render the popover.
|
||||
/// So, you should avoid creating new elements or entities in the content closure.
|
||||
pub fn content<F, E>(mut self, content: F) -> Self
|
||||
where
|
||||
E: IntoElement,
|
||||
F: Fn(&mut PopoverState, &mut Window, &mut Context<PopoverState>) -> E + 'static,
|
||||
{
|
||||
self.content = Some(Rc::new(move |state, window, cx| {
|
||||
content(state, window, cx).into_any_element()
|
||||
}));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set whether the popover no style, default is `false`.
|
||||
///
|
||||
/// If no style:
|
||||
///
|
||||
/// - The popover will not have a bg, border, shadow, or padding.
|
||||
/// - The click out of the popover will not dismiss it.
|
||||
pub fn appearance(mut self, appearance: bool) -> Self {
|
||||
self.appearance = appearance;
|
||||
self
|
||||
}
|
||||
|
||||
/// Bind the focus handle to receive focus when the popover is opened.
|
||||
/// If you not set this, a new focus handle will be created for the popover to
|
||||
///
|
||||
/// If popover is opened, the focus will be moved to the focus handle.
|
||||
pub fn track_focus(mut self, handle: &FocusHandle) -> Self {
|
||||
self.tracked_focus_handle = Some(handle.clone());
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn resolved_corner(anchor: Anchor, trigger_bounds: Bounds<Pixels>) -> Point<Pixels> {
|
||||
match anchor {
|
||||
Anchor::TopLeft => trigger_bounds.origin,
|
||||
Anchor::TopCenter => trigger_bounds.top_center(),
|
||||
Anchor::TopRight => trigger_bounds.top_right(),
|
||||
Anchor::BottomLeft => Point {
|
||||
x: trigger_bounds.origin.x,
|
||||
y: trigger_bounds.origin.y - trigger_bounds.size.height,
|
||||
},
|
||||
Anchor::BottomCenter => Point {
|
||||
x: trigger_bounds.top_center().x,
|
||||
y: trigger_bounds.origin.y - trigger_bounds.size.height,
|
||||
},
|
||||
Anchor::BottomRight => Point {
|
||||
x: trigger_bounds.top_right().x,
|
||||
y: trigger_bounds.origin.y - trigger_bounds.size.height,
|
||||
},
|
||||
// Fallback for LeftCenter/RightCenter – adjust as needed.
|
||||
_ => trigger_bounds.origin,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for Popover {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements);
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for Popover {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
&mut self.style
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PopoverState {
|
||||
focus_handle: FocusHandle,
|
||||
pub(crate) tracked_focus_handle: Option<FocusHandle>,
|
||||
trigger_bounds: Bounds<Pixels>,
|
||||
open: bool,
|
||||
#[allow(clippy::type_complexity)]
|
||||
on_open_change: Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>,
|
||||
|
||||
_dismiss_subscription: Option<Subscription>,
|
||||
}
|
||||
|
||||
impl PopoverState {
|
||||
pub fn new(default_open: bool, cx: &mut App) -> Self {
|
||||
Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
tracked_focus_handle: None,
|
||||
trigger_bounds: Bounds::default(),
|
||||
open: default_open,
|
||||
on_open_change: None,
|
||||
_dismiss_subscription: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the popover is open.
|
||||
pub fn is_open(&self) -> bool {
|
||||
self.open
|
||||
}
|
||||
|
||||
/// Dismiss the popover if it is open.
|
||||
pub fn dismiss(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.open {
|
||||
self.toggle_open(window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Open the popover if it is closed.
|
||||
pub fn show(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if !self.open {
|
||||
self.toggle_open(window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_open(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.open = !self.open;
|
||||
if self.open {
|
||||
let state = cx.entity();
|
||||
let focus_handle = if let Some(tracked_focus_handle) = self.tracked_focus_handle.clone()
|
||||
{
|
||||
tracked_focus_handle
|
||||
} else {
|
||||
self.focus_handle.clone()
|
||||
};
|
||||
focus_handle.focus(window, cx);
|
||||
|
||||
self._dismiss_subscription =
|
||||
Some(
|
||||
window.subscribe(&cx.entity(), cx, move |_, _: &DismissEvent, window, cx| {
|
||||
state.update(cx, |state, cx| {
|
||||
state.dismiss(window, cx);
|
||||
});
|
||||
window.refresh();
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
self._dismiss_subscription = None;
|
||||
}
|
||||
|
||||
if let Some(callback) = self.on_open_change.as_ref() {
|
||||
callback(&self.open, window, cx);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn on_action_cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.dismiss(window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for PopoverState {
|
||||
fn focus_handle(&self, _: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for PopoverState {
|
||||
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for PopoverState {}
|
||||
|
||||
impl Popover {
|
||||
pub(crate) fn render_popover<E>(
|
||||
anchor: Anchor,
|
||||
trigger_bounds: Bounds<Pixels>,
|
||||
content: E,
|
||||
_: &mut Window,
|
||||
_: &mut App,
|
||||
) -> Deferred
|
||||
where
|
||||
E: IntoElement + 'static,
|
||||
{
|
||||
deferred(
|
||||
anchored()
|
||||
.snap_to_window_with_margin(px(8.))
|
||||
.anchor(anchor)
|
||||
.position(Self::resolved_corner(anchor, trigger_bounds))
|
||||
.child(div().relative().child(content)),
|
||||
)
|
||||
.with_priority(1)
|
||||
}
|
||||
|
||||
pub(crate) fn render_popover_content(
|
||||
anchor: Anchor,
|
||||
appearance: bool,
|
||||
_: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Stateful<Div> {
|
||||
v_flex()
|
||||
.id("content")
|
||||
.occlude()
|
||||
.tab_group()
|
||||
.when(appearance, |this| this.popover_style(cx).p_3())
|
||||
.map(|this| match anchor {
|
||||
Anchor::TopLeft | Anchor::TopCenter | Anchor::TopRight => this.top_1(),
|
||||
Anchor::BottomLeft | Anchor::BottomCenter | Anchor::BottomRight => this.bottom_1(),
|
||||
Anchor::LeftCenter | Anchor::RightCenter => this.top_1(), // Fallback for centered
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Popover {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let force_open = self.open;
|
||||
let default_open = self.default_open;
|
||||
let tracked_focus_handle = self.tracked_focus_handle.clone();
|
||||
let state = window.use_keyed_state(self.id.clone(), cx, |_, cx| {
|
||||
PopoverState::new(default_open, cx)
|
||||
});
|
||||
|
||||
state.update(cx, |state, _| {
|
||||
if let Some(tracked_focus_handle) = tracked_focus_handle {
|
||||
state.tracked_focus_handle = Some(tracked_focus_handle);
|
||||
}
|
||||
state.on_open_change = self.on_open_change.clone();
|
||||
if let Some(force_open) = force_open {
|
||||
state.open = force_open;
|
||||
}
|
||||
});
|
||||
|
||||
let open = state.read(cx).open;
|
||||
let focus_handle = state.read(cx).focus_handle.clone();
|
||||
let trigger_bounds = state.read(cx).trigger_bounds;
|
||||
|
||||
let Some(trigger) = self.trigger else {
|
||||
return div().id("empty");
|
||||
};
|
||||
|
||||
let parent_view_id = window.current_view();
|
||||
|
||||
let el = div()
|
||||
.id(self.id)
|
||||
.child((trigger)(open, window, cx))
|
||||
.on_mouse_down(self.mouse_button, {
|
||||
let state = state.clone();
|
||||
move |_, window, cx| {
|
||||
cx.stop_propagation();
|
||||
state.update(cx, |state, cx| {
|
||||
// We force set open to false to toggle it correctly.
|
||||
// Because if the mouse down out will toggle open first.
|
||||
state.open = open;
|
||||
state.toggle_open(window, cx);
|
||||
});
|
||||
cx.notify(parent_view_id);
|
||||
}
|
||||
})
|
||||
.on_prepaint({
|
||||
let state = state.clone();
|
||||
move |bounds, _, cx| {
|
||||
state.update(cx, |state, _| {
|
||||
state.trigger_bounds = bounds;
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
if !open {
|
||||
return el;
|
||||
}
|
||||
|
||||
let popover_content =
|
||||
Self::render_popover_content(self.anchor, self.appearance, window, cx)
|
||||
.track_focus(&focus_handle)
|
||||
.key_context(CONTEXT)
|
||||
.on_action(window.listener_for(&state, PopoverState::on_action_cancel))
|
||||
.when_some(self.content, |this, content| {
|
||||
this.child(state.update(cx, |state, cx| (content)(state, window, cx)))
|
||||
})
|
||||
.children(self.children)
|
||||
.when(self.overlay_closable, |this| {
|
||||
this.on_mouse_down_out({
|
||||
let state = state.clone();
|
||||
move |_, window, cx| {
|
||||
state.update(cx, |state, cx| {
|
||||
state.dismiss(window, cx);
|
||||
});
|
||||
cx.notify(parent_view_id);
|
||||
}
|
||||
})
|
||||
})
|
||||
.refine_style(&self.style);
|
||||
|
||||
el.child(Self::render_popover(
|
||||
self.anchor,
|
||||
trigger_bounds,
|
||||
popover_content,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -1,294 +0,0 @@
|
||||
use std::ops::Range;
|
||||
|
||||
use gpui::{
|
||||
Along, App, Axis, Bounds, Context, ElementId, EventEmitter, IsZero, Pixels, Window, px,
|
||||
};
|
||||
|
||||
mod panel;
|
||||
mod resize_handle;
|
||||
pub use panel::*;
|
||||
pub(crate) use resize_handle::*;
|
||||
|
||||
pub(crate) const PANEL_MIN_SIZE: Pixels = px(100.);
|
||||
|
||||
/// Create a [`ResizablePanelGroup`] with horizontal resizing
|
||||
pub fn h_resizable(id: impl Into<ElementId>) -> ResizablePanelGroup {
|
||||
ResizablePanelGroup::new(id).axis(Axis::Horizontal)
|
||||
}
|
||||
|
||||
/// Create a [`ResizablePanelGroup`] with vertical resizing
|
||||
pub fn v_resizable(id: impl Into<ElementId>) -> ResizablePanelGroup {
|
||||
ResizablePanelGroup::new(id).axis(Axis::Vertical)
|
||||
}
|
||||
|
||||
/// Create a [`ResizablePanel`].
|
||||
pub fn resizable_panel() -> ResizablePanel {
|
||||
ResizablePanel::new()
|
||||
}
|
||||
|
||||
/// State for a [`ResizablePanel`]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ResizableState {
|
||||
/// The `axis` will sync to actual axis of the ResizablePanelGroup in use.
|
||||
axis: Axis,
|
||||
panels: Vec<ResizablePanelState>,
|
||||
sizes: Vec<Pixels>,
|
||||
pub(crate) resizing_panel_ix: Option<usize>,
|
||||
bounds: Bounds<Pixels>,
|
||||
}
|
||||
|
||||
impl Default for ResizableState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
axis: Axis::Horizontal,
|
||||
panels: vec![],
|
||||
sizes: vec![],
|
||||
resizing_panel_ix: None,
|
||||
bounds: Bounds::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ResizableState {
|
||||
/// Get the size of the panels.
|
||||
pub fn sizes(&self) -> &Vec<Pixels> {
|
||||
&self.sizes
|
||||
}
|
||||
|
||||
pub(crate) fn insert_panel(
|
||||
&mut self,
|
||||
size: Option<Pixels>,
|
||||
ix: Option<usize>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let panel_state = ResizablePanelState {
|
||||
size,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let size = size.unwrap_or(PANEL_MIN_SIZE);
|
||||
|
||||
// We make sure that the size always sums up to the container size
|
||||
// by reducing the size of all other panels first.
|
||||
let container_size = self.container_size().max(px(1.));
|
||||
let total_leftover_size = (container_size - size).max(px(1.));
|
||||
|
||||
for (i, panel) in self.panels.iter_mut().enumerate() {
|
||||
let ratio = self.sizes[i] / container_size;
|
||||
self.sizes[i] = total_leftover_size * ratio;
|
||||
panel.size = Some(self.sizes[i]);
|
||||
}
|
||||
|
||||
if let Some(ix) = ix {
|
||||
self.panels.insert(ix, panel_state);
|
||||
self.sizes.insert(ix, size);
|
||||
} else {
|
||||
self.panels.push(panel_state);
|
||||
self.sizes.push(size);
|
||||
};
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub(crate) fn sync_panels_count(
|
||||
&mut self,
|
||||
axis: Axis,
|
||||
panels_count: usize,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let mut changed = self.axis != axis;
|
||||
self.axis = axis;
|
||||
|
||||
if panels_count > self.panels.len() {
|
||||
let diff = panels_count - self.panels.len();
|
||||
self.panels
|
||||
.extend(vec![ResizablePanelState::default(); diff]);
|
||||
self.sizes.extend(vec![PANEL_MIN_SIZE; diff]);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if panels_count < self.panels.len() {
|
||||
self.panels.truncate(panels_count);
|
||||
self.sizes.truncate(panels_count);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if changed {
|
||||
// We need to make sure the total size is in line with the container size.
|
||||
self.adjust_to_container_size(cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn update_panel_size(
|
||||
&mut self,
|
||||
panel_ix: usize,
|
||||
bounds: Bounds<Pixels>,
|
||||
size_range: Range<Pixels>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let size = bounds.size.along(self.axis);
|
||||
// This check is only necessary to stop the very first panel from resizing on its own
|
||||
// it needs to be passed when the panel is freshly created so we get the initial size,
|
||||
// but its also fine when it sometimes passes later.
|
||||
if self.sizes[panel_ix].to_f64() == PANEL_MIN_SIZE.to_f64() {
|
||||
self.sizes[panel_ix] = size;
|
||||
self.panels[panel_ix].size = Some(size);
|
||||
}
|
||||
self.panels[panel_ix].bounds = bounds;
|
||||
self.panels[panel_ix].size_range = size_range;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub(crate) fn remove_panel(&mut self, panel_ix: usize, cx: &mut Context<Self>) {
|
||||
self.panels.remove(panel_ix);
|
||||
self.sizes.remove(panel_ix);
|
||||
if let Some(resizing_panel_ix) = self.resizing_panel_ix
|
||||
&& resizing_panel_ix > panel_ix
|
||||
{
|
||||
self.resizing_panel_ix = Some(resizing_panel_ix - 1);
|
||||
}
|
||||
self.adjust_to_container_size(cx);
|
||||
}
|
||||
|
||||
pub(crate) fn replace_panel(
|
||||
&mut self,
|
||||
panel_ix: usize,
|
||||
panel: ResizablePanelState,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let old_size = self.sizes[panel_ix];
|
||||
|
||||
self.panels[panel_ix] = panel;
|
||||
self.sizes[panel_ix] = old_size;
|
||||
self.adjust_to_container_size(cx);
|
||||
}
|
||||
|
||||
pub(crate) fn clear(&mut self) {
|
||||
self.panels.clear();
|
||||
self.sizes.clear();
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn container_size(&self) -> Pixels {
|
||||
self.bounds.size.along(self.axis)
|
||||
}
|
||||
|
||||
pub(crate) fn done_resizing(&mut self, cx: &mut Context<Self>) {
|
||||
self.resizing_panel_ix = None;
|
||||
cx.emit(ResizablePanelEvent::Resized);
|
||||
}
|
||||
|
||||
fn panel_size_range(&self, ix: usize) -> Range<Pixels> {
|
||||
let Some(panel) = self.panels.get(ix) else {
|
||||
return PANEL_MIN_SIZE..Pixels::MAX;
|
||||
};
|
||||
|
||||
panel.size_range.clone()
|
||||
}
|
||||
|
||||
fn sync_real_panel_sizes(&mut self, _: &App) {
|
||||
for (i, panel) in self.panels.iter().enumerate() {
|
||||
self.sizes[i] = panel.bounds.size.along(self.axis);
|
||||
}
|
||||
}
|
||||
|
||||
/// The `ix`` is the index of the panel to resize,
|
||||
/// and the `size` is the new size for the panel.
|
||||
fn resize_panel(&mut self, ix: usize, size: Pixels, _: &mut Window, cx: &mut Context<Self>) {
|
||||
let old_sizes = self.sizes.clone();
|
||||
|
||||
let mut ix = ix;
|
||||
// Only resize the left panels.
|
||||
if ix >= old_sizes.len() - 1 {
|
||||
return;
|
||||
}
|
||||
let container_size = self.container_size();
|
||||
self.sync_real_panel_sizes(cx);
|
||||
|
||||
let move_changed = size - old_sizes[ix];
|
||||
if move_changed == px(0.) {
|
||||
return;
|
||||
}
|
||||
|
||||
let size_range = self.panel_size_range(ix);
|
||||
let new_size = size.clamp(size_range.start, size_range.end);
|
||||
let is_expand = move_changed > px(0.);
|
||||
|
||||
let main_ix = ix;
|
||||
let mut new_sizes = old_sizes.clone();
|
||||
|
||||
if is_expand {
|
||||
let mut changed = new_size - old_sizes[ix];
|
||||
new_sizes[ix] = new_size;
|
||||
|
||||
while changed > px(0.) && ix < old_sizes.len() - 1 {
|
||||
ix += 1;
|
||||
let size_range = self.panel_size_range(ix);
|
||||
let available_size = (new_sizes[ix] - size_range.start).max(px(0.));
|
||||
let to_reduce = changed.min(available_size);
|
||||
new_sizes[ix] -= to_reduce;
|
||||
changed -= to_reduce;
|
||||
}
|
||||
} else {
|
||||
let mut changed = new_size - size;
|
||||
new_sizes[ix] = new_size;
|
||||
|
||||
while changed > px(0.) && ix > 0 {
|
||||
ix -= 1;
|
||||
let size_range = self.panel_size_range(ix);
|
||||
let available_size = (new_sizes[ix] - size_range.start).max(px(0.));
|
||||
let to_reduce = changed.min(available_size);
|
||||
changed -= to_reduce;
|
||||
new_sizes[ix] -= to_reduce;
|
||||
}
|
||||
|
||||
new_sizes[main_ix + 1] += old_sizes[main_ix] - size - changed;
|
||||
}
|
||||
|
||||
let total_size: Pixels = new_sizes.iter().map(|s| s.to_f64()).sum::<f64>().into();
|
||||
|
||||
// If total size exceeds container size, adjust the main panel
|
||||
if total_size > container_size {
|
||||
let overflow = total_size - container_size;
|
||||
new_sizes[main_ix] = (new_sizes[main_ix] - overflow).max(size_range.start);
|
||||
}
|
||||
|
||||
for (i, _) in old_sizes.iter().enumerate() {
|
||||
let size = new_sizes[i];
|
||||
self.panels[i].size = Some(size);
|
||||
}
|
||||
self.sizes = new_sizes;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Adjust panel sizes according to the container size.
|
||||
///
|
||||
/// When the container size changes, the panels should take up the same percentage as they did before.
|
||||
fn adjust_to_container_size(&mut self, cx: &mut Context<Self>) {
|
||||
if self.container_size().is_zero() {
|
||||
return;
|
||||
}
|
||||
|
||||
let container_size = self.container_size();
|
||||
let total_size = px(self.sizes.iter().map(f32::from).sum::<f32>());
|
||||
|
||||
for i in 0..self.panels.len() {
|
||||
let size = self.sizes[i];
|
||||
let ratio = size / total_size;
|
||||
let new_size = container_size * ratio;
|
||||
|
||||
self.sizes[i] = new_size;
|
||||
self.panels[i].size = Some(new_size);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<ResizablePanelEvent> for ResizableState {}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub(crate) struct ResizablePanelState {
|
||||
pub size: Option<Pixels>,
|
||||
pub size_range: Range<Pixels>,
|
||||
bounds: Bounds<Pixels>,
|
||||
}
|
||||
@@ -1,408 +0,0 @@
|
||||
use std::ops::{Deref, Range};
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
Along, AnyElement, App, AppContext, Axis, Bounds, Context, Element, ElementId, Empty, Entity,
|
||||
EventEmitter, InteractiveElement as _, IntoElement, IsZero as _, MouseMoveEvent, MouseUpEvent,
|
||||
ParentElement, Pixels, Render, RenderOnce, Style, Styled, Window, div,
|
||||
};
|
||||
use theme::AxisExt;
|
||||
|
||||
use super::{ResizableState, resizable_panel, resize_handle};
|
||||
use crate::resizable::PANEL_MIN_SIZE;
|
||||
use crate::{ElementExt, h_flex, v_flex};
|
||||
|
||||
pub enum ResizablePanelEvent {
|
||||
Resized,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct DragPanel;
|
||||
impl Render for DragPanel {
|
||||
fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
|
||||
Empty
|
||||
}
|
||||
}
|
||||
|
||||
/// A group of resizable panels.
|
||||
#[allow(clippy::type_complexity)]
|
||||
#[derive(IntoElement)]
|
||||
pub struct ResizablePanelGroup {
|
||||
id: ElementId,
|
||||
state: Option<Entity<ResizableState>>,
|
||||
axis: Axis,
|
||||
size: Option<Pixels>,
|
||||
children: Vec<ResizablePanel>,
|
||||
on_resize: Rc<dyn Fn(&Entity<ResizableState>, &mut Window, &mut App)>,
|
||||
}
|
||||
|
||||
impl ResizablePanelGroup {
|
||||
/// Create a new resizable panel group.
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
axis: Axis::Horizontal,
|
||||
children: vec![],
|
||||
state: None,
|
||||
size: None,
|
||||
on_resize: Rc::new(|_, _, _| {}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Bind yourself to a resizable state entity.
|
||||
///
|
||||
/// If not provided, it will handle its own state internally.
|
||||
pub fn with_state(mut self, state: &Entity<ResizableState>) -> Self {
|
||||
self.state = Some(state.clone());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the axis of the resizable panel group, default is horizontal.
|
||||
pub fn axis(mut self, axis: Axis) -> Self {
|
||||
self.axis = axis;
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a panel to the group.
|
||||
///
|
||||
/// - The `axis` will be set to the same axis as the group.
|
||||
/// - The `initial_size` will be set to the average size of all panels if not provided.
|
||||
/// - The `group` will be set to the group entity.
|
||||
pub fn child(mut self, panel: impl Into<ResizablePanel>) -> Self {
|
||||
self.children.push(panel.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Add multiple panels to the group.
|
||||
pub fn children<I>(mut self, panels: impl IntoIterator<Item = I>) -> Self
|
||||
where
|
||||
I: Into<ResizablePanel>,
|
||||
{
|
||||
self.children = panels.into_iter().map(|panel| panel.into()).collect();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set size of the resizable panel group
|
||||
///
|
||||
/// - When the axis is horizontal, the size is the height of the group.
|
||||
/// - When the axis is vertical, the size is the width of the group.
|
||||
pub fn size(mut self, size: Pixels) -> Self {
|
||||
self.size = Some(size);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the callback to be called when the panels are resized.
|
||||
///
|
||||
/// ## Callback arguments
|
||||
///
|
||||
/// - Entity<ResizableState>: The state of the ResizablePanelGroup.
|
||||
pub fn on_resize(
|
||||
mut self,
|
||||
on_resize: impl Fn(&Entity<ResizableState>, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_resize = Rc::new(on_resize);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for ResizablePanel
|
||||
where
|
||||
T: Into<AnyElement>,
|
||||
{
|
||||
fn from(value: T) -> Self {
|
||||
resizable_panel().child(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ResizablePanelGroup> for ResizablePanel {
|
||||
fn from(value: ResizablePanelGroup) -> Self {
|
||||
resizable_panel().child(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<ResizablePanelEvent> for ResizablePanelGroup {}
|
||||
|
||||
impl RenderOnce for ResizablePanelGroup {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let state = self.state.unwrap_or(
|
||||
window.use_keyed_state(self.id.clone(), cx, |_, _| ResizableState::default()),
|
||||
);
|
||||
let container = if self.axis.is_horizontal() {
|
||||
h_flex()
|
||||
} else {
|
||||
v_flex()
|
||||
};
|
||||
|
||||
// Sync panels to the state
|
||||
let panels_count = self.children.len();
|
||||
state.update(cx, |state, cx| {
|
||||
state.sync_panels_count(self.axis, panels_count, cx);
|
||||
});
|
||||
|
||||
container
|
||||
.id(self.id)
|
||||
.size_full()
|
||||
.children(
|
||||
self.children
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(ix, mut panel)| {
|
||||
panel.panel_ix = ix;
|
||||
panel.axis = self.axis;
|
||||
panel.state = Some(state.clone());
|
||||
panel
|
||||
}),
|
||||
)
|
||||
.on_prepaint({
|
||||
let state = state.clone();
|
||||
move |bounds, _, cx| {
|
||||
state.update(cx, |state, cx| {
|
||||
let size_changed =
|
||||
state.bounds.size.along(self.axis) != bounds.size.along(self.axis);
|
||||
|
||||
state.bounds = bounds;
|
||||
|
||||
if size_changed {
|
||||
state.adjust_to_container_size(cx);
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
.child(ResizePanelGroupElement {
|
||||
state: state.clone(),
|
||||
axis: self.axis,
|
||||
on_resize: self.on_resize.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A resizable panel inside a [`ResizablePanelGroup`].
|
||||
#[derive(IntoElement)]
|
||||
pub struct ResizablePanel {
|
||||
axis: Axis,
|
||||
panel_ix: usize,
|
||||
state: Option<Entity<ResizableState>>,
|
||||
/// Initial size is the size that the panel has when it is created.
|
||||
initial_size: Option<Pixels>,
|
||||
/// size range limit of this panel.
|
||||
size_range: Range<Pixels>,
|
||||
children: Vec<AnyElement>,
|
||||
visible: bool,
|
||||
}
|
||||
|
||||
impl ResizablePanel {
|
||||
/// Create a new resizable panel.
|
||||
pub(super) fn new() -> Self {
|
||||
Self {
|
||||
panel_ix: 0,
|
||||
initial_size: None,
|
||||
state: None,
|
||||
size_range: (PANEL_MIN_SIZE..Pixels::MAX),
|
||||
axis: Axis::Horizontal,
|
||||
children: vec![],
|
||||
visible: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the visibility of the panel, default is true.
|
||||
pub fn visible(mut self, visible: bool) -> Self {
|
||||
self.visible = visible;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the initial size of the panel.
|
||||
pub fn size(mut self, size: impl Into<Pixels>) -> Self {
|
||||
self.initial_size = Some(size.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the size range to limit panel resize.
|
||||
///
|
||||
/// Default is [`PANEL_MIN_SIZE`] to [`Pixels::MAX`].
|
||||
pub fn size_range(mut self, range: impl Into<Range<Pixels>>) -> Self {
|
||||
self.size_range = range.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for ResizablePanel {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements);
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ResizablePanel {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
if !self.visible {
|
||||
return div().id(("resizable-panel", self.panel_ix));
|
||||
}
|
||||
|
||||
let state = self
|
||||
.state
|
||||
.expect("BUG: The `state` in ResizablePanel should be present.");
|
||||
|
||||
let panel_state = state
|
||||
.read(cx)
|
||||
.panels
|
||||
.get(self.panel_ix)
|
||||
.expect("BUG: The `index` of ResizablePanel should be one of in `state`.");
|
||||
|
||||
let size_range = self.size_range.clone();
|
||||
|
||||
div()
|
||||
.id(("resizable-panel", self.panel_ix))
|
||||
.flex()
|
||||
.flex_grow_1()
|
||||
.size_full()
|
||||
.relative()
|
||||
.when(self.axis.is_vertical(), |this| {
|
||||
this.min_h(size_range.start).max_h(size_range.end)
|
||||
})
|
||||
.when(self.axis.is_horizontal(), |this| {
|
||||
this.min_w(size_range.start).max_w(size_range.end)
|
||||
})
|
||||
// 1. initial_size is None, to use auto size.
|
||||
// 2. initial_size is Some and size is none, to use the initial size of the panel for first time render.
|
||||
// 3. initial_size is Some and size is Some, use `size`.
|
||||
.when(self.initial_size.is_none(), |this| this.flex_shrink_1())
|
||||
.when_some(self.initial_size, |this, initial_size| {
|
||||
// The `self.size` is None, that mean the initial size for the panel,
|
||||
// so we need set `flex_shrink_0` To let it keep the initial size.
|
||||
this.when(
|
||||
panel_state.size.is_none() && !initial_size.is_zero(),
|
||||
|this| this.flex_none(),
|
||||
)
|
||||
.flex_basis(initial_size)
|
||||
})
|
||||
.map(|this| match panel_state.size {
|
||||
Some(size) => this.flex_basis(size.min(size_range.end).max(size_range.start)),
|
||||
None => this,
|
||||
})
|
||||
.on_prepaint({
|
||||
let state = state.clone();
|
||||
move |bounds, _, cx| {
|
||||
state.update(cx, |state, cx| {
|
||||
state.update_panel_size(self.panel_ix, bounds, self.size_range, cx)
|
||||
})
|
||||
}
|
||||
})
|
||||
.children(self.children)
|
||||
.when(self.panel_ix > 0, |this| {
|
||||
let ix = self.panel_ix - 1;
|
||||
this.child(resize_handle(("resizable-handle", ix), self.axis).on_drag(
|
||||
DragPanel,
|
||||
move |drag_panel, _, _, cx| {
|
||||
cx.stop_propagation();
|
||||
// Set current resizing panel ix
|
||||
state.update(cx, |state, _| {
|
||||
state.resizing_panel_ix = Some(ix);
|
||||
});
|
||||
cx.new(|_| drag_panel.deref().clone())
|
||||
},
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
struct ResizePanelGroupElement {
|
||||
state: Entity<ResizableState>,
|
||||
on_resize: Rc<dyn Fn(&Entity<ResizableState>, &mut Window, &mut App)>,
|
||||
axis: Axis,
|
||||
}
|
||||
|
||||
impl IntoElement for ResizePanelGroupElement {
|
||||
type Element = Self;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for ResizePanelGroupElement {
|
||||
type PrepaintState = ();
|
||||
type RequestLayoutState = ();
|
||||
|
||||
fn id(&self) -> Option<gpui::ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
_: Option<&gpui::GlobalElementId>,
|
||||
_: Option<&gpui::InspectorElementId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (gpui::LayoutId, Self::RequestLayoutState) {
|
||||
(window.request_layout(Style::default(), None, cx), ())
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_: Option<&gpui::GlobalElementId>,
|
||||
_: Option<&gpui::InspectorElementId>,
|
||||
_: Bounds<Pixels>,
|
||||
_: &mut Self::RequestLayoutState,
|
||||
_window: &mut Window,
|
||||
_cx: &mut App,
|
||||
) -> Self::PrepaintState {
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_: Option<&gpui::GlobalElementId>,
|
||||
_: Option<&gpui::InspectorElementId>,
|
||||
_: Bounds<Pixels>,
|
||||
_: &mut Self::RequestLayoutState,
|
||||
_: &mut Self::PrepaintState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
window.on_mouse_event({
|
||||
let state = self.state.clone();
|
||||
let axis = self.axis;
|
||||
let current_ix = state.read(cx).resizing_panel_ix;
|
||||
move |e: &MouseMoveEvent, phase, window, cx| {
|
||||
if !phase.bubble() {
|
||||
return;
|
||||
}
|
||||
let Some(ix) = current_ix else { return };
|
||||
|
||||
state.update(cx, |state, cx| {
|
||||
let panel = state.panels.get(ix).expect("BUG: invalid panel index");
|
||||
|
||||
match axis {
|
||||
Axis::Horizontal => {
|
||||
state.resize_panel(ix, e.position.x - panel.bounds.left(), window, cx)
|
||||
}
|
||||
Axis::Vertical => {
|
||||
state.resize_panel(ix, e.position.y - panel.bounds.top(), window, cx);
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
// When any mouse up, stop dragging
|
||||
window.on_mouse_event({
|
||||
let state = self.state.clone();
|
||||
let current_ix = state.read(cx).resizing_panel_ix;
|
||||
let on_resize = self.on_resize.clone();
|
||||
move |_: &MouseUpEvent, phase, window, cx| {
|
||||
if current_ix.is_none() {
|
||||
return;
|
||||
}
|
||||
if phase.bubble() {
|
||||
state.update(cx, |state, cx| state.done_resizing(cx));
|
||||
on_resize(&state, window, cx);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,226 +0,0 @@
|
||||
use std::cell::Cell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
AnyElement, App, Axis, Element, ElementId, Entity, GlobalElementId, InteractiveElement,
|
||||
IntoElement, MouseDownEvent, MouseUpEvent, ParentElement as _, Pixels, Point, Render,
|
||||
StatefulInteractiveElement, Styled as _, Window, div, px,
|
||||
};
|
||||
use theme::{ActiveTheme, AxisExt};
|
||||
|
||||
use crate::dock::DockPlacement;
|
||||
|
||||
pub(crate) const HANDLE_PADDING: Pixels = px(4.);
|
||||
pub(crate) const HANDLE_SIZE: Pixels = px(1.);
|
||||
|
||||
/// Create a resize handle for a resizable panel.
|
||||
pub(crate) fn resize_handle<T: 'static, E: 'static + Render>(
|
||||
id: impl Into<ElementId>,
|
||||
axis: Axis,
|
||||
) -> ResizeHandle<T, E> {
|
||||
ResizeHandle::new(id, axis)
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub(crate) struct ResizeHandle<T: 'static, E: 'static + Render> {
|
||||
id: ElementId,
|
||||
axis: Axis,
|
||||
drag_value: Option<Rc<T>>,
|
||||
placement: Option<DockPlacement>,
|
||||
on_drag: Option<Rc<dyn Fn(&Point<Pixels>, &mut Window, &mut App) -> Entity<E>>>,
|
||||
}
|
||||
|
||||
impl<T: 'static, E: 'static + Render> ResizeHandle<T, E> {
|
||||
fn new(id: impl Into<ElementId>, axis: Axis) -> Self {
|
||||
let id = id.into();
|
||||
Self {
|
||||
id: id.clone(),
|
||||
on_drag: None,
|
||||
drag_value: None,
|
||||
placement: None,
|
||||
axis,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn on_drag(
|
||||
mut self,
|
||||
value: T,
|
||||
f: impl Fn(Rc<T>, &Point<Pixels>, &mut Window, &mut App) -> Entity<E> + 'static,
|
||||
) -> Self {
|
||||
let value = Rc::new(value);
|
||||
self.drag_value = Some(value.clone());
|
||||
self.on_drag = Some(Rc::new(move |p, window, cx| {
|
||||
f(value.clone(), p, window, cx)
|
||||
}));
|
||||
self
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn placement(mut self, placement: DockPlacement) -> Self {
|
||||
self.placement = Some(placement);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
struct ResizeHandleState {
|
||||
active: Cell<bool>,
|
||||
}
|
||||
|
||||
impl ResizeHandleState {
|
||||
fn set_active(&self, active: bool) {
|
||||
self.active.set(active);
|
||||
}
|
||||
|
||||
fn is_active(&self) -> bool {
|
||||
self.active.get()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static, E: 'static + Render> IntoElement for ResizeHandle<T, E> {
|
||||
type Element = ResizeHandle<T, E>;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static, E: 'static + Render> Element for ResizeHandle<T, E> {
|
||||
type PrepaintState = ();
|
||||
type RequestLayoutState = AnyElement;
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
Some(self.id.clone())
|
||||
}
|
||||
|
||||
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
id: Option<&GlobalElementId>,
|
||||
_: Option<&gpui::InspectorElementId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (gpui::LayoutId, Self::RequestLayoutState) {
|
||||
let neg_offset = -HANDLE_PADDING;
|
||||
let axis = self.axis;
|
||||
|
||||
window.with_element_state(id.unwrap(), |state, window| {
|
||||
let state = state.unwrap_or(ResizeHandleState::default());
|
||||
|
||||
let bg_color = if state.is_active() {
|
||||
cx.theme().border_selected
|
||||
} else {
|
||||
cx.theme().border
|
||||
};
|
||||
|
||||
let mut el = div()
|
||||
.id(self.id.clone())
|
||||
.occlude()
|
||||
.absolute()
|
||||
.flex_shrink_0()
|
||||
.group("handle")
|
||||
.when_some(self.on_drag.clone(), |this, on_drag| {
|
||||
this.on_drag(
|
||||
self.drag_value.clone().unwrap(),
|
||||
move |_, position, window, cx| on_drag(&position, window, cx),
|
||||
)
|
||||
})
|
||||
.map(|this| match self.placement {
|
||||
Some(DockPlacement::Left) => {
|
||||
// Special for Left Dock
|
||||
// FIXME: Improve this to let the scroll bar have px(HANDLE_PADDING)
|
||||
this.cursor_col_resize()
|
||||
.top_0()
|
||||
.right(px(1.))
|
||||
.h_full()
|
||||
.w(HANDLE_SIZE)
|
||||
.pl(HANDLE_PADDING)
|
||||
}
|
||||
_ => this
|
||||
.when(axis.is_horizontal(), |this| {
|
||||
this.cursor_col_resize()
|
||||
.top_0()
|
||||
.left(neg_offset)
|
||||
.h_full()
|
||||
.w(HANDLE_SIZE)
|
||||
.px(HANDLE_PADDING)
|
||||
})
|
||||
.when(axis.is_vertical(), |this| {
|
||||
this.cursor_row_resize()
|
||||
.top(neg_offset)
|
||||
.left_0()
|
||||
.w_full()
|
||||
.h(HANDLE_SIZE)
|
||||
.py(HANDLE_PADDING)
|
||||
}),
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.bg(bg_color)
|
||||
.group_hover("handle", |this| this.bg(bg_color))
|
||||
.when(axis.is_horizontal(), |this| this.h_full().w(HANDLE_SIZE))
|
||||
.when(axis.is_vertical(), |this| this.w_full().h(HANDLE_SIZE)),
|
||||
)
|
||||
.into_any_element();
|
||||
|
||||
let layout_id = el.request_layout(window, cx);
|
||||
|
||||
((layout_id, el), state)
|
||||
})
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_: Option<&GlobalElementId>,
|
||||
_: Option<&gpui::InspectorElementId>,
|
||||
_: gpui::Bounds<Pixels>,
|
||||
request_layout: &mut Self::RequestLayoutState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Self::PrepaintState {
|
||||
request_layout.prepaint(window, cx);
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
id: Option<&GlobalElementId>,
|
||||
_: Option<&gpui::InspectorElementId>,
|
||||
bounds: gpui::Bounds<Pixels>,
|
||||
request_layout: &mut Self::RequestLayoutState,
|
||||
_: &mut Self::PrepaintState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
request_layout.paint(window, cx);
|
||||
|
||||
window.with_element_state(id.unwrap(), |state: Option<ResizeHandleState>, window| {
|
||||
let state = state.unwrap_or_default();
|
||||
|
||||
window.on_mouse_event({
|
||||
let state = state.clone();
|
||||
move |ev: &MouseDownEvent, phase, window, _| {
|
||||
if bounds.contains(&ev.position) && phase.bubble() {
|
||||
state.set_active(true);
|
||||
window.refresh();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
window.on_mouse_event({
|
||||
let state = state.clone();
|
||||
move |_: &MouseUpEvent, _, window, _| {
|
||||
if state.is_active() {
|
||||
state.set_active(false);
|
||||
window.refresh();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
((), state)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,483 +0,0 @@
|
||||
use std::any::TypeId;
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
AnyView, App, AppContext, Bounds, Context, CursorStyle, Decorations, Edges, ElementId, Entity,
|
||||
FocusHandle, HitboxBehavior, Hsla, InteractiveElement, IntoElement, MouseButton,
|
||||
ParentElement as _, Pixels, Point, Render, ResizeEdge, Size, Styled, Tiling, WeakFocusHandle,
|
||||
Window, canvas, div, point, px, size,
|
||||
};
|
||||
use theme::{
|
||||
ActiveTheme, CLIENT_SIDE_DECORATION_BORDER, CLIENT_SIDE_DECORATION_ROUNDING,
|
||||
CLIENT_SIDE_DECORATION_SHADOW,
|
||||
};
|
||||
|
||||
use crate::input::InputState;
|
||||
use crate::modal::Modal;
|
||||
use crate::notification::{Notification, NotificationList};
|
||||
|
||||
#[derive(Clone)]
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub struct ActiveModal {
|
||||
focus_handle: FocusHandle,
|
||||
/// The previous focused handle before opening the modal.
|
||||
previous_focused_handle: Option<WeakFocusHandle>,
|
||||
builder: Rc<dyn Fn(Modal, &mut Window, &mut App) -> Modal + 'static>,
|
||||
}
|
||||
|
||||
impl ActiveModal {
|
||||
fn new(
|
||||
focus_handle: FocusHandle,
|
||||
previous_focused_handle: Option<WeakFocusHandle>,
|
||||
builder: impl Fn(Modal, &mut Window, &mut App) -> Modal + 'static,
|
||||
) -> Self {
|
||||
Self {
|
||||
focus_handle,
|
||||
previous_focused_handle,
|
||||
builder: Rc::new(builder),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Root is a view for the App window for as the top level view (Must be the first view in the window).
|
||||
///
|
||||
/// It is used to manage the Modal, and Notification.
|
||||
pub struct Root {
|
||||
/// All active models
|
||||
pub(crate) active_modals: Vec<ActiveModal>,
|
||||
|
||||
/// Notification layer
|
||||
pub(crate) notification: Entity<NotificationList>,
|
||||
|
||||
/// Current focused input
|
||||
pub(crate) focused_input: Option<Entity<InputState>>,
|
||||
|
||||
/// App view
|
||||
view: AnyView,
|
||||
}
|
||||
|
||||
impl Root {
|
||||
pub fn new(view: AnyView, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
Self {
|
||||
focused_input: None,
|
||||
active_modals: Vec::new(),
|
||||
notification: cx.new(|cx| NotificationList::new(window, cx)),
|
||||
view,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update<F>(window: &mut Window, cx: &mut App, f: F)
|
||||
where
|
||||
F: FnOnce(&mut Self, &mut Window, &mut Context<Self>) + 'static,
|
||||
{
|
||||
if let Some(Some(root)) = window.root::<Root>() {
|
||||
root.update(cx, |root, cx| f(root, window, cx));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read<'a>(window: &'a mut Window, cx: &'a mut App) -> &'a Self {
|
||||
window
|
||||
.root::<Root>()
|
||||
.expect("The window root view should be of type `ui::Root`.")
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
}
|
||||
|
||||
pub fn view(&self) -> &AnyView {
|
||||
&self.view
|
||||
}
|
||||
|
||||
/// Render the notification layer.
|
||||
pub fn render_notification_layer(
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<impl IntoElement + use<>> {
|
||||
let root = window.root::<Root>()??;
|
||||
|
||||
Some(
|
||||
div()
|
||||
.absolute()
|
||||
.top_0()
|
||||
.right_0()
|
||||
.child(root.read(cx).notification.clone()),
|
||||
)
|
||||
}
|
||||
|
||||
/// Render the modal layer.
|
||||
pub fn render_modal_layer(
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<impl IntoElement + use<>> {
|
||||
let root = window.root::<Root>()??;
|
||||
let active_modals = root.read(cx).active_modals.clone();
|
||||
|
||||
if active_modals.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut show_overlay_ix = None;
|
||||
|
||||
let mut modals = active_modals
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, active_modal)| {
|
||||
let mut modal = Modal::new(window, cx);
|
||||
|
||||
modal = (active_modal.builder)(modal, window, cx);
|
||||
|
||||
// Give the modal the focus handle, because `modal` is a temporary value, is not possible to
|
||||
// keep the focus handle in the modal.
|
||||
//
|
||||
// So we keep the focus handle in the `active_modal`, this is owned by the `Root`.
|
||||
modal.focus_handle = active_modal.focus_handle.clone();
|
||||
|
||||
modal.layer_ix = i;
|
||||
// Find the modal which one needs to show overlay.
|
||||
if modal.has_overlay() {
|
||||
show_overlay_ix = Some(i);
|
||||
}
|
||||
|
||||
modal
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if let Some(ix) = show_overlay_ix
|
||||
&& let Some(modal) = modals.get_mut(ix)
|
||||
{
|
||||
modal.overlay_visible = true;
|
||||
}
|
||||
|
||||
Some(div().children(modals))
|
||||
}
|
||||
|
||||
/// Open a modal.
|
||||
pub fn open_modal<F>(&mut self, builder: F, window: &mut Window, cx: &mut Context<'_, Self>)
|
||||
where
|
||||
F: Fn(Modal, &mut Window, &mut App) -> Modal + 'static,
|
||||
{
|
||||
let previous_focused_handle = window.focused(cx).map(|h| h.downgrade());
|
||||
let focus_handle = cx.focus_handle();
|
||||
focus_handle.focus(window, cx);
|
||||
|
||||
self.active_modals.push(ActiveModal::new(
|
||||
focus_handle,
|
||||
previous_focused_handle,
|
||||
builder,
|
||||
));
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Close the topmost modal.
|
||||
pub fn close_modal(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.focused_input = None;
|
||||
|
||||
if let Some(handle) = self
|
||||
.active_modals
|
||||
.pop()
|
||||
.and_then(|d| d.previous_focused_handle)
|
||||
.and_then(|h| h.upgrade())
|
||||
{
|
||||
window.focus(&handle, cx);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Close all modals.
|
||||
pub fn close_all_modals(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.focused_input = None;
|
||||
self.active_modals.clear();
|
||||
|
||||
let previous_focused_handle = self
|
||||
.active_modals
|
||||
.first()
|
||||
.and_then(|d| d.previous_focused_handle.clone());
|
||||
|
||||
if let Some(handle) = previous_focused_handle.and_then(|h| h.upgrade()) {
|
||||
window.focus(&handle, cx);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Check if there are any active modals.
|
||||
pub fn has_active_modals(&self) -> bool {
|
||||
!self.active_modals.is_empty()
|
||||
}
|
||||
|
||||
/// Push a notification to the notification layer.
|
||||
pub fn push_notification<T>(&mut self, note: T, window: &mut Window, cx: &mut Context<'_, Root>)
|
||||
where
|
||||
T: Into<Notification>,
|
||||
{
|
||||
self.notification
|
||||
.update(cx, |view, cx| view.push(note, window, cx));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Clear a notification by its type.
|
||||
pub fn clear_notification<T: Sized + 'static>(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<'_, Root>,
|
||||
) {
|
||||
self.notification.update(cx, |view, cx| {
|
||||
let id = TypeId::of::<T>();
|
||||
view.close(id, window, cx);
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Clear a notification by its type.
|
||||
pub fn clear_notification_by_id<T: Sized + 'static>(
|
||||
&mut self,
|
||||
key: impl Into<ElementId>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<'_, Root>,
|
||||
) {
|
||||
self.notification.update(cx, |view, cx| {
|
||||
let id = (TypeId::of::<T>(), key.into());
|
||||
view.close(id, window, cx);
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Clear all notifications from the notification layer.
|
||||
pub fn clear_notifications(&mut self, window: &mut Window, cx: &mut Context<'_, Root>) {
|
||||
self.notification
|
||||
.update(cx, |view, cx| view.clear(window, cx));
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Root {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let rem_size = cx.theme().font_size;
|
||||
let font_family = cx.theme().font_family.clone();
|
||||
let decorations = window.window_decorations();
|
||||
|
||||
// Set the base font size
|
||||
window.set_rem_size(rem_size);
|
||||
|
||||
// Set the client inset (linux only)
|
||||
match decorations {
|
||||
Decorations::Client { .. } => window.set_client_inset(CLIENT_SIDE_DECORATION_SHADOW),
|
||||
Decorations::Server => window.set_client_inset(px(0.0)),
|
||||
}
|
||||
|
||||
div()
|
||||
.id("window")
|
||||
.size_full()
|
||||
.map(|div| match decorations {
|
||||
Decorations::Server => div,
|
||||
Decorations::Client { tiling } => div
|
||||
.bg(gpui::transparent_black())
|
||||
.child(
|
||||
canvas(
|
||||
|_bounds, window, _cx| {
|
||||
window.insert_hitbox(
|
||||
Bounds::new(
|
||||
point(px(0.0), px(0.0)),
|
||||
window.window_bounds().get_bounds().size,
|
||||
),
|
||||
HitboxBehavior::Normal,
|
||||
)
|
||||
},
|
||||
move |_bounds, hitbox, window, _cx| {
|
||||
let mouse = window.mouse_position();
|
||||
let size = window.window_bounds().get_bounds().size;
|
||||
|
||||
let Some(edge) =
|
||||
resize_edge(mouse, CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
window.set_cursor_style(
|
||||
match edge {
|
||||
ResizeEdge::Top | ResizeEdge::Bottom => {
|
||||
CursorStyle::ResizeUpDown
|
||||
}
|
||||
ResizeEdge::Left | ResizeEdge::Right => {
|
||||
CursorStyle::ResizeLeftRight
|
||||
}
|
||||
ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
|
||||
CursorStyle::ResizeUpLeftDownRight
|
||||
}
|
||||
ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
|
||||
CursorStyle::ResizeUpRightDownLeft
|
||||
}
|
||||
},
|
||||
&hitbox,
|
||||
);
|
||||
},
|
||||
)
|
||||
.size_full()
|
||||
.absolute(),
|
||||
)
|
||||
.when(!(tiling.top || tiling.right), |div| {
|
||||
div.rounded_tr(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
})
|
||||
.when(!(tiling.top || tiling.left), |div| {
|
||||
div.rounded_tl(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
})
|
||||
.when(!(tiling.bottom || tiling.right), |div| {
|
||||
div.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
})
|
||||
.when(!(tiling.bottom || tiling.left), |div| {
|
||||
div.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
})
|
||||
.when(!tiling.top, |div| div.pt(CLIENT_SIDE_DECORATION_SHADOW))
|
||||
.when(!tiling.bottom, |div| div.pb(CLIENT_SIDE_DECORATION_SHADOW))
|
||||
.when(!tiling.left, |div| div.pl(CLIENT_SIDE_DECORATION_SHADOW))
|
||||
.when(!tiling.right, |div| div.pr(CLIENT_SIDE_DECORATION_SHADOW))
|
||||
.on_mouse_down(MouseButton::Left, move |e, window, _cx| {
|
||||
let size = window.window_bounds().get_bounds().size;
|
||||
let pos = e.position;
|
||||
|
||||
if let Some(edge) =
|
||||
resize_edge(pos, CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
|
||||
{
|
||||
window.start_window_resize(edge)
|
||||
};
|
||||
}),
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.map(|div| match decorations {
|
||||
Decorations::Server => div,
|
||||
Decorations::Client { tiling } => div
|
||||
.border_color(cx.theme().border)
|
||||
.when(!(tiling.top || tiling.right), |div| {
|
||||
div.rounded_tr(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
})
|
||||
.when(!(tiling.top || tiling.left), |div| {
|
||||
div.rounded_tl(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
})
|
||||
.when(!(tiling.bottom || tiling.right), |div| {
|
||||
div.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
})
|
||||
.when(!(tiling.bottom || tiling.left), |div| {
|
||||
div.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
})
|
||||
.when(!tiling.top, |div| {
|
||||
div.border_t(CLIENT_SIDE_DECORATION_BORDER)
|
||||
})
|
||||
.when(!tiling.bottom, |div| {
|
||||
div.border_b(CLIENT_SIDE_DECORATION_BORDER)
|
||||
})
|
||||
.when(!tiling.left, |div| {
|
||||
div.border_l(CLIENT_SIDE_DECORATION_BORDER)
|
||||
})
|
||||
.when(!tiling.right, |div| {
|
||||
div.border_r(CLIENT_SIDE_DECORATION_BORDER)
|
||||
})
|
||||
.when(!tiling.is_tiled(), |div| {
|
||||
div.shadow(vec![gpui::BoxShadow {
|
||||
color: Hsla {
|
||||
h: 0.,
|
||||
s: 0.,
|
||||
l: 0.,
|
||||
a: 0.4,
|
||||
},
|
||||
blur_radius: CLIENT_SIDE_DECORATION_SHADOW / 2.,
|
||||
spread_radius: px(0.),
|
||||
offset: point(px(0.0), px(0.0)),
|
||||
inset: false,
|
||||
}])
|
||||
}),
|
||||
})
|
||||
.on_mouse_move(|_e, _, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.size_full()
|
||||
.font_family(font_family)
|
||||
.bg(cx.theme().surface_background)
|
||||
.text_color(cx.theme().text)
|
||||
.child(self.view.clone()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the window paddings.
|
||||
pub fn window_paddings(window: &Window, _cx: &App) -> Edges<Pixels> {
|
||||
match window.window_decorations() {
|
||||
Decorations::Server => Edges::all(px(0.0)),
|
||||
Decorations::Client { tiling } => {
|
||||
let mut paddings = Edges::all(CLIENT_SIDE_DECORATION_SHADOW);
|
||||
if tiling.top {
|
||||
paddings.top = px(0.0);
|
||||
}
|
||||
if tiling.bottom {
|
||||
paddings.bottom = px(0.0);
|
||||
}
|
||||
if tiling.left {
|
||||
paddings.left = px(0.0);
|
||||
}
|
||||
if tiling.right {
|
||||
paddings.right = px(0.0);
|
||||
}
|
||||
paddings
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the window resize edge.
|
||||
fn resize_edge(
|
||||
pos: Point<Pixels>,
|
||||
shadow_size: Pixels,
|
||||
window_size: Size<Pixels>,
|
||||
tiling: Tiling,
|
||||
) -> Option<ResizeEdge> {
|
||||
let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
|
||||
if bounds.contains(&pos) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
|
||||
let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
|
||||
if !tiling.top && top_left_bounds.contains(&pos) {
|
||||
return Some(ResizeEdge::TopLeft);
|
||||
}
|
||||
|
||||
let top_right_bounds = Bounds::new(
|
||||
Point::new(window_size.width - corner_size.width, px(0.)),
|
||||
corner_size,
|
||||
);
|
||||
if !tiling.top && top_right_bounds.contains(&pos) {
|
||||
return Some(ResizeEdge::TopRight);
|
||||
}
|
||||
|
||||
let bottom_left_bounds = Bounds::new(
|
||||
Point::new(px(0.), window_size.height - corner_size.height),
|
||||
corner_size,
|
||||
);
|
||||
if !tiling.bottom && bottom_left_bounds.contains(&pos) {
|
||||
return Some(ResizeEdge::BottomLeft);
|
||||
}
|
||||
|
||||
let bottom_right_bounds = Bounds::new(
|
||||
Point::new(
|
||||
window_size.width - corner_size.width,
|
||||
window_size.height - corner_size.height,
|
||||
),
|
||||
corner_size,
|
||||
);
|
||||
if !tiling.bottom && bottom_right_bounds.contains(&pos) {
|
||||
return Some(ResizeEdge::BottomRight);
|
||||
}
|
||||
|
||||
if !tiling.top && pos.y < shadow_size {
|
||||
Some(ResizeEdge::Top)
|
||||
} else if !tiling.bottom && pos.y > window_size.height - shadow_size {
|
||||
Some(ResizeEdge::Bottom)
|
||||
} else if !tiling.left && pos.x < shadow_size {
|
||||
Some(ResizeEdge::Left)
|
||||
} else if !tiling.right && pos.x > window_size.width - shadow_size {
|
||||
Some(ResizeEdge::Right)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
mod scrollable;
|
||||
mod scrollable_mask;
|
||||
mod scrollbar;
|
||||
|
||||
pub use scrollable::*;
|
||||
pub use scrollable_mask::*;
|
||||
pub use scrollbar::*;
|
||||
@@ -1,211 +0,0 @@
|
||||
use std::panic::Location;
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
App, Div, Element, ElementId, InteractiveElement, IntoElement, ParentElement, RenderOnce,
|
||||
ScrollHandle, Stateful, StatefulInteractiveElement, StyleRefinement, Styled, Window, div,
|
||||
};
|
||||
|
||||
use super::{Scrollbar, ScrollbarAxis};
|
||||
use crate::StyledExt;
|
||||
use crate::scroll::ScrollbarHandle;
|
||||
|
||||
/// A trait for elements that can be made scrollable with scrollbars.
|
||||
pub trait ScrollableElement: InteractiveElement + Styled + ParentElement + Element {
|
||||
/// Adds a scrollbar to the element.
|
||||
#[track_caller]
|
||||
fn scrollbar<H: ScrollbarHandle + Clone>(
|
||||
self,
|
||||
scroll_handle: &H,
|
||||
axis: impl Into<ScrollbarAxis>,
|
||||
) -> Self {
|
||||
self.child(ScrollbarLayer {
|
||||
id: "scrollbar_layer".into(),
|
||||
axis: axis.into(),
|
||||
scroll_handle: Rc::new(scroll_handle.clone()),
|
||||
})
|
||||
}
|
||||
|
||||
/// Adds a vertical scrollbar to the element.
|
||||
#[track_caller]
|
||||
fn vertical_scrollbar<H: ScrollbarHandle + Clone>(self, scroll_handle: &H) -> Self {
|
||||
self.scrollbar(scroll_handle, ScrollbarAxis::Vertical)
|
||||
}
|
||||
/// Adds a horizontal scrollbar to the element.
|
||||
#[track_caller]
|
||||
fn horizontal_scrollbar<H: ScrollbarHandle + Clone>(self, scroll_handle: &H) -> Self {
|
||||
self.scrollbar(scroll_handle, ScrollbarAxis::Horizontal)
|
||||
}
|
||||
|
||||
/// Almost equivalent to [`StatefulInteractiveElement::overflow_scroll`], but adds scrollbars.
|
||||
#[track_caller]
|
||||
fn overflow_scrollbar(self) -> Scrollable<Self> {
|
||||
Scrollable::new(self, ScrollbarAxis::Both)
|
||||
}
|
||||
|
||||
/// Almost equivalent to [`StatefulInteractiveElement::overflow_x_scroll`], but adds Horizontal scrollbar.
|
||||
#[track_caller]
|
||||
fn overflow_x_scrollbar(self) -> Scrollable<Self> {
|
||||
Scrollable::new(self, ScrollbarAxis::Horizontal)
|
||||
}
|
||||
|
||||
/// Almost equivalent to [`StatefulInteractiveElement::overflow_y_scroll`], but adds Vertical scrollbar.
|
||||
#[track_caller]
|
||||
fn overflow_y_scrollbar(self) -> Scrollable<Self> {
|
||||
Scrollable::new(self, ScrollbarAxis::Vertical)
|
||||
}
|
||||
}
|
||||
|
||||
/// A scrollable element wrapper that adds scrollbars to an interactive element.
|
||||
#[derive(IntoElement)]
|
||||
pub struct Scrollable<E: InteractiveElement + Styled + ParentElement + Element> {
|
||||
id: ElementId,
|
||||
element: E,
|
||||
axis: ScrollbarAxis,
|
||||
}
|
||||
|
||||
impl<E> Scrollable<E>
|
||||
where
|
||||
E: InteractiveElement + Styled + ParentElement + Element,
|
||||
{
|
||||
#[track_caller]
|
||||
fn new(element: E, axis: impl Into<ScrollbarAxis>) -> Self {
|
||||
let caller = Location::caller();
|
||||
Self {
|
||||
id: ElementId::CodeLocation(*caller),
|
||||
element,
|
||||
axis: axis.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> Styled for Scrollable<E>
|
||||
where
|
||||
E: InteractiveElement + Styled + ParentElement + Element,
|
||||
{
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
self.element.style()
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> ParentElement for Scrollable<E>
|
||||
where
|
||||
E: InteractiveElement + Styled + ParentElement + Element,
|
||||
{
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = gpui::AnyElement>) {
|
||||
self.element.extend(elements)
|
||||
}
|
||||
}
|
||||
|
||||
impl InteractiveElement for Scrollable<Div> {
|
||||
fn interactivity(&mut self) -> &mut gpui::Interactivity {
|
||||
self.element.interactivity()
|
||||
}
|
||||
}
|
||||
|
||||
impl InteractiveElement for Scrollable<Stateful<Div>> {
|
||||
fn interactivity(&mut self) -> &mut gpui::Interactivity {
|
||||
self.element.interactivity()
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> RenderOnce for Scrollable<E>
|
||||
where
|
||||
E: InteractiveElement + Styled + ParentElement + Element + 'static,
|
||||
{
|
||||
fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let scroll_handle = window
|
||||
.use_keyed_state(self.id.clone(), cx, |_, _| ScrollHandle::default())
|
||||
.read(cx)
|
||||
.clone();
|
||||
|
||||
// Inherit the size from the element style.
|
||||
let style = StyleRefinement {
|
||||
size: self.element.style().size.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
div()
|
||||
.id(self.id)
|
||||
.size_full()
|
||||
.refine_style(&style)
|
||||
.relative()
|
||||
.child(
|
||||
div()
|
||||
.id("scroll-area")
|
||||
.flex()
|
||||
.size_full()
|
||||
.track_scroll(&scroll_handle)
|
||||
.map(|this| match self.axis {
|
||||
ScrollbarAxis::Vertical => this.flex_col().overflow_y_scroll(),
|
||||
ScrollbarAxis::Horizontal => this.flex_row().overflow_x_scroll(),
|
||||
ScrollbarAxis::Both => this.overflow_scroll(),
|
||||
})
|
||||
.child(
|
||||
self.element
|
||||
// Refine element size to `flex_1`.
|
||||
.size_auto()
|
||||
.flex_1(),
|
||||
),
|
||||
)
|
||||
.child(render_scrollbar(
|
||||
"scrollbar",
|
||||
&scroll_handle,
|
||||
self.axis,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl ScrollableElement for Div {}
|
||||
|
||||
impl<E> ScrollableElement for Stateful<E>
|
||||
where
|
||||
E: ParentElement + Styled + Element,
|
||||
Self: InteractiveElement,
|
||||
{
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
struct ScrollbarLayer<H: ScrollbarHandle + Clone> {
|
||||
id: ElementId,
|
||||
axis: ScrollbarAxis,
|
||||
scroll_handle: Rc<H>,
|
||||
}
|
||||
|
||||
impl<H> RenderOnce for ScrollbarLayer<H>
|
||||
where
|
||||
H: ScrollbarHandle + Clone + 'static,
|
||||
{
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
render_scrollbar(self.id, self.scroll_handle.as_ref(), self.axis, window, cx)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[track_caller]
|
||||
fn render_scrollbar<H: ScrollbarHandle + Clone>(
|
||||
id: impl Into<ElementId>,
|
||||
scroll_handle: &H,
|
||||
axis: ScrollbarAxis,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Div {
|
||||
// Do not render scrollbar when inspector is picking elements,
|
||||
// to allow us to pick the background elements.
|
||||
let is_inspector_picking = window.is_inspector_picking(cx);
|
||||
|
||||
if is_inspector_picking {
|
||||
return div();
|
||||
}
|
||||
|
||||
div()
|
||||
.absolute()
|
||||
.top_0()
|
||||
.left_0()
|
||||
.right_0()
|
||||
.bottom_0()
|
||||
.child(Scrollbar::new(scroll_handle).id(id).axis(axis))
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
use gpui::{
|
||||
App, Axis, BorderStyle, Bounds, ContentMask, Corners, Edges, Element, ElementId, EntityId,
|
||||
GlobalElementId, Hitbox, Hsla, IntoElement, IsZero as _, LayoutId, PaintQuad, Pixels, Point,
|
||||
Position, ScrollHandle, ScrollWheelEvent, Size, Style, Window, px, relative,
|
||||
};
|
||||
use theme::AxisExt;
|
||||
|
||||
/// Make a scrollable mask element to cover the parent view with the mouse wheel event listening.
|
||||
///
|
||||
/// When the mouse wheel is scrolled, will move the `scroll_handle` scrolling with the `axis` direction.
|
||||
/// You can use this `scroll_handle` to control what you want to scroll.
|
||||
/// This is only can handle once axis scrolling.
|
||||
pub struct ScrollableMask {
|
||||
view_id: EntityId,
|
||||
axis: Axis,
|
||||
scroll_handle: ScrollHandle,
|
||||
debug: Option<Hsla>,
|
||||
}
|
||||
|
||||
impl ScrollableMask {
|
||||
/// Create a new scrollable mask element.
|
||||
pub fn new(view_id: EntityId, axis: Axis, scroll_handle: &ScrollHandle) -> Self {
|
||||
Self {
|
||||
view_id,
|
||||
scroll_handle: scroll_handle.clone(),
|
||||
axis,
|
||||
debug: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Enable the debug border, to show the mask bounds.
|
||||
#[allow(dead_code)]
|
||||
pub fn debug(mut self) -> Self {
|
||||
self.debug = Some(gpui::yellow());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoElement for ScrollableMask {
|
||||
type Element = Self;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for ScrollableMask {
|
||||
type PrepaintState = Hitbox;
|
||||
type RequestLayoutState = ();
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
_: Option<&GlobalElementId>,
|
||||
_: Option<&gpui::InspectorElementId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let style = Style {
|
||||
position: Position::Absolute,
|
||||
flex_grow: 1.0,
|
||||
flex_shrink: 1.0,
|
||||
size: Size {
|
||||
width: relative(1.).into(),
|
||||
height: relative(1.).into(),
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
(window.request_layout(style, None, cx), ())
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_: Option<&GlobalElementId>,
|
||||
_: Option<&gpui::InspectorElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
_: &mut Self::RequestLayoutState,
|
||||
window: &mut Window,
|
||||
_: &mut App,
|
||||
) -> Self::PrepaintState {
|
||||
// Move y to bounds height to cover the parent view.
|
||||
let cover_bounds = Bounds {
|
||||
origin: Point {
|
||||
x: bounds.origin.x,
|
||||
y: bounds.origin.y - bounds.size.height,
|
||||
},
|
||||
size: bounds.size,
|
||||
};
|
||||
|
||||
window.insert_hitbox(cover_bounds, gpui::HitboxBehavior::Normal)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_: Option<&GlobalElementId>,
|
||||
_: Option<&gpui::InspectorElementId>,
|
||||
_: Bounds<Pixels>,
|
||||
_: &mut Self::RequestLayoutState,
|
||||
hitbox: &mut Self::PrepaintState,
|
||||
window: &mut Window,
|
||||
_: &mut App,
|
||||
) {
|
||||
let line_height = window.line_height();
|
||||
let bounds = hitbox.bounds;
|
||||
|
||||
window.with_content_mask(Some(ContentMask { bounds }), |window| {
|
||||
if let Some(color) = self.debug {
|
||||
window.paint_quad(PaintQuad {
|
||||
bounds,
|
||||
border_widths: Edges::all(px(1.0)),
|
||||
border_color: color,
|
||||
background: gpui::transparent_white().into(),
|
||||
corner_radii: Corners::all(px(0.)),
|
||||
border_style: BorderStyle::default(),
|
||||
});
|
||||
}
|
||||
|
||||
window.on_mouse_event({
|
||||
let view_id = self.view_id;
|
||||
let is_horizontal = self.axis.is_horizontal();
|
||||
let scroll_handle = self.scroll_handle.clone();
|
||||
let hitbox = hitbox.clone();
|
||||
let mouse_position = window.mouse_position();
|
||||
let last_offset = scroll_handle.offset();
|
||||
|
||||
move |event: &ScrollWheelEvent, phase, window, cx| {
|
||||
if bounds.contains(&mouse_position)
|
||||
&& phase.bubble()
|
||||
&& hitbox.is_hovered(window)
|
||||
{
|
||||
let mut offset = scroll_handle.offset();
|
||||
let mut delta = event.delta.pixel_delta(line_height);
|
||||
|
||||
// Limit for only one way scrolling at same time.
|
||||
// When use MacBook touchpad we may get both x and y delta,
|
||||
// only allows the one that more to scroll.
|
||||
if !delta.x.is_zero() && !delta.y.is_zero() {
|
||||
if delta.x.abs() > delta.y.abs() {
|
||||
delta.y = px(0.);
|
||||
} else {
|
||||
delta.x = px(0.);
|
||||
}
|
||||
}
|
||||
|
||||
if is_horizontal {
|
||||
offset.x += delta.x;
|
||||
} else {
|
||||
offset.y += delta.y;
|
||||
}
|
||||
|
||||
if last_offset != offset {
|
||||
scroll_handle.set_offset(offset);
|
||||
cx.notify(view_id);
|
||||
cx.stop_propagation();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,945 +0,0 @@
|
||||
use std::cell::Cell;
|
||||
use std::ops::Deref;
|
||||
use std::panic::Location;
|
||||
use std::rc::Rc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use gpui::{
|
||||
Anchor, App, Axis, BorderStyle, Bounds, ContentMask, CursorStyle, Edges, Element, ElementId,
|
||||
GlobalElementId, Hitbox, HitboxBehavior, Hsla, InspectorElementId, IntoElement, IsZero,
|
||||
LayoutId, ListState, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, Pixels, Point,
|
||||
Position, ScrollHandle, ScrollWheelEvent, Size, Style, UniformListScrollHandle, Window, fill,
|
||||
point, px, relative, size,
|
||||
};
|
||||
use theme::{ActiveTheme, AxisExt, ScrollbarMode};
|
||||
|
||||
/// The width of the scrollbar (THUMB_ACTIVE_INSET * 2 + THUMB_ACTIVE_WIDTH)
|
||||
const WIDTH: Pixels = px(1. * 2. + 8.);
|
||||
const MIN_THUMB_SIZE: f32 = 48.;
|
||||
|
||||
const THUMB_WIDTH: Pixels = px(6.);
|
||||
const THUMB_RADIUS: Pixels = px(6. / 2.);
|
||||
const THUMB_INSET: Pixels = px(1.);
|
||||
|
||||
const THUMB_ACTIVE_WIDTH: Pixels = px(8.);
|
||||
const THUMB_ACTIVE_RADIUS: Pixels = px(8. / 2.);
|
||||
const THUMB_ACTIVE_INSET: Pixels = px(1.);
|
||||
|
||||
const FADE_OUT_DURATION: f32 = 3.0;
|
||||
const FADE_OUT_DELAY: f32 = 2.0;
|
||||
|
||||
/// A trait for scroll handles that can get and set offset.
|
||||
pub trait ScrollbarHandle: 'static {
|
||||
/// Get the current offset of the scroll handle.
|
||||
fn offset(&self) -> Point<Pixels>;
|
||||
/// Set the offset of the scroll handle.
|
||||
fn set_offset(&self, offset: Point<Pixels>);
|
||||
/// The full size of the content, including padding.
|
||||
fn content_size(&self) -> Size<Pixels>;
|
||||
/// Called when start dragging the scrollbar thumb.
|
||||
fn start_drag(&self) {}
|
||||
/// Called when end dragging the scrollbar thumb.
|
||||
fn end_drag(&self) {}
|
||||
}
|
||||
|
||||
impl ScrollbarHandle for ScrollHandle {
|
||||
fn offset(&self) -> Point<Pixels> {
|
||||
self.offset()
|
||||
}
|
||||
|
||||
fn set_offset(&self, offset: Point<Pixels>) {
|
||||
self.set_offset(offset);
|
||||
}
|
||||
|
||||
fn content_size(&self) -> Size<Pixels> {
|
||||
Size::from(self.max_offset()) + self.bounds().size
|
||||
}
|
||||
}
|
||||
|
||||
impl ScrollbarHandle for UniformListScrollHandle {
|
||||
fn offset(&self) -> Point<Pixels> {
|
||||
self.0.borrow().base_handle.offset()
|
||||
}
|
||||
|
||||
fn set_offset(&self, offset: Point<Pixels>) {
|
||||
self.0.borrow_mut().base_handle.set_offset(offset)
|
||||
}
|
||||
|
||||
fn content_size(&self) -> Size<Pixels> {
|
||||
let base_handle = &self.0.borrow().base_handle;
|
||||
Size::from(base_handle.max_offset()) + base_handle.bounds().size
|
||||
}
|
||||
}
|
||||
|
||||
impl ScrollbarHandle for ListState {
|
||||
fn offset(&self) -> Point<Pixels> {
|
||||
self.scroll_px_offset_for_scrollbar()
|
||||
}
|
||||
|
||||
fn set_offset(&self, offset: Point<Pixels>) {
|
||||
self.set_offset_from_scrollbar(offset);
|
||||
}
|
||||
|
||||
fn content_size(&self) -> Size<Pixels> {
|
||||
Size::from(self.max_offset_for_scrollbar()) + self.viewport_bounds().size
|
||||
}
|
||||
|
||||
fn start_drag(&self) {
|
||||
self.scrollbar_drag_started();
|
||||
}
|
||||
|
||||
fn end_drag(&self) {
|
||||
self.scrollbar_drag_ended();
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[derive(Debug, Clone)]
|
||||
struct ScrollbarState(Rc<Cell<ScrollbarStateInner>>);
|
||||
|
||||
#[doc(hidden)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct ScrollbarStateInner {
|
||||
hovered_axis: Option<Axis>,
|
||||
hovered_on_thumb: Option<Axis>,
|
||||
dragged_axis: Option<Axis>,
|
||||
drag_pos: Point<Pixels>,
|
||||
last_scroll_offset: Point<Pixels>,
|
||||
last_scroll_time: Option<Instant>,
|
||||
// Last update offset
|
||||
last_update: Instant,
|
||||
idle_timer_scheduled: bool,
|
||||
}
|
||||
|
||||
impl Default for ScrollbarState {
|
||||
fn default() -> Self {
|
||||
Self(Rc::new(Cell::new(ScrollbarStateInner {
|
||||
hovered_axis: None,
|
||||
hovered_on_thumb: None,
|
||||
dragged_axis: None,
|
||||
drag_pos: point(px(0.), px(0.)),
|
||||
last_scroll_offset: point(px(0.), px(0.)),
|
||||
last_scroll_time: None,
|
||||
last_update: Instant::now(),
|
||||
idle_timer_scheduled: false,
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for ScrollbarState {
|
||||
type Target = Rc<Cell<ScrollbarStateInner>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl ScrollbarStateInner {
|
||||
fn with_drag_pos(&self, axis: Axis, pos: Point<Pixels>) -> Self {
|
||||
let mut state = *self;
|
||||
if axis.is_vertical() {
|
||||
state.drag_pos.y = pos.y;
|
||||
} else {
|
||||
state.drag_pos.x = pos.x;
|
||||
}
|
||||
|
||||
state.dragged_axis = Some(axis);
|
||||
state
|
||||
}
|
||||
|
||||
fn with_unset_drag_pos(&self) -> Self {
|
||||
let mut state = *self;
|
||||
state.dragged_axis = None;
|
||||
state
|
||||
}
|
||||
|
||||
fn with_hovered(&self, axis: Option<Axis>) -> Self {
|
||||
let mut state = *self;
|
||||
state.hovered_axis = axis;
|
||||
if axis.is_some() {
|
||||
state.last_scroll_time = Some(std::time::Instant::now());
|
||||
}
|
||||
state
|
||||
}
|
||||
|
||||
fn with_hovered_on_thumb(&self, axis: Option<Axis>) -> Self {
|
||||
let mut state = *self;
|
||||
state.hovered_on_thumb = axis;
|
||||
if self.is_scrollbar_visible() && axis.is_some() {
|
||||
state.last_scroll_time = Some(std::time::Instant::now());
|
||||
}
|
||||
state
|
||||
}
|
||||
|
||||
fn with_last_scroll(
|
||||
&self,
|
||||
last_scroll_offset: Point<Pixels>,
|
||||
last_scroll_time: Option<Instant>,
|
||||
) -> Self {
|
||||
let mut state = *self;
|
||||
state.last_scroll_offset = last_scroll_offset;
|
||||
state.last_scroll_time = last_scroll_time;
|
||||
state
|
||||
}
|
||||
|
||||
fn with_last_scroll_time(&self, t: Option<Instant>) -> Self {
|
||||
let mut state = *self;
|
||||
state.last_scroll_time = t;
|
||||
state
|
||||
}
|
||||
|
||||
fn with_last_update(&self, t: Instant) -> Self {
|
||||
let mut state = *self;
|
||||
state.last_update = t;
|
||||
state
|
||||
}
|
||||
|
||||
fn with_idle_timer_scheduled(&self, scheduled: bool) -> Self {
|
||||
let mut state = *self;
|
||||
state.idle_timer_scheduled = scheduled;
|
||||
state
|
||||
}
|
||||
|
||||
fn is_scrollbar_visible(&self) -> bool {
|
||||
// On drag
|
||||
if self.dragged_axis.is_some() {
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Some(last_time) = self.last_scroll_time {
|
||||
let elapsed = Instant::now().duration_since(last_time).as_secs_f32();
|
||||
elapsed < FADE_OUT_DURATION
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Scrollbar axis.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ScrollbarAxis {
|
||||
/// Vertical scrollbar.
|
||||
Vertical,
|
||||
/// Horizontal scrollbar.
|
||||
Horizontal,
|
||||
/// Show both vertical and horizontal scrollbars.
|
||||
Both,
|
||||
}
|
||||
|
||||
impl From<Axis> for ScrollbarAxis {
|
||||
fn from(axis: Axis) -> Self {
|
||||
match axis {
|
||||
Axis::Vertical => Self::Vertical,
|
||||
Axis::Horizontal => Self::Horizontal,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ScrollbarAxis {
|
||||
/// Return true if the scrollbar axis is vertical.
|
||||
#[inline]
|
||||
pub fn is_vertical(&self) -> bool {
|
||||
matches!(self, Self::Vertical)
|
||||
}
|
||||
|
||||
/// Return true if the scrollbar axis is horizontal.
|
||||
#[inline]
|
||||
pub fn is_horizontal(&self) -> bool {
|
||||
matches!(self, Self::Horizontal)
|
||||
}
|
||||
|
||||
/// Return true if the scrollbar axis is both vertical and horizontal.
|
||||
#[inline]
|
||||
pub fn is_both(&self) -> bool {
|
||||
matches!(self, Self::Both)
|
||||
}
|
||||
|
||||
/// Return true if the scrollbar has vertical axis.
|
||||
#[inline]
|
||||
pub fn has_vertical(&self) -> bool {
|
||||
matches!(self, Self::Vertical | Self::Both)
|
||||
}
|
||||
|
||||
/// Return true if the scrollbar has horizontal axis.
|
||||
#[inline]
|
||||
pub fn has_horizontal(&self) -> bool {
|
||||
matches!(self, Self::Horizontal | Self::Both)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn all(&self) -> Vec<Axis> {
|
||||
match self {
|
||||
Self::Vertical => vec![Axis::Vertical],
|
||||
Self::Horizontal => vec![Axis::Horizontal],
|
||||
// This should keep Horizontal first, Vertical is the primary axis
|
||||
// if Vertical not need display, then Horizontal will not keep right margin.
|
||||
Self::Both => vec![Axis::Horizontal, Axis::Vertical],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Scrollbar control for scroll-area or a uniform-list.
|
||||
pub struct Scrollbar {
|
||||
pub(crate) id: ElementId,
|
||||
axis: ScrollbarAxis,
|
||||
scrollbar_mode: Option<ScrollbarMode>,
|
||||
scroll_handle: Rc<dyn ScrollbarHandle>,
|
||||
scroll_size: Option<Size<Pixels>>,
|
||||
/// Maximum frames per second for scrolling by drag. Default is 120 FPS.
|
||||
///
|
||||
/// This is used to limit the update rate of the scrollbar when it is
|
||||
/// being dragged for some complex interactions for reducing CPU usage.
|
||||
max_fps: usize,
|
||||
}
|
||||
|
||||
impl Scrollbar {
|
||||
/// Create a new scrollbar.
|
||||
///
|
||||
/// This will have both vertical and horizontal scrollbars.
|
||||
#[track_caller]
|
||||
pub fn new<H: ScrollbarHandle + Clone>(scroll_handle: &H) -> Self {
|
||||
let caller = Location::caller();
|
||||
Self {
|
||||
id: ElementId::CodeLocation(*caller),
|
||||
axis: ScrollbarAxis::Both,
|
||||
scrollbar_mode: None,
|
||||
scroll_handle: Rc::new(scroll_handle.clone()),
|
||||
max_fps: 120,
|
||||
scroll_size: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create with horizontal scrollbar.
|
||||
#[track_caller]
|
||||
pub fn horizontal<H: ScrollbarHandle + Clone>(scroll_handle: &H) -> Self {
|
||||
Self::new(scroll_handle).axis(ScrollbarAxis::Horizontal)
|
||||
}
|
||||
|
||||
/// Create with vertical scrollbar.
|
||||
#[track_caller]
|
||||
pub fn vertical<H: ScrollbarHandle + Clone>(scroll_handle: &H) -> Self {
|
||||
Self::new(scroll_handle).axis(ScrollbarAxis::Vertical)
|
||||
}
|
||||
|
||||
/// Set a specific element id, default is the [`Location::caller`].
|
||||
///
|
||||
/// NOTE: In most cases, you don't need to set a specific id for scrollbar.
|
||||
pub fn id(mut self, id: impl Into<ElementId>) -> Self {
|
||||
self.id = id.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the scrollbar show mode [`ScrollbarShow`], if not set use the `cx.theme().scrollbar_show`.
|
||||
pub fn scrollbar_mode(mut self, mode: ScrollbarMode) -> Self {
|
||||
self.scrollbar_mode = Some(mode);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set a special scroll size of the content area, default is None.
|
||||
///
|
||||
/// Default will sync the `content_size` from `scroll_handle`.
|
||||
pub fn scroll_size(mut self, scroll_size: Size<Pixels>) -> Self {
|
||||
self.scroll_size = Some(scroll_size);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set scrollbar axis.
|
||||
pub fn axis(mut self, axis: impl Into<ScrollbarAxis>) -> Self {
|
||||
self.axis = axis.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set maximum frames per second for scrolling by drag. Default is 120 FPS.
|
||||
///
|
||||
/// If you have very high CPU usage, consider reducing this value to improve performance.
|
||||
///
|
||||
/// Available values: 30..120
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn max_fps(mut self, max_fps: usize) -> Self {
|
||||
self.max_fps = max_fps.clamp(30, 120);
|
||||
self
|
||||
}
|
||||
|
||||
// Get the width of the scrollbar.
|
||||
#[allow(dead_code)]
|
||||
pub(crate) const fn width() -> Pixels {
|
||||
WIDTH
|
||||
}
|
||||
|
||||
fn style_for_active(cx: &App) -> (Hsla, Hsla, Hsla, Pixels, Pixels, Pixels) {
|
||||
(
|
||||
cx.theme().scrollbar_thumb_hover_background,
|
||||
cx.theme().scrollbar_thumb_background,
|
||||
cx.theme().scrollbar_thumb_border,
|
||||
THUMB_ACTIVE_WIDTH,
|
||||
THUMB_ACTIVE_INSET,
|
||||
THUMB_ACTIVE_RADIUS,
|
||||
)
|
||||
}
|
||||
|
||||
fn style_for_hovered_thumb(cx: &App) -> (Hsla, Hsla, Hsla, Pixels, Pixels, Pixels) {
|
||||
(
|
||||
cx.theme().scrollbar_thumb_hover_background,
|
||||
cx.theme().scrollbar_thumb_background,
|
||||
cx.theme().scrollbar_thumb_border,
|
||||
THUMB_ACTIVE_WIDTH,
|
||||
THUMB_ACTIVE_INSET,
|
||||
THUMB_ACTIVE_RADIUS,
|
||||
)
|
||||
}
|
||||
|
||||
fn style_for_hovered_bar(cx: &App) -> (Hsla, Hsla, Hsla, Pixels, Pixels, Pixels) {
|
||||
(
|
||||
cx.theme().scrollbar_thumb_background,
|
||||
cx.theme().scrollbar_thumb_border,
|
||||
gpui::transparent_black(),
|
||||
THUMB_ACTIVE_WIDTH,
|
||||
THUMB_ACTIVE_INSET,
|
||||
THUMB_ACTIVE_RADIUS,
|
||||
)
|
||||
}
|
||||
|
||||
fn style_for_normal(&self, cx: &App) -> (Hsla, Hsla, Hsla, Pixels, Pixels, Pixels) {
|
||||
let scrollbar_mode = self.scrollbar_mode.unwrap_or(cx.theme().scrollbar_mode);
|
||||
let (width, inset, radius) = match scrollbar_mode {
|
||||
ScrollbarMode::Scrolling => (THUMB_WIDTH, THUMB_INSET, THUMB_RADIUS),
|
||||
_ => (THUMB_ACTIVE_WIDTH, THUMB_ACTIVE_INSET, THUMB_ACTIVE_RADIUS),
|
||||
};
|
||||
(
|
||||
cx.theme().scrollbar_thumb_background,
|
||||
cx.theme().scrollbar_track_background,
|
||||
gpui::transparent_black(),
|
||||
width,
|
||||
inset,
|
||||
radius,
|
||||
)
|
||||
}
|
||||
|
||||
fn style_for_idle(&self, _cx: &App) -> (Hsla, Hsla, Hsla, Pixels, Pixels, Pixels) {
|
||||
let scrollbar_mode = self.scrollbar_mode.unwrap_or(ScrollbarMode::Always);
|
||||
let (width, inset, radius) = match scrollbar_mode {
|
||||
ScrollbarMode::Scrolling => (THUMB_WIDTH, THUMB_INSET, THUMB_RADIUS),
|
||||
_ => (THUMB_ACTIVE_WIDTH, THUMB_ACTIVE_INSET, THUMB_ACTIVE_RADIUS),
|
||||
};
|
||||
|
||||
(
|
||||
gpui::transparent_black(),
|
||||
gpui::transparent_black(),
|
||||
gpui::transparent_black(),
|
||||
width,
|
||||
inset,
|
||||
radius,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoElement for Scrollbar {
|
||||
type Element = Self;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub struct PrepaintState {
|
||||
hitbox: Hitbox,
|
||||
scrollbar_state: ScrollbarState,
|
||||
states: Vec<AxisPrepaintState>,
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub struct AxisPrepaintState {
|
||||
axis: Axis,
|
||||
bar_hitbox: Hitbox,
|
||||
bounds: Bounds<Pixels>,
|
||||
radius: Pixels,
|
||||
bg: Hsla,
|
||||
border: Hsla,
|
||||
thumb_bounds: Bounds<Pixels>,
|
||||
// Bounds of thumb to be rendered.
|
||||
thumb_fill_bounds: Bounds<Pixels>,
|
||||
thumb_bg: Hsla,
|
||||
scroll_size: Pixels,
|
||||
container_size: Pixels,
|
||||
thumb_size: Pixels,
|
||||
margin_end: Pixels,
|
||||
}
|
||||
|
||||
impl Element for Scrollbar {
|
||||
type PrepaintState = PrepaintState;
|
||||
type RequestLayoutState = ();
|
||||
|
||||
fn id(&self) -> Option<gpui::ElementId> {
|
||||
Some(self.id.clone())
|
||||
}
|
||||
|
||||
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
_: Option<&GlobalElementId>,
|
||||
_: Option<&InspectorElementId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let style = Style {
|
||||
position: Position::Absolute,
|
||||
flex_grow: 1.0,
|
||||
flex_shrink: 1.0,
|
||||
size: Size {
|
||||
width: relative(1.).into(),
|
||||
height: relative(1.).into(),
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
(window.request_layout(style, None, cx), ())
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_: Option<&GlobalElementId>,
|
||||
_: Option<&InspectorElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
_: &mut Self::RequestLayoutState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Self::PrepaintState {
|
||||
let hitbox = window.with_content_mask(Some(ContentMask { bounds }), |window| {
|
||||
window.insert_hitbox(bounds, HitboxBehavior::Normal)
|
||||
});
|
||||
|
||||
let state = window
|
||||
.use_state(cx, |_, _| ScrollbarState::default())
|
||||
.read(cx)
|
||||
.clone();
|
||||
|
||||
let mut states = vec![];
|
||||
let mut has_both = self.axis.is_both();
|
||||
|
||||
let scroll_size = self
|
||||
.scroll_size
|
||||
.unwrap_or(self.scroll_handle.content_size());
|
||||
|
||||
for axis in self.axis.all().into_iter() {
|
||||
let is_vertical = axis.is_vertical();
|
||||
let (scroll_area_size, container_size, scroll_position) = if is_vertical {
|
||||
(
|
||||
scroll_size.height,
|
||||
hitbox.size.height,
|
||||
self.scroll_handle.offset().y,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
scroll_size.width,
|
||||
hitbox.size.width,
|
||||
self.scroll_handle.offset().x,
|
||||
)
|
||||
};
|
||||
|
||||
// The horizontal scrollbar is set avoid overlapping with the vertical scrollbar, if the vertical scrollbar is visible.
|
||||
let margin_end = if has_both && !is_vertical {
|
||||
WIDTH
|
||||
} else {
|
||||
px(0.)
|
||||
};
|
||||
|
||||
// Hide scrollbar, if the scroll area is smaller than the container.
|
||||
if scroll_area_size <= container_size {
|
||||
has_both = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
let thumb_length =
|
||||
(container_size / scroll_area_size * container_size).max(px(MIN_THUMB_SIZE));
|
||||
let thumb_start = -(scroll_position / (scroll_area_size - container_size)
|
||||
* (container_size - margin_end - thumb_length));
|
||||
let thumb_end = (thumb_start + thumb_length).min(container_size - margin_end);
|
||||
|
||||
let bounds = Bounds {
|
||||
origin: if is_vertical {
|
||||
point(hitbox.origin.x + hitbox.size.width - WIDTH, hitbox.origin.y)
|
||||
} else {
|
||||
point(
|
||||
hitbox.origin.x,
|
||||
hitbox.origin.y + hitbox.size.height - WIDTH,
|
||||
)
|
||||
},
|
||||
size: gpui::Size {
|
||||
width: if is_vertical {
|
||||
WIDTH
|
||||
} else {
|
||||
hitbox.size.width
|
||||
},
|
||||
height: if is_vertical {
|
||||
hitbox.size.height
|
||||
} else {
|
||||
WIDTH
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let scrollbar_show = self.scrollbar_mode.unwrap_or(cx.theme().scrollbar_mode);
|
||||
let is_always_to_show = scrollbar_show.is_always();
|
||||
let is_hover_to_show = scrollbar_show.is_hover();
|
||||
let is_hovered_on_bar = state.get().hovered_axis == Some(axis);
|
||||
let is_hovered_on_thumb = state.get().hovered_on_thumb == Some(axis);
|
||||
let is_offset_changed = state.get().last_scroll_offset != self.scroll_handle.offset();
|
||||
|
||||
let (thumb_bg, bar_bg, bar_border, thumb_width, inset, radius) =
|
||||
if state.get().dragged_axis == Some(axis) {
|
||||
Self::style_for_active(cx)
|
||||
} else if is_hover_to_show && (is_hovered_on_bar || is_hovered_on_thumb) {
|
||||
if is_hovered_on_thumb {
|
||||
Self::style_for_hovered_thumb(cx)
|
||||
} else {
|
||||
Self::style_for_hovered_bar(cx)
|
||||
}
|
||||
} else if is_offset_changed {
|
||||
self.style_for_normal(cx)
|
||||
} else if is_always_to_show {
|
||||
if is_hovered_on_thumb {
|
||||
Self::style_for_hovered_thumb(cx)
|
||||
} else {
|
||||
Self::style_for_hovered_bar(cx)
|
||||
}
|
||||
} else {
|
||||
let mut idle_state = self.style_for_idle(cx);
|
||||
// Delay 2s to fade out the scrollbar thumb (in 1s)
|
||||
if let Some(last_time) = state.get().last_scroll_time {
|
||||
let elapsed = Instant::now().duration_since(last_time).as_secs_f32();
|
||||
if is_hovered_on_bar {
|
||||
state.set(state.get().with_last_scroll_time(Some(Instant::now())));
|
||||
idle_state = if is_hovered_on_thumb {
|
||||
Self::style_for_hovered_thumb(cx)
|
||||
} else {
|
||||
Self::style_for_hovered_bar(cx)
|
||||
};
|
||||
} else if elapsed < FADE_OUT_DELAY {
|
||||
idle_state.0 = cx.theme().scrollbar_thumb_background;
|
||||
|
||||
if !state.get().idle_timer_scheduled {
|
||||
let state = state.clone();
|
||||
state.set(state.get().with_idle_timer_scheduled(true));
|
||||
let current_view = window.current_view();
|
||||
let next_delay = Duration::from_secs_f32(FADE_OUT_DELAY - elapsed);
|
||||
window
|
||||
.spawn(cx, async move |cx| {
|
||||
cx.background_executor().timer(next_delay).await;
|
||||
state.set(state.get().with_idle_timer_scheduled(false));
|
||||
cx.update(|_, cx| cx.notify(current_view)).ok();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
} else if elapsed < FADE_OUT_DURATION {
|
||||
let opacity = 1.0 - (elapsed - FADE_OUT_DELAY).powi(10);
|
||||
idle_state.0 = cx.theme().scrollbar_thumb_background.opacity(opacity);
|
||||
|
||||
window.request_animation_frame();
|
||||
}
|
||||
}
|
||||
|
||||
idle_state
|
||||
};
|
||||
|
||||
// The clickable area of the thumb
|
||||
let thumb_length = thumb_end - thumb_start - inset * 2;
|
||||
let thumb_bounds = if is_vertical {
|
||||
Bounds::from_anchor_and_size(
|
||||
Anchor::TopRight,
|
||||
bounds.top_right() + point(-inset, inset + thumb_start),
|
||||
size(WIDTH, thumb_length),
|
||||
)
|
||||
} else {
|
||||
Bounds::from_anchor_and_size(
|
||||
Anchor::BottomLeft,
|
||||
bounds.bottom_left() + point(inset + thumb_start, -inset),
|
||||
size(thumb_length, WIDTH),
|
||||
)
|
||||
};
|
||||
|
||||
// The actual render area of the thumb
|
||||
let thumb_fill_bounds = if is_vertical {
|
||||
Bounds::from_anchor_and_size(
|
||||
Anchor::TopRight,
|
||||
bounds.top_right() + point(-inset, inset + thumb_start),
|
||||
size(thumb_width, thumb_length),
|
||||
)
|
||||
} else {
|
||||
Bounds::from_anchor_and_size(
|
||||
Anchor::BottomLeft,
|
||||
bounds.bottom_left() + point(inset + thumb_start, -inset),
|
||||
size(thumb_length, thumb_width),
|
||||
)
|
||||
};
|
||||
|
||||
let bar_hitbox = window.with_content_mask(Some(ContentMask { bounds }), |window| {
|
||||
window.insert_hitbox(bounds, gpui::HitboxBehavior::Normal)
|
||||
});
|
||||
|
||||
states.push(AxisPrepaintState {
|
||||
axis,
|
||||
bar_hitbox,
|
||||
bounds,
|
||||
radius,
|
||||
bg: bar_bg,
|
||||
border: bar_border,
|
||||
thumb_bounds,
|
||||
thumb_fill_bounds,
|
||||
thumb_bg,
|
||||
scroll_size: scroll_area_size,
|
||||
container_size,
|
||||
thumb_size: thumb_length,
|
||||
margin_end,
|
||||
})
|
||||
}
|
||||
|
||||
PrepaintState {
|
||||
hitbox,
|
||||
states,
|
||||
scrollbar_state: state,
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_: Option<&GlobalElementId>,
|
||||
_: Option<&InspectorElementId>,
|
||||
_: Bounds<Pixels>,
|
||||
_: &mut Self::RequestLayoutState,
|
||||
prepaint: &mut Self::PrepaintState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let scrollbar_state = &prepaint.scrollbar_state;
|
||||
let scrollbar_show = self.scrollbar_mode.unwrap_or(cx.theme().scrollbar_mode);
|
||||
let view_id = window.current_view();
|
||||
let hitbox_bounds = prepaint.hitbox.bounds;
|
||||
let is_visible = scrollbar_state.get().is_scrollbar_visible() || scrollbar_show.is_always();
|
||||
let is_hover_to_show = scrollbar_show.is_hover();
|
||||
|
||||
// Update last_scroll_time when offset is changed.
|
||||
if self.scroll_handle.offset() != scrollbar_state.get().last_scroll_offset {
|
||||
scrollbar_state.set(
|
||||
scrollbar_state
|
||||
.get()
|
||||
.with_last_scroll(self.scroll_handle.offset(), Some(Instant::now())),
|
||||
);
|
||||
cx.notify(view_id);
|
||||
}
|
||||
|
||||
window.with_content_mask(
|
||||
Some(ContentMask {
|
||||
bounds: hitbox_bounds,
|
||||
}),
|
||||
|window| {
|
||||
for state in prepaint.states.iter() {
|
||||
let axis = state.axis;
|
||||
let mut radius = state.radius;
|
||||
if cx.theme().radius.is_zero() {
|
||||
radius = px(0.);
|
||||
}
|
||||
let bounds = state.bounds;
|
||||
let thumb_bounds = state.thumb_bounds;
|
||||
let scroll_area_size = state.scroll_size;
|
||||
let container_size = state.container_size;
|
||||
let thumb_size = state.thumb_size;
|
||||
let margin_end = state.margin_end;
|
||||
let is_vertical = axis.is_vertical();
|
||||
|
||||
window.set_cursor_style(CursorStyle::default(), &state.bar_hitbox);
|
||||
|
||||
window.paint_layer(hitbox_bounds, |cx| {
|
||||
cx.paint_quad(fill(state.bounds, state.bg));
|
||||
|
||||
cx.paint_quad(PaintQuad {
|
||||
bounds,
|
||||
corner_radii: (0.).into(),
|
||||
background: gpui::transparent_black().into(),
|
||||
border_widths: Edges {
|
||||
top: px(0.),
|
||||
right: px(0.),
|
||||
bottom: px(0.),
|
||||
left: px(0.),
|
||||
},
|
||||
border_color: state.border,
|
||||
border_style: BorderStyle::default(),
|
||||
});
|
||||
|
||||
cx.paint_quad(
|
||||
fill(state.thumb_fill_bounds, state.thumb_bg).corner_radii(radius),
|
||||
);
|
||||
});
|
||||
|
||||
window.on_mouse_event({
|
||||
let state = scrollbar_state.clone();
|
||||
let scroll_handle = self.scroll_handle.clone();
|
||||
|
||||
move |event: &ScrollWheelEvent, phase, _, cx| {
|
||||
if phase.bubble()
|
||||
&& hitbox_bounds.contains(&event.position)
|
||||
&& scroll_handle.offset() != state.get().last_scroll_offset
|
||||
{
|
||||
state.set(state.get().with_last_scroll(
|
||||
scroll_handle.offset(),
|
||||
Some(Instant::now()),
|
||||
));
|
||||
cx.notify(view_id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let safe_range = (-scroll_area_size + container_size)..px(0.);
|
||||
|
||||
if is_hover_to_show || is_visible {
|
||||
window.on_mouse_event({
|
||||
let state = scrollbar_state.clone();
|
||||
let scroll_handle = self.scroll_handle.clone();
|
||||
|
||||
move |event: &MouseDownEvent, phase, _, cx| {
|
||||
if phase.bubble() && bounds.contains(&event.position) {
|
||||
cx.stop_propagation();
|
||||
|
||||
if thumb_bounds.contains(&event.position) {
|
||||
// click on the thumb bar, set the drag position
|
||||
let pos = event.position - thumb_bounds.origin;
|
||||
|
||||
scroll_handle.start_drag();
|
||||
state.set(state.get().with_drag_pos(axis, pos));
|
||||
|
||||
cx.notify(view_id);
|
||||
} else {
|
||||
// click on the scrollbar, jump to the position
|
||||
// Set the thumb bar center to the click position
|
||||
let offset = scroll_handle.offset();
|
||||
let percentage = if is_vertical {
|
||||
(event.position.y - thumb_size / 2. - bounds.origin.y)
|
||||
/ (bounds.size.height - thumb_size)
|
||||
} else {
|
||||
(event.position.x - thumb_size / 2. - bounds.origin.x)
|
||||
/ (bounds.size.width - thumb_size)
|
||||
}
|
||||
.min(1.);
|
||||
|
||||
if is_vertical {
|
||||
scroll_handle.set_offset(point(
|
||||
offset.x,
|
||||
(-scroll_area_size * percentage)
|
||||
.clamp(safe_range.start, safe_range.end),
|
||||
));
|
||||
} else {
|
||||
scroll_handle.set_offset(point(
|
||||
(-scroll_area_size * percentage)
|
||||
.clamp(safe_range.start, safe_range.end),
|
||||
offset.y,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.on_mouse_event({
|
||||
let scroll_handle = self.scroll_handle.clone();
|
||||
let state = scrollbar_state.clone();
|
||||
let max_fps_duration = Duration::from_millis((1000 / self.max_fps) as u64);
|
||||
|
||||
move |event: &MouseMoveEvent, _, _, cx| {
|
||||
let mut notify = false;
|
||||
// When is hover to show mode or it was visible,
|
||||
// we need to update the hovered state and increase the last_scroll_time.
|
||||
let need_hover_to_update = is_hover_to_show || is_visible;
|
||||
// Update hovered state for scrollbar
|
||||
if bounds.contains(&event.position) && need_hover_to_update {
|
||||
state.set(state.get().with_hovered(Some(axis)));
|
||||
|
||||
if state.get().hovered_axis != Some(axis) {
|
||||
notify = true;
|
||||
}
|
||||
} else if state.get().hovered_axis == Some(axis) {
|
||||
state.set(state.get().with_hovered(None));
|
||||
notify = true;
|
||||
}
|
||||
|
||||
// Update hovered state for scrollbar thumb
|
||||
if thumb_bounds.contains(&event.position) {
|
||||
if state.get().hovered_on_thumb != Some(axis) {
|
||||
state.set(state.get().with_hovered_on_thumb(Some(axis)));
|
||||
notify = true;
|
||||
}
|
||||
} else if state.get().hovered_on_thumb == Some(axis) {
|
||||
state.set(state.get().with_hovered_on_thumb(None));
|
||||
notify = true;
|
||||
}
|
||||
|
||||
// Move thumb position on dragging
|
||||
if state.get().dragged_axis == Some(axis) && event.dragging() {
|
||||
// Stop the event propagation to avoid selecting text or other side effects.
|
||||
cx.stop_propagation();
|
||||
|
||||
// drag_pos is the position of the mouse down event
|
||||
// We need to keep the thumb bar still at the origin down position
|
||||
let drag_pos = state.get().drag_pos;
|
||||
|
||||
let percentage = (if is_vertical {
|
||||
(event.position.y - drag_pos.y - bounds.origin.y)
|
||||
/ (bounds.size.height - thumb_size)
|
||||
} else {
|
||||
(event.position.x - drag_pos.x - bounds.origin.x)
|
||||
/ (bounds.size.width - thumb_size - margin_end)
|
||||
})
|
||||
.clamp(0., 1.);
|
||||
|
||||
let offset = if is_vertical {
|
||||
point(
|
||||
scroll_handle.offset().x,
|
||||
(-(scroll_area_size - container_size) * percentage)
|
||||
.clamp(safe_range.start, safe_range.end),
|
||||
)
|
||||
} else {
|
||||
point(
|
||||
(-(scroll_area_size - container_size) * percentage)
|
||||
.clamp(safe_range.start, safe_range.end),
|
||||
scroll_handle.offset().y,
|
||||
)
|
||||
};
|
||||
|
||||
if (scroll_handle.offset().y - offset.y).abs() > px(1.)
|
||||
|| (scroll_handle.offset().x - offset.x).abs() > px(1.)
|
||||
{
|
||||
// Limit update rate
|
||||
if state.get().last_update.elapsed() > max_fps_duration {
|
||||
scroll_handle.set_offset(offset);
|
||||
state.set(state.get().with_last_update(Instant::now()));
|
||||
notify = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if notify {
|
||||
cx.notify(view_id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
window.on_mouse_event({
|
||||
let state = scrollbar_state.clone();
|
||||
let scroll_handle = self.scroll_handle.clone();
|
||||
|
||||
move |_event: &MouseUpEvent, phase, _, cx| {
|
||||
if phase.bubble() {
|
||||
scroll_handle.end_drag();
|
||||
state.set(state.get().with_unset_drag_pos());
|
||||
cx.notify(view_id);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use gpui::{
|
||||
bounce, div, ease_in_out, Animation, AnimationExt, IntoElement, RenderOnce, StyleRefinement,
|
||||
Styled,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use crate::StyledExt;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct Skeleton {
|
||||
style: StyleRefinement,
|
||||
secondary: bool,
|
||||
}
|
||||
|
||||
impl Skeleton {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
style: StyleRefinement::default(),
|
||||
secondary: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn secondary(mut self) -> Self {
|
||||
self.secondary = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Skeleton {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for Skeleton {
|
||||
fn style(&mut self) -> &mut gpui::StyleRefinement {
|
||||
&mut self.style
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Skeleton {
|
||||
fn render(self, _window: &mut gpui::Window, cx: &mut gpui::App) -> impl IntoElement {
|
||||
let color = if self.secondary {
|
||||
cx.theme().ghost_element_active.opacity(0.5)
|
||||
} else {
|
||||
cx.theme().ghost_element_active
|
||||
};
|
||||
|
||||
div()
|
||||
.w_full()
|
||||
.h_4()
|
||||
.rounded_md()
|
||||
.refine_style(&self.style)
|
||||
.bg(color)
|
||||
.with_animation(
|
||||
"skeleton",
|
||||
Animation::new(Duration::from_secs(3))
|
||||
.repeat()
|
||||
.with_easing(bounce(ease_in_out)),
|
||||
move |this, delta| {
|
||||
let v = 1.0 - delta * 0.5;
|
||||
this.opacity(v)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,275 +0,0 @@
|
||||
use gpui::{App, DefiniteLength, Div, Edges, Pixels, Refineable, StyleRefinement, Styled, div, px};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
/// Returns a `Div` as horizontal flex layout.
|
||||
pub fn h_flex() -> Div {
|
||||
div().h_flex()
|
||||
}
|
||||
|
||||
/// Returns a `Div` as vertical flex layout.
|
||||
pub fn v_flex() -> Div {
|
||||
div().v_flex()
|
||||
}
|
||||
|
||||
/// Returns a `Div` as divider.
|
||||
pub fn divider(cx: &App) -> Div {
|
||||
div().my_2().w_full().h_px().bg(cx.theme().border_variant)
|
||||
}
|
||||
|
||||
macro_rules! font_weight {
|
||||
($fn:ident, $const:ident) => {
|
||||
/// [docs](https://tailwindcss.com/docs/font-weight)
|
||||
fn $fn(self) -> Self {
|
||||
self.font_weight(gpui::FontWeight::$const)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Extends [`gpui::Styled`] with specific styling methods.
|
||||
pub trait StyledExt: Styled + Sized {
|
||||
/// Refine the style of this element, applying the given style refinement.
|
||||
fn refine_style(mut self, style: &StyleRefinement) -> Self {
|
||||
self.style().refine(style);
|
||||
self
|
||||
}
|
||||
|
||||
/// Apply self into a horizontal flex layout.
|
||||
#[inline]
|
||||
fn h_flex(self) -> Self {
|
||||
self.flex().flex_row().items_center()
|
||||
}
|
||||
|
||||
/// Apply self into a vertical flex layout.
|
||||
#[inline]
|
||||
fn v_flex(self) -> Self {
|
||||
self.flex().flex_col()
|
||||
}
|
||||
|
||||
/// Apply paddings to the element.
|
||||
fn paddings<L>(self, paddings: impl Into<Edges<L>>) -> Self
|
||||
where
|
||||
L: Into<DefiniteLength> + Clone + Default + std::fmt::Debug + PartialEq,
|
||||
{
|
||||
let paddings = paddings.into();
|
||||
self.pt(paddings.top.into())
|
||||
.pb(paddings.bottom.into())
|
||||
.pl(paddings.left.into())
|
||||
.pr(paddings.right.into())
|
||||
}
|
||||
|
||||
/// Apply margins to the element.
|
||||
fn margins<L>(self, margins: impl Into<Edges<L>>) -> Self
|
||||
where
|
||||
L: Into<DefiniteLength> + Clone + Default + std::fmt::Debug + PartialEq,
|
||||
{
|
||||
let margins = margins.into();
|
||||
self.mt(margins.top.into())
|
||||
.mb(margins.bottom.into())
|
||||
.ml(margins.left.into())
|
||||
.mr(margins.right.into())
|
||||
}
|
||||
|
||||
font_weight!(font_thin, THIN);
|
||||
font_weight!(font_extralight, EXTRA_LIGHT);
|
||||
font_weight!(font_light, LIGHT);
|
||||
font_weight!(font_normal, NORMAL);
|
||||
font_weight!(font_medium, MEDIUM);
|
||||
font_weight!(font_semibold, SEMIBOLD);
|
||||
font_weight!(font_bold, BOLD);
|
||||
font_weight!(font_extrabold, EXTRA_BOLD);
|
||||
font_weight!(font_black, BLACK);
|
||||
|
||||
/// Set as Popover style
|
||||
#[inline]
|
||||
fn popover_style(self, cx: &mut App) -> Self {
|
||||
self.bg(cx.theme().background)
|
||||
.border_1()
|
||||
.border_color(cx.theme().border)
|
||||
.shadow_md()
|
||||
.rounded(cx.theme().radius)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Styled> StyledExt for E {}
|
||||
|
||||
/// A size for elements.
|
||||
#[derive(Clone, Default, Copy, PartialEq, Eq, Debug, Deserialize, Serialize)]
|
||||
pub enum Size {
|
||||
Size(Pixels),
|
||||
XSmall,
|
||||
Small,
|
||||
#[default]
|
||||
Medium,
|
||||
Large,
|
||||
}
|
||||
|
||||
impl From<Pixels> for Size {
|
||||
fn from(size: Pixels) -> Self {
|
||||
Size::Size(size)
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for defining element that can be selected.
|
||||
pub trait Selectable: Sized {
|
||||
/// Set the selected state of the element.
|
||||
fn selected(self, selected: bool) -> Self;
|
||||
|
||||
/// Returns true if the element is selected.
|
||||
fn is_selected(&self) -> bool;
|
||||
|
||||
/// Set is the element mouse right clicked, default do nothing.
|
||||
fn secondary_selected(self, _: bool) -> Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for defining element that can be disabled.
|
||||
pub trait Disableable {
|
||||
/// Set the disabled state of the element.
|
||||
fn disabled(self, disabled: bool) -> Self;
|
||||
}
|
||||
|
||||
/// A trait for setting the size of an element.
|
||||
pub trait Sizable: Sized {
|
||||
/// Set the ui::Size of this element.
|
||||
///
|
||||
/// Also can receive a `ButtonSize` to convert to `IconSize`,
|
||||
/// Or a `Pixels` to set a custom size: `px(30.)`
|
||||
fn with_size(self, size: impl Into<Size>) -> Self;
|
||||
|
||||
/// Set to Size::XSmall
|
||||
fn xsmall(self) -> Self {
|
||||
self.with_size(Size::XSmall)
|
||||
}
|
||||
|
||||
/// Set to Size::Small
|
||||
fn small(self) -> Self {
|
||||
self.with_size(Size::Small)
|
||||
}
|
||||
|
||||
/// Set to Size::Medium
|
||||
fn medium(self) -> Self {
|
||||
self.with_size(Size::Medium)
|
||||
}
|
||||
|
||||
/// Set to Size::Large
|
||||
fn large(self) -> Self {
|
||||
self.with_size(Size::Large)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub trait StyleSized<T: Styled> {
|
||||
fn input_font_size(self, size: Size) -> Self;
|
||||
fn input_size(self, size: Size) -> Self;
|
||||
fn input_pl(self, size: Size) -> Self;
|
||||
fn input_pr(self, size: Size) -> Self;
|
||||
fn input_px(self, size: Size) -> Self;
|
||||
fn input_py(self, size: Size) -> Self;
|
||||
fn input_h(self, size: Size) -> Self;
|
||||
fn list_size(self, size: Size) -> Self;
|
||||
fn list_px(self, size: Size) -> Self;
|
||||
fn list_py(self, size: Size) -> Self;
|
||||
/// Apply size with the given `Size`.
|
||||
fn size_with(self, size: Size) -> Self;
|
||||
}
|
||||
|
||||
impl<T: Styled> StyleSized<T> for T {
|
||||
fn input_font_size(self, size: Size) -> Self {
|
||||
match size {
|
||||
Size::XSmall => self.text_xs(),
|
||||
Size::Small => self.text_sm(),
|
||||
Size::Medium => self.text_base(),
|
||||
Size::Large => self.text_lg(),
|
||||
Size::Size(size) => self.text_size(size),
|
||||
}
|
||||
}
|
||||
|
||||
fn input_size(self, size: Size) -> Self {
|
||||
self.input_px(size).input_py(size).input_h(size)
|
||||
}
|
||||
|
||||
fn input_pl(self, size: Size) -> Self {
|
||||
match size {
|
||||
Size::XSmall => self.pl_1(),
|
||||
Size::Medium => self.pl_3(),
|
||||
Size::Large => self.pl_5(),
|
||||
_ => self.pl_2(),
|
||||
}
|
||||
}
|
||||
|
||||
fn input_pr(self, size: Size) -> Self {
|
||||
match size {
|
||||
Size::XSmall => self.pr_1(),
|
||||
Size::Medium => self.pr_3(),
|
||||
Size::Large => self.pr_5(),
|
||||
_ => self.pr_2(),
|
||||
}
|
||||
}
|
||||
|
||||
fn input_px(self, size: Size) -> Self {
|
||||
match size {
|
||||
Size::XSmall => self.px_1(),
|
||||
Size::Medium => self.px_3(),
|
||||
Size::Large => self.px_5(),
|
||||
_ => self.px_2(),
|
||||
}
|
||||
}
|
||||
|
||||
fn input_py(self, size: Size) -> Self {
|
||||
match size {
|
||||
Size::XSmall => self.py_0p5(),
|
||||
Size::Medium => self.py_2(),
|
||||
Size::Large => self.py_5(),
|
||||
_ => self.py_1(),
|
||||
}
|
||||
}
|
||||
|
||||
fn input_h(self, size: Size) -> Self {
|
||||
match size {
|
||||
Size::XSmall => self.h_6(),
|
||||
Size::Small => self.h_8(),
|
||||
Size::Medium => self.h_9(),
|
||||
Size::Large => self.h_12(),
|
||||
_ => self.h(px(24.)),
|
||||
}
|
||||
.input_font_size(size)
|
||||
}
|
||||
|
||||
fn list_size(self, size: Size) -> Self {
|
||||
self.list_px(size).list_py(size).input_font_size(size)
|
||||
}
|
||||
|
||||
fn list_px(self, size: Size) -> Self {
|
||||
match size {
|
||||
Size::Small => self.px_2(),
|
||||
_ => self.px_3(),
|
||||
}
|
||||
}
|
||||
|
||||
fn list_py(self, size: Size) -> Self {
|
||||
match size {
|
||||
Size::Large => self.py_2(),
|
||||
Size::Medium => self.py_1(),
|
||||
Size::Small => self.py_0p5(),
|
||||
_ => self.py_1(),
|
||||
}
|
||||
}
|
||||
|
||||
fn size_with(self, size: Size) -> Self {
|
||||
match size {
|
||||
Size::Large => self.size_11(),
|
||||
Size::Medium => self.size_8(),
|
||||
Size::Small => self.size_5(),
|
||||
Size::XSmall => self.size_4(),
|
||||
Size::Size(size) => self.size(size),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for defining element that can be collapsed.
|
||||
pub trait Collapsible {
|
||||
fn collapsed(self, collapsed: bool) -> Self;
|
||||
fn is_collapsed(&self) -> bool;
|
||||
}
|
||||
@@ -1,287 +0,0 @@
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
Animation, AnimationExt as _, AnyElement, App, Element, ElementId, GlobalElementId,
|
||||
InteractiveElement, IntoElement, LayoutId, ParentElement as _, SharedString, Styled as _,
|
||||
Window, div, px, white,
|
||||
};
|
||||
use theme::{ActiveTheme, Side};
|
||||
|
||||
use crate::{Disableable, Sizable, Size};
|
||||
|
||||
type OnClick = Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>;
|
||||
|
||||
pub struct Switch {
|
||||
id: ElementId,
|
||||
checked: bool,
|
||||
disabled: bool,
|
||||
label: Option<SharedString>,
|
||||
description: Option<SharedString>,
|
||||
label_side: Side,
|
||||
on_click: OnClick,
|
||||
size: Size,
|
||||
}
|
||||
|
||||
impl Switch {
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
let id: ElementId = id.into();
|
||||
|
||||
Self {
|
||||
id: id.clone(),
|
||||
checked: false,
|
||||
disabled: false,
|
||||
label: None,
|
||||
description: None,
|
||||
on_click: None,
|
||||
label_side: Side::Left,
|
||||
size: Size::Medium,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn checked(mut self, checked: bool) -> Self {
|
||||
self.checked = checked;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn label(mut self, label: impl Into<SharedString>) -> Self {
|
||||
self.label = Some(label.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn description(mut self, description: impl Into<SharedString>) -> Self {
|
||||
self.description = Some(description.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_click<F>(mut self, handler: F) -> Self
|
||||
where
|
||||
F: Fn(&bool, &mut Window, &mut App) + 'static,
|
||||
{
|
||||
self.on_click = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn label_side(mut self, label_side: Side) -> Self {
|
||||
self.label_side = label_side;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Sizable for Switch {
|
||||
fn with_size(mut self, size: impl Into<Size>) -> Self {
|
||||
self.size = size.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Disableable for Switch {
|
||||
fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoElement for Switch {
|
||||
type Element = Self;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SwitchState {
|
||||
prev_checked: Rc<RefCell<Option<bool>>>,
|
||||
}
|
||||
|
||||
impl Element for Switch {
|
||||
type PrepaintState = ();
|
||||
type RequestLayoutState = AnyElement;
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
Some(self.id.clone())
|
||||
}
|
||||
|
||||
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
_: Option<&gpui::InspectorElementId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
window.with_element_state::<SwitchState, _>(global_id.unwrap(), move |state, window| {
|
||||
let state = state.unwrap_or_default();
|
||||
|
||||
let theme = cx.theme();
|
||||
let checked = self.checked;
|
||||
let on_click = self.on_click.clone();
|
||||
|
||||
let (bg, toggle_bg) = match self.checked {
|
||||
true => (theme.element_background, white()),
|
||||
false => (theme.elevated_surface_background, white()),
|
||||
};
|
||||
|
||||
let (bg, toggle_bg) = match self.disabled {
|
||||
true => (bg.opacity(0.3), toggle_bg.opacity(0.8)),
|
||||
false => (bg, toggle_bg),
|
||||
};
|
||||
|
||||
let (bg_width, bg_height) = match self.size {
|
||||
Size::XSmall | Size::Small => (px(28.), px(16.)),
|
||||
_ => (px(36.), px(20.)),
|
||||
};
|
||||
|
||||
let bar_width = match self.size {
|
||||
Size::XSmall | Size::Small => px(12.),
|
||||
_ => px(16.),
|
||||
};
|
||||
|
||||
let inset = px(2.);
|
||||
|
||||
let mut element = div()
|
||||
.child(
|
||||
div()
|
||||
.id(self.id.clone())
|
||||
.when(self.label_side.is_left(), |this| this.flex_row_reverse())
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
.flex()
|
||||
.justify_between()
|
||||
.items_center()
|
||||
.gap_4()
|
||||
.when_some(self.label.clone(), |this, label| {
|
||||
// Label
|
||||
this.child(
|
||||
div().text_sm().text_color(cx.theme().text).child(label),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
// Switch Bar
|
||||
div()
|
||||
.id(self.id.clone())
|
||||
.flex_shrink_0()
|
||||
.w(bg_width)
|
||||
.h(bg_height)
|
||||
.rounded(bg_height / 2.)
|
||||
.flex()
|
||||
.items_center()
|
||||
.border(inset)
|
||||
.border_color(theme.border_transparent)
|
||||
.bg(bg)
|
||||
.when(!self.disabled, |this| this.cursor_pointer())
|
||||
.child(
|
||||
// Switch Toggle
|
||||
div()
|
||||
.rounded_full()
|
||||
.when(cx.theme().shadow, |this| this.shadow_sm())
|
||||
.bg(toggle_bg)
|
||||
.size(bar_width)
|
||||
.map(|this| {
|
||||
let prev_checked = state.prev_checked.clone();
|
||||
if !self.disabled
|
||||
&& prev_checked
|
||||
.borrow()
|
||||
.is_some_and(|prev| prev != checked)
|
||||
{
|
||||
let dur = Duration::from_secs_f64(0.15);
|
||||
cx.spawn(async move |cx| {
|
||||
cx.background_executor()
|
||||
.timer(dur)
|
||||
.await;
|
||||
*prev_checked.borrow_mut() =
|
||||
Some(checked);
|
||||
})
|
||||
.detach();
|
||||
this.with_animation(
|
||||
ElementId::NamedInteger(
|
||||
"move".into(),
|
||||
checked as u64,
|
||||
),
|
||||
Animation::new(dur),
|
||||
move |this, delta| {
|
||||
let max_x = bg_width
|
||||
- bar_width
|
||||
- inset * 2;
|
||||
let x = if checked {
|
||||
max_x * delta
|
||||
} else {
|
||||
max_x - max_x * delta
|
||||
};
|
||||
this.left(x)
|
||||
},
|
||||
)
|
||||
.into_any_element()
|
||||
} else {
|
||||
let max_x =
|
||||
bg_width - bar_width - inset * 2;
|
||||
let x =
|
||||
if checked { max_x } else { px(0.) };
|
||||
this.left(x).into_any_element()
|
||||
}
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
.when_some(self.description.clone(), |this, description| {
|
||||
this.child(
|
||||
div()
|
||||
.pr_3()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(description),
|
||||
)
|
||||
})
|
||||
.when_some(
|
||||
on_click
|
||||
.as_ref()
|
||||
.map(|c| c.clone())
|
||||
.filter(|_| !self.disabled),
|
||||
|this, on_click| {
|
||||
let prev_checked = state.prev_checked.clone();
|
||||
this.on_mouse_down(gpui::MouseButton::Left, move |_, window, cx| {
|
||||
cx.stop_propagation();
|
||||
*prev_checked.borrow_mut() = Some(checked);
|
||||
on_click(&!checked, window, cx);
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
.into_any_element();
|
||||
|
||||
((element.request_layout(window, cx), element), state)
|
||||
})
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_: Option<&gpui::GlobalElementId>,
|
||||
_: Option<&gpui::InspectorElementId>,
|
||||
_: gpui::Bounds<gpui::Pixels>,
|
||||
element: &mut Self::RequestLayoutState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
element.prepaint(window, cx);
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_: Option<&gpui::GlobalElementId>,
|
||||
_: Option<&gpui::InspectorElementId>,
|
||||
_: gpui::Bounds<gpui::Pixels>,
|
||||
element: &mut Self::RequestLayoutState,
|
||||
_: &mut Self::PrepaintState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
element.paint(window, cx)
|
||||
}
|
||||
}
|
||||
@@ -1,702 +0,0 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
AnyElement, App, ClickEvent, Div, Edges, Hsla, InteractiveElement, IntoElement, MouseButton,
|
||||
ParentElement, Pixels, RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window,
|
||||
div, px, relative,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use crate::{Icon, IconName, Selectable, Sizable, Size, StyledExt, h_flex};
|
||||
|
||||
pub mod tab_bar;
|
||||
|
||||
/// Tab variants.
|
||||
#[derive(Debug, Clone, Default, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum TabVariant {
|
||||
#[default]
|
||||
Tab,
|
||||
Outline,
|
||||
Pill,
|
||||
Segmented,
|
||||
Underline,
|
||||
}
|
||||
|
||||
impl TabVariant {
|
||||
fn height(&self, size: Size) -> Pixels {
|
||||
match size {
|
||||
Size::XSmall => match self {
|
||||
TabVariant::Underline => px(26.),
|
||||
_ => px(20.),
|
||||
},
|
||||
Size::Small => match self {
|
||||
TabVariant::Underline => px(30.),
|
||||
_ => px(24.),
|
||||
},
|
||||
Size::Large => match self {
|
||||
TabVariant::Underline => px(44.),
|
||||
_ => px(36.),
|
||||
},
|
||||
_ => match self {
|
||||
TabVariant::Underline => px(36.),
|
||||
_ => px(32.),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn inner_height(&self, size: Size) -> Pixels {
|
||||
match size {
|
||||
Size::XSmall => match self {
|
||||
TabVariant::Tab | TabVariant::Outline | TabVariant::Pill => px(18.),
|
||||
TabVariant::Segmented => px(16.),
|
||||
TabVariant::Underline => px(20.),
|
||||
},
|
||||
Size::Small => match self {
|
||||
TabVariant::Tab | TabVariant::Outline | TabVariant::Pill => px(22.),
|
||||
TabVariant::Segmented => px(18.),
|
||||
TabVariant::Underline => px(22.),
|
||||
},
|
||||
Size::Large => match self {
|
||||
TabVariant::Tab | TabVariant::Outline | TabVariant::Pill => px(36.),
|
||||
TabVariant::Segmented => px(28.),
|
||||
TabVariant::Underline => px(32.),
|
||||
},
|
||||
_ => match self {
|
||||
TabVariant::Tab => px(30.),
|
||||
TabVariant::Outline | TabVariant::Pill => px(26.),
|
||||
TabVariant::Segmented => px(24.),
|
||||
TabVariant::Underline => px(26.),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Default px(12) to match panel px_3, See [`crate::dock::TabPanel`]
|
||||
fn inner_paddings(&self, size: Size) -> Edges<Pixels> {
|
||||
let mut padding_x = match size {
|
||||
Size::XSmall => px(8.),
|
||||
Size::Small => px(10.),
|
||||
Size::Large => px(16.),
|
||||
_ => px(12.),
|
||||
};
|
||||
|
||||
if matches!(self, TabVariant::Underline) {
|
||||
padding_x = px(0.);
|
||||
}
|
||||
|
||||
Edges {
|
||||
left: padding_x,
|
||||
right: padding_x,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn inner_margins(&self, size: Size) -> Edges<Pixels> {
|
||||
match size {
|
||||
Size::XSmall => match self {
|
||||
TabVariant::Underline => Edges {
|
||||
top: px(1.),
|
||||
bottom: px(2.),
|
||||
..Default::default()
|
||||
},
|
||||
_ => Edges::all(px(0.)),
|
||||
},
|
||||
Size::Small => match self {
|
||||
TabVariant::Underline => Edges {
|
||||
top: px(2.),
|
||||
bottom: px(3.),
|
||||
..Default::default()
|
||||
},
|
||||
_ => Edges::all(px(0.)),
|
||||
},
|
||||
Size::Large => match self {
|
||||
TabVariant::Underline => Edges {
|
||||
top: px(5.),
|
||||
bottom: px(6.),
|
||||
..Default::default()
|
||||
},
|
||||
_ => Edges::all(px(0.)),
|
||||
},
|
||||
_ => match self {
|
||||
TabVariant::Underline => Edges {
|
||||
top: px(3.),
|
||||
bottom: px(4.),
|
||||
..Default::default()
|
||||
},
|
||||
_ => Edges::all(px(0.)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn normal(&self, cx: &App) -> TabStyle {
|
||||
match self {
|
||||
TabVariant::Tab => TabStyle {
|
||||
fg: cx.theme().tab_foreground,
|
||||
bg: gpui::transparent_black(),
|
||||
borders: Edges {
|
||||
left: px(1.),
|
||||
right: px(1.),
|
||||
..Default::default()
|
||||
},
|
||||
border_color: gpui::transparent_black(),
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Outline => TabStyle {
|
||||
fg: cx.theme().tab_foreground,
|
||||
bg: gpui::transparent_black(),
|
||||
borders: Edges::all(px(1.)),
|
||||
border_color: cx.theme().border,
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Pill => TabStyle {
|
||||
fg: cx.theme().text,
|
||||
bg: gpui::transparent_black(),
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Segmented => TabStyle {
|
||||
fg: cx.theme().tab_foreground,
|
||||
bg: gpui::transparent_black(),
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Underline => TabStyle {
|
||||
fg: cx.theme().tab_foreground,
|
||||
bg: gpui::transparent_black(),
|
||||
inner_bg: gpui::transparent_black(),
|
||||
borders: Edges {
|
||||
bottom: px(2.),
|
||||
..Default::default()
|
||||
},
|
||||
border_color: gpui::transparent_black(),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn hovered(&self, selected: bool, cx: &App) -> TabStyle {
|
||||
match self {
|
||||
TabVariant::Tab => TabStyle {
|
||||
fg: cx.theme().tab_foreground,
|
||||
bg: gpui::transparent_black(),
|
||||
borders: Edges {
|
||||
left: px(1.),
|
||||
right: px(1.),
|
||||
..Default::default()
|
||||
},
|
||||
border_color: gpui::transparent_black(),
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Outline => TabStyle {
|
||||
fg: cx.theme().secondary_foreground,
|
||||
bg: cx.theme().secondary_hover,
|
||||
borders: Edges::all(px(1.)),
|
||||
border_color: cx.theme().border,
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Pill => TabStyle {
|
||||
fg: cx.theme().secondary_foreground,
|
||||
bg: cx.theme().secondary_background,
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Segmented => TabStyle {
|
||||
fg: cx.theme().tab_foreground,
|
||||
bg: gpui::transparent_black(),
|
||||
inner_bg: if selected {
|
||||
cx.theme().background
|
||||
} else {
|
||||
gpui::transparent_black()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Underline => TabStyle {
|
||||
fg: cx.theme().tab_foreground,
|
||||
bg: gpui::transparent_black(),
|
||||
inner_bg: gpui::transparent_black(),
|
||||
borders: Edges {
|
||||
bottom: px(2.),
|
||||
..Default::default()
|
||||
},
|
||||
border_color: gpui::transparent_black(),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn selected(&self, cx: &App) -> TabStyle {
|
||||
match self {
|
||||
TabVariant::Tab => TabStyle {
|
||||
fg: cx.theme().tab_active_foreground,
|
||||
bg: cx.theme().tab_active_background,
|
||||
borders: Edges {
|
||||
left: px(1.),
|
||||
right: px(1.),
|
||||
..Default::default()
|
||||
},
|
||||
border_color: cx.theme().border,
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Outline => TabStyle {
|
||||
fg: cx.theme().text_accent,
|
||||
bg: gpui::transparent_black(),
|
||||
borders: Edges::all(px(1.)),
|
||||
border_color: cx.theme().element_active,
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Pill => TabStyle {
|
||||
fg: cx.theme().element_foreground,
|
||||
bg: cx.theme().element_background,
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Segmented => TabStyle {
|
||||
fg: cx.theme().tab_active_foreground,
|
||||
bg: gpui::transparent_black(),
|
||||
inner_bg: cx.theme().background,
|
||||
shadow: true,
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Underline => TabStyle {
|
||||
fg: cx.theme().tab_active_foreground,
|
||||
bg: gpui::transparent_black(),
|
||||
borders: Edges {
|
||||
bottom: px(2.),
|
||||
..Default::default()
|
||||
},
|
||||
border_color: cx.theme().element_active,
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn disabled(&self, selected: bool, cx: &App) -> TabStyle {
|
||||
match self {
|
||||
TabVariant::Tab => TabStyle {
|
||||
fg: cx.theme().text_muted,
|
||||
bg: gpui::transparent_black(),
|
||||
border_color: if selected {
|
||||
cx.theme().border
|
||||
} else {
|
||||
gpui::transparent_black()
|
||||
},
|
||||
borders: Edges {
|
||||
left: px(1.),
|
||||
right: px(1.),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Outline => TabStyle {
|
||||
fg: cx.theme().text_muted,
|
||||
bg: gpui::transparent_black(),
|
||||
borders: Edges::all(px(1.)),
|
||||
border_color: if selected {
|
||||
cx.theme().element_active
|
||||
} else {
|
||||
cx.theme().border
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Pill => TabStyle {
|
||||
fg: if selected {
|
||||
cx.theme().element_foreground.opacity(0.5)
|
||||
} else {
|
||||
cx.theme().text_muted
|
||||
},
|
||||
bg: if selected {
|
||||
cx.theme().element_background.opacity(0.5)
|
||||
} else {
|
||||
gpui::transparent_black()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Segmented => TabStyle {
|
||||
fg: cx.theme().text_muted,
|
||||
bg: cx.theme().tab_background,
|
||||
inner_bg: if selected {
|
||||
cx.theme().background
|
||||
} else {
|
||||
gpui::transparent_black()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Underline => TabStyle {
|
||||
fg: cx.theme().text_muted,
|
||||
bg: gpui::transparent_black(),
|
||||
border_color: if selected {
|
||||
cx.theme().border
|
||||
} else {
|
||||
gpui::transparent_black()
|
||||
},
|
||||
borders: Edges {
|
||||
bottom: px(2.),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn tab_bar_radius(&self, size: Size, cx: &App) -> Pixels {
|
||||
if *self != TabVariant::Segmented {
|
||||
return px(0.);
|
||||
}
|
||||
|
||||
match size {
|
||||
Size::XSmall | Size::Small => cx.theme().radius,
|
||||
Size::Large => cx.theme().radius_lg,
|
||||
_ => cx.theme().radius_lg,
|
||||
}
|
||||
}
|
||||
|
||||
fn radius(&self, size: Size, cx: &App) -> Pixels {
|
||||
match self {
|
||||
TabVariant::Outline | TabVariant::Pill => px(99.),
|
||||
TabVariant::Segmented => match size {
|
||||
Size::XSmall | Size::Small => cx.theme().radius,
|
||||
Size::Large => cx.theme().radius_lg,
|
||||
_ => cx.theme().radius_lg,
|
||||
},
|
||||
_ => px(0.),
|
||||
}
|
||||
}
|
||||
|
||||
fn inner_radius(&self, size: Size, cx: &App) -> Pixels {
|
||||
match self {
|
||||
TabVariant::Segmented => match size {
|
||||
Size::Large => self.tab_bar_radius(size, cx) - px(3.),
|
||||
_ => self.tab_bar_radius(size, cx) - px(2.),
|
||||
},
|
||||
_ => px(0.),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
struct TabStyle {
|
||||
borders: Edges<Pixels>,
|
||||
border_color: Hsla,
|
||||
bg: Hsla,
|
||||
fg: Hsla,
|
||||
shadow: bool,
|
||||
inner_bg: Hsla,
|
||||
}
|
||||
|
||||
impl Default for TabStyle {
|
||||
fn default() -> Self {
|
||||
TabStyle {
|
||||
borders: Edges::all(px(0.)),
|
||||
border_color: gpui::transparent_white(),
|
||||
bg: gpui::transparent_white(),
|
||||
fg: gpui::transparent_white(),
|
||||
shadow: false,
|
||||
inner_bg: gpui::transparent_white(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
/// A Tab element for the [`super::TabBar`].
|
||||
#[derive(IntoElement)]
|
||||
pub struct Tab {
|
||||
ix: usize,
|
||||
base: Div,
|
||||
pub(super) label: Option<SharedString>,
|
||||
icon: Option<Icon>,
|
||||
prefix: Option<AnyElement>,
|
||||
pub(super) tab_bar_prefix: Option<bool>,
|
||||
suffix: Option<AnyElement>,
|
||||
children: Vec<AnyElement>,
|
||||
variant: TabVariant,
|
||||
size: Size,
|
||||
pub(super) disabled: bool,
|
||||
pub(super) selected: bool,
|
||||
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
|
||||
}
|
||||
|
||||
impl From<&'static str> for Tab {
|
||||
fn from(label: &'static str) -> Self {
|
||||
Self::new().label(label)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Tab {
|
||||
fn from(label: String) -> Self {
|
||||
Self::new().label(label)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SharedString> for Tab {
|
||||
fn from(label: SharedString) -> Self {
|
||||
Self::new().label(label)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Icon> for Tab {
|
||||
fn from(icon: Icon) -> Self {
|
||||
Self::default().icon(icon)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IconName> for Tab {
|
||||
fn from(icon_name: IconName) -> Self {
|
||||
Self::default().icon(Icon::new(icon_name))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Tab {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
ix: 0,
|
||||
base: div(),
|
||||
label: None,
|
||||
icon: None,
|
||||
tab_bar_prefix: None,
|
||||
children: Vec::new(),
|
||||
disabled: false,
|
||||
selected: false,
|
||||
prefix: None,
|
||||
suffix: None,
|
||||
variant: TabVariant::default(),
|
||||
size: Size::default(),
|
||||
on_click: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Tab {
|
||||
/// Create a new tab with a label.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Set label for the tab.
|
||||
pub fn label(mut self, label: impl Into<SharedString>) -> Self {
|
||||
self.label = Some(label.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set icon for the tab.
|
||||
pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
|
||||
self.icon = Some(icon.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set Tab Variant.
|
||||
pub fn with_variant(mut self, variant: TabVariant) -> Self {
|
||||
self.variant = variant;
|
||||
self
|
||||
}
|
||||
|
||||
/// Use Pill variant.
|
||||
pub fn pill(mut self) -> Self {
|
||||
self.variant = TabVariant::Pill;
|
||||
self
|
||||
}
|
||||
|
||||
/// Use outline variant.
|
||||
pub fn outline(mut self) -> Self {
|
||||
self.variant = TabVariant::Outline;
|
||||
self
|
||||
}
|
||||
|
||||
/// Use Segmented variant.
|
||||
pub fn segmented(mut self) -> Self {
|
||||
self.variant = TabVariant::Segmented;
|
||||
self
|
||||
}
|
||||
|
||||
/// Use Underline variant.
|
||||
pub fn underline(mut self) -> Self {
|
||||
self.variant = TabVariant::Underline;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the left side of the tab
|
||||
pub fn prefix(mut self, prefix: impl IntoElement) -> Self {
|
||||
self.prefix = Some(prefix.into_any_element());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the right side of the tab
|
||||
pub fn suffix(mut self, suffix: impl IntoElement) -> Self {
|
||||
self.suffix = Some(suffix.into_any_element());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set disabled state to the tab, default false.
|
||||
pub fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the click handler for the tab.
|
||||
pub fn on_click(
|
||||
mut self,
|
||||
on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_click = Some(Rc::new(on_click));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set index to the tab.
|
||||
pub(crate) fn ix(mut self, ix: usize) -> Self {
|
||||
self.ix = ix;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set if the tab bar has a prefix.
|
||||
pub(crate) fn tab_bar_prefix(mut self, tab_bar_prefix: bool) -> Self {
|
||||
self.tab_bar_prefix = Some(tab_bar_prefix);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for Tab {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements);
|
||||
}
|
||||
}
|
||||
|
||||
impl Selectable for Tab {
|
||||
fn selected(mut self, selected: bool) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
|
||||
fn is_selected(&self) -> bool {
|
||||
self.selected
|
||||
}
|
||||
}
|
||||
|
||||
impl InteractiveElement for Tab {
|
||||
fn interactivity(&mut self) -> &mut gpui::Interactivity {
|
||||
self.base.interactivity()
|
||||
}
|
||||
}
|
||||
|
||||
impl StatefulInteractiveElement for Tab {}
|
||||
|
||||
impl Styled for Tab {
|
||||
fn style(&mut self) -> &mut gpui::StyleRefinement {
|
||||
self.base.style()
|
||||
}
|
||||
}
|
||||
|
||||
impl Sizable for Tab {
|
||||
fn with_size(mut self, size: impl Into<Size>) -> Self {
|
||||
self.size = size.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Tab {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let mut tab_style = if self.selected {
|
||||
self.variant.selected(cx)
|
||||
} else {
|
||||
self.variant.normal(cx)
|
||||
};
|
||||
|
||||
let mut hover_style = self.variant.hovered(self.selected, cx);
|
||||
|
||||
if self.disabled {
|
||||
tab_style = self.variant.disabled(self.selected, cx);
|
||||
hover_style = self.variant.disabled(self.selected, cx);
|
||||
}
|
||||
|
||||
let tab_bar_prefix = self.tab_bar_prefix.unwrap_or_default();
|
||||
|
||||
if !tab_bar_prefix && self.ix == 0 && self.variant == TabVariant::Tab {
|
||||
tab_style.borders.left = px(0.);
|
||||
hover_style.borders.left = px(0.);
|
||||
}
|
||||
|
||||
let radius = self.variant.radius(self.size, cx);
|
||||
let inner_radius = self.variant.inner_radius(self.size, cx);
|
||||
let inner_paddings = self.variant.inner_paddings(self.size);
|
||||
let inner_margins = self.variant.inner_margins(self.size);
|
||||
let inner_height = self.variant.inner_height(self.size);
|
||||
let height = self.variant.height(self.size);
|
||||
|
||||
self.base
|
||||
.id(self.ix)
|
||||
.flex()
|
||||
.flex_wrap()
|
||||
.gap_1()
|
||||
.items_center()
|
||||
.flex_shrink_0()
|
||||
.h(height)
|
||||
.overflow_hidden()
|
||||
.text_color(tab_style.fg)
|
||||
.map(|this| match self.size {
|
||||
Size::XSmall => this.text_xs(),
|
||||
Size::Large => this.text_base(),
|
||||
_ => this.text_sm(),
|
||||
})
|
||||
.bg(tab_style.bg)
|
||||
.border_l(tab_style.borders.left)
|
||||
.border_r(tab_style.borders.right)
|
||||
.border_t(tab_style.borders.top)
|
||||
.border_b(tab_style.borders.bottom)
|
||||
.border_color(tab_style.border_color)
|
||||
.rounded(radius)
|
||||
.when(!self.selected && !self.disabled, |this| {
|
||||
this.hover(|this| {
|
||||
this.text_color(hover_style.fg)
|
||||
.bg(hover_style.bg)
|
||||
.border_l(hover_style.borders.left)
|
||||
.border_r(hover_style.borders.right)
|
||||
.border_t(hover_style.borders.top)
|
||||
.border_b(hover_style.borders.bottom)
|
||||
.border_color(hover_style.border_color)
|
||||
.rounded(radius)
|
||||
})
|
||||
})
|
||||
.when_some(self.prefix, |this, prefix| this.child(prefix))
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_1()
|
||||
.h(inner_height)
|
||||
.line_height(relative(1.))
|
||||
.whitespace_nowrap()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.overflow_hidden()
|
||||
.margins(inner_margins)
|
||||
.flex_shrink_0()
|
||||
.map(|this| match self.icon {
|
||||
Some(icon) => {
|
||||
this.w(inner_height * 1.25)
|
||||
.child(icon.map(|this| match self.size {
|
||||
Size::XSmall => this.size_2p5(),
|
||||
Size::Small => this.size_3p5(),
|
||||
Size::Large => this.size_4(),
|
||||
_ => this.size_4(),
|
||||
}))
|
||||
}
|
||||
None => this
|
||||
.paddings(inner_paddings)
|
||||
.map(|this| match self.label {
|
||||
Some(label) => this.child(label),
|
||||
None => this,
|
||||
})
|
||||
.children(self.children),
|
||||
})
|
||||
.bg(tab_style.inner_bg)
|
||||
.rounded(inner_radius)
|
||||
.when(tab_style.shadow, |this| this.shadow_xs())
|
||||
.hover(|this| this.bg(hover_style.inner_bg).rounded(inner_radius)),
|
||||
)
|
||||
.when_some(self.suffix, |this, suffix| {
|
||||
this.child(div().pr_2().child(suffix))
|
||||
})
|
||||
.on_mouse_down(MouseButton::Left, |_, _, cx| {
|
||||
// Stop propagation behavior, for works on TitleBar.
|
||||
// https://github.com/longbridge/gpui-component/issues/1836
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.when(!self.disabled, |this| {
|
||||
this.when_some(self.on_click.clone(), |this, on_click| {
|
||||
this.on_click(move |event, window, cx| on_click(event, window, cx))
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,290 +0,0 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
Anchor, AnyElement, App, Div, Edges, ElementId, InteractiveElement, IntoElement, ParentElement,
|
||||
RenderOnce, ScrollHandle, Stateful, StatefulInteractiveElement as _, StyleRefinement, Styled,
|
||||
Window, div, px,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use super::{Tab, TabVariant};
|
||||
use crate::button::{Button, ButtonVariants as _};
|
||||
use crate::menu::{DropdownMenu as _, PopupMenuItem};
|
||||
use crate::{IconName, Selectable, Sizable, Size, StyledExt, h_flex};
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
/// A TabBar element that contains multiple [`Tab`] items.
|
||||
#[derive(IntoElement)]
|
||||
pub struct TabBar {
|
||||
base: Stateful<Div>,
|
||||
style: StyleRefinement,
|
||||
scroll_handle: Option<ScrollHandle>,
|
||||
prefix: Option<AnyElement>,
|
||||
suffix: Option<AnyElement>,
|
||||
children: SmallVec<[Tab; 2]>,
|
||||
last_empty_space: AnyElement,
|
||||
selected_index: Option<usize>,
|
||||
variant: TabVariant,
|
||||
size: Size,
|
||||
menu: bool,
|
||||
on_click: Option<Rc<dyn Fn(&usize, &mut Window, &mut App) + 'static>>,
|
||||
}
|
||||
|
||||
impl TabBar {
|
||||
/// Create a new TabBar.
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
Self {
|
||||
base: div().id(id).px(px(-1.)),
|
||||
style: StyleRefinement::default(),
|
||||
children: SmallVec::new(),
|
||||
scroll_handle: None,
|
||||
prefix: None,
|
||||
suffix: None,
|
||||
variant: TabVariant::default(),
|
||||
size: Size::default(),
|
||||
last_empty_space: div().w_3().into_any_element(),
|
||||
selected_index: None,
|
||||
on_click: None,
|
||||
menu: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the Tab variant, all children will inherit the variant.
|
||||
pub fn with_variant(mut self, variant: TabVariant) -> Self {
|
||||
self.variant = variant;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the Tab variant to Pill, all children will inherit the variant.
|
||||
pub fn pill(mut self) -> Self {
|
||||
self.variant = TabVariant::Pill;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the Tab variant to Outline, all children will inherit the variant.
|
||||
pub fn outline(mut self) -> Self {
|
||||
self.variant = TabVariant::Outline;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the Tab variant to Segmented, all children will inherit the variant.
|
||||
pub fn segmented(mut self) -> Self {
|
||||
self.variant = TabVariant::Segmented;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the Tab variant to Underline, all children will inherit the variant.
|
||||
pub fn underline(mut self) -> Self {
|
||||
self.variant = TabVariant::Underline;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set whether to show the menu button when tabs overflow, default is false.
|
||||
pub fn menu(mut self, menu: bool) -> Self {
|
||||
self.menu = menu;
|
||||
self
|
||||
}
|
||||
|
||||
/// Track the scroll of the TabBar.
|
||||
pub fn track_scroll(mut self, scroll_handle: &ScrollHandle) -> Self {
|
||||
self.scroll_handle = Some(scroll_handle.clone());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the prefix element of the TabBar
|
||||
pub fn prefix(mut self, prefix: impl IntoElement) -> Self {
|
||||
self.prefix = Some(prefix.into_any_element());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the suffix element of the TabBar
|
||||
pub fn suffix(mut self, suffix: impl IntoElement) -> Self {
|
||||
self.suffix = Some(suffix.into_any_element());
|
||||
self
|
||||
}
|
||||
|
||||
/// Add children of the TabBar, all children will inherit the variant.
|
||||
pub fn children(mut self, children: impl IntoIterator<Item = impl Into<Tab>>) -> Self {
|
||||
self.children.extend(children.into_iter().map(Into::into));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add child of the TabBar, tab will inherit the variant.
|
||||
pub fn child(mut self, child: impl Into<Tab>) -> Self {
|
||||
self.children.push(child.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the selected index of the TabBar.
|
||||
pub fn selected_index(mut self, index: usize) -> Self {
|
||||
self.selected_index = Some(index);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the last empty space element of the TabBar.
|
||||
pub fn last_empty_space(mut self, last_empty_space: impl IntoElement) -> Self {
|
||||
self.last_empty_space = last_empty_space.into_any_element();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the on_click callback of the TabBar, the first parameter is the index of the clicked tab.
|
||||
///
|
||||
/// When this is set, the children's on_click will be ignored.
|
||||
pub fn on_click<F>(mut self, on_click: F) -> Self
|
||||
where
|
||||
F: Fn(&usize, &mut Window, &mut App) + 'static,
|
||||
{
|
||||
self.on_click = Some(Rc::new(on_click));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for TabBar {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
&mut self.style
|
||||
}
|
||||
}
|
||||
|
||||
impl Sizable for TabBar {
|
||||
fn with_size(mut self, size: impl Into<Size>) -> Self {
|
||||
self.size = size.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for TabBar {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let default_gap = match self.size {
|
||||
Size::Small | Size::XSmall => px(8.),
|
||||
Size::Large => px(16.),
|
||||
_ => px(12.),
|
||||
};
|
||||
let (bg, paddings, gap) = match self.variant {
|
||||
TabVariant::Tab => {
|
||||
let padding = Edges::all(px(0.));
|
||||
(cx.theme().tab_background, padding, px(0.))
|
||||
}
|
||||
TabVariant::Outline => {
|
||||
let padding = Edges::all(px(0.));
|
||||
(gpui::transparent_black(), padding, default_gap)
|
||||
}
|
||||
TabVariant::Pill => {
|
||||
let padding = Edges::all(px(0.));
|
||||
(gpui::transparent_black(), padding, px(4.))
|
||||
}
|
||||
TabVariant::Segmented => {
|
||||
let padding_x = match self.size {
|
||||
Size::XSmall => px(2.),
|
||||
Size::Small => px(3.),
|
||||
_ => px(4.),
|
||||
};
|
||||
let padding = Edges {
|
||||
left: padding_x,
|
||||
right: padding_x,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
(cx.theme().tab_background, padding, px(2.))
|
||||
}
|
||||
TabVariant::Underline => {
|
||||
// This gap is same as the tab inner_paddings
|
||||
let gap = match self.size {
|
||||
Size::XSmall => px(10.),
|
||||
Size::Small => px(12.),
|
||||
Size::Large => px(20.),
|
||||
_ => px(16.),
|
||||
};
|
||||
|
||||
(gpui::transparent_black(), Edges::all(px(0.)), gap)
|
||||
}
|
||||
};
|
||||
|
||||
let mut item_labels = Vec::new();
|
||||
let selected_index = self.selected_index;
|
||||
let on_click = self.on_click.clone();
|
||||
|
||||
self.base
|
||||
.group("tab-bar")
|
||||
.relative()
|
||||
.flex()
|
||||
.items_center()
|
||||
.bg(bg)
|
||||
.text_color(cx.theme().tab_foreground)
|
||||
.when(
|
||||
self.variant == TabVariant::Underline || self.variant == TabVariant::Tab,
|
||||
|this| {
|
||||
this.child(
|
||||
div()
|
||||
.id("border-b")
|
||||
.absolute()
|
||||
.left_0()
|
||||
.bottom_0()
|
||||
.size_full()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().border),
|
||||
)
|
||||
},
|
||||
)
|
||||
.rounded(self.variant.tab_bar_radius(self.size, cx))
|
||||
.paddings(paddings)
|
||||
.refine_style(&self.style)
|
||||
.when_some(self.prefix, |this, prefix| this.child(prefix))
|
||||
.child(
|
||||
h_flex()
|
||||
.id("tabs")
|
||||
.flex_1()
|
||||
.overflow_x_scroll()
|
||||
.when_some(self.scroll_handle, |this, scroll_handle| {
|
||||
this.track_scroll(&scroll_handle)
|
||||
})
|
||||
.gap(gap)
|
||||
.children(self.children.into_iter().enumerate().map(|(ix, child)| {
|
||||
item_labels.push((child.label.clone(), child.disabled));
|
||||
let tab_bar_prefix = child.tab_bar_prefix.unwrap_or(true);
|
||||
child
|
||||
.ix(ix)
|
||||
.tab_bar_prefix(tab_bar_prefix)
|
||||
.with_variant(self.variant)
|
||||
.with_size(self.size)
|
||||
.when_some(self.selected_index, |this, selected_ix| {
|
||||
this.selected(selected_ix == ix)
|
||||
})
|
||||
.when_some(self.on_click.clone(), move |this, on_click| {
|
||||
this.on_click(move |_, window, cx| on_click(&ix, window, cx))
|
||||
})
|
||||
}))
|
||||
.when(self.suffix.is_some() || self.menu, |this| {
|
||||
this.child(self.last_empty_space)
|
||||
}),
|
||||
)
|
||||
.when(self.menu, |this| {
|
||||
this.child(
|
||||
Button::new("more")
|
||||
.xsmall()
|
||||
.ghost()
|
||||
.icon(IconName::ChevronDown)
|
||||
.dropdown_menu(move |mut this, _, _| {
|
||||
this = this.scrollable(true);
|
||||
for (ix, (label, disabled)) in item_labels.iter().enumerate() {
|
||||
this = this.item(
|
||||
PopupMenuItem::new(label.clone().unwrap_or_default())
|
||||
.checked(selected_index == Some(ix))
|
||||
.disabled(*disabled)
|
||||
.when_some(on_click.clone(), |this, on_click| {
|
||||
this.on_click(move |_, window, cx| {
|
||||
on_click(&ix, window, cx)
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
this
|
||||
})
|
||||
.anchor(Anchor::TopRight),
|
||||
)
|
||||
})
|
||||
.when_some(self.suffix, |this, suffix| this.child(suffix))
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, relative, App, AppContext, Context, Entity, IntoElement, ParentElement, Render,
|
||||
SharedString, Styled, Window,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
pub struct Tooltip {
|
||||
text: SharedString,
|
||||
}
|
||||
|
||||
impl Tooltip {
|
||||
pub fn new(text: impl Into<SharedString>, _window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||
cx.new(|_| Self { text: text.into() })
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Tooltip {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div().child(
|
||||
div()
|
||||
.font_family(".SystemUIFont")
|
||||
.m_3()
|
||||
.p_1p5()
|
||||
.border_1()
|
||||
.border_color(cx.theme().border)
|
||||
.bg(cx.theme().surface_background)
|
||||
.when(cx.theme().shadow, |this| this.shadow_sm())
|
||||
.rounded(cx.theme().radius)
|
||||
.text_xs()
|
||||
.text_color(cx.theme().text)
|
||||
.line_height(relative(1.25))
|
||||
.child(self.text.clone()),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{App, ElementId, Entity, Window};
|
||||
|
||||
use crate::Root;
|
||||
use crate::input::InputState;
|
||||
use crate::modal::Modal;
|
||||
use crate::notification::Notification;
|
||||
|
||||
/// Extension trait for [`Window`] to add modal, notification .. functionality.
|
||||
pub trait WindowExtension: Sized {
|
||||
/// Opens a Modal.
|
||||
fn open_modal<F>(&mut self, cx: &mut App, builder: F)
|
||||
where
|
||||
F: Fn(Modal, &mut Window, &mut App) -> Modal + 'static;
|
||||
|
||||
/// Return true, if there is an active Modal.
|
||||
fn has_active_modal(&mut self, cx: &mut App) -> bool;
|
||||
|
||||
/// Closes the last active Modal.
|
||||
fn close_modal(&mut self, cx: &mut App);
|
||||
|
||||
/// Closes all active Modals.
|
||||
fn close_all_modals(&mut self, cx: &mut App);
|
||||
|
||||
/// Returns number of notifications.
|
||||
fn notifications(&mut self, cx: &mut App) -> Rc<Vec<Entity<Notification>>>;
|
||||
|
||||
/// Pushes a notification to the notification list.
|
||||
fn push_notification<T>(&mut self, note: T, cx: &mut App)
|
||||
where
|
||||
T: Into<Notification>;
|
||||
|
||||
/// Clear the unique notification.
|
||||
fn clear_notification<T: Sized + 'static>(&mut self, cx: &mut App);
|
||||
|
||||
/// Clear the unique notification with the given id.
|
||||
fn clear_notification_by_id<T: Sized + 'static>(
|
||||
&mut self,
|
||||
key: impl Into<ElementId>,
|
||||
cx: &mut App,
|
||||
);
|
||||
|
||||
/// Clear all notifications
|
||||
fn clear_notifications(&mut self, cx: &mut App);
|
||||
|
||||
/// Return current focused Input entity.
|
||||
fn focused_input(&mut self, cx: &mut App) -> Option<Entity<InputState>>;
|
||||
|
||||
/// Returns true if there is a focused Input entity.
|
||||
fn has_focused_input(&mut self, cx: &mut App) -> bool;
|
||||
}
|
||||
|
||||
impl WindowExtension for Window {
|
||||
#[inline]
|
||||
fn open_modal<F>(&mut self, cx: &mut App, builder: F)
|
||||
where
|
||||
F: Fn(Modal, &mut Window, &mut App) -> Modal + 'static,
|
||||
{
|
||||
Root::update(self, cx, move |root, window, cx| {
|
||||
root.open_modal(builder, window, cx);
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn has_active_modal(&mut self, cx: &mut App) -> bool {
|
||||
Root::read(self, cx).has_active_modals()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn close_modal(&mut self, cx: &mut App) {
|
||||
Root::update(self, cx, move |root, window, cx| {
|
||||
root.close_modal(window, cx);
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn close_all_modals(&mut self, cx: &mut App) {
|
||||
Root::update(self, cx, |root, window, cx| {
|
||||
root.close_all_modals(window, cx);
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn push_notification<T>(&mut self, note: T, cx: &mut App)
|
||||
where
|
||||
T: Into<Notification>,
|
||||
{
|
||||
let note = note.into();
|
||||
Root::update(self, cx, move |root, window, cx| {
|
||||
root.push_notification(note, window, cx);
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn clear_notification<T: Sized + 'static>(&mut self, cx: &mut App) {
|
||||
Root::update(self, cx, |root, window, cx| {
|
||||
root.clear_notification::<T>(window, cx);
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn clear_notification_by_id<T: Sized + 'static>(
|
||||
&mut self,
|
||||
key: impl Into<ElementId>,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let key: ElementId = key.into();
|
||||
Root::update(self, cx, |root, window, cx| {
|
||||
root.clear_notification_by_id::<T>(key, window, cx);
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn clear_notifications(&mut self, cx: &mut App) {
|
||||
Root::update(self, cx, move |root, window, cx| {
|
||||
root.clear_notifications(window, cx);
|
||||
})
|
||||
}
|
||||
|
||||
fn notifications(&mut self, cx: &mut App) -> Rc<Vec<Entity<Notification>>> {
|
||||
let entity = Root::read(self, cx).notification.clone();
|
||||
Rc::new(entity.read(cx).notifications())
|
||||
}
|
||||
|
||||
fn has_focused_input(&mut self, cx: &mut App) -> bool {
|
||||
Root::read(self, cx).focused_input.is_some()
|
||||
}
|
||||
|
||||
fn focused_input(&mut self, cx: &mut App) -> Option<Entity<InputState>> {
|
||||
Root::read(self, cx).focused_input.clone()
|
||||
}
|
||||
}
|
||||
@@ -28,9 +28,7 @@ icons = [
|
||||
|
||||
[dependencies]
|
||||
assets = { path = "../crates/assets" }
|
||||
ui = { path = "../crates/ui" }
|
||||
title_bar = { path = "../crates/title_bar" }
|
||||
theme = { path = "../crates/theme" }
|
||||
common = { path = "../crates/common" }
|
||||
state = { path = "../crates/state" }
|
||||
device = { path = "../crates/device" }
|
||||
@@ -47,6 +45,7 @@ gpui_linux.workspace = true
|
||||
gpui_windows.workspace = true
|
||||
gpui_macos.workspace = true
|
||||
gpui_tokio.workspace = true
|
||||
gpui-component.workspace = true
|
||||
reqwest_client.workspace = true
|
||||
|
||||
nostr-connect.workspace = true
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
use anyhow::Error;
|
||||
use common::CoopIcon;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
App, AppContext, Context, Entity, InteractiveElement, IntoElement, ParentElement, Render,
|
||||
SharedString, StatefulInteractiveElement, Styled, Subscription, Task, Window, div, px,
|
||||
};
|
||||
use gpui_component::avatar::Avatar;
|
||||
use gpui_component::button::{Button, ButtonVariants};
|
||||
use gpui_component::spinner::Spinner;
|
||||
use gpui_component::{ActiveTheme, Disableable, Icon, Sizable, WindowExt, h_flex, v_flex};
|
||||
use nostr_sdk::prelude::*;
|
||||
use person::PersonRegistry;
|
||||
use state::{NostrRegistry, StateEvent};
|
||||
use theme::ActiveTheme;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::indicator::Indicator;
|
||||
use ui::{Disableable, Icon, IconName, Sizable, WindowExtension, h_flex, v_flex};
|
||||
|
||||
use crate::dialogs::connect::ConnectSigner;
|
||||
use crate::dialogs::import::ImportKey;
|
||||
@@ -45,7 +45,7 @@ impl AccountSelector {
|
||||
let subscription = cx.subscribe_in(&nostr, window, |this, _state, event, window, cx| {
|
||||
match event {
|
||||
StateEvent::SignerSet => {
|
||||
window.close_all_modals(cx);
|
||||
window.close_all_dialogs(cx);
|
||||
window.refresh();
|
||||
}
|
||||
StateEvent::Error(e) => {
|
||||
@@ -124,10 +124,10 @@ impl AccountSelector {
|
||||
fn open_import(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let import = cx.new(|cx| ImportKey::new(window, cx));
|
||||
|
||||
window.open_modal(cx, move |this, _window, _cx| {
|
||||
window.open_dialog(cx, move |this, _window, _cx| {
|
||||
this.width(px(460.))
|
||||
.title("Import a Secret Key or Bunker Connection")
|
||||
.show_close(true)
|
||||
.close_button(true)
|
||||
.pb_2()
|
||||
.child(import.clone())
|
||||
});
|
||||
@@ -136,10 +136,10 @@ impl AccountSelector {
|
||||
fn open_connect(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let connect = cx.new(|cx| ConnectSigner::new(window, cx));
|
||||
|
||||
window.open_modal(cx, move |this, _window, _cx| {
|
||||
window.open_dialog(cx, move |this, _window, _cx| {
|
||||
this.width(px(460.))
|
||||
.title("Scan QR Code to Connect")
|
||||
.show_close(true)
|
||||
.close_button(true)
|
||||
.pb_2()
|
||||
.child(connect.clone())
|
||||
});
|
||||
@@ -162,7 +162,7 @@ impl Render for AccountSelector {
|
||||
.italic()
|
||||
.text_xs()
|
||||
.text_center()
|
||||
.text_color(cx.theme().text_danger)
|
||||
.text_color(cx.theme().danger_active)
|
||||
.child(error.clone()),
|
||||
)
|
||||
})
|
||||
@@ -182,15 +182,15 @@ impl Render for AccountSelector {
|
||||
.justify_between()
|
||||
.w_full()
|
||||
.rounded(cx.theme().radius)
|
||||
.bg(cx.theme().ghost_element_background)
|
||||
.hover(|this| this.bg(cx.theme().ghost_element_hover))
|
||||
.bg(cx.theme().secondary)
|
||||
.hover(|this| this.bg(cx.theme().secondary_hover))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Avatar::new(profile.avatar()).small())
|
||||
.child(Avatar::new().src(profile.avatar()).small())
|
||||
.child(div().text_sm().child(profile.name())),
|
||||
)
|
||||
.when(logging_in, |this| this.child(Indicator::new().small()))
|
||||
.when(logging_in, |this| this.child(Spinner::new().small()))
|
||||
.when(!logging_in, |this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
@@ -199,7 +199,7 @@ impl Render for AccountSelector {
|
||||
.group_hover("", |this| this.visible())
|
||||
.child(
|
||||
Button::new(format!("del-{ix}"))
|
||||
.icon(IconName::Close)
|
||||
.icon(CoopIcon::Close)
|
||||
.ghost()
|
||||
.small()
|
||||
.disabled(logging_in)
|
||||
@@ -224,7 +224,7 @@ impl Render for AccountSelector {
|
||||
|
||||
items
|
||||
})
|
||||
.child(div().w_full().h_px().bg(cx.theme().border_variant))
|
||||
.child(div().w_full().h_px().bg(cx.theme().border))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
@@ -232,7 +232,7 @@ impl Render for AccountSelector {
|
||||
.w_full()
|
||||
.child(
|
||||
Button::new("input")
|
||||
.icon(Icon::new(IconName::Usb))
|
||||
.icon(Icon::new(CoopIcon::Usb))
|
||||
.label("Import")
|
||||
.ghost()
|
||||
.small()
|
||||
@@ -243,7 +243,7 @@ impl Render for AccountSelector {
|
||||
)
|
||||
.child(
|
||||
Button::new("qr")
|
||||
.icon(Icon::new(IconName::Scan))
|
||||
.icon(Icon::new(CoopIcon::Scan))
|
||||
.label("Scan QR to connect")
|
||||
.ghost()
|
||||
.small()
|
||||
|
||||
@@ -7,13 +7,12 @@ use gpui::{
|
||||
AppContext, Context, Entity, Image, IntoElement, ParentElement, Render, SharedString, Styled,
|
||||
Subscription, Window, div, img, px,
|
||||
};
|
||||
use gpui_component::{ActiveTheme, v_flex};
|
||||
use nostr_connect::prelude::*;
|
||||
use state::{
|
||||
CLIENT_NAME, CoopAuthUrlHandler, NOSTR_CONNECT_RELAY, NOSTR_CONNECT_TIMEOUT, NostrRegistry,
|
||||
StateEvent,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use ui::v_flex;
|
||||
|
||||
pub struct ConnectSigner {
|
||||
/// QR Code
|
||||
@@ -101,14 +100,14 @@ impl Render for ConnectSigner {
|
||||
div()
|
||||
.text_xs()
|
||||
.text_center()
|
||||
.text_color(cx.theme().text_danger)
|
||||
.text_color(cx.theme().danger_active)
|
||||
.child(error.clone()),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(SharedString::from(MSG)),
|
||||
)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user