yeah
This commit is contained in:
commit
5fad11ec7b
184 changed files with 63932 additions and 0 deletions
19
.direnv/bin/nix-direnv-reload
Executable file
19
.direnv/bin/nix-direnv-reload
Executable file
|
@ -0,0 +1,19 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
if [[ ! -d "/mnt/k/minky/nite" ]]; then
|
||||||
|
echo "Cannot find source directory; Did you move it?"
|
||||||
|
echo "(Looking for "/mnt/k/minky/nite")"
|
||||||
|
echo 'Cannot force reload with this script - use "direnv reload" manually and then try again'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# rebuild the cache forcefully
|
||||||
|
_nix_direnv_force_reload=1 direnv exec "/mnt/k/minky/nite" true
|
||||||
|
|
||||||
|
# Update the mtime for .envrc.
|
||||||
|
# This will cause direnv to reload again - but without re-building.
|
||||||
|
touch "/mnt/k/minky/nite/.envrc"
|
||||||
|
|
||||||
|
# Also update the timestamp of whatever profile_rc we have.
|
||||||
|
# This makes sure that we know we are up to date.
|
||||||
|
touch -r "/mnt/k/minky/nite/.envrc" "/mnt/k/minky/nite/.direnv"/*.rc
|
1
.direnv/flake-inputs/05yb0d179k1pw74yxnlhdq9ld30yp9pk-source
Symbolic link
1
.direnv/flake-inputs/05yb0d179k1pw74yxnlhdq9ld30yp9pk-source
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
/nix/store/05yb0d179k1pw74yxnlhdq9ld30yp9pk-source
|
1
.direnv/flake-inputs/3cp7q2d0ywi20zhva9bcczisxmq1jxgb-source
Symbolic link
1
.direnv/flake-inputs/3cp7q2d0ywi20zhva9bcczisxmq1jxgb-source
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
/nix/store/3cp7q2d0ywi20zhva9bcczisxmq1jxgb-source
|
1
.direnv/flake-inputs/49xf0m8xlwppfgx9xa45ybvcsn9yiy18-source
Symbolic link
1
.direnv/flake-inputs/49xf0m8xlwppfgx9xa45ybvcsn9yiy18-source
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
/nix/store/49xf0m8xlwppfgx9xa45ybvcsn9yiy18-source
|
1
.direnv/flake-inputs/5a09v6jw29b21vvc35rsv5czv0z0nlq8-source
Symbolic link
1
.direnv/flake-inputs/5a09v6jw29b21vvc35rsv5czv0z0nlq8-source
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
/nix/store/5a09v6jw29b21vvc35rsv5czv0z0nlq8-source
|
1
.direnv/flake-inputs/6q5b11kr46mrvipv2fm6wx2qnvsdi8mh-source
Symbolic link
1
.direnv/flake-inputs/6q5b11kr46mrvipv2fm6wx2qnvsdi8mh-source
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
/nix/store/6q5b11kr46mrvipv2fm6wx2qnvsdi8mh-source
|
1
.direnv/flake-inputs/a299nv68x7dm4fc9mj60qwrjn31zvw3z-source
Symbolic link
1
.direnv/flake-inputs/a299nv68x7dm4fc9mj60qwrjn31zvw3z-source
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
/nix/store/a299nv68x7dm4fc9mj60qwrjn31zvw3z-source
|
1
.direnv/flake-inputs/ah2qh833bkpxg8girwyl6vs30fkp1109-source
Symbolic link
1
.direnv/flake-inputs/ah2qh833bkpxg8girwyl6vs30fkp1109-source
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
/nix/store/ah2qh833bkpxg8girwyl6vs30fkp1109-source
|
1
.direnv/flake-inputs/br885sqy62q1bblwi2bslcfg2193ly75-source
Symbolic link
1
.direnv/flake-inputs/br885sqy62q1bblwi2bslcfg2193ly75-source
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
/nix/store/br885sqy62q1bblwi2bslcfg2193ly75-source
|
1
.direnv/flake-inputs/gzf4zwcakda1nykn6h0avh45xhjhvsz4-source
Symbolic link
1
.direnv/flake-inputs/gzf4zwcakda1nykn6h0avh45xhjhvsz4-source
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
/nix/store/gzf4zwcakda1nykn6h0avh45xhjhvsz4-source
|
1
.direnv/flake-inputs/nmf1ggxf77gzv7cw5h91d6l1wh4y6qyj-source
Symbolic link
1
.direnv/flake-inputs/nmf1ggxf77gzv7cw5h91d6l1wh4y6qyj-source
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
/nix/store/nmf1ggxf77gzv7cw5h91d6l1wh4y6qyj-source
|
1
.direnv/flake-inputs/paqmjg18kvzmbrbil9g2mq9k4015fd7p-source
Symbolic link
1
.direnv/flake-inputs/paqmjg18kvzmbrbil9g2mq9k4015fd7p-source
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
/nix/store/paqmjg18kvzmbrbil9g2mq9k4015fd7p-source
|
1
.direnv/flake-inputs/pfc56yr7y3wflvbgnrpscf2n1m4j3xd7-source
Symbolic link
1
.direnv/flake-inputs/pfc56yr7y3wflvbgnrpscf2n1m4j3xd7-source
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
/nix/store/pfc56yr7y3wflvbgnrpscf2n1m4j3xd7-source
|
1
.direnv/flake-inputs/pgid9c9xfcrbqx2giry0an0bi0df7s5c-source
Symbolic link
1
.direnv/flake-inputs/pgid9c9xfcrbqx2giry0an0bi0df7s5c-source
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
/nix/store/pgid9c9xfcrbqx2giry0an0bi0df7s5c-source
|
1
.direnv/flake-inputs/qkig73szmrhgp0qhncxy5vb36lw2g3jj-source
Symbolic link
1
.direnv/flake-inputs/qkig73szmrhgp0qhncxy5vb36lw2g3jj-source
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
/nix/store/qkig73szmrhgp0qhncxy5vb36lw2g3jj-source
|
1
.direnv/flake-inputs/rzkl4xygy3z1glq8cgrv5cc075ylxs0g-source
Symbolic link
1
.direnv/flake-inputs/rzkl4xygy3z1glq8cgrv5cc075ylxs0g-source
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
/nix/store/rzkl4xygy3z1glq8cgrv5cc075ylxs0g-source
|
1
.direnv/flake-inputs/vm4qsaala00i8q5js7i3am3w0m766k1d-source
Symbolic link
1
.direnv/flake-inputs/vm4qsaala00i8q5js7i3am3w0m766k1d-source
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
/nix/store/vm4qsaala00i8q5js7i3am3w0m766k1d-source
|
1
.direnv/flake-inputs/vpddlysgdvzcqixkqgx49zyx2whhzpkb-source
Symbolic link
1
.direnv/flake-inputs/vpddlysgdvzcqixkqgx49zyx2whhzpkb-source
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
/nix/store/vpddlysgdvzcqixkqgx49zyx2whhzpkb-source
|
1
.direnv/flake-inputs/y1nw9w1s0ly6442igksfq29v0cfbnmfd-source
Symbolic link
1
.direnv/flake-inputs/y1nw9w1s0ly6442igksfq29v0cfbnmfd-source
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
/nix/store/y1nw9w1s0ly6442igksfq29v0cfbnmfd-source
|
1
.direnv/flake-inputs/yj1wxm9hh8610iyzqnz75kvs6xl8j3my-source
Symbolic link
1
.direnv/flake-inputs/yj1wxm9hh8610iyzqnz75kvs6xl8j3my-source
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
/nix/store/yj1wxm9hh8610iyzqnz75kvs6xl8j3my-source
|
1
.direnv/flake-inputs/z9wkyy0bbdjfvsmkkw16bmn56502hd1k-source
Symbolic link
1
.direnv/flake-inputs/z9wkyy0bbdjfvsmkkw16bmn56502hd1k-source
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
/nix/store/z9wkyy0bbdjfvsmkkw16bmn56502hd1k-source
|
1
.direnv/flake-profile-a5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa
Symbolic link
1
.direnv/flake-profile-a5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
/nix/store/fbldsappzwwr5acj8k1km1dy9ahpx9dj-nite-env
|
|
@ -0,0 +1,68 @@
|
||||||
|
unset shellHook
|
||||||
|
PATH=${PATH:-}
|
||||||
|
nix_saved_PATH="$PATH"
|
||||||
|
XDG_DATA_DIRS=${XDG_DATA_DIRS:-}
|
||||||
|
nix_saved_XDG_DATA_DIRS="$XDG_DATA_DIRS"
|
||||||
|
BASH='/noshell'
|
||||||
|
HOSTTYPE='x86_64'
|
||||||
|
IFS='
|
||||||
|
'
|
||||||
|
IN_NIX_SHELL='impure'
|
||||||
|
export IN_NIX_SHELL
|
||||||
|
LINENO='76'
|
||||||
|
MACHTYPE='x86_64-pc-linux-gnu'
|
||||||
|
NIX_BUILD_CORES='0'
|
||||||
|
export NIX_BUILD_CORES
|
||||||
|
NIX_STORE='/nix/store'
|
||||||
|
export NIX_STORE
|
||||||
|
OLDPWD=''
|
||||||
|
export OLDPWD
|
||||||
|
OPTERR='1'
|
||||||
|
OSTYPE='linux-gnu'
|
||||||
|
PATH='/path-not-set'
|
||||||
|
export PATH
|
||||||
|
PS4='+ '
|
||||||
|
builder='/nix/store/nnvsjd3f3dh9wdl4s9mwg5cfri8kds5j-bash-interactive-5.2p26/bin/bash'
|
||||||
|
export builder
|
||||||
|
dontAddDisableDepTrack='1'
|
||||||
|
export dontAddDisableDepTrack
|
||||||
|
name='nite-env'
|
||||||
|
export name
|
||||||
|
out='/mnt/k/minky/nite/outputs/out'
|
||||||
|
export out
|
||||||
|
outputs='out'
|
||||||
|
shellHook='# Remove all the unnecessary noise that is set by the build env
|
||||||
|
unset NIX_BUILD_TOP NIX_BUILD_CORES NIX_STORE
|
||||||
|
unset TEMP TEMPDIR TMP TMPDIR
|
||||||
|
# $name variable is preserved to keep it compatible with pure shell https://github.com/sindresorhus/pure/blob/47c0c881f0e7cfdb5eaccd335f52ad17b897c060/pure.zsh#L235
|
||||||
|
unset builder out shellHook stdenv system
|
||||||
|
# Flakes stuff
|
||||||
|
unset dontAddDisableDepTrack outputs
|
||||||
|
|
||||||
|
# For `nix develop`. We get /noshell on Linux and /sbin/nologin on macOS.
|
||||||
|
if [[ "$SHELL" == "/noshell" || "$SHELL" == "/sbin/nologin" ]]; then
|
||||||
|
export SHELL=/nix/store/nnvsjd3f3dh9wdl4s9mwg5cfri8kds5j-bash-interactive-5.2p26/bin/bash
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Load the environment
|
||||||
|
source "/nix/store/gw8h24v1c8p8xjf4q604hb7fggxig31j-nite-dir/env.bash"
|
||||||
|
'
|
||||||
|
export shellHook
|
||||||
|
stdenv='/nix/store/nqq1afagwrm4azc5ahh48qgkdzqi0jx7-naked-stdenv'
|
||||||
|
export stdenv
|
||||||
|
system='x86_64-linux'
|
||||||
|
export system
|
||||||
|
runHook ()
|
||||||
|
{
|
||||||
|
|
||||||
|
eval "$shellHook";
|
||||||
|
unset runHook
|
||||||
|
}
|
||||||
|
PATH="$PATH${nix_saved_PATH:+:$nix_saved_PATH}"
|
||||||
|
XDG_DATA_DIRS="$XDG_DATA_DIRS${nix_saved_XDG_DATA_DIRS:+:$nix_saved_XDG_DATA_DIRS}"
|
||||||
|
export NIX_BUILD_TOP="$(mktemp -d -t nix-shell.XXXXXX)"
|
||||||
|
export TMP="$NIX_BUILD_TOP"
|
||||||
|
export TMPDIR="$NIX_BUILD_TOP"
|
||||||
|
export TEMP="$NIX_BUILD_TOP"
|
||||||
|
export TEMPDIR="$NIX_BUILD_TOP"
|
||||||
|
eval "$shellHook"
|
4
.envrc
Normal file
4
.envrc
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
if ! has nix_direnv_version || ! nix_direnv_version 3.0.4; then
|
||||||
|
source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.4/direnvrc" "sha256-DzlYZ33mWF/Gs8DDeyjr8mnVmQGx7ASYqA5WlxwvBG4="
|
||||||
|
fi
|
||||||
|
use flake . --impure
|
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/target
|
4105
Cargo.lock
generated
Normal file
4105
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
84
Cargo.toml
Normal file
84
Cargo.toml
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
[workspace]
|
||||||
|
members = [
|
||||||
|
"crates/*"
|
||||||
|
]
|
||||||
|
resolver = "2"
|
||||||
|
|
||||||
|
[workspace.package]
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
license = "GPL-3.0"
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
## crates/*
|
||||||
|
collections.path = "crates/collections"
|
||||||
|
ming.path = "crates/ming"
|
||||||
|
ming_macros.path = "crates/ming_macros"
|
||||||
|
refineable.path = "crates/refineable"
|
||||||
|
util.path = "crates/util"
|
||||||
|
|
||||||
|
## Minky deps
|
||||||
|
funnylog = { git = "https://codeberg.org/minky/funnylog.git", version = "0.1.0", features = ["terminal", "log"] }
|
||||||
|
|
||||||
|
## Other deps
|
||||||
|
blade-graphics = { git = "https://github.com/kvark/blade", rev = "e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c" }
|
||||||
|
blade-macros = { git = "https://github.com/kvark/blade", rev = "e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c" }
|
||||||
|
git2 = { version = "0.18", default-features = false }
|
||||||
|
globset = "0.4"
|
||||||
|
regex = "1"
|
||||||
|
rust-embed = { version = "8.4", features = ["include-exclude"] }
|
||||||
|
tempfile = "3.9"
|
||||||
|
unicase = "2.6"
|
||||||
|
derive_more = "0.99.17"
|
||||||
|
anyhow = "1"
|
||||||
|
ctor = "0.2.6"
|
||||||
|
log = "0.4"
|
||||||
|
itertools = "0.12"
|
||||||
|
lazy_static = "1.4.0"
|
||||||
|
parking_lot = "0.12"
|
||||||
|
postage = { version = "0.5", features = ["futures-traits"] }
|
||||||
|
profiling = "1"
|
||||||
|
rand = "0.8.5"
|
||||||
|
schemars = "0.8"
|
||||||
|
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||||
|
serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
|
||||||
|
serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
|
||||||
|
serde_json_lenient = { version = "0.1", features = [
|
||||||
|
"preserve_order",
|
||||||
|
"raw_value",
|
||||||
|
] }
|
||||||
|
serde_repr = "0.1"
|
||||||
|
smallvec = { version = "1.6", features = ["union"] }
|
||||||
|
sha2 = "0.10"
|
||||||
|
thiserror = "1"
|
||||||
|
time = { version = "0.3", features = [
|
||||||
|
"macros",
|
||||||
|
"parsing",
|
||||||
|
"serde",
|
||||||
|
"serde-well-known",
|
||||||
|
"formatting",
|
||||||
|
] }
|
||||||
|
uuid = "1"
|
||||||
|
|
||||||
|
### Async stuff
|
||||||
|
futures = "0.3"
|
||||||
|
tokio = { version = "1", features = [ "full" ] }
|
||||||
|
tokio-stream = { version = "0.1", features = [ "full" ] }
|
||||||
|
|
||||||
|
[workspace.lints.clippy]
|
||||||
|
dbg_macro = "deny"
|
||||||
|
todo = "deny"
|
||||||
|
|
||||||
|
style = "allow"
|
||||||
|
|
||||||
|
almost_complete_range = "allow"
|
||||||
|
arc_with_non_send_sync = "allow"
|
||||||
|
borrowed_box = "allow"
|
||||||
|
let_underscore_future = "allow"
|
||||||
|
map_entry = "allow"
|
||||||
|
non_canonical_partial_ord_impl = "allow"
|
||||||
|
reversed_empty_ranges = "allow"
|
||||||
|
type_complexity = "allow"
|
||||||
|
|
||||||
|
[workspace.metadata.cargo-machete]
|
||||||
|
ignored = ["bindgen", "cbindgen", "prost_build", "serde"]
|
200
LICENSE-GPL
Normal file
200
LICENSE-GPL
Normal file
|
@ -0,0 +1,200 @@
|
||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright © 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU General Public License is a free, copyleft license for software and other kinds of works.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights.
|
||||||
|
|
||||||
|
Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it.
|
||||||
|
|
||||||
|
For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions.
|
||||||
|
|
||||||
|
Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users.
|
||||||
|
|
||||||
|
Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
"This License" refers to version 3 of the GNU General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work.
|
||||||
|
A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures.
|
||||||
|
When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program.
|
||||||
|
You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
a) The work must carry prominent notices stating that you modified it, and giving a relevant date.
|
||||||
|
b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices".
|
||||||
|
c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it.
|
||||||
|
d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so.
|
||||||
|
A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways:
|
||||||
|
a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange.
|
||||||
|
b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge.
|
||||||
|
c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b.
|
||||||
|
d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements.
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d.
|
||||||
|
A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
"Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions.
|
||||||
|
When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or authors of the material; or
|
||||||
|
e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or
|
||||||
|
f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors.
|
||||||
|
All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11).
|
||||||
|
However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so.
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License.
|
||||||
|
An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version".
|
||||||
|
A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program.
|
||||||
|
13. Use with the GNU Affero General Public License.
|
||||||
|
Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such.
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
|
||||||
|
Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
16. Limitation of Liability.
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode:
|
||||||
|
|
||||||
|
<program> Copyright (C) <year> <name of author>
|
||||||
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
|
This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box".
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read <https: //www.gnu.org/licenses/why-not-lgpl.html>.
|
34
README.md
Normal file
34
README.md
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
# nite
|
||||||
|
|
||||||
|
Nite is (will soon be) a GUI code editor with a vim-ish feel.
|
||||||
|
|
||||||
|
It's intended to be an (almost) drop-in replacement for Neovim, aside from nite being a GUI app, which means having items listed in the [TODO](#todo) paragraph.
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
|
||||||
|
The following list of features and tasks required to have them is highly opinionated and consists of things I want a code editor to have.
|
||||||
|
|
||||||
|
- [ ] Make it compile :)
|
||||||
|
- [ ] Vim-like keybindings and motions
|
||||||
|
- [ ] A Telescope-ey file finder
|
||||||
|
- [ ] Something like [harpoon](https://github.com/theprimeagen/harpoon) and maybe a topbar with marks
|
||||||
|
- [ ] LSP client (note: [async-lsp](https://docs.rs/async-lsp))
|
||||||
|
- [ ] Popup terminal (note: [kitti3](https://github.com/LandingEllipse/kitti3))
|
||||||
|
- [ ] Workspace splitting
|
||||||
|
- [ ] (optional) Git integration
|
||||||
|
|
||||||
|
## Info on [ming]
|
||||||
|
|
||||||
|
Ming is a GPU-accelerated native UI framework.
|
||||||
|
|
||||||
|
It is planned to have full Linux support and (maybe) a Material 3 implementation.
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
A **LOT** of code (including: crates/util, crates/ming*) is taken from [the Zed codebase](https://github.com/zed-industries/zed) precisely at commit [c90263d].
|
||||||
|
|
||||||
|
Also, [ming] is a "fork" (blatant copy with no intent of syncing upstream changes) of [gpui], also made by the Zed team for use in Zed.
|
||||||
|
|
||||||
|
[ming]: ./crates/ming/README.md
|
||||||
|
[c90263d]: https://github.com/zed-industries/zed/tree/c90263d59b8cde9e007f16b286f040fa849ecb24
|
||||||
|
[gpui]: https://github.com/zed-industries/zed/tree/c90263d59b8cde9e007f16b286f040fa849ecb24/crates/gpui
|
19
crates/collections/Cargo.toml
Normal file
19
crates/collections/Cargo.toml
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
[package]
|
||||||
|
name = "collections"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
license = "Apache-2.0"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/collections.rs"
|
||||||
|
doctest = false
|
||||||
|
|
||||||
|
[features]
|
||||||
|
test-support = []
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
rustc-hash = "1.1"
|
15
crates/collections/src/collections.rs
Normal file
15
crates/collections/src/collections.rs
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
#[cfg(feature = "test-support")]
|
||||||
|
pub type HashMap<K, V> = FxHashMap<K, V>;
|
||||||
|
|
||||||
|
#[cfg(feature = "test-support")]
|
||||||
|
pub type HashSet<T> = FxHashSet<T>;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "test-support"))]
|
||||||
|
pub type HashMap<K, V> = std::collections::HashMap<K, V>;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "test-support"))]
|
||||||
|
pub type HashSet<T> = std::collections::HashSet<T>;
|
||||||
|
|
||||||
|
pub use rustc_hash::FxHasher;
|
||||||
|
pub use rustc_hash::{FxHashMap, FxHashSet};
|
||||||
|
pub use std::collections::*;
|
138
crates/ming/Cargo.toml
Normal file
138
crates/ming/Cargo.toml
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
[package]
|
||||||
|
name = "ming"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
authors = ["Nathan Sobo <nathan@zed.dev>", "Ilya Borodinov <borodinov.ilya@gmail.com>"]
|
||||||
|
description = "GPU-accelerated native UI framework"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
test-support = ["backtrace", "collections/test-support", "util/test-support"]
|
||||||
|
runtime_shaders = []
|
||||||
|
macos-blade = ["blade-graphics", "blade-macros", "bytemuck"]
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/gpui.rs"
|
||||||
|
doctest = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
async-task = "4.7"
|
||||||
|
backtrace = { version = "0.3", optional = true }
|
||||||
|
blade-graphics = { workspace = true, optional = true }
|
||||||
|
blade-macros = { workspace = true, optional = true }
|
||||||
|
bytemuck = { version = "1", optional = true }
|
||||||
|
collections.workspace = true
|
||||||
|
ctor.workspace = true
|
||||||
|
derive_more.workspace = true
|
||||||
|
etagere = "0.2"
|
||||||
|
futures.workspace = true
|
||||||
|
font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "5a5c4d4" }
|
||||||
|
ming_macros.workspace = true
|
||||||
|
image = "0.23"
|
||||||
|
itertools.workspace = true
|
||||||
|
lazy_static.workspace = true
|
||||||
|
linkme = "0.3"
|
||||||
|
log.workspace = true
|
||||||
|
num_cpus = "1.13"
|
||||||
|
parking = "2.0.0"
|
||||||
|
parking_lot.workspace = true
|
||||||
|
pathfinder_geometry = "0.5"
|
||||||
|
postage.workspace = true
|
||||||
|
profiling.workspace = true
|
||||||
|
rand.workspace = true
|
||||||
|
raw-window-handle = "0.6"
|
||||||
|
refineable.workspace = true
|
||||||
|
resvg = { version = "0.41.0", default-features = false }
|
||||||
|
usvg = { version = "0.41.0", default-features = false }
|
||||||
|
schemars.workspace = true
|
||||||
|
seahash = "4.1"
|
||||||
|
serde.workspace = true
|
||||||
|
serde_derive.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
slotmap = "1.0.6"
|
||||||
|
smallvec.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
|
taffy = "0.4.3"
|
||||||
|
thiserror.workspace = true
|
||||||
|
time.workspace = true
|
||||||
|
util.workspace = true
|
||||||
|
uuid.workspace = true
|
||||||
|
waker-fn = "1.1.0"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
backtrace = "0.3"
|
||||||
|
collections = { workspace = true, features = ["test-support"] }
|
||||||
|
util = { workspace = true, features = ["test-support"] }
|
||||||
|
|
||||||
|
# [target.'cfg(target_os = "macos")'.build-dependencies]
|
||||||
|
# bindgen = "0.65.1"
|
||||||
|
# cbindgen = "0.26.0"
|
||||||
|
|
||||||
|
# [target.'cfg(target_os = "macos")'.dependencies]
|
||||||
|
# block = "0.1"
|
||||||
|
# cocoa = "0.25"
|
||||||
|
# core-foundation.workspace = true
|
||||||
|
# core-graphics = "0.23"
|
||||||
|
# core-text = "20.1"
|
||||||
|
# foreign-types = "0.5"
|
||||||
|
# log.workspace = true
|
||||||
|
# media.workspace = true
|
||||||
|
# metal = "0.25"
|
||||||
|
# objc = "0.2"
|
||||||
|
|
||||||
|
[target.'cfg(any(target_os = "linux", target_os = "windows"))'.dependencies]
|
||||||
|
flume = "0.11"
|
||||||
|
#TODO: use these on all platforms
|
||||||
|
blade-graphics.workspace = true
|
||||||
|
blade-macros.workspace = true
|
||||||
|
bytemuck = "1"
|
||||||
|
cosmic-text = "0.11.2"
|
||||||
|
copypasta = "0.10.1"
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
|
as-raw-xcb-connection = "1"
|
||||||
|
ashpd = "0.8.0"
|
||||||
|
calloop = "0.12.4"
|
||||||
|
calloop-wayland-source = "0.2.0"
|
||||||
|
wayland-backend = { version = "0.3.3", features = ["client_system"] }
|
||||||
|
wayland-client = { version = "0.31.2" }
|
||||||
|
wayland-cursor = "0.31.1"
|
||||||
|
wayland-protocols = { version = "0.31.2", features = [
|
||||||
|
"client",
|
||||||
|
"staging",
|
||||||
|
"unstable",
|
||||||
|
] }
|
||||||
|
wayland-protocols-plasma = { version = "0.2.0", features = ["client"] }
|
||||||
|
oo7 = "0.3.0"
|
||||||
|
open = "5.1.2"
|
||||||
|
filedescriptor = "0.8.2"
|
||||||
|
x11rb = { version = "0.13.0", features = [
|
||||||
|
"allow-unsafe-code",
|
||||||
|
"xkb",
|
||||||
|
"randr",
|
||||||
|
"xinput",
|
||||||
|
"cursor",
|
||||||
|
"resource_manager",
|
||||||
|
] }
|
||||||
|
xkbcommon = { version = "0.7", features = ["wayland", "x11"] }
|
||||||
|
|
||||||
|
# [target.'cfg(windows)'.dependencies]
|
||||||
|
# windows.workspace = true
|
||||||
|
#
|
||||||
|
# [target.'cfg(windows)'.build-dependencies]
|
||||||
|
# embed-resource = "2.4"
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "hello_world"
|
||||||
|
path = "examples/hello_world.rs"
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "image"
|
||||||
|
path = "examples/image/image.rs"
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "set_menus"
|
||||||
|
path = "examples/set_menus.rs"
|
40
crates/ming/README.md
Normal file
40
crates/ming/README.md
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
# Welcome to GPUI!
|
||||||
|
|
||||||
|
GPUI is a hybrid immediate and retained mode, GPU accelerated, UI framework
|
||||||
|
for Rust, designed to support a wide variety of applications.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
GPUI is still in active development as we work on the Zed code editor and isn't yet on crates.io. You'll also need to use the latest version of stable rust and be on macOS. Add the following to your Cargo.toml:
|
||||||
|
|
||||||
|
```
|
||||||
|
gpui = { git = "https://github.com/zed-industries/zed" }
|
||||||
|
```
|
||||||
|
|
||||||
|
Everything in GPUI starts with an `App`. You can create one with `App::new()`, and kick off your application by passing a callback to `App::run()`. Inside this callback, you can create a new window with `AppContext::open_window()`, and register your first root view. See [gpui.rs](https://www.gpui.rs/) for a complete example.
|
||||||
|
|
||||||
|
## The Big Picture
|
||||||
|
|
||||||
|
GPUI offers three different [registers](https://en.wikipedia.org/wiki/Register_(sociolinguistics)) depending on your needs:
|
||||||
|
|
||||||
|
- State management and communication with Models. Whenever you need to store application state that communicates between different parts of your application, you'll want to use GPUI's models. Models are owned by GPUI and are only accessible through an owned smart pointer similar to an `Rc`. See the `app::model_context` module for more information.
|
||||||
|
|
||||||
|
- High level, declarative UI with Views. All UI in GPUI starts with a View. A view is simply a model that can be rendered, via the `Render` trait. At the start of each frame, GPUI will call this render method on the root view of a given window. Views build a tree of `elements`, lay them out and style them with a tailwind-style API, and then give them to GPUI to turn into pixels. See the `div` element for an all purpose swiss-army knife of rendering.
|
||||||
|
|
||||||
|
- Low level, imperative UI with Elements. Elements are the building blocks of UI in GPUI, and they provide a nice wrapper around an imperative API that provides as much flexibility and control as you need. Elements have total control over how they and their child elements are rendered and can be used for making efficient views into large lists, implement custom layouting for a code editor, and anything else you can think of. See the `element` module for more information.
|
||||||
|
|
||||||
|
Each of these registers has one or more corresponding contexts that can be accessed from all GPUI services. This context is your main interface to GPUI, and is used extensively throughout the framework.
|
||||||
|
|
||||||
|
## Other Resources
|
||||||
|
|
||||||
|
In addition to the systems above, GPUI provides a range of smaller services that are useful for building complex applications:
|
||||||
|
|
||||||
|
- Actions are user-defined structs that are used for converting keystrokes into logical operations in your UI. Use this for implementing keyboard shortcuts, such as cmd-q. See the `action` module for more information.
|
||||||
|
|
||||||
|
- Platform services, such as `quit the app` or `open a URL` are available as methods on the `app::AppContext`.
|
||||||
|
|
||||||
|
- An async executor that is integrated with the platform's event loop. See the `executor` module for more information.,
|
||||||
|
|
||||||
|
- The `[gpui::test]` macro provides a convenient way to write tests for your GPUI applications. Tests also have their own kind of context, a `TestAppContext` which provides ways of simulating common platform input. See `app::test_context` and `test` modules for more details.
|
||||||
|
|
||||||
|
Currently, the best way to learn about these APIs is to read the Zed source code, ask us about it at a fireside hack, or drop a question in the [Zed Discord](https://discord.gg/U4qhCEhMXP). We're working on improving the documentation, creating more examples, and will be publishing more guides to GPUI on our [blog](https://zed.dev/blog).
|
205
crates/ming/build.rs
Normal file
205
crates/ming/build.rs
Normal file
|
@ -0,0 +1,205 @@
|
||||||
|
#![cfg_attr(any(not(target_os = "macos"), feature = "macos-blade"), allow(unused))]
|
||||||
|
|
||||||
|
//TODO: consider generating shader code for WGSL
|
||||||
|
//TODO: deprecate "runtime-shaders" and "macos-blade"
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
macos::build();
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
let manifest = std::path::Path::new("resources/windows/gpui.manifest.xml");
|
||||||
|
let rc_file = std::path::Path::new("resources/windows/gpui.rc");
|
||||||
|
println!("cargo:rerun-if-changed={}", manifest.display());
|
||||||
|
println!("cargo:rerun-if-changed={}", rc_file.display());
|
||||||
|
embed_resource::compile(rc_file, embed_resource::NONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
mod macos {
|
||||||
|
use std::{
|
||||||
|
env,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
use cbindgen::Config;
|
||||||
|
|
||||||
|
pub(super) fn build() {
|
||||||
|
generate_dispatch_bindings();
|
||||||
|
#[cfg(not(feature = "macos-blade"))]
|
||||||
|
{
|
||||||
|
let header_path = generate_shader_bindings();
|
||||||
|
|
||||||
|
#[cfg(feature = "runtime_shaders")]
|
||||||
|
emit_stitched_shaders(&header_path);
|
||||||
|
#[cfg(not(feature = "runtime_shaders"))]
|
||||||
|
compile_metal_shaders(&header_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_dispatch_bindings() {
|
||||||
|
println!("cargo:rustc-link-lib=framework=System");
|
||||||
|
println!("cargo:rerun-if-changed=src/platform/mac/dispatch.h");
|
||||||
|
|
||||||
|
let bindings = bindgen::Builder::default()
|
||||||
|
.header("src/platform/mac/dispatch.h")
|
||||||
|
.allowlist_var("_dispatch_main_q")
|
||||||
|
.allowlist_var("_dispatch_source_type_data_add")
|
||||||
|
.allowlist_var("DISPATCH_QUEUE_PRIORITY_HIGH")
|
||||||
|
.allowlist_var("DISPATCH_TIME_NOW")
|
||||||
|
.allowlist_function("dispatch_get_global_queue")
|
||||||
|
.allowlist_function("dispatch_async_f")
|
||||||
|
.allowlist_function("dispatch_after_f")
|
||||||
|
.allowlist_function("dispatch_time")
|
||||||
|
.allowlist_function("dispatch_source_merge_data")
|
||||||
|
.allowlist_function("dispatch_source_create")
|
||||||
|
.allowlist_function("dispatch_source_set_event_handler_f")
|
||||||
|
.allowlist_function("dispatch_resume")
|
||||||
|
.allowlist_function("dispatch_suspend")
|
||||||
|
.allowlist_function("dispatch_source_cancel")
|
||||||
|
.allowlist_function("dispatch_set_context")
|
||||||
|
.parse_callbacks(Box::new(bindgen::CargoCallbacks))
|
||||||
|
.layout_tests(false)
|
||||||
|
.generate()
|
||||||
|
.expect("unable to generate bindings");
|
||||||
|
|
||||||
|
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||||
|
bindings
|
||||||
|
.write_to_file(out_path.join("dispatch_sys.rs"))
|
||||||
|
.expect("couldn't write dispatch bindings");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_shader_bindings() -> PathBuf {
|
||||||
|
let output_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("scene.h");
|
||||||
|
let crate_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||||
|
let mut config = Config::default();
|
||||||
|
config.include_guard = Some("SCENE_H".into());
|
||||||
|
config.language = cbindgen::Language::C;
|
||||||
|
config.export.include.extend([
|
||||||
|
"Bounds".into(),
|
||||||
|
"Corners".into(),
|
||||||
|
"Edges".into(),
|
||||||
|
"Size".into(),
|
||||||
|
"Pixels".into(),
|
||||||
|
"PointF".into(),
|
||||||
|
"Hsla".into(),
|
||||||
|
"ContentMask".into(),
|
||||||
|
"Uniforms".into(),
|
||||||
|
"AtlasTile".into(),
|
||||||
|
"PathRasterizationInputIndex".into(),
|
||||||
|
"PathVertex_ScaledPixels".into(),
|
||||||
|
"ShadowInputIndex".into(),
|
||||||
|
"Shadow".into(),
|
||||||
|
"QuadInputIndex".into(),
|
||||||
|
"Underline".into(),
|
||||||
|
"UnderlineInputIndex".into(),
|
||||||
|
"Quad".into(),
|
||||||
|
"SpriteInputIndex".into(),
|
||||||
|
"MonochromeSprite".into(),
|
||||||
|
"PolychromeSprite".into(),
|
||||||
|
"PathSprite".into(),
|
||||||
|
"SurfaceInputIndex".into(),
|
||||||
|
"SurfaceBounds".into(),
|
||||||
|
"TransformationMatrix".into(),
|
||||||
|
]);
|
||||||
|
config.no_includes = true;
|
||||||
|
config.enumeration.prefix_with_name = true;
|
||||||
|
|
||||||
|
let mut builder = cbindgen::Builder::new();
|
||||||
|
|
||||||
|
let src_paths = [
|
||||||
|
crate_dir.join("src/scene.rs"),
|
||||||
|
crate_dir.join("src/geometry.rs"),
|
||||||
|
crate_dir.join("src/color.rs"),
|
||||||
|
crate_dir.join("src/window.rs"),
|
||||||
|
crate_dir.join("src/platform.rs"),
|
||||||
|
crate_dir.join("src/platform/mac/metal_renderer.rs"),
|
||||||
|
];
|
||||||
|
for src_path in src_paths {
|
||||||
|
println!("cargo:rerun-if-changed={}", src_path.display());
|
||||||
|
builder = builder.with_src(src_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
builder
|
||||||
|
.with_config(config)
|
||||||
|
.generate()
|
||||||
|
.expect("Unable to generate bindings")
|
||||||
|
.write_to_file(&output_path);
|
||||||
|
|
||||||
|
output_path
|
||||||
|
}
|
||||||
|
|
||||||
|
/// To enable runtime compilation, we need to "stitch" the shaders file with the generated header
|
||||||
|
/// so that it is self-contained.
|
||||||
|
#[cfg(feature = "runtime_shaders")]
|
||||||
|
fn emit_stitched_shaders(header_path: &Path) {
|
||||||
|
use std::str::FromStr;
|
||||||
|
fn stitch_header(header: &Path, shader_path: &Path) -> std::io::Result<PathBuf> {
|
||||||
|
let header_contents = std::fs::read_to_string(header)?;
|
||||||
|
let shader_contents = std::fs::read_to_string(shader_path)?;
|
||||||
|
let stitched_contents = format!("{header_contents}\n{shader_contents}");
|
||||||
|
let out_path =
|
||||||
|
PathBuf::from(env::var("OUT_DIR").unwrap()).join("stitched_shaders.metal");
|
||||||
|
std::fs::write(&out_path, stitched_contents)?;
|
||||||
|
Ok(out_path)
|
||||||
|
}
|
||||||
|
let shader_source_path = "./src/platform/mac/shaders.metal";
|
||||||
|
let shader_path = PathBuf::from_str(shader_source_path).unwrap();
|
||||||
|
stitch_header(header_path, &shader_path).unwrap();
|
||||||
|
println!("cargo:rerun-if-changed={}", &shader_source_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "runtime_shaders"))]
|
||||||
|
fn compile_metal_shaders(header_path: &Path) {
|
||||||
|
use std::process::{self, Command};
|
||||||
|
let shader_path = "./src/platform/mac/shaders.metal";
|
||||||
|
let air_output_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("shaders.air");
|
||||||
|
let metallib_output_path =
|
||||||
|
PathBuf::from(env::var("OUT_DIR").unwrap()).join("shaders.metallib");
|
||||||
|
println!("cargo:rerun-if-changed={}", shader_path);
|
||||||
|
|
||||||
|
let output = Command::new("xcrun")
|
||||||
|
.args([
|
||||||
|
"-sdk",
|
||||||
|
"macosx",
|
||||||
|
"metal",
|
||||||
|
"-gline-tables-only",
|
||||||
|
"-mmacosx-version-min=10.15.7",
|
||||||
|
"-MO",
|
||||||
|
"-c",
|
||||||
|
shader_path,
|
||||||
|
"-include",
|
||||||
|
&header_path.to_str().unwrap(),
|
||||||
|
"-o",
|
||||||
|
])
|
||||||
|
.arg(&air_output_path)
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
eprintln!(
|
||||||
|
"metal shader compilation failed:\n{}",
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = Command::new("xcrun")
|
||||||
|
.args(["-sdk", "macosx", "metallib"])
|
||||||
|
.arg(air_output_path)
|
||||||
|
.arg("-o")
|
||||||
|
.arg(metallib_output_path)
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
eprintln!(
|
||||||
|
"metallib compilation failed:\n{}",
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
41
crates/ming/docs/contexts.md
Normal file
41
crates/ming/docs/contexts.md
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
# Contexts
|
||||||
|
|
||||||
|
GPUI makes extensive use of _context parameters_, typically named `cx` and positioned at the end of the parameter list, unless they're before a final function parameter. A context reference provides access to application state and services.
|
||||||
|
|
||||||
|
There are multiple kinds of contexts, and contexts implement the `Deref` trait so that a function taking `&mut AppContext` could be passed a `&mut WindowContext` or `&mut ViewContext` instead.
|
||||||
|
|
||||||
|
```
|
||||||
|
AppContext
|
||||||
|
/ \
|
||||||
|
ModelContext WindowContext
|
||||||
|
/
|
||||||
|
ViewContext
|
||||||
|
```
|
||||||
|
|
||||||
|
- The `AppContext` forms the root of the hierarchy
|
||||||
|
- `ModelContext` and `WindowContext` both dereference to `AppContext`
|
||||||
|
- `ViewContext` dereferences to `WindowContext`
|
||||||
|
|
||||||
|
## `AppContext`
|
||||||
|
|
||||||
|
Provides access to the global application state. All other kinds of contexts ultimately deref to an `AppContext`. You can update a `Model<T>` by passing an `AppContext`, but you can't update a view. For that you need a `WindowContext`...
|
||||||
|
|
||||||
|
## `WindowContext`
|
||||||
|
|
||||||
|
Provides access to the state of an application window, and also derefs to an `AppContext`, so you can pass a window context reference to any method taking an app context. Obtain this context by calling `WindowHandle::update`.
|
||||||
|
|
||||||
|
## `ModelContext<T>`
|
||||||
|
|
||||||
|
Available when you create or update a `Model<T>`. It derefs to an `AppContext`, but also contains methods specific to the particular model, such as the ability to notify change observers or emit events.
|
||||||
|
|
||||||
|
## `ViewContext<V>`
|
||||||
|
|
||||||
|
Available when you create or update a `View<V>`. It derefs to a `WindowContext`, but also contains methods specific to the particular view, such as the ability to notify change observers or emit events.
|
||||||
|
|
||||||
|
## `AsyncAppContext` and `AsyncWindowContext`
|
||||||
|
|
||||||
|
Whereas the above contexts are always passed to your code as references, you can call `to_async` on the reference to create an async context, which has a static lifetime and can be held across `await` points in async code. When you interact with `Model`s or `View`s with an async context, the calls become fallible, because the context may outlive the window or even the app itself.
|
||||||
|
|
||||||
|
## `TestAppContext` and `TestVisualContext`
|
||||||
|
|
||||||
|
These are similar to the async contexts above, but they panic if you attempt to access a non-existent app or window, and they also contain other features specific to tests.
|
101
crates/ming/docs/key_dispatch.md
Normal file
101
crates/ming/docs/key_dispatch.md
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
# Key Dispatch
|
||||||
|
|
||||||
|
GPUI is designed for keyboard-first interactivity.
|
||||||
|
|
||||||
|
To expose functionality to the mouse, you render a button with a click handler.
|
||||||
|
|
||||||
|
To expose functionality to the keyboard, you bind an *action* in a *key context*.
|
||||||
|
|
||||||
|
Actions are similar to framework-level events like `MouseDown`, `KeyDown`, etc, but you can define them yourself:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
mod menu {
|
||||||
|
#[gpui::action]
|
||||||
|
struct MoveUp;
|
||||||
|
|
||||||
|
#[gpui::action]
|
||||||
|
struct MoveDown;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Actions are frequently unit structs, for which we have a macro. The above could also be written:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
mod menu {
|
||||||
|
actions!(gpui, [MoveUp, MoveDown]);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Actions can also be more complex types:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
mod menu {
|
||||||
|
#[gpui::action]
|
||||||
|
struct Move {
|
||||||
|
direction: Direction,
|
||||||
|
select: bool,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
To bind actions, chain `on_action` on to your element:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
impl Render for Menu {
|
||||||
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Component {
|
||||||
|
div()
|
||||||
|
.on_action(|this: &mut Menu, move: &MoveUp, cx: &mut ViewContext<Menu>| {
|
||||||
|
// ...
|
||||||
|
})
|
||||||
|
.on_action(|this, move: &MoveDown, cx| {
|
||||||
|
// ...
|
||||||
|
})
|
||||||
|
.children(unimplemented!())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In order to bind keys to actions, you need to declare a *key context* for part of the element tree by calling `key_context`.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
impl Render for Menu {
|
||||||
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Component {
|
||||||
|
div()
|
||||||
|
.key_context("menu")
|
||||||
|
.on_action(|this: &mut Menu, move: &MoveUp, cx: &mut ViewContext<Menu>| {
|
||||||
|
// ...
|
||||||
|
})
|
||||||
|
.on_action(|this, move: &MoveDown, cx| {
|
||||||
|
// ...
|
||||||
|
})
|
||||||
|
.children(unimplemented!())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now you can target your context in the keymap. Note how actions are identified in the keymap by their fully-qualified type name.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"context": "menu",
|
||||||
|
"bindings": {
|
||||||
|
"up": "menu::MoveUp",
|
||||||
|
"down": "menu::MoveDown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If you had opted for the more complex type definition, you'd provide the serialized representation of the action alongside the name:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"context": "menu",
|
||||||
|
"bindings": {
|
||||||
|
"up": ["menu::Move", {direction: "up", select: false}]
|
||||||
|
"down": ["menu::Move", {direction: "down", select: false}]
|
||||||
|
"shift-up": ["menu::Move", {direction: "up", select: true}]
|
||||||
|
"shift-down": ["menu::Move", {direction: "down", select: true}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
78
crates/ming/examples/animation.rs
Normal file
78
crates/ming/examples/animation.rs
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use gpui::*;
|
||||||
|
|
||||||
|
struct Assets {}
|
||||||
|
|
||||||
|
impl AssetSource for Assets {
|
||||||
|
fn load(&self, path: &str) -> Result<std::borrow::Cow<'static, [u8]>> {
|
||||||
|
std::fs::read(path).map(Into::into).map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list(&self, path: &str) -> Result<Vec<SharedString>> {
|
||||||
|
Ok(std::fs::read_dir(path)?
|
||||||
|
.filter_map(|entry| {
|
||||||
|
Some(SharedString::from(
|
||||||
|
entry.ok()?.path().to_string_lossy().to_string(),
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AnimationExample {}
|
||||||
|
|
||||||
|
impl Render for AnimationExample {
|
||||||
|
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
|
div().flex().flex_col().size_full().justify_around().child(
|
||||||
|
div().flex().flex_row().w_full().justify_around().child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.bg(rgb(0x2e7d32))
|
||||||
|
.size(Length::Definite(Pixels(300.0).into()))
|
||||||
|
.justify_center()
|
||||||
|
.items_center()
|
||||||
|
.shadow_lg()
|
||||||
|
.text_xl()
|
||||||
|
.text_color(black())
|
||||||
|
.child("hello")
|
||||||
|
.child(
|
||||||
|
svg()
|
||||||
|
.size_8()
|
||||||
|
.path("examples/image/arrow_circle.svg")
|
||||||
|
.text_color(black())
|
||||||
|
.with_animation(
|
||||||
|
"image_circle",
|
||||||
|
Animation::new(Duration::from_secs(2))
|
||||||
|
.repeat()
|
||||||
|
.with_easing(bounce(ease_in_out)),
|
||||||
|
|svg, delta| {
|
||||||
|
svg.with_transformation(Transformation::rotate(percentage(
|
||||||
|
delta,
|
||||||
|
)))
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
App::new()
|
||||||
|
.with_assets(Assets {})
|
||||||
|
.run(|cx: &mut AppContext| {
|
||||||
|
let options = WindowOptions {
|
||||||
|
window_bounds: Some(WindowBounds::Windowed(Bounds::centered(
|
||||||
|
None,
|
||||||
|
size(px(300.), px(300.)),
|
||||||
|
cx,
|
||||||
|
))),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
cx.open_window(options, |cx| {
|
||||||
|
cx.activate(false);
|
||||||
|
cx.new_view(|_cx| AnimationExample {})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
39
crates/ming/examples/hello_world.rs
Normal file
39
crates/ming/examples/hello_world.rs
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
use gpui::*;
|
||||||
|
|
||||||
|
struct HelloWorld {
|
||||||
|
text: SharedString,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for HelloWorld {
|
||||||
|
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.bg(rgb(0x2e7d32))
|
||||||
|
.size(Length::Definite(Pixels(300.0).into()))
|
||||||
|
.justify_center()
|
||||||
|
.items_center()
|
||||||
|
.shadow_lg()
|
||||||
|
.border_1()
|
||||||
|
.border_color(rgb(0x0000ff))
|
||||||
|
.text_xl()
|
||||||
|
.text_color(rgb(0xffffff))
|
||||||
|
.child(format!("Hello, {}!", &self.text))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
App::new().run(|cx: &mut AppContext| {
|
||||||
|
let bounds = Bounds::centered(None, size(px(600.0), px(600.0)), cx);
|
||||||
|
cx.open_window(
|
||||||
|
WindowOptions {
|
||||||
|
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
|cx| {
|
||||||
|
cx.new_view(|_cx| HelloWorld {
|
||||||
|
text: "World".into(),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
BIN
crates/ming/examples/image/app-icon.png
Normal file
BIN
crates/ming/examples/image/app-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 164 KiB |
6
crates/ming/examples/image/arrow_circle.svg
Normal file
6
crates/ming/examples/image/arrow_circle.svg
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M3 8C3 6.67392 3.52678 5.40215 4.46446 4.46447C5.40214 3.52679 6.67391 3.00001 7.99999 3.00001C9.39779 3.00527 10.7394 3.55069 11.7444 4.52223L13 5.77778" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M13 3.00001V5.77778H10.2222" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M13 8C13 9.32608 12.4732 10.5978 11.5355 11.5355C10.5978 12.4732 9.32607 13 7.99999 13C6.60219 12.9947 5.26054 12.4493 4.25555 11.4778L3 10.2222" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M5.77777 10.2222H3V13" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 748 B |
98
crates/ming/examples/image/image.rs
Normal file
98
crates/ming/examples/image/image.rs
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use gpui::*;
|
||||||
|
|
||||||
|
#[derive(IntoElement)]
|
||||||
|
struct ImageContainer {
|
||||||
|
text: SharedString,
|
||||||
|
src: ImageSource,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImageContainer {
|
||||||
|
pub fn new(text: impl Into<SharedString>, src: impl Into<ImageSource>) -> Self {
|
||||||
|
Self {
|
||||||
|
text: text.into(),
|
||||||
|
src: src.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderOnce for ImageContainer {
|
||||||
|
fn render(self, _: &mut WindowContext) -> impl IntoElement {
|
||||||
|
div().child(
|
||||||
|
div()
|
||||||
|
.flex_row()
|
||||||
|
.size_full()
|
||||||
|
.gap_4()
|
||||||
|
.child(self.text)
|
||||||
|
.child(img(self.src).w(px(512.0)).h(px(512.0))),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ImageShowcase {
|
||||||
|
local_resource: Arc<PathBuf>,
|
||||||
|
remote_resource: SharedUri,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for ImageShowcase {
|
||||||
|
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_row()
|
||||||
|
.size_full()
|
||||||
|
.justify_center()
|
||||||
|
.items_center()
|
||||||
|
.gap_8()
|
||||||
|
.bg(rgb(0xFFFFFF))
|
||||||
|
.child(ImageContainer::new(
|
||||||
|
"Image loaded from a local file",
|
||||||
|
self.local_resource.clone(),
|
||||||
|
))
|
||||||
|
.child(ImageContainer::new(
|
||||||
|
"Image loaded from a remote resource",
|
||||||
|
self.remote_resource.clone(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actions!(image, [Quit]);
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
env_logger::init();
|
||||||
|
|
||||||
|
App::new().run(|cx: &mut AppContext| {
|
||||||
|
cx.activate(true);
|
||||||
|
cx.on_action(|_: &Quit, cx| cx.quit());
|
||||||
|
cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);
|
||||||
|
cx.set_menus(vec![Menu {
|
||||||
|
name: "Image",
|
||||||
|
items: vec![MenuItem::action("Quit", Quit)],
|
||||||
|
}]);
|
||||||
|
|
||||||
|
let window_options = WindowOptions {
|
||||||
|
titlebar: Some(TitlebarOptions {
|
||||||
|
title: Some(SharedString::from("Image Example")),
|
||||||
|
appears_transparent: false,
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
|
||||||
|
window_bounds: Some(WindowBounds::Windowed(Bounds {
|
||||||
|
size: size(px(1100.), px(600.)).into(),
|
||||||
|
origin: Point::new(DevicePixels::from(200), DevicePixels::from(200)),
|
||||||
|
})),
|
||||||
|
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
cx.open_window(window_options, |cx| {
|
||||||
|
cx.new_view(|_cx| ImageShowcase {
|
||||||
|
// Relative path to your root project path
|
||||||
|
local_resource: Arc::new(PathBuf::from_str("examples/image/app-icon.png").unwrap()),
|
||||||
|
remote_resource: "https://picsum.photos/512/512".into(),
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
35
crates/ming/examples/ownership_post.rs
Normal file
35
crates/ming/examples/ownership_post.rs
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
use gpui::{prelude::*, App, AppContext, EventEmitter, Model, ModelContext};
|
||||||
|
|
||||||
|
struct Counter {
|
||||||
|
count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Change {
|
||||||
|
increment: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<Change> for Counter {}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
App::new().run(|cx: &mut AppContext| {
|
||||||
|
let counter: Model<Counter> = cx.new_model(|_cx| Counter { count: 0 });
|
||||||
|
let subscriber = cx.new_model(|cx: &mut ModelContext<Counter>| {
|
||||||
|
cx.subscribe(&counter, |subscriber, _emitter, event, _cx| {
|
||||||
|
subscriber.count += event.increment * 2;
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
Counter {
|
||||||
|
count: counter.read(cx).count * 2,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
counter.update(cx, |counter, cx| {
|
||||||
|
counter.count += 2;
|
||||||
|
cx.notify();
|
||||||
|
cx.emit(Change { increment: 2 });
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(subscriber.read(cx).count, 4);
|
||||||
|
});
|
||||||
|
}
|
43
crates/ming/examples/set_menus.rs
Normal file
43
crates/ming/examples/set_menus.rs
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
use gpui::*;
|
||||||
|
|
||||||
|
struct SetMenus;
|
||||||
|
|
||||||
|
impl Render for SetMenus {
|
||||||
|
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.bg(rgb(0x2e7d32))
|
||||||
|
.size_full()
|
||||||
|
.justify_center()
|
||||||
|
.items_center()
|
||||||
|
.text_xl()
|
||||||
|
.text_color(rgb(0xffffff))
|
||||||
|
.child("Set Menus Example")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
App::new().run(|cx: &mut AppContext| {
|
||||||
|
// Bring the menu bar to the foreground (so you can see the menu bar)
|
||||||
|
cx.activate(true);
|
||||||
|
// Register the `quit` function so it can be referenced by the `MenuItem::action` in the menu bar
|
||||||
|
cx.on_action(quit);
|
||||||
|
// Add menu items
|
||||||
|
cx.set_menus(vec![Menu {
|
||||||
|
name: "set_menus",
|
||||||
|
items: vec![MenuItem::action("Quit", Quit)],
|
||||||
|
}]);
|
||||||
|
cx.open_window(WindowOptions::default(), |cx| {
|
||||||
|
cx.new_view(|_cx| SetMenus {})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Associate actions using the `actions!` macro (or `impl_actions!` macro)
|
||||||
|
actions!(set_menus, [Quit]);
|
||||||
|
|
||||||
|
// Define the quit function that is registered with the AppContext
|
||||||
|
fn quit(_: &Quit, cx: &mut AppContext) {
|
||||||
|
println!("Gracefully quitting the application . . .");
|
||||||
|
cx.quit();
|
||||||
|
}
|
67
crates/ming/examples/window_positioning.rs
Normal file
67
crates/ming/examples/window_positioning.rs
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
use gpui::*;
|
||||||
|
|
||||||
|
struct WindowContent {
|
||||||
|
text: SharedString,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for WindowContent {
|
||||||
|
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.bg(rgb(0x1e2025))
|
||||||
|
.size_full()
|
||||||
|
.justify_center()
|
||||||
|
.items_center()
|
||||||
|
.text_xl()
|
||||||
|
.text_color(rgb(0xffffff))
|
||||||
|
.child(self.text.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
App::new().run(|cx: &mut AppContext| {
|
||||||
|
// Create several new windows, positioned in the top right corner of each screen
|
||||||
|
|
||||||
|
for screen in cx.displays() {
|
||||||
|
let options = {
|
||||||
|
let popup_margin_width = DevicePixels::from(16);
|
||||||
|
let popup_margin_height = DevicePixels::from(-0) - DevicePixels::from(48);
|
||||||
|
|
||||||
|
let window_size = Size {
|
||||||
|
width: px(400.),
|
||||||
|
height: px(72.),
|
||||||
|
};
|
||||||
|
|
||||||
|
let screen_bounds = screen.bounds();
|
||||||
|
let size: Size<DevicePixels> = window_size.into();
|
||||||
|
|
||||||
|
let bounds = gpui::Bounds::<DevicePixels> {
|
||||||
|
origin: screen_bounds.upper_right()
|
||||||
|
- point(size.width + popup_margin_width, popup_margin_height),
|
||||||
|
size: window_size.into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
WindowOptions {
|
||||||
|
// Set the bounds of the window in screen coordinates
|
||||||
|
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||||
|
// Specify the display_id to ensure the window is created on the correct screen
|
||||||
|
display_id: Some(screen.id()),
|
||||||
|
|
||||||
|
titlebar: None,
|
||||||
|
window_background: WindowBackgroundAppearance::default(),
|
||||||
|
focus: false,
|
||||||
|
show: true,
|
||||||
|
kind: WindowKind::PopUp,
|
||||||
|
is_movable: false,
|
||||||
|
app_id: None,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
cx.open_window(options, |cx| {
|
||||||
|
cx.new_view(|_| WindowContent {
|
||||||
|
text: format!("{:?}", screen.id()).into(),
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
16
crates/ming/resources/windows/gpui.manifest.xml
Normal file
16
crates/ming/resources/windows/gpui.manifest.xml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<asmv3:application>
|
||||||
|
<asmv3:windowsSettings>
|
||||||
|
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
|
||||||
|
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||||
|
</asmv3:windowsSettings>
|
||||||
|
</asmv3:application>
|
||||||
|
<dependency>
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity type='win32'
|
||||||
|
name='Microsoft.Windows.Common-Controls'
|
||||||
|
version='6.0.0.0' processorArchitecture='*'
|
||||||
|
publicKeyToken='6595b64144ccf1df' />
|
||||||
|
</dependentAssembly>
|
||||||
|
</dependency>
|
||||||
|
</assembly>
|
2
crates/ming/resources/windows/gpui.rc
Normal file
2
crates/ming/resources/windows/gpui.rc
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
#define RT_MANIFEST 24
|
||||||
|
1 RT_MANIFEST "resources/windows/gpui.manifest.xml"
|
268
crates/ming/src/action.rs
Normal file
268
crates/ming/src/action.rs
Normal file
|
@ -0,0 +1,268 @@
|
||||||
|
use crate::SharedString;
|
||||||
|
use anyhow::{anyhow, Context, Result};
|
||||||
|
use collections::HashMap;
|
||||||
|
pub use no_action::NoAction;
|
||||||
|
use serde_json::json;
|
||||||
|
use std::any::{Any, TypeId};
|
||||||
|
|
||||||
|
/// Actions are used to implement keyboard-driven UI.
|
||||||
|
/// When you declare an action, you can bind keys to the action in the keymap and
|
||||||
|
/// listeners for that action in the element tree.
|
||||||
|
///
|
||||||
|
/// To declare a list of simple actions, you can use the actions! macro, which defines a simple unit struct
|
||||||
|
/// action for each listed action name in the given namespace.
|
||||||
|
/// ```rust
|
||||||
|
/// actions!(editor, [MoveUp, MoveDown, MoveLeft, MoveRight, Newline]);
|
||||||
|
/// ```
|
||||||
|
/// More complex data types can also be actions, providing they implement Clone, PartialEq,
|
||||||
|
/// and serde_derive::Deserialize.
|
||||||
|
/// Use `impl_actions!` to automatically implement the action in the given namespace.
|
||||||
|
/// ```
|
||||||
|
/// #[derive(Clone, PartialEq, serde_derive::Deserialize)]
|
||||||
|
/// pub struct SelectNext {
|
||||||
|
/// pub replace_newest: bool,
|
||||||
|
/// }
|
||||||
|
/// impl_actions!(editor, [SelectNext]);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// If you want to control the behavior of the action trait manually, you can use the lower-level `#[register_action]`
|
||||||
|
/// macro, which only generates the code needed to register your action before `main`.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// #[derive(gpui::private::serde::Deserialize, std::cmp::PartialEq, std::clone::Clone)]
|
||||||
|
/// pub struct Paste {
|
||||||
|
/// pub content: SharedString,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// impl gpui::Action for Paste {
|
||||||
|
/// ///...
|
||||||
|
/// }
|
||||||
|
/// register_action!(Paste);
|
||||||
|
/// ```
|
||||||
|
pub trait Action: 'static + Send {
|
||||||
|
/// Clone the action into a new box
|
||||||
|
fn boxed_clone(&self) -> Box<dyn Action>;
|
||||||
|
|
||||||
|
/// Cast the action to the any type
|
||||||
|
fn as_any(&self) -> &dyn Any;
|
||||||
|
|
||||||
|
/// Do a partial equality check on this action and the other
|
||||||
|
fn partial_eq(&self, action: &dyn Action) -> bool;
|
||||||
|
|
||||||
|
/// Get the name of this action, for displaying in UI
|
||||||
|
fn name(&self) -> &str;
|
||||||
|
|
||||||
|
/// Get the name of this action for debugging
|
||||||
|
fn debug_name() -> &'static str
|
||||||
|
where
|
||||||
|
Self: Sized;
|
||||||
|
|
||||||
|
/// Build this action from a JSON value. This is used to construct actions from the keymap.
|
||||||
|
/// A value of `{}` will be passed for actions that don't have any parameters.
|
||||||
|
fn build(value: serde_json::Value) -> Result<Box<dyn Action>>
|
||||||
|
where
|
||||||
|
Self: Sized;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for dyn Action {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("dyn Action")
|
||||||
|
.field("name", &self.name())
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl dyn Action {
|
||||||
|
/// Get the type id of this action
|
||||||
|
pub fn type_id(&self) -> TypeId {
|
||||||
|
self.as_any().type_id()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionBuilder = fn(json: serde_json::Value) -> anyhow::Result<Box<dyn Action>>;
|
||||||
|
|
||||||
|
pub(crate) struct ActionRegistry {
|
||||||
|
builders_by_name: HashMap<SharedString, ActionBuilder>,
|
||||||
|
names_by_type_id: HashMap<TypeId, SharedString>,
|
||||||
|
all_names: Vec<SharedString>, // So we can return a static slice.
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ActionRegistry {
|
||||||
|
fn default() -> Self {
|
||||||
|
let mut this = ActionRegistry {
|
||||||
|
builders_by_name: Default::default(),
|
||||||
|
names_by_type_id: Default::default(),
|
||||||
|
all_names: Default::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.load_actions();
|
||||||
|
|
||||||
|
this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This type must be public so that our macros can build it in other crates.
|
||||||
|
/// But this is an implementation detail and should not be used directly.
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub type MacroActionBuilder = fn() -> ActionData;
|
||||||
|
|
||||||
|
/// This type must be public so that our macros can build it in other crates.
|
||||||
|
/// But this is an implementation detail and should not be used directly.
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub struct ActionData {
|
||||||
|
pub name: &'static str,
|
||||||
|
pub type_id: TypeId,
|
||||||
|
pub build: ActionBuilder,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This constant must be public to be accessible from other crates.
|
||||||
|
/// But its existence is an implementation detail and should not be used directly.
|
||||||
|
#[doc(hidden)]
|
||||||
|
#[linkme::distributed_slice]
|
||||||
|
pub static __GPUI_ACTIONS: [MacroActionBuilder];
|
||||||
|
|
||||||
|
impl ActionRegistry {
|
||||||
|
/// Load all registered actions into the registry.
|
||||||
|
pub(crate) fn load_actions(&mut self) {
|
||||||
|
for builder in __GPUI_ACTIONS {
|
||||||
|
let action = builder();
|
||||||
|
self.insert_action(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn load_action<A: Action>(&mut self) {
|
||||||
|
self.insert_action(ActionData {
|
||||||
|
name: A::debug_name(),
|
||||||
|
type_id: TypeId::of::<A>(),
|
||||||
|
build: A::build,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_action(&mut self, action: ActionData) {
|
||||||
|
let name: SharedString = action.name.into();
|
||||||
|
self.builders_by_name.insert(name.clone(), action.build);
|
||||||
|
self.names_by_type_id.insert(action.type_id, name.clone());
|
||||||
|
self.all_names.push(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct an action based on its name and optional JSON parameters sourced from the keymap.
|
||||||
|
pub fn build_action_type(&self, type_id: &TypeId) -> Result<Box<dyn Action>> {
|
||||||
|
let name = self
|
||||||
|
.names_by_type_id
|
||||||
|
.get(type_id)
|
||||||
|
.ok_or_else(|| anyhow!("no action type registered for {:?}", type_id))?
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
self.build_action(&name, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct an action based on its name and optional JSON parameters sourced from the keymap.
|
||||||
|
pub fn build_action(
|
||||||
|
&self,
|
||||||
|
name: &str,
|
||||||
|
params: Option<serde_json::Value>,
|
||||||
|
) -> Result<Box<dyn Action>> {
|
||||||
|
let build_action = self
|
||||||
|
.builders_by_name
|
||||||
|
.get(name)
|
||||||
|
.ok_or_else(|| anyhow!("no action type registered for {}", name))?;
|
||||||
|
(build_action)(params.unwrap_or_else(|| json!({})))
|
||||||
|
.with_context(|| format!("Attempting to build action {}", name))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn all_action_names(&self) -> &[SharedString] {
|
||||||
|
self.all_names.as_slice()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Defines unit structs that can be used as actions.
|
||||||
|
/// To use more complex data types as actions, use `impl_actions!`
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! actions {
|
||||||
|
($namespace:path, [ $($name:ident),* $(,)? ]) => {
|
||||||
|
$(
|
||||||
|
#[doc = "The `"]
|
||||||
|
#[doc = stringify!($name)]
|
||||||
|
#[doc = "` action, see [`gpui::actions!`]"]
|
||||||
|
#[derive(::std::cmp::PartialEq, ::std::clone::Clone, ::std::default::Default, ::std::fmt::Debug, gpui::private::serde_derive::Deserialize)]
|
||||||
|
#[serde(crate = "gpui::private::serde")]
|
||||||
|
pub struct $name;
|
||||||
|
|
||||||
|
gpui::__impl_action!($namespace, $name,
|
||||||
|
fn build(_: gpui::private::serde_json::Value) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
|
||||||
|
Ok(Box::new(Self))
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
gpui::register_action!($name);
|
||||||
|
)*
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implements the Action trait for any struct that implements Clone, Default, PartialEq, and serde_deserialize::Deserialize
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! impl_actions {
|
||||||
|
($namespace:path, [ $($name:ident),* $(,)? ]) => {
|
||||||
|
$(
|
||||||
|
gpui::__impl_action!($namespace, $name,
|
||||||
|
fn build(value: gpui::private::serde_json::Value) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
|
||||||
|
Ok(std::boxed::Box::new(gpui::private::serde_json::from_value::<Self>(value)?))
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
gpui::register_action!($name);
|
||||||
|
)*
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! __impl_action {
|
||||||
|
($namespace:path, $name:ident, $build:item) => {
|
||||||
|
impl gpui::Action for $name {
|
||||||
|
fn name(&self) -> &'static str
|
||||||
|
{
|
||||||
|
concat!(
|
||||||
|
stringify!($namespace),
|
||||||
|
"::",
|
||||||
|
stringify!($name),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn debug_name() -> &'static str
|
||||||
|
where
|
||||||
|
Self: ::std::marker::Sized
|
||||||
|
{
|
||||||
|
concat!(
|
||||||
|
stringify!($namespace),
|
||||||
|
"::",
|
||||||
|
stringify!($name),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
$build
|
||||||
|
|
||||||
|
fn partial_eq(&self, action: &dyn gpui::Action) -> bool {
|
||||||
|
action
|
||||||
|
.as_any()
|
||||||
|
.downcast_ref::<Self>()
|
||||||
|
.map_or(false, |a| self == a)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn boxed_clone(&self) -> std::boxed::Box<dyn gpui::Action> {
|
||||||
|
::std::boxed::Box::new(self.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_any(&self) -> &dyn ::std::any::Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
mod no_action {
|
||||||
|
use crate as gpui;
|
||||||
|
|
||||||
|
actions!(zed, [NoAction]);
|
||||||
|
}
|
1440
crates/ming/src/app.rs
Normal file
1440
crates/ming/src/app.rs
Normal file
File diff suppressed because it is too large
Load diff
417
crates/ming/src/app/async_context.rs
Normal file
417
crates/ming/src/app/async_context.rs
Normal file
|
@ -0,0 +1,417 @@
|
||||||
|
use crate::{
|
||||||
|
AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, BorrowAppContext, Context,
|
||||||
|
DismissEvent, FocusableView, ForegroundExecutor, Global, Model, ModelContext, PromptLevel,
|
||||||
|
Render, Reservation, Result, Task, View, ViewContext, VisualContext, WindowContext,
|
||||||
|
WindowHandle,
|
||||||
|
};
|
||||||
|
use anyhow::{anyhow, Context as _};
|
||||||
|
use derive_more::{Deref, DerefMut};
|
||||||
|
use futures::channel::oneshot;
|
||||||
|
use std::{future::Future, rc::Weak};
|
||||||
|
|
||||||
|
/// An async-friendly version of [AppContext] with a static lifetime so it can be held across `await` points in async code.
|
||||||
|
/// You're provided with an instance when calling [AppContext::spawn], and you can also create one with [AppContext::to_async].
|
||||||
|
/// Internally, this holds a weak reference to an `AppContext`, so its methods are fallible to protect against cases where the [AppContext] is dropped.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AsyncAppContext {
|
||||||
|
pub(crate) app: Weak<AppCell>,
|
||||||
|
pub(crate) background_executor: BackgroundExecutor,
|
||||||
|
pub(crate) foreground_executor: ForegroundExecutor,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Context for AsyncAppContext {
|
||||||
|
type Result<T> = Result<T>;
|
||||||
|
|
||||||
|
fn new_model<T: 'static>(
|
||||||
|
&mut self,
|
||||||
|
build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T,
|
||||||
|
) -> Self::Result<Model<T>> {
|
||||||
|
let app = self
|
||||||
|
.app
|
||||||
|
.upgrade()
|
||||||
|
.ok_or_else(|| anyhow!("app was released"))?;
|
||||||
|
let mut app = app.borrow_mut();
|
||||||
|
Ok(app.new_model(build_model))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reserve_model<T: 'static>(&mut self) -> Result<Reservation<T>> {
|
||||||
|
let app = self
|
||||||
|
.app
|
||||||
|
.upgrade()
|
||||||
|
.ok_or_else(|| anyhow!("app was released"))?;
|
||||||
|
let mut app = app.borrow_mut();
|
||||||
|
Ok(app.reserve_model())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_model<T: 'static>(
|
||||||
|
&mut self,
|
||||||
|
reservation: Reservation<T>,
|
||||||
|
build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T,
|
||||||
|
) -> Result<Model<T>> {
|
||||||
|
let app = self
|
||||||
|
.app
|
||||||
|
.upgrade()
|
||||||
|
.ok_or_else(|| anyhow!("app was released"))?;
|
||||||
|
let mut app = app.borrow_mut();
|
||||||
|
Ok(app.insert_model(reservation, build_model))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_model<T: 'static, R>(
|
||||||
|
&mut self,
|
||||||
|
handle: &Model<T>,
|
||||||
|
update: impl FnOnce(&mut T, &mut ModelContext<'_, T>) -> R,
|
||||||
|
) -> Self::Result<R> {
|
||||||
|
let app = self
|
||||||
|
.app
|
||||||
|
.upgrade()
|
||||||
|
.ok_or_else(|| anyhow!("app was released"))?;
|
||||||
|
let mut app = app.borrow_mut();
|
||||||
|
Ok(app.update_model(handle, update))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_model<T, R>(
|
||||||
|
&self,
|
||||||
|
handle: &Model<T>,
|
||||||
|
callback: impl FnOnce(&T, &AppContext) -> R,
|
||||||
|
) -> Self::Result<R>
|
||||||
|
where
|
||||||
|
T: 'static,
|
||||||
|
{
|
||||||
|
let app = self.app.upgrade().context("app was released")?;
|
||||||
|
let lock = app.borrow();
|
||||||
|
Ok(lock.read_model(handle, callback))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Result<T>
|
||||||
|
where
|
||||||
|
F: FnOnce(AnyView, &mut WindowContext<'_>) -> T,
|
||||||
|
{
|
||||||
|
let app = self.app.upgrade().context("app was released")?;
|
||||||
|
let mut lock = app.borrow_mut();
|
||||||
|
lock.update_window(window, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_window<T, R>(
|
||||||
|
&self,
|
||||||
|
window: &WindowHandle<T>,
|
||||||
|
read: impl FnOnce(View<T>, &AppContext) -> R,
|
||||||
|
) -> Result<R>
|
||||||
|
where
|
||||||
|
T: 'static,
|
||||||
|
{
|
||||||
|
let app = self.app.upgrade().context("app was released")?;
|
||||||
|
let lock = app.borrow();
|
||||||
|
lock.read_window(window, read)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsyncAppContext {
|
||||||
|
/// Schedules all windows in the application to be redrawn.
|
||||||
|
pub fn refresh(&mut self) -> Result<()> {
|
||||||
|
let app = self
|
||||||
|
.app
|
||||||
|
.upgrade()
|
||||||
|
.ok_or_else(|| anyhow!("app was released"))?;
|
||||||
|
let mut lock = app.borrow_mut();
|
||||||
|
lock.refresh();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get an executor which can be used to spawn futures in the background.
|
||||||
|
pub fn background_executor(&self) -> &BackgroundExecutor {
|
||||||
|
&self.background_executor
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get an executor which can be used to spawn futures in the foreground.
|
||||||
|
pub fn foreground_executor(&self) -> &ForegroundExecutor {
|
||||||
|
&self.foreground_executor
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Invoke the given function in the context of the app, then flush any effects produced during its invocation.
|
||||||
|
pub fn update<R>(&self, f: impl FnOnce(&mut AppContext) -> R) -> Result<R> {
|
||||||
|
let app = self
|
||||||
|
.app
|
||||||
|
.upgrade()
|
||||||
|
.ok_or_else(|| anyhow!("app was released"))?;
|
||||||
|
let mut lock = app.borrow_mut();
|
||||||
|
Ok(f(&mut lock))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Open a window with the given options based on the root view returned by the given function.
|
||||||
|
pub fn open_window<V>(
|
||||||
|
&self,
|
||||||
|
options: crate::WindowOptions,
|
||||||
|
build_root_view: impl FnOnce(&mut WindowContext) -> View<V>,
|
||||||
|
) -> Result<WindowHandle<V>>
|
||||||
|
where
|
||||||
|
V: 'static + Render,
|
||||||
|
{
|
||||||
|
let app = self
|
||||||
|
.app
|
||||||
|
.upgrade()
|
||||||
|
.ok_or_else(|| anyhow!("app was released"))?;
|
||||||
|
let mut lock = app.borrow_mut();
|
||||||
|
Ok(lock.open_window(options, build_root_view))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Schedule a future to be polled in the background.
|
||||||
|
pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task<R>
|
||||||
|
where
|
||||||
|
Fut: Future<Output = R> + 'static,
|
||||||
|
R: 'static,
|
||||||
|
{
|
||||||
|
self.foreground_executor.spawn(f(self.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determine whether global state of the specified type has been assigned.
|
||||||
|
/// Returns an error if the `AppContext` has been dropped.
|
||||||
|
pub fn has_global<G: Global>(&self) -> Result<bool> {
|
||||||
|
let app = self
|
||||||
|
.app
|
||||||
|
.upgrade()
|
||||||
|
.ok_or_else(|| anyhow!("app was released"))?;
|
||||||
|
let app = app.borrow_mut();
|
||||||
|
Ok(app.has_global::<G>())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads the global state of the specified type, passing it to the given callback.
|
||||||
|
///
|
||||||
|
/// Panics if no global state of the specified type has been assigned.
|
||||||
|
/// Returns an error if the `AppContext` has been dropped.
|
||||||
|
pub fn read_global<G: Global, R>(&self, read: impl FnOnce(&G, &AppContext) -> R) -> Result<R> {
|
||||||
|
let app = self
|
||||||
|
.app
|
||||||
|
.upgrade()
|
||||||
|
.ok_or_else(|| anyhow!("app was released"))?;
|
||||||
|
let app = app.borrow_mut();
|
||||||
|
Ok(read(app.global(), &app))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads the global state of the specified type, passing it to the given callback.
|
||||||
|
///
|
||||||
|
/// Similar to [`AsyncAppContext::read_global`], but returns an error instead of panicking
|
||||||
|
/// if no state of the specified type has been assigned.
|
||||||
|
///
|
||||||
|
/// Returns an error if no state of the specified type has been assigned the `AppContext` has been dropped.
|
||||||
|
pub fn try_read_global<G: Global, R>(
|
||||||
|
&self,
|
||||||
|
read: impl FnOnce(&G, &AppContext) -> R,
|
||||||
|
) -> Option<R> {
|
||||||
|
let app = self.app.upgrade()?;
|
||||||
|
let app = app.borrow_mut();
|
||||||
|
Some(read(app.try_global()?, &app))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A convenience method for [AppContext::update_global]
|
||||||
|
/// for updating the global state of the specified type.
|
||||||
|
pub fn update_global<G: Global, R>(
|
||||||
|
&mut self,
|
||||||
|
update: impl FnOnce(&mut G, &mut AppContext) -> R,
|
||||||
|
) -> Result<R> {
|
||||||
|
let app = self
|
||||||
|
.app
|
||||||
|
.upgrade()
|
||||||
|
.ok_or_else(|| anyhow!("app was released"))?;
|
||||||
|
let mut app = app.borrow_mut();
|
||||||
|
Ok(app.update(|cx| cx.update_global(update)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A cloneable, owned handle to the application context,
|
||||||
|
/// composed with the window associated with the current task.
|
||||||
|
#[derive(Clone, Deref, DerefMut)]
|
||||||
|
pub struct AsyncWindowContext {
|
||||||
|
#[deref]
|
||||||
|
#[deref_mut]
|
||||||
|
app: AsyncAppContext,
|
||||||
|
window: AnyWindowHandle,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsyncWindowContext {
|
||||||
|
pub(crate) fn new(app: AsyncAppContext, window: AnyWindowHandle) -> Self {
|
||||||
|
Self { app, window }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the handle of the window this context is associated with.
|
||||||
|
pub fn window_handle(&self) -> AnyWindowHandle {
|
||||||
|
self.window
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A convenience method for [`AppContext::update_window`].
|
||||||
|
pub fn update<R>(&mut self, update: impl FnOnce(&mut WindowContext) -> R) -> Result<R> {
|
||||||
|
self.app.update_window(self.window, |_, cx| update(cx))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A convenience method for [`AppContext::update_window`].
|
||||||
|
pub fn update_root<R>(
|
||||||
|
&mut self,
|
||||||
|
update: impl FnOnce(AnyView, &mut WindowContext) -> R,
|
||||||
|
) -> Result<R> {
|
||||||
|
self.app.update_window(self.window, update)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A convenience method for [`WindowContext::on_next_frame`].
|
||||||
|
pub fn on_next_frame(&mut self, f: impl FnOnce(&mut WindowContext) + 'static) {
|
||||||
|
self.window.update(self, |_, cx| cx.on_next_frame(f)).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A convenience method for [`AppContext::global`].
|
||||||
|
pub fn read_global<G: Global, R>(
|
||||||
|
&mut self,
|
||||||
|
read: impl FnOnce(&G, &WindowContext) -> R,
|
||||||
|
) -> Result<R> {
|
||||||
|
self.window.update(self, |_, cx| read(cx.global(), cx))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A convenience method for [`AppContext::update_global`].
|
||||||
|
/// for updating the global state of the specified type.
|
||||||
|
pub fn update_global<G, R>(
|
||||||
|
&mut self,
|
||||||
|
update: impl FnOnce(&mut G, &mut WindowContext) -> R,
|
||||||
|
) -> Result<R>
|
||||||
|
where
|
||||||
|
G: Global,
|
||||||
|
{
|
||||||
|
self.window.update(self, |_, cx| cx.update_global(update))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Schedule a future to be executed on the main thread. This is used for collecting
|
||||||
|
/// the results of background tasks and updating the UI.
|
||||||
|
pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncWindowContext) -> Fut) -> Task<R>
|
||||||
|
where
|
||||||
|
Fut: Future<Output = R> + 'static,
|
||||||
|
R: 'static,
|
||||||
|
{
|
||||||
|
self.foreground_executor.spawn(f(self.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Present a platform dialog.
|
||||||
|
/// The provided message will be presented, along with buttons for each answer.
|
||||||
|
/// When a button is clicked, the returned Receiver will receive the index of the clicked button.
|
||||||
|
pub fn prompt(
|
||||||
|
&mut self,
|
||||||
|
level: PromptLevel,
|
||||||
|
message: &str,
|
||||||
|
detail: Option<&str>,
|
||||||
|
answers: &[&str],
|
||||||
|
) -> oneshot::Receiver<usize> {
|
||||||
|
self.window
|
||||||
|
.update(self, |_, cx| cx.prompt(level, message, detail, answers))
|
||||||
|
.unwrap_or_else(|_| oneshot::channel().1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Context for AsyncWindowContext {
|
||||||
|
type Result<T> = Result<T>;
|
||||||
|
|
||||||
|
fn new_model<T>(
|
||||||
|
&mut self,
|
||||||
|
build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T,
|
||||||
|
) -> Result<Model<T>>
|
||||||
|
where
|
||||||
|
T: 'static,
|
||||||
|
{
|
||||||
|
self.window.update(self, |_, cx| cx.new_model(build_model))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reserve_model<T: 'static>(&mut self) -> Result<Reservation<T>> {
|
||||||
|
self.window.update(self, |_, cx| cx.reserve_model())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_model<T: 'static>(
|
||||||
|
&mut self,
|
||||||
|
reservation: Reservation<T>,
|
||||||
|
build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T,
|
||||||
|
) -> Self::Result<Model<T>> {
|
||||||
|
self.window
|
||||||
|
.update(self, |_, cx| cx.insert_model(reservation, build_model))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_model<T: 'static, R>(
|
||||||
|
&mut self,
|
||||||
|
handle: &Model<T>,
|
||||||
|
update: impl FnOnce(&mut T, &mut ModelContext<'_, T>) -> R,
|
||||||
|
) -> Result<R> {
|
||||||
|
self.window
|
||||||
|
.update(self, |_, cx| cx.update_model(handle, update))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_model<T, R>(
|
||||||
|
&self,
|
||||||
|
handle: &Model<T>,
|
||||||
|
read: impl FnOnce(&T, &AppContext) -> R,
|
||||||
|
) -> Self::Result<R>
|
||||||
|
where
|
||||||
|
T: 'static,
|
||||||
|
{
|
||||||
|
self.app.read_model(handle, read)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_window<T, F>(&mut self, window: AnyWindowHandle, update: F) -> Result<T>
|
||||||
|
where
|
||||||
|
F: FnOnce(AnyView, &mut WindowContext<'_>) -> T,
|
||||||
|
{
|
||||||
|
self.app.update_window(window, update)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_window<T, R>(
|
||||||
|
&self,
|
||||||
|
window: &WindowHandle<T>,
|
||||||
|
read: impl FnOnce(View<T>, &AppContext) -> R,
|
||||||
|
) -> Result<R>
|
||||||
|
where
|
||||||
|
T: 'static,
|
||||||
|
{
|
||||||
|
self.app.read_window(window, read)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VisualContext for AsyncWindowContext {
|
||||||
|
fn new_view<V>(
|
||||||
|
&mut self,
|
||||||
|
build_view_state: impl FnOnce(&mut ViewContext<'_, V>) -> V,
|
||||||
|
) -> Self::Result<View<V>>
|
||||||
|
where
|
||||||
|
V: 'static + Render,
|
||||||
|
{
|
||||||
|
self.window
|
||||||
|
.update(self, |_, cx| cx.new_view(build_view_state))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_view<V: 'static, R>(
|
||||||
|
&mut self,
|
||||||
|
view: &View<V>,
|
||||||
|
update: impl FnOnce(&mut V, &mut ViewContext<'_, V>) -> R,
|
||||||
|
) -> Self::Result<R> {
|
||||||
|
self.window
|
||||||
|
.update(self, |_, cx| cx.update_view(view, update))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replace_root_view<V>(
|
||||||
|
&mut self,
|
||||||
|
build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V,
|
||||||
|
) -> Self::Result<View<V>>
|
||||||
|
where
|
||||||
|
V: 'static + Render,
|
||||||
|
{
|
||||||
|
self.window
|
||||||
|
.update(self, |_, cx| cx.replace_root_view(build_view))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn focus_view<V>(&mut self, view: &View<V>) -> Self::Result<()>
|
||||||
|
where
|
||||||
|
V: FocusableView,
|
||||||
|
{
|
||||||
|
self.window.update(self, |_, cx| {
|
||||||
|
view.read(cx).focus_handle(cx).clone().focus(cx);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dismiss_view<V>(&mut self, view: &View<V>) -> Self::Result<()>
|
||||||
|
where
|
||||||
|
V: crate::ManagedView,
|
||||||
|
{
|
||||||
|
self.window
|
||||||
|
.update(self, |_, cx| view.update(cx, |_, cx| cx.emit(DismissEvent)))
|
||||||
|
}
|
||||||
|
}
|
744
crates/ming/src/app/entity_map.rs
Normal file
744
crates/ming/src/app/entity_map.rs
Normal file
|
@ -0,0 +1,744 @@
|
||||||
|
use crate::{seal::Sealed, AppContext, Context, Entity, ModelContext};
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use derive_more::{Deref, DerefMut};
|
||||||
|
use parking_lot::{RwLock, RwLockUpgradableReadGuard};
|
||||||
|
use slotmap::{KeyData, SecondaryMap, SlotMap};
|
||||||
|
use std::{
|
||||||
|
any::{type_name, Any, TypeId},
|
||||||
|
fmt::{self, Display},
|
||||||
|
hash::{Hash, Hasher},
|
||||||
|
marker::PhantomData,
|
||||||
|
mem,
|
||||||
|
num::NonZeroU64,
|
||||||
|
sync::{
|
||||||
|
atomic::{AtomicUsize, Ordering::SeqCst},
|
||||||
|
Arc, Weak,
|
||||||
|
},
|
||||||
|
thread::panicking,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
use collections::HashMap;
|
||||||
|
|
||||||
|
slotmap::new_key_type! {
|
||||||
|
/// A unique identifier for a model or view across the application.
|
||||||
|
pub struct EntityId;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u64> for EntityId {
|
||||||
|
fn from(value: u64) -> Self {
|
||||||
|
Self(KeyData::from_ffi(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EntityId {
|
||||||
|
/// Converts this entity id to a [NonZeroU64]
|
||||||
|
pub fn as_non_zero_u64(self) -> NonZeroU64 {
|
||||||
|
NonZeroU64::new(self.0.as_ffi()).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts this entity id to a [u64]
|
||||||
|
pub fn as_u64(self) -> u64 {
|
||||||
|
self.0.as_ffi()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for EntityId {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}", self.as_u64())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct EntityMap {
|
||||||
|
entities: SecondaryMap<EntityId, Box<dyn Any>>,
|
||||||
|
ref_counts: Arc<RwLock<EntityRefCounts>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct EntityRefCounts {
|
||||||
|
counts: SlotMap<EntityId, AtomicUsize>,
|
||||||
|
dropped_entity_ids: Vec<EntityId>,
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
leak_detector: LeakDetector,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EntityMap {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
entities: SecondaryMap::new(),
|
||||||
|
ref_counts: Arc::new(RwLock::new(EntityRefCounts {
|
||||||
|
counts: SlotMap::with_key(),
|
||||||
|
dropped_entity_ids: Vec::new(),
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
leak_detector: LeakDetector {
|
||||||
|
next_handle_id: 0,
|
||||||
|
entity_handles: HashMap::default(),
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reserve a slot for an entity, which you can subsequently use with `insert`.
|
||||||
|
pub fn reserve<T: 'static>(&self) -> Slot<T> {
|
||||||
|
let id = self.ref_counts.write().counts.insert(1.into());
|
||||||
|
Slot(Model::new(id, Arc::downgrade(&self.ref_counts)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert an entity into a slot obtained by calling `reserve`.
|
||||||
|
pub fn insert<T>(&mut self, slot: Slot<T>, entity: T) -> Model<T>
|
||||||
|
where
|
||||||
|
T: 'static,
|
||||||
|
{
|
||||||
|
let model = slot.0;
|
||||||
|
self.entities.insert(model.entity_id, Box::new(entity));
|
||||||
|
model
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move an entity to the stack.
|
||||||
|
#[track_caller]
|
||||||
|
pub fn lease<'a, T>(&mut self, model: &'a Model<T>) -> Lease<'a, T> {
|
||||||
|
self.assert_valid_context(model);
|
||||||
|
let entity = Some(self.entities.remove(model.entity_id).unwrap_or_else(|| {
|
||||||
|
panic!(
|
||||||
|
"Circular entity lease of {}. Is it already being updated?",
|
||||||
|
std::any::type_name::<T>()
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
Lease {
|
||||||
|
model,
|
||||||
|
entity,
|
||||||
|
entity_type: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an entity after moving it to the stack.
|
||||||
|
pub fn end_lease<T>(&mut self, mut lease: Lease<T>) {
|
||||||
|
self.entities
|
||||||
|
.insert(lease.model.entity_id, lease.entity.take().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read<T: 'static>(&self, model: &Model<T>) -> &T {
|
||||||
|
self.assert_valid_context(model);
|
||||||
|
self.entities[model.entity_id].downcast_ref().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_valid_context(&self, model: &AnyModel) {
|
||||||
|
debug_assert!(
|
||||||
|
Weak::ptr_eq(&model.entity_map, &Arc::downgrade(&self.ref_counts)),
|
||||||
|
"used a model with the wrong context"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn take_dropped(&mut self) -> Vec<(EntityId, Box<dyn Any>)> {
|
||||||
|
let mut ref_counts = self.ref_counts.write();
|
||||||
|
let dropped_entity_ids = mem::take(&mut ref_counts.dropped_entity_ids);
|
||||||
|
|
||||||
|
dropped_entity_ids
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|entity_id| {
|
||||||
|
let count = ref_counts.counts.remove(entity_id).unwrap();
|
||||||
|
debug_assert_eq!(
|
||||||
|
count.load(SeqCst),
|
||||||
|
0,
|
||||||
|
"dropped an entity that was referenced"
|
||||||
|
);
|
||||||
|
// If the EntityId was allocated with `Context::reserve`,
|
||||||
|
// the entity may not have been inserted.
|
||||||
|
Some((entity_id, self.entities.remove(entity_id)?))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct Lease<'a, T> {
|
||||||
|
entity: Option<Box<dyn Any>>,
|
||||||
|
pub model: &'a Model<T>,
|
||||||
|
entity_type: PhantomData<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T: 'static> core::ops::Deref for Lease<'a, T> {
|
||||||
|
type Target = T;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
self.entity.as_ref().unwrap().downcast_ref().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T: 'static> core::ops::DerefMut for Lease<'a, T> {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
self.entity.as_mut().unwrap().downcast_mut().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T> Drop for Lease<'a, T> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if self.entity.is_some() && !panicking() {
|
||||||
|
panic!("Leases must be ended with EntityMap::end_lease")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deref, DerefMut)]
|
||||||
|
pub(crate) struct Slot<T>(Model<T>);
|
||||||
|
|
||||||
|
/// A dynamically typed reference to a model, which can be downcast into a `Model<T>`.
|
||||||
|
pub struct AnyModel {
|
||||||
|
pub(crate) entity_id: EntityId,
|
||||||
|
pub(crate) entity_type: TypeId,
|
||||||
|
entity_map: Weak<RwLock<EntityRefCounts>>,
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
handle_id: HandleId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AnyModel {
|
||||||
|
fn new(id: EntityId, entity_type: TypeId, entity_map: Weak<RwLock<EntityRefCounts>>) -> Self {
|
||||||
|
Self {
|
||||||
|
entity_id: id,
|
||||||
|
entity_type,
|
||||||
|
entity_map: entity_map.clone(),
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
handle_id: entity_map
|
||||||
|
.upgrade()
|
||||||
|
.unwrap()
|
||||||
|
.write()
|
||||||
|
.leak_detector
|
||||||
|
.handle_created(id),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the id associated with this model.
|
||||||
|
pub fn entity_id(&self) -> EntityId {
|
||||||
|
self.entity_id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the [TypeId] associated with this model.
|
||||||
|
pub fn entity_type(&self) -> TypeId {
|
||||||
|
self.entity_type
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts this model handle into a weak variant, which does not prevent it from being released.
|
||||||
|
pub fn downgrade(&self) -> AnyWeakModel {
|
||||||
|
AnyWeakModel {
|
||||||
|
entity_id: self.entity_id,
|
||||||
|
entity_type: self.entity_type,
|
||||||
|
entity_ref_counts: self.entity_map.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts this model handle into a strongly-typed model handle of the given type.
|
||||||
|
/// If this model handle is not of the specified type, returns itself as an error variant.
|
||||||
|
pub fn downcast<T: 'static>(self) -> Result<Model<T>, AnyModel> {
|
||||||
|
if TypeId::of::<T>() == self.entity_type {
|
||||||
|
Ok(Model {
|
||||||
|
any_model: self,
|
||||||
|
entity_type: PhantomData,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Err(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for AnyModel {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
if let Some(entity_map) = self.entity_map.upgrade() {
|
||||||
|
let entity_map = entity_map.read();
|
||||||
|
let count = entity_map
|
||||||
|
.counts
|
||||||
|
.get(self.entity_id)
|
||||||
|
.expect("detected over-release of a model");
|
||||||
|
let prev_count = count.fetch_add(1, SeqCst);
|
||||||
|
assert_ne!(prev_count, 0, "Detected over-release of a model.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
entity_id: self.entity_id,
|
||||||
|
entity_type: self.entity_type,
|
||||||
|
entity_map: self.entity_map.clone(),
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
handle_id: self
|
||||||
|
.entity_map
|
||||||
|
.upgrade()
|
||||||
|
.unwrap()
|
||||||
|
.write()
|
||||||
|
.leak_detector
|
||||||
|
.handle_created(self.entity_id),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for AnyModel {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Some(entity_map) = self.entity_map.upgrade() {
|
||||||
|
let entity_map = entity_map.upgradable_read();
|
||||||
|
let count = entity_map
|
||||||
|
.counts
|
||||||
|
.get(self.entity_id)
|
||||||
|
.expect("detected over-release of a handle.");
|
||||||
|
let prev_count = count.fetch_sub(1, SeqCst);
|
||||||
|
assert_ne!(prev_count, 0, "Detected over-release of a model.");
|
||||||
|
if prev_count == 1 {
|
||||||
|
// We were the last reference to this entity, so we can remove it.
|
||||||
|
let mut entity_map = RwLockUpgradableReadGuard::upgrade(entity_map);
|
||||||
|
entity_map.dropped_entity_ids.push(self.entity_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
if let Some(entity_map) = self.entity_map.upgrade() {
|
||||||
|
entity_map
|
||||||
|
.write()
|
||||||
|
.leak_detector
|
||||||
|
.handle_released(self.entity_id, self.handle_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<Model<T>> for AnyModel {
|
||||||
|
fn from(model: Model<T>) -> Self {
|
||||||
|
model.any_model
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hash for AnyModel {
|
||||||
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
|
self.entity_id.hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for AnyModel {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.entity_id == other.entity_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for AnyModel {}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for AnyModel {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.debug_struct("AnyModel")
|
||||||
|
.field("entity_id", &self.entity_id.as_u64())
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A strong, well typed reference to a struct which is managed
|
||||||
|
/// by GPUI
|
||||||
|
#[derive(Deref, DerefMut)]
|
||||||
|
pub struct Model<T> {
|
||||||
|
#[deref]
|
||||||
|
#[deref_mut]
|
||||||
|
pub(crate) any_model: AnyModel,
|
||||||
|
pub(crate) entity_type: PhantomData<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl<T> Send for Model<T> {}
|
||||||
|
unsafe impl<T> Sync for Model<T> {}
|
||||||
|
impl<T> Sealed for Model<T> {}
|
||||||
|
|
||||||
|
impl<T: 'static> Entity<T> for Model<T> {
|
||||||
|
type Weak = WeakModel<T>;
|
||||||
|
|
||||||
|
fn entity_id(&self) -> EntityId {
|
||||||
|
self.any_model.entity_id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn downgrade(&self) -> Self::Weak {
|
||||||
|
WeakModel {
|
||||||
|
any_model: self.any_model.downgrade(),
|
||||||
|
entity_type: self.entity_type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn upgrade_from(weak: &Self::Weak) -> Option<Self>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
Some(Model {
|
||||||
|
any_model: weak.any_model.upgrade()?,
|
||||||
|
entity_type: weak.entity_type,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: 'static> Model<T> {
|
||||||
|
fn new(id: EntityId, entity_map: Weak<RwLock<EntityRefCounts>>) -> Self
|
||||||
|
where
|
||||||
|
T: 'static,
|
||||||
|
{
|
||||||
|
Self {
|
||||||
|
any_model: AnyModel::new(id, TypeId::of::<T>(), entity_map),
|
||||||
|
entity_type: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Downgrade the this to a weak model reference
|
||||||
|
pub fn downgrade(&self) -> WeakModel<T> {
|
||||||
|
// Delegate to the trait implementation to keep behavior in one place.
|
||||||
|
// This method was included to improve method resolution in the presence of
|
||||||
|
// the Model's deref
|
||||||
|
Entity::downgrade(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert this into a dynamically typed model.
|
||||||
|
pub fn into_any(self) -> AnyModel {
|
||||||
|
self.any_model
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Grab a reference to this entity from the context.
|
||||||
|
pub fn read<'a>(&self, cx: &'a AppContext) -> &'a T {
|
||||||
|
cx.entities.read(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read the entity referenced by this model with the given function.
|
||||||
|
pub fn read_with<R, C: Context>(
|
||||||
|
&self,
|
||||||
|
cx: &C,
|
||||||
|
f: impl FnOnce(&T, &AppContext) -> R,
|
||||||
|
) -> C::Result<R> {
|
||||||
|
cx.read_model(self, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates the entity referenced by this model with the given function.
|
||||||
|
///
|
||||||
|
/// The update function receives a context appropriate for its environment.
|
||||||
|
/// When updating in an `AppContext`, it receives a `ModelContext`.
|
||||||
|
/// When updating in a `WindowContext`, it receives a `ViewContext`.
|
||||||
|
pub fn update<C, R>(
|
||||||
|
&self,
|
||||||
|
cx: &mut C,
|
||||||
|
update: impl FnOnce(&mut T, &mut ModelContext<'_, T>) -> R,
|
||||||
|
) -> C::Result<R>
|
||||||
|
where
|
||||||
|
C: Context,
|
||||||
|
{
|
||||||
|
cx.update_model(self, update)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Clone for Model<T> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
any_model: self.any_model.clone(),
|
||||||
|
entity_type: self.entity_type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> std::fmt::Debug for Model<T> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Model {{ entity_id: {:?}, entity_type: {:?} }}",
|
||||||
|
self.any_model.entity_id,
|
||||||
|
type_name::<T>()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Hash for Model<T> {
|
||||||
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
|
self.any_model.hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> PartialEq for Model<T> {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.any_model == other.any_model
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Eq for Model<T> {}
|
||||||
|
|
||||||
|
impl<T> PartialEq<WeakModel<T>> for Model<T> {
|
||||||
|
fn eq(&self, other: &WeakModel<T>) -> bool {
|
||||||
|
self.any_model.entity_id() == other.entity_id()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A type erased, weak reference to a model.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AnyWeakModel {
|
||||||
|
pub(crate) entity_id: EntityId,
|
||||||
|
entity_type: TypeId,
|
||||||
|
entity_ref_counts: Weak<RwLock<EntityRefCounts>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AnyWeakModel {
|
||||||
|
/// Get the entity ID associated with this weak reference.
|
||||||
|
pub fn entity_id(&self) -> EntityId {
|
||||||
|
self.entity_id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if this weak handle can be upgraded, or if the model has already been dropped
|
||||||
|
pub fn is_upgradable(&self) -> bool {
|
||||||
|
let ref_count = self
|
||||||
|
.entity_ref_counts
|
||||||
|
.upgrade()
|
||||||
|
.and_then(|ref_counts| Some(ref_counts.read().counts.get(self.entity_id)?.load(SeqCst)))
|
||||||
|
.unwrap_or(0);
|
||||||
|
ref_count > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Upgrade this weak model reference to a strong reference.
|
||||||
|
pub fn upgrade(&self) -> Option<AnyModel> {
|
||||||
|
let ref_counts = &self.entity_ref_counts.upgrade()?;
|
||||||
|
let ref_counts = ref_counts.read();
|
||||||
|
let ref_count = ref_counts.counts.get(self.entity_id)?;
|
||||||
|
|
||||||
|
// entity_id is in dropped_entity_ids
|
||||||
|
if ref_count.load(SeqCst) == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
ref_count.fetch_add(1, SeqCst);
|
||||||
|
drop(ref_counts);
|
||||||
|
|
||||||
|
Some(AnyModel {
|
||||||
|
entity_id: self.entity_id,
|
||||||
|
entity_type: self.entity_type,
|
||||||
|
entity_map: self.entity_ref_counts.clone(),
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
handle_id: self
|
||||||
|
.entity_ref_counts
|
||||||
|
.upgrade()
|
||||||
|
.unwrap()
|
||||||
|
.write()
|
||||||
|
.leak_detector
|
||||||
|
.handle_created(self.entity_id),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Assert that model referenced by this weak handle has been released.
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub fn assert_released(&self) {
|
||||||
|
self.entity_ref_counts
|
||||||
|
.upgrade()
|
||||||
|
.unwrap()
|
||||||
|
.write()
|
||||||
|
.leak_detector
|
||||||
|
.assert_released(self.entity_id);
|
||||||
|
|
||||||
|
if self
|
||||||
|
.entity_ref_counts
|
||||||
|
.upgrade()
|
||||||
|
.and_then(|ref_counts| Some(ref_counts.read().counts.get(self.entity_id)?.load(SeqCst)))
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
panic!(
|
||||||
|
"entity was recently dropped but resources are retained until the end of the effect cycle."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<WeakModel<T>> for AnyWeakModel {
|
||||||
|
fn from(model: WeakModel<T>) -> Self {
|
||||||
|
model.any_model
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hash for AnyWeakModel {
|
||||||
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
|
self.entity_id.hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for AnyWeakModel {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.entity_id == other.entity_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for AnyWeakModel {}
|
||||||
|
|
||||||
|
/// A weak reference to a model of the given type.
|
||||||
|
#[derive(Deref, DerefMut)]
|
||||||
|
pub struct WeakModel<T> {
|
||||||
|
#[deref]
|
||||||
|
#[deref_mut]
|
||||||
|
any_model: AnyWeakModel,
|
||||||
|
entity_type: PhantomData<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl<T> Send for WeakModel<T> {}
|
||||||
|
unsafe impl<T> Sync for WeakModel<T> {}
|
||||||
|
|
||||||
|
impl<T> Clone for WeakModel<T> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
any_model: self.any_model.clone(),
|
||||||
|
entity_type: self.entity_type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: 'static> WeakModel<T> {
|
||||||
|
/// Upgrade this weak model reference into a strong model reference
|
||||||
|
pub fn upgrade(&self) -> Option<Model<T>> {
|
||||||
|
// Delegate to the trait implementation to keep behavior in one place.
|
||||||
|
Model::upgrade_from(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates the entity referenced by this model with the given function if
|
||||||
|
/// the referenced entity still exists. Returns an error if the entity has
|
||||||
|
/// been released.
|
||||||
|
pub fn update<C, R>(
|
||||||
|
&self,
|
||||||
|
cx: &mut C,
|
||||||
|
update: impl FnOnce(&mut T, &mut ModelContext<'_, T>) -> R,
|
||||||
|
) -> Result<R>
|
||||||
|
where
|
||||||
|
C: Context,
|
||||||
|
Result<C::Result<R>>: crate::Flatten<R>,
|
||||||
|
{
|
||||||
|
crate::Flatten::flatten(
|
||||||
|
self.upgrade()
|
||||||
|
.ok_or_else(|| anyhow!("entity release"))
|
||||||
|
.map(|this| cx.update_model(&this, update)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads the entity referenced by this model with the given function if
|
||||||
|
/// the referenced entity still exists. Returns an error if the entity has
|
||||||
|
/// been released.
|
||||||
|
pub fn read_with<C, R>(&self, cx: &C, read: impl FnOnce(&T, &AppContext) -> R) -> Result<R>
|
||||||
|
where
|
||||||
|
C: Context,
|
||||||
|
Result<C::Result<R>>: crate::Flatten<R>,
|
||||||
|
{
|
||||||
|
crate::Flatten::flatten(
|
||||||
|
self.upgrade()
|
||||||
|
.ok_or_else(|| anyhow!("entity release"))
|
||||||
|
.map(|this| cx.read_model(&this, read)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Hash for WeakModel<T> {
|
||||||
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
|
self.any_model.hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> PartialEq for WeakModel<T> {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.any_model == other.any_model
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Eq for WeakModel<T> {}
|
||||||
|
|
||||||
|
impl<T> PartialEq<Model<T>> for WeakModel<T> {
|
||||||
|
fn eq(&self, other: &Model<T>) -> bool {
|
||||||
|
self.entity_id() == other.any_model.entity_id()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
lazy_static::lazy_static! {
|
||||||
|
static ref LEAK_BACKTRACE: bool =
|
||||||
|
std::env::var("LEAK_BACKTRACE").map_or(false, |b| !b.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)]
|
||||||
|
pub(crate) struct HandleId {
|
||||||
|
id: u64, // id of the handle itself, not the pointed at object
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub(crate) struct LeakDetector {
|
||||||
|
next_handle_id: u64,
|
||||||
|
entity_handles: HashMap<EntityId, HashMap<HandleId, Option<backtrace::Backtrace>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
impl LeakDetector {
|
||||||
|
#[track_caller]
|
||||||
|
pub fn handle_created(&mut self, entity_id: EntityId) -> HandleId {
|
||||||
|
let id = util::post_inc(&mut self.next_handle_id);
|
||||||
|
let handle_id = HandleId { id };
|
||||||
|
let handles = self.entity_handles.entry(entity_id).or_default();
|
||||||
|
handles.insert(
|
||||||
|
handle_id,
|
||||||
|
LEAK_BACKTRACE.then(|| backtrace::Backtrace::new_unresolved()),
|
||||||
|
);
|
||||||
|
handle_id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_released(&mut self, entity_id: EntityId, handle_id: HandleId) {
|
||||||
|
let handles = self.entity_handles.entry(entity_id).or_default();
|
||||||
|
handles.remove(&handle_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn assert_released(&mut self, entity_id: EntityId) {
|
||||||
|
let handles = self.entity_handles.entry(entity_id).or_default();
|
||||||
|
if !handles.is_empty() {
|
||||||
|
for (_, backtrace) in handles {
|
||||||
|
if let Some(mut backtrace) = backtrace.take() {
|
||||||
|
backtrace.resolve();
|
||||||
|
eprintln!("Leaked handle: {:#?}", backtrace);
|
||||||
|
} else {
|
||||||
|
eprintln!("Leaked handle: export LEAK_BACKTRACE to find allocation site");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panic!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use crate::EntityMap;
|
||||||
|
|
||||||
|
struct TestEntity {
|
||||||
|
pub i: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_entity_map_slot_assignment_before_cleanup() {
|
||||||
|
// Tests that slots are not re-used before take_dropped.
|
||||||
|
let mut entity_map = EntityMap::new();
|
||||||
|
|
||||||
|
let slot = entity_map.reserve::<TestEntity>();
|
||||||
|
entity_map.insert(slot, TestEntity { i: 1 });
|
||||||
|
|
||||||
|
let slot = entity_map.reserve::<TestEntity>();
|
||||||
|
entity_map.insert(slot, TestEntity { i: 2 });
|
||||||
|
|
||||||
|
let dropped = entity_map.take_dropped();
|
||||||
|
assert_eq!(dropped.len(), 2);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
dropped
|
||||||
|
.into_iter()
|
||||||
|
.map(|(_, entity)| entity.downcast::<TestEntity>().unwrap().i)
|
||||||
|
.collect::<Vec<i32>>(),
|
||||||
|
vec![1, 2],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_entity_map_weak_upgrade_before_cleanup() {
|
||||||
|
// Tests that weak handles are not upgraded before take_dropped
|
||||||
|
let mut entity_map = EntityMap::new();
|
||||||
|
|
||||||
|
let slot = entity_map.reserve::<TestEntity>();
|
||||||
|
let handle = entity_map.insert(slot, TestEntity { i: 1 });
|
||||||
|
let weak = handle.downgrade();
|
||||||
|
drop(handle);
|
||||||
|
|
||||||
|
let strong = weak.upgrade();
|
||||||
|
assert_eq!(strong, None);
|
||||||
|
|
||||||
|
let dropped = entity_map.take_dropped();
|
||||||
|
assert_eq!(dropped.len(), 1);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
dropped
|
||||||
|
.into_iter()
|
||||||
|
.map(|(_, entity)| entity.downcast::<TestEntity>().unwrap().i)
|
||||||
|
.collect::<Vec<i32>>(),
|
||||||
|
vec![1],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
293
crates/ming/src/app/model_context.rs
Normal file
293
crates/ming/src/app/model_context.rs
Normal file
|
@ -0,0 +1,293 @@
|
||||||
|
use crate::{
|
||||||
|
AnyView, AnyWindowHandle, AppContext, AsyncAppContext, Context, Effect, Entity, EntityId,
|
||||||
|
EventEmitter, Model, Reservation, Subscription, Task, View, WeakModel, WindowContext,
|
||||||
|
WindowHandle,
|
||||||
|
};
|
||||||
|
use anyhow::Result;
|
||||||
|
use derive_more::{Deref, DerefMut};
|
||||||
|
use futures::FutureExt;
|
||||||
|
use std::{
|
||||||
|
any::{Any, TypeId},
|
||||||
|
borrow::{Borrow, BorrowMut},
|
||||||
|
future::Future,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The app context, with specialized behavior for the given model.
|
||||||
|
#[derive(Deref, DerefMut)]
|
||||||
|
pub struct ModelContext<'a, T> {
|
||||||
|
#[deref]
|
||||||
|
#[deref_mut]
|
||||||
|
app: &'a mut AppContext,
|
||||||
|
model_state: WeakModel<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T: 'static> ModelContext<'a, T> {
|
||||||
|
pub(crate) fn new(app: &'a mut AppContext, model_state: WeakModel<T>) -> Self {
|
||||||
|
Self { app, model_state }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The entity id of the model backing this context.
|
||||||
|
pub fn entity_id(&self) -> EntityId {
|
||||||
|
self.model_state.entity_id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a handle to the model belonging to this context.
|
||||||
|
pub fn handle(&self) -> Model<T> {
|
||||||
|
self.weak_model()
|
||||||
|
.upgrade()
|
||||||
|
.expect("The entity must be alive if we have a model context")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a weak handle to the model belonging to this context.
|
||||||
|
pub fn weak_model(&self) -> WeakModel<T> {
|
||||||
|
self.model_state.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Arranges for the given function to be called whenever [`ModelContext::notify`] or
|
||||||
|
/// [`ViewContext::notify`](crate::ViewContext::notify) is called with the given model or view.
|
||||||
|
pub fn observe<W, E>(
|
||||||
|
&mut self,
|
||||||
|
entity: &E,
|
||||||
|
mut on_notify: impl FnMut(&mut T, E, &mut ModelContext<'_, T>) + 'static,
|
||||||
|
) -> Subscription
|
||||||
|
where
|
||||||
|
T: 'static,
|
||||||
|
W: 'static,
|
||||||
|
E: Entity<W>,
|
||||||
|
{
|
||||||
|
let this = self.weak_model();
|
||||||
|
self.app.observe_internal(entity, move |e, cx| {
|
||||||
|
if let Some(this) = this.upgrade() {
|
||||||
|
this.update(cx, |this, cx| on_notify(this, e, cx));
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subscribe to an event type from another model or view
|
||||||
|
pub fn subscribe<T2, E, Evt>(
|
||||||
|
&mut self,
|
||||||
|
entity: &E,
|
||||||
|
mut on_event: impl FnMut(&mut T, E, &Evt, &mut ModelContext<'_, T>) + 'static,
|
||||||
|
) -> Subscription
|
||||||
|
where
|
||||||
|
T: 'static,
|
||||||
|
T2: 'static + EventEmitter<Evt>,
|
||||||
|
E: Entity<T2>,
|
||||||
|
Evt: 'static,
|
||||||
|
{
|
||||||
|
let this = self.weak_model();
|
||||||
|
self.app.subscribe_internal(entity, move |e, event, cx| {
|
||||||
|
if let Some(this) = this.upgrade() {
|
||||||
|
this.update(cx, |this, cx| on_event(this, e, event, cx));
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Register a callback to be invoked when GPUI releases this model.
|
||||||
|
pub fn on_release(
|
||||||
|
&mut self,
|
||||||
|
on_release: impl FnOnce(&mut T, &mut AppContext) + 'static,
|
||||||
|
) -> Subscription
|
||||||
|
where
|
||||||
|
T: 'static,
|
||||||
|
{
|
||||||
|
let (subscription, activate) = self.app.release_listeners.insert(
|
||||||
|
self.model_state.entity_id,
|
||||||
|
Box::new(move |this, cx| {
|
||||||
|
let this = this.downcast_mut().expect("invalid entity type");
|
||||||
|
on_release(this, cx);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
activate();
|
||||||
|
subscription
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Register a callback to be run on the release of another model or view
|
||||||
|
pub fn observe_release<T2, E>(
|
||||||
|
&mut self,
|
||||||
|
entity: &E,
|
||||||
|
on_release: impl FnOnce(&mut T, &mut T2, &mut ModelContext<'_, T>) + 'static,
|
||||||
|
) -> Subscription
|
||||||
|
where
|
||||||
|
T: Any,
|
||||||
|
T2: 'static,
|
||||||
|
E: Entity<T2>,
|
||||||
|
{
|
||||||
|
let entity_id = entity.entity_id();
|
||||||
|
let this = self.weak_model();
|
||||||
|
let (subscription, activate) = self.app.release_listeners.insert(
|
||||||
|
entity_id,
|
||||||
|
Box::new(move |entity, cx| {
|
||||||
|
let entity = entity.downcast_mut().expect("invalid entity type");
|
||||||
|
if let Some(this) = this.upgrade() {
|
||||||
|
this.update(cx, |this, cx| on_release(this, entity, cx));
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
activate();
|
||||||
|
subscription
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Register a callback to for updates to the given global
|
||||||
|
pub fn observe_global<G: 'static>(
|
||||||
|
&mut self,
|
||||||
|
mut f: impl FnMut(&mut T, &mut ModelContext<'_, T>) + 'static,
|
||||||
|
) -> Subscription
|
||||||
|
where
|
||||||
|
T: 'static,
|
||||||
|
{
|
||||||
|
let handle = self.weak_model();
|
||||||
|
let (subscription, activate) = self.global_observers.insert(
|
||||||
|
TypeId::of::<G>(),
|
||||||
|
Box::new(move |cx| handle.update(cx, |view, cx| f(view, cx)).is_ok()),
|
||||||
|
);
|
||||||
|
self.defer(move |_| activate());
|
||||||
|
subscription
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Arrange for the given function to be invoked whenever the application is quit.
|
||||||
|
/// The future returned from this callback will be polled for up to [crate::SHUTDOWN_TIMEOUT] until the app fully quits.
|
||||||
|
pub fn on_app_quit<Fut>(
|
||||||
|
&mut self,
|
||||||
|
mut on_quit: impl FnMut(&mut T, &mut ModelContext<T>) -> Fut + 'static,
|
||||||
|
) -> Subscription
|
||||||
|
where
|
||||||
|
Fut: 'static + Future<Output = ()>,
|
||||||
|
T: 'static,
|
||||||
|
{
|
||||||
|
let handle = self.weak_model();
|
||||||
|
let (subscription, activate) = self.app.quit_observers.insert(
|
||||||
|
(),
|
||||||
|
Box::new(move |cx| {
|
||||||
|
let future = handle.update(cx, |entity, cx| on_quit(entity, cx)).ok();
|
||||||
|
async move {
|
||||||
|
if let Some(future) = future {
|
||||||
|
future.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.boxed_local()
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
activate();
|
||||||
|
subscription
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tell GPUI that this model has changed and observers of it should be notified.
|
||||||
|
pub fn notify(&mut self) {
|
||||||
|
if self
|
||||||
|
.app
|
||||||
|
.pending_notifications
|
||||||
|
.insert(self.model_state.entity_id)
|
||||||
|
{
|
||||||
|
self.app.pending_effects.push_back(Effect::Notify {
|
||||||
|
emitter: self.model_state.entity_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spawn the future returned by the given function.
|
||||||
|
/// The function is provided a weak handle to the model owned by this context and a context that can be held across await points.
|
||||||
|
/// The returned task must be held or detached.
|
||||||
|
pub fn spawn<Fut, R>(&self, f: impl FnOnce(WeakModel<T>, AsyncAppContext) -> Fut) -> Task<R>
|
||||||
|
where
|
||||||
|
T: 'static,
|
||||||
|
Fut: Future<Output = R> + 'static,
|
||||||
|
R: 'static,
|
||||||
|
{
|
||||||
|
let this = self.weak_model();
|
||||||
|
self.app.spawn(|cx| f(this, cx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T> ModelContext<'a, T> {
|
||||||
|
/// Emit an event of the specified type, which can be handled by other entities that have subscribed via `subscribe` methods on their respective contexts.
|
||||||
|
pub fn emit<Evt>(&mut self, event: Evt)
|
||||||
|
where
|
||||||
|
T: EventEmitter<Evt>,
|
||||||
|
Evt: 'static,
|
||||||
|
{
|
||||||
|
self.app.pending_effects.push_back(Effect::Emit {
|
||||||
|
emitter: self.model_state.entity_id,
|
||||||
|
event_type: TypeId::of::<Evt>(),
|
||||||
|
event: Box::new(event),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T> Context for ModelContext<'a, T> {
|
||||||
|
type Result<U> = U;
|
||||||
|
|
||||||
|
fn new_model<U: 'static>(
|
||||||
|
&mut self,
|
||||||
|
build_model: impl FnOnce(&mut ModelContext<'_, U>) -> U,
|
||||||
|
) -> Model<U> {
|
||||||
|
self.app.new_model(build_model)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reserve_model<U: 'static>(&mut self) -> Reservation<U> {
|
||||||
|
self.app.reserve_model()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_model<U: 'static>(
|
||||||
|
&mut self,
|
||||||
|
reservation: Reservation<U>,
|
||||||
|
build_model: impl FnOnce(&mut ModelContext<'_, U>) -> U,
|
||||||
|
) -> Self::Result<Model<U>> {
|
||||||
|
self.app.insert_model(reservation, build_model)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_model<U: 'static, R>(
|
||||||
|
&mut self,
|
||||||
|
handle: &Model<U>,
|
||||||
|
update: impl FnOnce(&mut U, &mut ModelContext<'_, U>) -> R,
|
||||||
|
) -> R {
|
||||||
|
self.app.update_model(handle, update)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_model<U, R>(
|
||||||
|
&self,
|
||||||
|
handle: &Model<U>,
|
||||||
|
read: impl FnOnce(&U, &AppContext) -> R,
|
||||||
|
) -> Self::Result<R>
|
||||||
|
where
|
||||||
|
U: 'static,
|
||||||
|
{
|
||||||
|
self.app.read_model(handle, read)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_window<R, F>(&mut self, window: AnyWindowHandle, update: F) -> Result<R>
|
||||||
|
where
|
||||||
|
F: FnOnce(AnyView, &mut WindowContext<'_>) -> R,
|
||||||
|
{
|
||||||
|
self.app.update_window(window, update)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_window<U, R>(
|
||||||
|
&self,
|
||||||
|
window: &WindowHandle<U>,
|
||||||
|
read: impl FnOnce(View<U>, &AppContext) -> R,
|
||||||
|
) -> Result<R>
|
||||||
|
where
|
||||||
|
U: 'static,
|
||||||
|
{
|
||||||
|
self.app.read_window(window, read)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Borrow<AppContext> for ModelContext<'_, T> {
|
||||||
|
fn borrow(&self) -> &AppContext {
|
||||||
|
self.app
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> BorrowMut<AppContext> for ModelContext<'_, T> {
|
||||||
|
fn borrow_mut(&mut self) -> &mut AppContext {
|
||||||
|
self.app
|
||||||
|
}
|
||||||
|
}
|
974
crates/ming/src/app/test_context.rs
Normal file
974
crates/ming/src/app/test_context.rs
Normal file
|
@ -0,0 +1,974 @@
|
||||||
|
use crate::{
|
||||||
|
Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, AvailableSpace,
|
||||||
|
BackgroundExecutor, BorrowAppContext, Bounds, ClipboardItem, Context, DrawPhase, Drawable,
|
||||||
|
Element, Empty, Entity, EventEmitter, ForegroundExecutor, Global, InputEvent, Keystroke, Model,
|
||||||
|
ModelContext, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent,
|
||||||
|
MouseUpEvent, Pixels, Platform, Point, Render, Result, Size, Task, TestDispatcher,
|
||||||
|
TestPlatform, TestWindow, TextSystem, View, ViewContext, VisualContext, WindowBounds,
|
||||||
|
WindowContext, WindowHandle, WindowOptions,
|
||||||
|
};
|
||||||
|
use anyhow::{anyhow, bail};
|
||||||
|
use futures::{channel::oneshot, Stream, StreamExt};
|
||||||
|
use std::{cell::RefCell, future::Future, ops::Deref, rc::Rc, sync::Arc, time::Duration};
|
||||||
|
|
||||||
|
/// A TestAppContext is provided to tests created with `#[gpui::test]`, it provides
|
||||||
|
/// an implementation of `Context` with additional methods that are useful in tests.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct TestAppContext {
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub app: Rc<AppCell>,
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub background_executor: BackgroundExecutor,
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub foreground_executor: ForegroundExecutor,
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub dispatcher: TestDispatcher,
|
||||||
|
test_platform: Rc<TestPlatform>,
|
||||||
|
text_system: Arc<TextSystem>,
|
||||||
|
fn_name: Option<&'static str>,
|
||||||
|
on_quit: Rc<RefCell<Vec<Box<dyn FnOnce() + 'static>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Context for TestAppContext {
|
||||||
|
type Result<T> = T;
|
||||||
|
|
||||||
|
fn new_model<T: 'static>(
|
||||||
|
&mut self,
|
||||||
|
build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T,
|
||||||
|
) -> Self::Result<Model<T>> {
|
||||||
|
let mut app = self.app.borrow_mut();
|
||||||
|
app.new_model(build_model)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reserve_model<T: 'static>(&mut self) -> Self::Result<crate::Reservation<T>> {
|
||||||
|
let mut app = self.app.borrow_mut();
|
||||||
|
app.reserve_model()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_model<T: 'static>(
|
||||||
|
&mut self,
|
||||||
|
reservation: crate::Reservation<T>,
|
||||||
|
build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T,
|
||||||
|
) -> Self::Result<Model<T>> {
|
||||||
|
let mut app = self.app.borrow_mut();
|
||||||
|
app.insert_model(reservation, build_model)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_model<T: 'static, R>(
|
||||||
|
&mut self,
|
||||||
|
handle: &Model<T>,
|
||||||
|
update: impl FnOnce(&mut T, &mut ModelContext<'_, T>) -> R,
|
||||||
|
) -> Self::Result<R> {
|
||||||
|
let mut app = self.app.borrow_mut();
|
||||||
|
app.update_model(handle, update)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_model<T, R>(
|
||||||
|
&self,
|
||||||
|
handle: &Model<T>,
|
||||||
|
read: impl FnOnce(&T, &AppContext) -> R,
|
||||||
|
) -> Self::Result<R>
|
||||||
|
where
|
||||||
|
T: 'static,
|
||||||
|
{
|
||||||
|
let app = self.app.borrow();
|
||||||
|
app.read_model(handle, read)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Result<T>
|
||||||
|
where
|
||||||
|
F: FnOnce(AnyView, &mut WindowContext<'_>) -> T,
|
||||||
|
{
|
||||||
|
let mut lock = self.app.borrow_mut();
|
||||||
|
lock.update_window(window, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_window<T, R>(
|
||||||
|
&self,
|
||||||
|
window: &WindowHandle<T>,
|
||||||
|
read: impl FnOnce(View<T>, &AppContext) -> R,
|
||||||
|
) -> Result<R>
|
||||||
|
where
|
||||||
|
T: 'static,
|
||||||
|
{
|
||||||
|
let app = self.app.borrow();
|
||||||
|
app.read_window(window, read)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestAppContext {
|
||||||
|
/// Creates a new `TestAppContext`. Usually you can rely on `#[gpui::test]` to do this for you.
|
||||||
|
pub fn new(dispatcher: TestDispatcher, fn_name: Option<&'static str>) -> Self {
|
||||||
|
let arc_dispatcher = Arc::new(dispatcher.clone());
|
||||||
|
let background_executor = BackgroundExecutor::new(arc_dispatcher.clone());
|
||||||
|
let foreground_executor = ForegroundExecutor::new(arc_dispatcher);
|
||||||
|
let platform = TestPlatform::new(background_executor.clone(), foreground_executor.clone());
|
||||||
|
let asset_source = Arc::new(());
|
||||||
|
let http_client = http::FakeHttpClient::with_404_response();
|
||||||
|
let text_system = Arc::new(TextSystem::new(platform.text_system()));
|
||||||
|
|
||||||
|
Self {
|
||||||
|
app: AppContext::new(platform.clone(), asset_source, http_client),
|
||||||
|
background_executor,
|
||||||
|
foreground_executor,
|
||||||
|
dispatcher: dispatcher.clone(),
|
||||||
|
test_platform: platform,
|
||||||
|
text_system,
|
||||||
|
fn_name,
|
||||||
|
on_quit: Rc::new(RefCell::new(Vec::default())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The name of the test function that created this `TestAppContext`
|
||||||
|
pub fn test_function_name(&self) -> Option<&'static str> {
|
||||||
|
self.fn_name
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks whether there have been any new path prompts received by the platform.
|
||||||
|
pub fn did_prompt_for_new_path(&self) -> bool {
|
||||||
|
self.test_platform.did_prompt_for_new_path()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// returns a new `TestAppContext` re-using the same executors to interleave tasks.
|
||||||
|
pub fn new_app(&self) -> TestAppContext {
|
||||||
|
Self::new(self.dispatcher.clone(), self.fn_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called by the test helper to end the test.
|
||||||
|
/// public so the macro can call it.
|
||||||
|
pub fn quit(&self) {
|
||||||
|
self.on_quit.borrow_mut().drain(..).for_each(|f| f());
|
||||||
|
self.app.borrow_mut().shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Register cleanup to run when the test ends.
|
||||||
|
pub fn on_quit(&mut self, f: impl FnOnce() + 'static) {
|
||||||
|
self.on_quit.borrow_mut().push(Box::new(f));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Schedules all windows to be redrawn on the next effect cycle.
|
||||||
|
pub fn refresh(&mut self) -> Result<()> {
|
||||||
|
let mut app = self.app.borrow_mut();
|
||||||
|
app.refresh();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an executor (for running tasks in the background)
|
||||||
|
pub fn executor(&self) -> BackgroundExecutor {
|
||||||
|
self.background_executor.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an executor (for running tasks on the main thread)
|
||||||
|
pub fn foreground_executor(&self) -> &ForegroundExecutor {
|
||||||
|
&self.foreground_executor
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gives you an `&mut AppContext` for the duration of the closure
|
||||||
|
pub fn update<R>(&self, f: impl FnOnce(&mut AppContext) -> R) -> R {
|
||||||
|
let mut cx = self.app.borrow_mut();
|
||||||
|
cx.update(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gives you an `&AppContext` for the duration of the closure
|
||||||
|
pub fn read<R>(&self, f: impl FnOnce(&AppContext) -> R) -> R {
|
||||||
|
let cx = self.app.borrow();
|
||||||
|
f(&cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a new window. The Window will always be backed by a `TestWindow` which
|
||||||
|
/// can be retrieved with `self.test_window(handle)`
|
||||||
|
pub fn add_window<F, V>(&mut self, build_window: F) -> WindowHandle<V>
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut ViewContext<V>) -> V,
|
||||||
|
V: 'static + Render,
|
||||||
|
{
|
||||||
|
let mut cx = self.app.borrow_mut();
|
||||||
|
|
||||||
|
// Some tests rely on the window size matching the bounds of the test display
|
||||||
|
let bounds = Bounds::maximized(None, &mut cx);
|
||||||
|
cx.open_window(
|
||||||
|
WindowOptions {
|
||||||
|
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
|cx| cx.new_view(build_window),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a new window with no content.
|
||||||
|
pub fn add_empty_window(&mut self) -> &mut VisualTestContext {
|
||||||
|
let mut cx = self.app.borrow_mut();
|
||||||
|
let bounds = Bounds::maximized(None, &mut cx);
|
||||||
|
let window = cx.open_window(
|
||||||
|
WindowOptions {
|
||||||
|
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
|cx| cx.new_view(|_| Empty),
|
||||||
|
);
|
||||||
|
drop(cx);
|
||||||
|
let cx = VisualTestContext::from_window(*window.deref(), self).as_mut();
|
||||||
|
cx.run_until_parked();
|
||||||
|
cx
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a new window, and returns its root view and a `VisualTestContext` which can be used
|
||||||
|
/// as a `WindowContext` for the rest of the test. Typically you would shadow this context with
|
||||||
|
/// the returned one. `let (view, cx) = cx.add_window_view(...);`
|
||||||
|
pub fn add_window_view<F, V>(&mut self, build_root_view: F) -> (View<V>, &mut VisualTestContext)
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut ViewContext<V>) -> V,
|
||||||
|
V: 'static + Render,
|
||||||
|
{
|
||||||
|
let mut cx = self.app.borrow_mut();
|
||||||
|
let bounds = Bounds::maximized(None, &mut cx);
|
||||||
|
let window = cx.open_window(
|
||||||
|
WindowOptions {
|
||||||
|
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
|cx| cx.new_view(build_root_view),
|
||||||
|
);
|
||||||
|
drop(cx);
|
||||||
|
let view = window.root_view(self).unwrap();
|
||||||
|
let cx = VisualTestContext::from_window(*window.deref(), self).as_mut();
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
// it might be nice to try and cleanup these at the end of each test.
|
||||||
|
(view, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// returns the TextSystem
|
||||||
|
pub fn text_system(&self) -> &Arc<TextSystem> {
|
||||||
|
&self.text_system
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulates writing to the platform clipboard
|
||||||
|
pub fn write_to_clipboard(&self, item: ClipboardItem) {
|
||||||
|
self.test_platform.write_to_clipboard(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulates reading from the platform clipboard.
|
||||||
|
/// This will return the most recent value from `write_to_clipboard`.
|
||||||
|
pub fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||||
|
self.test_platform.read_from_clipboard()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulates choosing a File in the platform's "Open" dialog.
|
||||||
|
pub fn simulate_new_path_selection(
|
||||||
|
&self,
|
||||||
|
select_path: impl FnOnce(&std::path::Path) -> Option<std::path::PathBuf>,
|
||||||
|
) {
|
||||||
|
self.test_platform.simulate_new_path_selection(select_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulates clicking a button in an platform-level alert dialog.
|
||||||
|
pub fn simulate_prompt_answer(&self, button_ix: usize) {
|
||||||
|
self.test_platform.simulate_prompt_answer(button_ix);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if there's an alert dialog open.
|
||||||
|
pub fn has_pending_prompt(&self) -> bool {
|
||||||
|
self.test_platform.has_pending_prompt()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All the urls that have been opened with cx.open_url() during this test.
|
||||||
|
pub fn opened_url(&self) -> Option<String> {
|
||||||
|
self.test_platform.opened_url.borrow().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulates the user resizing the window to the new size.
|
||||||
|
pub fn simulate_window_resize(&self, window_handle: AnyWindowHandle, size: Size<Pixels>) {
|
||||||
|
self.test_window(window_handle).simulate_resize(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns all windows open in the test.
|
||||||
|
pub fn windows(&self) -> Vec<AnyWindowHandle> {
|
||||||
|
self.app.borrow().windows().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run the given task on the main thread.
|
||||||
|
pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task<R>
|
||||||
|
where
|
||||||
|
Fut: Future<Output = R> + 'static,
|
||||||
|
R: 'static,
|
||||||
|
{
|
||||||
|
self.foreground_executor.spawn(f(self.to_async()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// true if the given global is defined
|
||||||
|
pub fn has_global<G: Global>(&self) -> bool {
|
||||||
|
let app = self.app.borrow();
|
||||||
|
app.has_global::<G>()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// runs the given closure with a reference to the global
|
||||||
|
/// panics if `has_global` would return false.
|
||||||
|
pub fn read_global<G: Global, R>(&self, read: impl FnOnce(&G, &AppContext) -> R) -> R {
|
||||||
|
let app = self.app.borrow();
|
||||||
|
read(app.global(), &app)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// runs the given closure with a reference to the global (if set)
|
||||||
|
pub fn try_read_global<G: Global, R>(
|
||||||
|
&self,
|
||||||
|
read: impl FnOnce(&G, &AppContext) -> R,
|
||||||
|
) -> Option<R> {
|
||||||
|
let lock = self.app.borrow();
|
||||||
|
Some(read(lock.try_global()?, &lock))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// sets the global in this context.
|
||||||
|
pub fn set_global<G: Global>(&mut self, global: G) {
|
||||||
|
let mut lock = self.app.borrow_mut();
|
||||||
|
lock.update(|cx| cx.set_global(global))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// updates the global in this context. (panics if `has_global` would return false)
|
||||||
|
pub fn update_global<G: Global, R>(
|
||||||
|
&mut self,
|
||||||
|
update: impl FnOnce(&mut G, &mut AppContext) -> R,
|
||||||
|
) -> R {
|
||||||
|
let mut lock = self.app.borrow_mut();
|
||||||
|
lock.update(|cx| cx.update_global(update))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an `AsyncAppContext` which can be used to run tasks that expect to be on a background
|
||||||
|
/// thread on the current thread in tests.
|
||||||
|
pub fn to_async(&self) -> AsyncAppContext {
|
||||||
|
AsyncAppContext {
|
||||||
|
app: Rc::downgrade(&self.app),
|
||||||
|
background_executor: self.background_executor.clone(),
|
||||||
|
foreground_executor: self.foreground_executor.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wait until there are no more pending tasks.
|
||||||
|
pub fn run_until_parked(&mut self) {
|
||||||
|
self.background_executor.run_until_parked()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulate dispatching an action to the currently focused node in the window.
|
||||||
|
pub fn dispatch_action<A>(&mut self, window: AnyWindowHandle, action: A)
|
||||||
|
where
|
||||||
|
A: Action,
|
||||||
|
{
|
||||||
|
window
|
||||||
|
.update(self, |_, cx| cx.dispatch_action(action.boxed_clone()))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
self.background_executor.run_until_parked()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// simulate_keystrokes takes a space-separated list of keys to type.
|
||||||
|
/// cx.simulate_keystrokes("cmd-shift-p b k s p enter")
|
||||||
|
/// in Zed, this will run backspace on the current editor through the command palette.
|
||||||
|
/// This will also run the background executor until it's parked.
|
||||||
|
pub fn simulate_keystrokes(&mut self, window: AnyWindowHandle, keystrokes: &str) {
|
||||||
|
for keystroke in keystrokes
|
||||||
|
.split(' ')
|
||||||
|
.map(Keystroke::parse)
|
||||||
|
.map(Result::unwrap)
|
||||||
|
{
|
||||||
|
self.dispatch_keystroke(window, keystroke);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.background_executor.run_until_parked()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// simulate_input takes a string of text to type.
|
||||||
|
/// cx.simulate_input("abc")
|
||||||
|
/// will type abc into your current editor
|
||||||
|
/// This will also run the background executor until it's parked.
|
||||||
|
pub fn simulate_input(&mut self, window: AnyWindowHandle, input: &str) {
|
||||||
|
for keystroke in input.split("").map(Keystroke::parse).map(Result::unwrap) {
|
||||||
|
self.dispatch_keystroke(window, keystroke);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.background_executor.run_until_parked()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// dispatches a single Keystroke (see also `simulate_keystrokes` and `simulate_input`)
|
||||||
|
pub fn dispatch_keystroke(&mut self, window: AnyWindowHandle, keystroke: Keystroke) {
|
||||||
|
self.update_window(window, |_, cx| cx.dispatch_keystroke(keystroke))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the `TestWindow` backing the given handle.
|
||||||
|
pub(crate) fn test_window(&self, window: AnyWindowHandle) -> TestWindow {
|
||||||
|
self.app
|
||||||
|
.borrow_mut()
|
||||||
|
.windows
|
||||||
|
.get_mut(window.id)
|
||||||
|
.unwrap()
|
||||||
|
.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
.platform_window
|
||||||
|
.as_test()
|
||||||
|
.unwrap()
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a stream of notifications whenever the View or Model is updated.
|
||||||
|
pub fn notifications<T: 'static>(&mut self, entity: &impl Entity<T>) -> impl Stream<Item = ()> {
|
||||||
|
let (tx, rx) = futures::channel::mpsc::unbounded();
|
||||||
|
self.update(|cx| {
|
||||||
|
cx.observe(entity, {
|
||||||
|
let tx = tx.clone();
|
||||||
|
move |_, _| {
|
||||||
|
let _ = tx.unbounded_send(());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
cx.observe_release(entity, move |_, _| tx.close_channel())
|
||||||
|
.detach()
|
||||||
|
});
|
||||||
|
rx
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retuens a stream of events emitted by the given Model.
|
||||||
|
pub fn events<Evt, T: 'static + EventEmitter<Evt>>(
|
||||||
|
&mut self,
|
||||||
|
entity: &Model<T>,
|
||||||
|
) -> futures::channel::mpsc::UnboundedReceiver<Evt>
|
||||||
|
where
|
||||||
|
Evt: 'static + Clone,
|
||||||
|
{
|
||||||
|
let (tx, rx) = futures::channel::mpsc::unbounded();
|
||||||
|
entity
|
||||||
|
.update(self, |_, cx: &mut ModelContext<T>| {
|
||||||
|
cx.subscribe(entity, move |_model, _handle, event, _cx| {
|
||||||
|
let _ = tx.unbounded_send(event.clone());
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
rx
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runs until the given condition becomes true. (Prefer `run_until_parked` if you
|
||||||
|
/// don't need to jump in at a specific time).
|
||||||
|
pub async fn condition<T: 'static>(
|
||||||
|
&mut self,
|
||||||
|
model: &Model<T>,
|
||||||
|
mut predicate: impl FnMut(&mut T, &mut ModelContext<T>) -> bool,
|
||||||
|
) {
|
||||||
|
let timer = self.executor().timer(Duration::from_secs(3));
|
||||||
|
let mut notifications = self.notifications(model);
|
||||||
|
|
||||||
|
use futures::FutureExt as _;
|
||||||
|
use smol::future::FutureExt as _;
|
||||||
|
|
||||||
|
async {
|
||||||
|
loop {
|
||||||
|
if model.update(self, &mut predicate) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if notifications.next().await.is_none() {
|
||||||
|
bail!("model dropped")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.race(timer.map(|_| Err(anyhow!("condition timed out"))))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: 'static> Model<T> {
|
||||||
|
/// Block until the next event is emitted by the model, then return it.
|
||||||
|
pub fn next_event<Event>(&self, cx: &mut TestAppContext) -> impl Future<Output = Event>
|
||||||
|
where
|
||||||
|
Event: Send + Clone + 'static,
|
||||||
|
T: EventEmitter<Event>,
|
||||||
|
{
|
||||||
|
let (tx, mut rx) = oneshot::channel();
|
||||||
|
let mut tx = Some(tx);
|
||||||
|
let subscription = self.update(cx, |_, cx| {
|
||||||
|
cx.subscribe(self, move |_, _, event, _| {
|
||||||
|
if let Some(tx) = tx.take() {
|
||||||
|
_ = tx.send(event.clone());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
async move {
|
||||||
|
let event = rx.await.expect("no event emitted");
|
||||||
|
drop(subscription);
|
||||||
|
event
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a future that resolves when the model notifies.
|
||||||
|
pub fn next_notification(&self, cx: &TestAppContext) -> impl Future<Output = ()> {
|
||||||
|
use postage::prelude::{Sink as _, Stream as _};
|
||||||
|
|
||||||
|
let (mut tx, mut rx) = postage::mpsc::channel(1);
|
||||||
|
let mut cx = cx.app.app.borrow_mut();
|
||||||
|
let subscription = cx.observe(self, move |_, _| {
|
||||||
|
tx.try_send(()).ok();
|
||||||
|
});
|
||||||
|
|
||||||
|
let duration = if std::env::var("CI").is_ok() {
|
||||||
|
Duration::from_secs(5)
|
||||||
|
} else {
|
||||||
|
Duration::from_secs(1)
|
||||||
|
};
|
||||||
|
|
||||||
|
async move {
|
||||||
|
let notification = crate::util::timeout(duration, rx.recv())
|
||||||
|
.await
|
||||||
|
.expect("next notification timed out");
|
||||||
|
drop(subscription);
|
||||||
|
notification.expect("model dropped while test was waiting for its next notification")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V: 'static> View<V> {
|
||||||
|
/// Returns a future that resolves when the view is next updated.
|
||||||
|
pub fn next_notification(&self, cx: &TestAppContext) -> impl Future<Output = ()> {
|
||||||
|
use postage::prelude::{Sink as _, Stream as _};
|
||||||
|
|
||||||
|
let (mut tx, mut rx) = postage::mpsc::channel(1);
|
||||||
|
let mut cx = cx.app.app.borrow_mut();
|
||||||
|
let subscription = cx.observe(self, move |_, _| {
|
||||||
|
tx.try_send(()).ok();
|
||||||
|
});
|
||||||
|
|
||||||
|
let duration = if std::env::var("CI").is_ok() {
|
||||||
|
Duration::from_secs(5)
|
||||||
|
} else {
|
||||||
|
Duration::from_secs(1)
|
||||||
|
};
|
||||||
|
|
||||||
|
async move {
|
||||||
|
let notification = crate::util::timeout(duration, rx.recv())
|
||||||
|
.await
|
||||||
|
.expect("next notification timed out");
|
||||||
|
drop(subscription);
|
||||||
|
notification.expect("model dropped while test was waiting for its next notification")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V> View<V> {
|
||||||
|
/// Returns a future that resolves when the condition becomes true.
|
||||||
|
pub fn condition<Evt>(
|
||||||
|
&self,
|
||||||
|
cx: &TestAppContext,
|
||||||
|
mut predicate: impl FnMut(&V, &AppContext) -> bool,
|
||||||
|
) -> impl Future<Output = ()>
|
||||||
|
where
|
||||||
|
Evt: 'static,
|
||||||
|
V: EventEmitter<Evt>,
|
||||||
|
{
|
||||||
|
use postage::prelude::{Sink as _, Stream as _};
|
||||||
|
|
||||||
|
let (tx, mut rx) = postage::mpsc::channel(1024);
|
||||||
|
let timeout_duration = Duration::from_millis(100);
|
||||||
|
|
||||||
|
let mut cx = cx.app.borrow_mut();
|
||||||
|
let subscriptions = (
|
||||||
|
cx.observe(self, {
|
||||||
|
let mut tx = tx.clone();
|
||||||
|
move |_, _| {
|
||||||
|
tx.blocking_send(()).ok();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
cx.subscribe(self, {
|
||||||
|
let mut tx = tx.clone();
|
||||||
|
move |_, _: &Evt, _| {
|
||||||
|
tx.blocking_send(()).ok();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
let cx = cx.this.upgrade().unwrap();
|
||||||
|
let handle = self.downgrade();
|
||||||
|
|
||||||
|
async move {
|
||||||
|
crate::util::timeout(timeout_duration, async move {
|
||||||
|
loop {
|
||||||
|
{
|
||||||
|
let cx = cx.borrow();
|
||||||
|
let cx = &*cx;
|
||||||
|
if predicate(
|
||||||
|
handle
|
||||||
|
.upgrade()
|
||||||
|
.expect("view dropped with pending condition")
|
||||||
|
.read(cx),
|
||||||
|
cx,
|
||||||
|
) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.borrow().background_executor().start_waiting();
|
||||||
|
rx.recv()
|
||||||
|
.await
|
||||||
|
.expect("view dropped with pending condition");
|
||||||
|
cx.borrow().background_executor().finish_waiting();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("condition timed out");
|
||||||
|
drop(subscriptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
use derive_more::{Deref, DerefMut};
|
||||||
|
#[derive(Deref, DerefMut, Clone)]
|
||||||
|
/// A VisualTestContext is the test-equivalent of a `WindowContext`. It allows you to
|
||||||
|
/// run window-specific test code.
|
||||||
|
pub struct VisualTestContext {
|
||||||
|
#[deref]
|
||||||
|
#[deref_mut]
|
||||||
|
/// cx is the original TestAppContext (you can more easily access this using Deref)
|
||||||
|
pub cx: TestAppContext,
|
||||||
|
window: AnyWindowHandle,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VisualTestContext {
|
||||||
|
/// Get the underlying window handle underlying this context.
|
||||||
|
pub fn handle(&self) -> AnyWindowHandle {
|
||||||
|
self.window
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Provides the `WindowContext` for the duration of the closure.
|
||||||
|
pub fn update<R>(&mut self, f: impl FnOnce(&mut WindowContext) -> R) -> R {
|
||||||
|
self.cx.update_window(self.window, |_, cx| f(cx)).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new VisualTestContext. You would typically shadow the passed in
|
||||||
|
/// TestAppContext with this, as this is typically more useful.
|
||||||
|
/// `let cx = VisualTestContext::from_window(window, cx);`
|
||||||
|
pub fn from_window(window: AnyWindowHandle, cx: &TestAppContext) -> Self {
|
||||||
|
Self {
|
||||||
|
cx: cx.clone(),
|
||||||
|
window,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wait until there are no more pending tasks.
|
||||||
|
pub fn run_until_parked(&self) {
|
||||||
|
self.cx.background_executor.run_until_parked();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dispatch the action to the currently focused node.
|
||||||
|
pub fn dispatch_action<A>(&mut self, action: A)
|
||||||
|
where
|
||||||
|
A: Action,
|
||||||
|
{
|
||||||
|
self.cx.dispatch_action(self.window, action)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read the title off the window (set by `WindowContext#set_window_title`)
|
||||||
|
pub fn window_title(&mut self) -> Option<String> {
|
||||||
|
self.cx.test_window(self.window).0.lock().title.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulate a sequence of keystrokes `cx.simulate_keystrokes("cmd-p escape")`
|
||||||
|
/// Automatically runs until parked.
|
||||||
|
pub fn simulate_keystrokes(&mut self, keystrokes: &str) {
|
||||||
|
self.cx.simulate_keystrokes(self.window, keystrokes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulate typing text `cx.simulate_input("hello")`
|
||||||
|
/// Automatically runs until parked.
|
||||||
|
pub fn simulate_input(&mut self, input: &str) {
|
||||||
|
self.cx.simulate_input(self.window, input)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulate a mouse move event to the given point
|
||||||
|
pub fn simulate_mouse_move(
|
||||||
|
&mut self,
|
||||||
|
position: Point<Pixels>,
|
||||||
|
button: impl Into<Option<MouseButton>>,
|
||||||
|
modifiers: Modifiers,
|
||||||
|
) {
|
||||||
|
self.simulate_event(MouseMoveEvent {
|
||||||
|
position,
|
||||||
|
modifiers,
|
||||||
|
pressed_button: button.into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulate a mouse down event to the given point
|
||||||
|
pub fn simulate_mouse_down(
|
||||||
|
&mut self,
|
||||||
|
position: Point<Pixels>,
|
||||||
|
button: MouseButton,
|
||||||
|
modifiers: Modifiers,
|
||||||
|
) {
|
||||||
|
self.simulate_event(MouseDownEvent {
|
||||||
|
position,
|
||||||
|
modifiers,
|
||||||
|
button,
|
||||||
|
click_count: 1,
|
||||||
|
first_mouse: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulate a mouse up event to the given point
|
||||||
|
pub fn simulate_mouse_up(
|
||||||
|
&mut self,
|
||||||
|
position: Point<Pixels>,
|
||||||
|
button: MouseButton,
|
||||||
|
modifiers: Modifiers,
|
||||||
|
) {
|
||||||
|
self.simulate_event(MouseUpEvent {
|
||||||
|
position,
|
||||||
|
modifiers,
|
||||||
|
button,
|
||||||
|
click_count: 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulate a primary mouse click at the given point
|
||||||
|
pub fn simulate_click(&mut self, position: Point<Pixels>, modifiers: Modifiers) {
|
||||||
|
self.simulate_event(MouseDownEvent {
|
||||||
|
position,
|
||||||
|
modifiers,
|
||||||
|
button: MouseButton::Left,
|
||||||
|
click_count: 1,
|
||||||
|
first_mouse: false,
|
||||||
|
});
|
||||||
|
self.simulate_event(MouseUpEvent {
|
||||||
|
position,
|
||||||
|
modifiers,
|
||||||
|
button: MouseButton::Left,
|
||||||
|
click_count: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulate a modifiers changed event
|
||||||
|
pub fn simulate_modifiers_change(&mut self, modifiers: Modifiers) {
|
||||||
|
self.simulate_event(ModifiersChangedEvent { modifiers })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulates the user resizing the window to the new size.
|
||||||
|
pub fn simulate_resize(&self, size: Size<Pixels>) {
|
||||||
|
self.simulate_window_resize(self.window, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// debug_bounds returns the bounds of the element with the given selector.
|
||||||
|
pub fn debug_bounds(&mut self, selector: &'static str) -> Option<Bounds<Pixels>> {
|
||||||
|
self.update(|cx| cx.window.rendered_frame.debug_bounds.get(selector).copied())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw an element to the window. Useful for simulating events or actions
|
||||||
|
pub fn draw<E>(
|
||||||
|
&mut self,
|
||||||
|
origin: Point<Pixels>,
|
||||||
|
space: impl Into<Size<AvailableSpace>>,
|
||||||
|
f: impl FnOnce(&mut WindowContext) -> E,
|
||||||
|
) -> (E::RequestLayoutState, E::PrepaintState)
|
||||||
|
where
|
||||||
|
E: Element,
|
||||||
|
{
|
||||||
|
self.update(|cx| {
|
||||||
|
cx.window.draw_phase = DrawPhase::Prepaint;
|
||||||
|
let mut element = Drawable::new(f(cx));
|
||||||
|
element.layout_as_root(space.into(), cx);
|
||||||
|
cx.with_absolute_element_offset(origin, |cx| element.prepaint(cx));
|
||||||
|
|
||||||
|
cx.window.draw_phase = DrawPhase::Paint;
|
||||||
|
let (request_layout_state, prepaint_state) = element.paint(cx);
|
||||||
|
|
||||||
|
cx.window.draw_phase = DrawPhase::None;
|
||||||
|
cx.refresh();
|
||||||
|
|
||||||
|
(request_layout_state, prepaint_state)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulate an event from the platform, e.g. a SrollWheelEvent
|
||||||
|
/// Make sure you've called [VisualTestContext::draw] first!
|
||||||
|
pub fn simulate_event<E: InputEvent>(&mut self, event: E) {
|
||||||
|
self.test_window(self.window)
|
||||||
|
.simulate_input(event.to_platform_input());
|
||||||
|
self.background_executor.run_until_parked();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulates the user blurring the window.
|
||||||
|
pub fn deactivate_window(&mut self) {
|
||||||
|
if Some(self.window) == self.test_platform.active_window() {
|
||||||
|
self.test_platform.set_active_window(None)
|
||||||
|
}
|
||||||
|
self.background_executor.run_until_parked();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulates the user closing the window.
|
||||||
|
/// Returns true if the window was closed.
|
||||||
|
pub fn simulate_close(&mut self) -> bool {
|
||||||
|
let handler = self
|
||||||
|
.cx
|
||||||
|
.update_window(self.window, |_, cx| {
|
||||||
|
cx.window
|
||||||
|
.platform_window
|
||||||
|
.as_test()
|
||||||
|
.unwrap()
|
||||||
|
.0
|
||||||
|
.lock()
|
||||||
|
.should_close_handler
|
||||||
|
.take()
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
if let Some(mut handler) = handler {
|
||||||
|
let should_close = handler();
|
||||||
|
self.cx
|
||||||
|
.update_window(self.window, |_, cx| {
|
||||||
|
cx.window.platform_window.on_should_close(handler);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
should_close
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get an &mut VisualTestContext (which is mostly what you need to pass to other methods).
|
||||||
|
/// This method internally retains the VisualTestContext until the end of the test.
|
||||||
|
pub fn as_mut(self) -> &'static mut Self {
|
||||||
|
let ptr = Box::into_raw(Box::new(self));
|
||||||
|
// safety: on_quit will be called after the test has finished.
|
||||||
|
// the executor will ensure that all tasks related to the test have stopped.
|
||||||
|
// so there is no way for cx to be accessed after on_quit is called.
|
||||||
|
let cx = Box::leak(unsafe { Box::from_raw(ptr) });
|
||||||
|
cx.on_quit(move || unsafe {
|
||||||
|
drop(Box::from_raw(ptr));
|
||||||
|
});
|
||||||
|
cx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Context for VisualTestContext {
|
||||||
|
type Result<T> = <TestAppContext as Context>::Result<T>;
|
||||||
|
|
||||||
|
fn new_model<T: 'static>(
|
||||||
|
&mut self,
|
||||||
|
build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T,
|
||||||
|
) -> Self::Result<Model<T>> {
|
||||||
|
self.cx.new_model(build_model)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reserve_model<T: 'static>(&mut self) -> Self::Result<crate::Reservation<T>> {
|
||||||
|
self.cx.reserve_model()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_model<T: 'static>(
|
||||||
|
&mut self,
|
||||||
|
reservation: crate::Reservation<T>,
|
||||||
|
build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T,
|
||||||
|
) -> Self::Result<Model<T>> {
|
||||||
|
self.cx.insert_model(reservation, build_model)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_model<T, R>(
|
||||||
|
&mut self,
|
||||||
|
handle: &Model<T>,
|
||||||
|
update: impl FnOnce(&mut T, &mut ModelContext<'_, T>) -> R,
|
||||||
|
) -> Self::Result<R>
|
||||||
|
where
|
||||||
|
T: 'static,
|
||||||
|
{
|
||||||
|
self.cx.update_model(handle, update)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_model<T, R>(
|
||||||
|
&self,
|
||||||
|
handle: &Model<T>,
|
||||||
|
read: impl FnOnce(&T, &AppContext) -> R,
|
||||||
|
) -> Self::Result<R>
|
||||||
|
where
|
||||||
|
T: 'static,
|
||||||
|
{
|
||||||
|
self.cx.read_model(handle, read)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Result<T>
|
||||||
|
where
|
||||||
|
F: FnOnce(AnyView, &mut WindowContext<'_>) -> T,
|
||||||
|
{
|
||||||
|
self.cx.update_window(window, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_window<T, R>(
|
||||||
|
&self,
|
||||||
|
window: &WindowHandle<T>,
|
||||||
|
read: impl FnOnce(View<T>, &AppContext) -> R,
|
||||||
|
) -> Result<R>
|
||||||
|
where
|
||||||
|
T: 'static,
|
||||||
|
{
|
||||||
|
self.cx.read_window(window, read)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VisualContext for VisualTestContext {
|
||||||
|
fn new_view<V>(
|
||||||
|
&mut self,
|
||||||
|
build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V,
|
||||||
|
) -> Self::Result<View<V>>
|
||||||
|
where
|
||||||
|
V: 'static + Render,
|
||||||
|
{
|
||||||
|
self.window
|
||||||
|
.update(&mut self.cx, |_, cx| cx.new_view(build_view))
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_view<V: 'static, R>(
|
||||||
|
&mut self,
|
||||||
|
view: &View<V>,
|
||||||
|
update: impl FnOnce(&mut V, &mut ViewContext<'_, V>) -> R,
|
||||||
|
) -> Self::Result<R> {
|
||||||
|
self.window
|
||||||
|
.update(&mut self.cx, |_, cx| cx.update_view(view, update))
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replace_root_view<V>(
|
||||||
|
&mut self,
|
||||||
|
build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V,
|
||||||
|
) -> Self::Result<View<V>>
|
||||||
|
where
|
||||||
|
V: 'static + Render,
|
||||||
|
{
|
||||||
|
self.window
|
||||||
|
.update(&mut self.cx, |_, cx| cx.replace_root_view(build_view))
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn focus_view<V: crate::FocusableView>(&mut self, view: &View<V>) -> Self::Result<()> {
|
||||||
|
self.window
|
||||||
|
.update(&mut self.cx, |_, cx| {
|
||||||
|
view.read(cx).focus_handle(cx).clone().focus(cx)
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dismiss_view<V>(&mut self, view: &View<V>) -> Self::Result<()>
|
||||||
|
where
|
||||||
|
V: crate::ManagedView,
|
||||||
|
{
|
||||||
|
self.window
|
||||||
|
.update(&mut self.cx, |_, cx| {
|
||||||
|
view.update(cx, |_, cx| cx.emit(crate::DismissEvent))
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AnyWindowHandle {
|
||||||
|
/// Creates the given view in this window.
|
||||||
|
pub fn build_view<V: Render + 'static>(
|
||||||
|
&self,
|
||||||
|
cx: &mut TestAppContext,
|
||||||
|
build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V,
|
||||||
|
) -> View<V> {
|
||||||
|
self.update(cx, |_, cx| cx.new_view(build_view)).unwrap()
|
||||||
|
}
|
||||||
|
}
|
250
crates/ming/src/arena.rs
Normal file
250
crates/ming/src/arena.rs
Normal file
|
@ -0,0 +1,250 @@
|
||||||
|
use std::{
|
||||||
|
alloc,
|
||||||
|
cell::Cell,
|
||||||
|
ops::{Deref, DerefMut},
|
||||||
|
ptr,
|
||||||
|
rc::Rc,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ArenaElement {
|
||||||
|
value: *mut u8,
|
||||||
|
drop: unsafe fn(*mut u8),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for ArenaElement {
|
||||||
|
#[inline(always)]
|
||||||
|
fn drop(&mut self) {
|
||||||
|
unsafe {
|
||||||
|
(self.drop)(self.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Arena {
|
||||||
|
start: *mut u8,
|
||||||
|
end: *mut u8,
|
||||||
|
offset: *mut u8,
|
||||||
|
elements: Vec<ArenaElement>,
|
||||||
|
valid: Rc<Cell<bool>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Arena {
|
||||||
|
pub fn new(size_in_bytes: usize) -> Self {
|
||||||
|
unsafe {
|
||||||
|
let layout = alloc::Layout::from_size_align(size_in_bytes, 1).unwrap();
|
||||||
|
let start = alloc::alloc(layout);
|
||||||
|
let end = start.add(size_in_bytes);
|
||||||
|
Self {
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
offset: start,
|
||||||
|
elements: Vec::new(),
|
||||||
|
valid: Rc::new(Cell::new(true)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.offset as usize - self.start as usize
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn capacity(&self) -> usize {
|
||||||
|
self.end as usize - self.start as usize
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.valid.set(false);
|
||||||
|
self.valid = Rc::new(Cell::new(true));
|
||||||
|
self.elements.clear();
|
||||||
|
self.offset = self.start;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn alloc<T>(&mut self, f: impl FnOnce() -> T) -> ArenaBox<T> {
|
||||||
|
#[inline(always)]
|
||||||
|
unsafe fn inner_writer<T, F>(ptr: *mut T, f: F)
|
||||||
|
where
|
||||||
|
F: FnOnce() -> T,
|
||||||
|
{
|
||||||
|
ptr::write(ptr, f());
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn drop<T>(ptr: *mut u8) {
|
||||||
|
std::ptr::drop_in_place(ptr.cast::<T>());
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let layout = alloc::Layout::new::<T>();
|
||||||
|
let offset = self.offset.add(self.offset.align_offset(layout.align()));
|
||||||
|
let next_offset = offset.add(layout.size());
|
||||||
|
assert!(next_offset <= self.end, "not enough space in Arena");
|
||||||
|
|
||||||
|
let result = ArenaBox {
|
||||||
|
ptr: offset.cast(),
|
||||||
|
valid: self.valid.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
inner_writer(result.ptr, f);
|
||||||
|
self.elements.push(ArenaElement {
|
||||||
|
value: offset,
|
||||||
|
drop: drop::<T>,
|
||||||
|
});
|
||||||
|
self.offset = next_offset;
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Arena {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ArenaBox<T: ?Sized> {
|
||||||
|
ptr: *mut T,
|
||||||
|
valid: Rc<Cell<bool>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: ?Sized> ArenaBox<T> {
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn map<U: ?Sized>(mut self, f: impl FnOnce(&mut T) -> &mut U) -> ArenaBox<U> {
|
||||||
|
ArenaBox {
|
||||||
|
ptr: f(&mut self),
|
||||||
|
valid: self.valid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate(&self) {
|
||||||
|
assert!(
|
||||||
|
self.valid.get(),
|
||||||
|
"attempted to dereference an ArenaRef after its Arena was cleared"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: ?Sized> Deref for ArenaBox<T> {
|
||||||
|
type Target = T;
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
self.validate();
|
||||||
|
unsafe { &*self.ptr }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: ?Sized> DerefMut for ArenaBox<T> {
|
||||||
|
#[inline(always)]
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
self.validate();
|
||||||
|
unsafe { &mut *self.ptr }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ArenaRef<T: ?Sized>(ArenaBox<T>);
|
||||||
|
|
||||||
|
impl<T: ?Sized> From<ArenaBox<T>> for ArenaRef<T> {
|
||||||
|
fn from(value: ArenaBox<T>) -> Self {
|
||||||
|
ArenaRef(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: ?Sized> Clone for ArenaRef<T> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self(ArenaBox {
|
||||||
|
ptr: self.0.ptr,
|
||||||
|
valid: self.0.valid.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: ?Sized> Deref for ArenaRef<T> {
|
||||||
|
type Target = T;
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
self.0.deref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::{cell::Cell, rc::Rc};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_arena() {
|
||||||
|
let mut arena = Arena::new(1024);
|
||||||
|
let a = arena.alloc(|| 1u64);
|
||||||
|
let b = arena.alloc(|| 2u32);
|
||||||
|
let c = arena.alloc(|| 3u16);
|
||||||
|
let d = arena.alloc(|| 4u8);
|
||||||
|
assert_eq!(*a, 1);
|
||||||
|
assert_eq!(*b, 2);
|
||||||
|
assert_eq!(*c, 3);
|
||||||
|
assert_eq!(*d, 4);
|
||||||
|
|
||||||
|
arena.clear();
|
||||||
|
let a = arena.alloc(|| 5u64);
|
||||||
|
let b = arena.alloc(|| 6u32);
|
||||||
|
let c = arena.alloc(|| 7u16);
|
||||||
|
let d = arena.alloc(|| 8u8);
|
||||||
|
assert_eq!(*a, 5);
|
||||||
|
assert_eq!(*b, 6);
|
||||||
|
assert_eq!(*c, 7);
|
||||||
|
assert_eq!(*d, 8);
|
||||||
|
|
||||||
|
// Ensure drop gets called.
|
||||||
|
let dropped = Rc::new(Cell::new(false));
|
||||||
|
struct DropGuard(Rc<Cell<bool>>);
|
||||||
|
impl Drop for DropGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.0.set(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
arena.alloc(|| DropGuard(dropped.clone()));
|
||||||
|
arena.clear();
|
||||||
|
assert!(dropped.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic(expected = "not enough space in Arena")]
|
||||||
|
fn test_arena_overflow() {
|
||||||
|
let mut arena = Arena::new(16);
|
||||||
|
arena.alloc(|| 1u64);
|
||||||
|
arena.alloc(|| 2u64);
|
||||||
|
// This should panic.
|
||||||
|
arena.alloc(|| 3u64);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_arena_alignment() {
|
||||||
|
let mut arena = Arena::new(256);
|
||||||
|
let x1 = arena.alloc(|| 1u8);
|
||||||
|
let x2 = arena.alloc(|| 2u16);
|
||||||
|
let x3 = arena.alloc(|| 3u32);
|
||||||
|
let x4 = arena.alloc(|| 4u64);
|
||||||
|
let x5 = arena.alloc(|| 5u64);
|
||||||
|
|
||||||
|
assert_eq!(*x1, 1);
|
||||||
|
assert_eq!(*x2, 2);
|
||||||
|
assert_eq!(*x3, 3);
|
||||||
|
assert_eq!(*x4, 4);
|
||||||
|
assert_eq!(*x5, 5);
|
||||||
|
|
||||||
|
assert_eq!(x1.ptr.align_offset(std::mem::align_of_val(&*x1)), 0);
|
||||||
|
assert_eq!(x2.ptr.align_offset(std::mem::align_of_val(&*x2)), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic(expected = "attempted to dereference an ArenaRef after its Arena was cleared")]
|
||||||
|
fn test_arena_use_after_clear() {
|
||||||
|
let mut arena = Arena::new(16);
|
||||||
|
let value = arena.alloc(|| 1u64);
|
||||||
|
|
||||||
|
arena.clear();
|
||||||
|
let _read_value = *value;
|
||||||
|
}
|
||||||
|
}
|
87
crates/ming/src/asset_cache.rs
Normal file
87
crates/ming/src/asset_cache.rs
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
use crate::{SharedUri, WindowContext};
|
||||||
|
use collections::FxHashMap;
|
||||||
|
use futures::Future;
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use std::any::TypeId;
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::{any::Any, path::PathBuf};
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
|
||||||
|
pub(crate) enum UriOrPath {
|
||||||
|
Uri(SharedUri),
|
||||||
|
Path(Arc<PathBuf>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SharedUri> for UriOrPath {
|
||||||
|
fn from(value: SharedUri) -> Self {
|
||||||
|
Self::Uri(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Arc<PathBuf>> for UriOrPath {
|
||||||
|
fn from(value: Arc<PathBuf>) -> Self {
|
||||||
|
Self::Path(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A trait for asynchronous asset loading.
|
||||||
|
pub trait Asset {
|
||||||
|
/// The source of the asset.
|
||||||
|
type Source: Clone + Hash + Send;
|
||||||
|
|
||||||
|
/// The loaded asset
|
||||||
|
type Output: Clone + Send;
|
||||||
|
|
||||||
|
/// Load the asset asynchronously
|
||||||
|
fn load(
|
||||||
|
source: Self::Source,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> impl Future<Output = Self::Output> + Send + 'static;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Use a quick, non-cryptographically secure hash function to get an identifier from data
|
||||||
|
pub fn hash<T: Hash>(data: &T) -> u64 {
|
||||||
|
let mut hasher = collections::FxHasher::default();
|
||||||
|
data.hash(&mut hasher);
|
||||||
|
hasher.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A cache for assets.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AssetCache {
|
||||||
|
assets: Arc<Mutex<FxHashMap<(TypeId, u64), Box<dyn Any + Send>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AssetCache {
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
assets: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the asset from the cache, if it exists.
|
||||||
|
pub fn get<A: Asset + 'static>(&self, source: &A::Source) -> Option<A::Output> {
|
||||||
|
self.assets
|
||||||
|
.lock()
|
||||||
|
.get(&(TypeId::of::<A>(), hash(&source)))
|
||||||
|
.and_then(|task| task.downcast_ref::<A::Output>())
|
||||||
|
.cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert the asset into the cache.
|
||||||
|
pub fn insert<A: Asset + 'static>(&mut self, source: A::Source, output: A::Output) {
|
||||||
|
self.assets
|
||||||
|
.lock()
|
||||||
|
.insert((TypeId::of::<A>(), hash(&source)), Box::new(output));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove an entry from the asset cache
|
||||||
|
pub fn remove<A: Asset + 'static>(&mut self, source: &A::Source) -> Option<A::Output> {
|
||||||
|
self.assets
|
||||||
|
.lock()
|
||||||
|
.remove(&(TypeId::of::<A>(), hash(&source)))
|
||||||
|
.and_then(|any| any.downcast::<A::Output>().ok())
|
||||||
|
.map(|boxed| *boxed)
|
||||||
|
}
|
||||||
|
}
|
79
crates/ming/src/assets.rs
Normal file
79
crates/ming/src/assets.rs
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
use crate::{size, DevicePixels, Result, SharedString, Size};
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use image::{Bgra, ImageBuffer};
|
||||||
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
|
fmt,
|
||||||
|
hash::Hash,
|
||||||
|
sync::atomic::{AtomicUsize, Ordering::SeqCst},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A source of assets for this app to use.
|
||||||
|
pub trait AssetSource: 'static + Send + Sync {
|
||||||
|
/// Load the given asset from the source path.
|
||||||
|
fn load(&self, path: &str) -> Result<Cow<'static, [u8]>>;
|
||||||
|
|
||||||
|
/// List the assets at the given path.
|
||||||
|
fn list(&self, path: &str) -> Result<Vec<SharedString>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AssetSource for () {
|
||||||
|
fn load(&self, path: &str) -> Result<Cow<'static, [u8]>> {
|
||||||
|
Err(anyhow!(
|
||||||
|
"load called on empty asset provider with \"{}\"",
|
||||||
|
path
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list(&self, _path: &str) -> Result<Vec<SharedString>> {
|
||||||
|
Ok(vec![])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A unique identifier for the image cache
|
||||||
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||||
|
pub struct ImageId(usize);
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Hash, Clone)]
|
||||||
|
pub(crate) struct RenderImageParams {
|
||||||
|
pub(crate) image_id: ImageId,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A cached and processed image.
|
||||||
|
pub struct ImageData {
|
||||||
|
/// The ID associated with this image
|
||||||
|
pub id: ImageId,
|
||||||
|
data: ImageBuffer<Bgra<u8>, Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImageData {
|
||||||
|
/// Create a new image from the given data.
|
||||||
|
pub fn new(data: ImageBuffer<Bgra<u8>, Vec<u8>>) -> Self {
|
||||||
|
static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
id: ImageId(NEXT_ID.fetch_add(1, SeqCst)),
|
||||||
|
data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert this image into a byte slice.
|
||||||
|
pub fn as_bytes(&self) -> &[u8] {
|
||||||
|
&self.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the size of this image, in pixels
|
||||||
|
pub fn size(&self) -> Size<DevicePixels> {
|
||||||
|
let (width, height) = self.data.dimensions();
|
||||||
|
size(width.into(), height.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for ImageData {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.debug_struct("ImageData")
|
||||||
|
.field("id", &self.id)
|
||||||
|
.field("size", &self.data.dimensions())
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
292
crates/ming/src/bounds_tree.rs
Normal file
292
crates/ming/src/bounds_tree.rs
Normal file
|
@ -0,0 +1,292 @@
|
||||||
|
use crate::{Bounds, Half};
|
||||||
|
use std::{
|
||||||
|
cmp,
|
||||||
|
fmt::Debug,
|
||||||
|
ops::{Add, Sub},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct BoundsTree<U>
|
||||||
|
where
|
||||||
|
U: Default + Clone + Debug,
|
||||||
|
{
|
||||||
|
root: Option<usize>,
|
||||||
|
nodes: Vec<Node<U>>,
|
||||||
|
stack: Vec<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<U> BoundsTree<U>
|
||||||
|
where
|
||||||
|
U: Clone + Debug + PartialOrd + Add<U, Output = U> + Sub<Output = U> + Half + Default,
|
||||||
|
{
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.root = None;
|
||||||
|
self.nodes.clear();
|
||||||
|
self.stack.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert(&mut self, new_bounds: Bounds<U>) -> u32 {
|
||||||
|
// If the tree is empty, make the root the new leaf.
|
||||||
|
if self.root.is_none() {
|
||||||
|
let new_node = self.push_leaf(new_bounds, 1);
|
||||||
|
self.root = Some(new_node);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search for the best place to add the new leaf based on heuristics.
|
||||||
|
let mut max_intersecting_ordering = 0;
|
||||||
|
let mut index = self.root.unwrap();
|
||||||
|
while let Node::Internal {
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
bounds: node_bounds,
|
||||||
|
..
|
||||||
|
} = &mut self.nodes[index]
|
||||||
|
{
|
||||||
|
let left = *left;
|
||||||
|
let right = *right;
|
||||||
|
*node_bounds = node_bounds.union(&new_bounds);
|
||||||
|
self.stack.push(index);
|
||||||
|
|
||||||
|
// Descend to the best-fit child, based on which one would increase
|
||||||
|
// the surface area the least. This attempts to keep the tree balanced
|
||||||
|
// in terms of surface area. If there is an intersection with the other child,
|
||||||
|
// add its keys to the intersections vector.
|
||||||
|
let left_cost = new_bounds
|
||||||
|
.union(&self.nodes[left].bounds())
|
||||||
|
.half_perimeter();
|
||||||
|
let right_cost = new_bounds
|
||||||
|
.union(&self.nodes[right].bounds())
|
||||||
|
.half_perimeter();
|
||||||
|
if left_cost < right_cost {
|
||||||
|
max_intersecting_ordering =
|
||||||
|
self.find_max_ordering(right, &new_bounds, max_intersecting_ordering);
|
||||||
|
index = left;
|
||||||
|
} else {
|
||||||
|
max_intersecting_ordering =
|
||||||
|
self.find_max_ordering(left, &new_bounds, max_intersecting_ordering);
|
||||||
|
index = right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We've found a leaf ('index' now refers to a leaf node).
|
||||||
|
// We'll insert a new parent node above the leaf and attach our new leaf to it.
|
||||||
|
let sibling = index;
|
||||||
|
|
||||||
|
// Check for collision with the located leaf node
|
||||||
|
let Node::Leaf {
|
||||||
|
bounds: sibling_bounds,
|
||||||
|
order: sibling_ordering,
|
||||||
|
..
|
||||||
|
} = &self.nodes[index]
|
||||||
|
else {
|
||||||
|
unreachable!();
|
||||||
|
};
|
||||||
|
if sibling_bounds.intersects(&new_bounds) {
|
||||||
|
max_intersecting_ordering = cmp::max(max_intersecting_ordering, *sibling_ordering);
|
||||||
|
}
|
||||||
|
|
||||||
|
let ordering = max_intersecting_ordering + 1;
|
||||||
|
let new_node = self.push_leaf(new_bounds, ordering);
|
||||||
|
let new_parent = self.push_internal(sibling, new_node);
|
||||||
|
|
||||||
|
// If there was an old parent, we need to update its children indices.
|
||||||
|
if let Some(old_parent) = self.stack.last().copied() {
|
||||||
|
let Node::Internal { left, right, .. } = &mut self.nodes[old_parent] else {
|
||||||
|
unreachable!();
|
||||||
|
};
|
||||||
|
|
||||||
|
if *left == sibling {
|
||||||
|
*left = new_parent;
|
||||||
|
} else {
|
||||||
|
*right = new_parent;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If the old parent was the root, the new parent is the new root.
|
||||||
|
self.root = Some(new_parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
for node_index in self.stack.drain(..) {
|
||||||
|
let Node::Internal {
|
||||||
|
max_order: max_ordering,
|
||||||
|
..
|
||||||
|
} = &mut self.nodes[node_index]
|
||||||
|
else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
*max_ordering = cmp::max(*max_ordering, ordering);
|
||||||
|
}
|
||||||
|
|
||||||
|
ordering
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_max_ordering(&self, index: usize, bounds: &Bounds<U>, mut max_ordering: u32) -> u32 {
|
||||||
|
match &self.nodes[index] {
|
||||||
|
Node::Leaf {
|
||||||
|
bounds: node_bounds,
|
||||||
|
order: ordering,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
if bounds.intersects(node_bounds) {
|
||||||
|
max_ordering = cmp::max(*ordering, max_ordering);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Node::Internal {
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
bounds: node_bounds,
|
||||||
|
max_order: node_max_ordering,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
if bounds.intersects(node_bounds) && max_ordering < *node_max_ordering {
|
||||||
|
let left_max_ordering = self.nodes[*left].max_ordering();
|
||||||
|
let right_max_ordering = self.nodes[*right].max_ordering();
|
||||||
|
if left_max_ordering > right_max_ordering {
|
||||||
|
max_ordering = self.find_max_ordering(*left, bounds, max_ordering);
|
||||||
|
max_ordering = self.find_max_ordering(*right, bounds, max_ordering);
|
||||||
|
} else {
|
||||||
|
max_ordering = self.find_max_ordering(*right, bounds, max_ordering);
|
||||||
|
max_ordering = self.find_max_ordering(*left, bounds, max_ordering);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
max_ordering
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_leaf(&mut self, bounds: Bounds<U>, order: u32) -> usize {
|
||||||
|
self.nodes.push(Node::Leaf { bounds, order });
|
||||||
|
self.nodes.len() - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_internal(&mut self, left: usize, right: usize) -> usize {
|
||||||
|
let left_node = &self.nodes[left];
|
||||||
|
let right_node = &self.nodes[right];
|
||||||
|
let new_bounds = left_node.bounds().union(right_node.bounds());
|
||||||
|
let max_ordering = cmp::max(left_node.max_ordering(), right_node.max_ordering());
|
||||||
|
self.nodes.push(Node::Internal {
|
||||||
|
bounds: new_bounds,
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
max_order: max_ordering,
|
||||||
|
});
|
||||||
|
self.nodes.len() - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<U> Default for BoundsTree<U>
|
||||||
|
where
|
||||||
|
U: Default + Clone + Debug,
|
||||||
|
{
|
||||||
|
fn default() -> Self {
|
||||||
|
BoundsTree {
|
||||||
|
root: None,
|
||||||
|
nodes: Vec::new(),
|
||||||
|
stack: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
enum Node<U>
|
||||||
|
where
|
||||||
|
U: Clone + Default + Debug,
|
||||||
|
{
|
||||||
|
Leaf {
|
||||||
|
bounds: Bounds<U>,
|
||||||
|
order: u32,
|
||||||
|
},
|
||||||
|
Internal {
|
||||||
|
left: usize,
|
||||||
|
right: usize,
|
||||||
|
bounds: Bounds<U>,
|
||||||
|
max_order: u32,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<U> Node<U>
|
||||||
|
where
|
||||||
|
U: Clone + Default + Debug,
|
||||||
|
{
|
||||||
|
fn bounds(&self) -> &Bounds<U> {
|
||||||
|
match self {
|
||||||
|
Node::Leaf { bounds, .. } => bounds,
|
||||||
|
Node::Internal { bounds, .. } => bounds,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn max_ordering(&self) -> u32 {
|
||||||
|
match self {
|
||||||
|
Node::Leaf {
|
||||||
|
order: ordering, ..
|
||||||
|
} => *ordering,
|
||||||
|
Node::Internal {
|
||||||
|
max_order: max_ordering,
|
||||||
|
..
|
||||||
|
} => *max_ordering,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::{Bounds, Point, Size};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_insert() {
|
||||||
|
let mut tree = BoundsTree::<f32>::default();
|
||||||
|
let bounds1 = Bounds {
|
||||||
|
origin: Point { x: 0.0, y: 0.0 },
|
||||||
|
size: Size {
|
||||||
|
width: 10.0,
|
||||||
|
height: 10.0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let bounds2 = Bounds {
|
||||||
|
origin: Point { x: 5.0, y: 5.0 },
|
||||||
|
size: Size {
|
||||||
|
width: 10.0,
|
||||||
|
height: 10.0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let bounds3 = Bounds {
|
||||||
|
origin: Point { x: 10.0, y: 10.0 },
|
||||||
|
size: Size {
|
||||||
|
width: 10.0,
|
||||||
|
height: 10.0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Insert the bounds into the tree and verify the order is correct
|
||||||
|
assert_eq!(tree.insert(bounds1), 1);
|
||||||
|
assert_eq!(tree.insert(bounds2), 2);
|
||||||
|
assert_eq!(tree.insert(bounds3), 3);
|
||||||
|
|
||||||
|
// Insert non-overlapping bounds and verify they can reuse orders
|
||||||
|
let bounds4 = Bounds {
|
||||||
|
origin: Point { x: 20.0, y: 20.0 },
|
||||||
|
size: Size {
|
||||||
|
width: 10.0,
|
||||||
|
height: 10.0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let bounds5 = Bounds {
|
||||||
|
origin: Point { x: 40.0, y: 40.0 },
|
||||||
|
size: Size {
|
||||||
|
width: 10.0,
|
||||||
|
height: 10.0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let bounds6 = Bounds {
|
||||||
|
origin: Point { x: 25.0, y: 25.0 },
|
||||||
|
size: Size {
|
||||||
|
width: 10.0,
|
||||||
|
height: 10.0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
assert_eq!(tree.insert(bounds4), 1); // bounds4 does not overlap with bounds1, bounds2, or bounds3
|
||||||
|
assert_eq!(tree.insert(bounds5), 1); // bounds5 does not overlap with any other bounds
|
||||||
|
assert_eq!(tree.insert(bounds6), 2); // bounds6 overlaps with bounds4, so it should have a different order
|
||||||
|
}
|
||||||
|
}
|
492
crates/ming/src/color.rs
Normal file
492
crates/ming/src/color.rs
Normal file
|
@ -0,0 +1,492 @@
|
||||||
|
use anyhow::bail;
|
||||||
|
use serde::de::{self, Deserialize, Deserializer, Visitor};
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
/// Convert an RGB hex color code number to a color type
|
||||||
|
pub fn rgb(hex: u32) -> Rgba {
|
||||||
|
let r = ((hex >> 16) & 0xFF) as f32 / 255.0;
|
||||||
|
let g = ((hex >> 8) & 0xFF) as f32 / 255.0;
|
||||||
|
let b = (hex & 0xFF) as f32 / 255.0;
|
||||||
|
Rgba { r, g, b, a: 1.0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert an RGBA hex color code number to [`Rgba`]
|
||||||
|
pub fn rgba(hex: u32) -> Rgba {
|
||||||
|
let r = ((hex >> 24) & 0xFF) as f32 / 255.0;
|
||||||
|
let g = ((hex >> 16) & 0xFF) as f32 / 255.0;
|
||||||
|
let b = ((hex >> 8) & 0xFF) as f32 / 255.0;
|
||||||
|
let a = (hex & 0xFF) as f32 / 255.0;
|
||||||
|
Rgba { r, g, b, a }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An RGBA color
|
||||||
|
#[derive(PartialEq, Clone, Copy, Default)]
|
||||||
|
pub struct Rgba {
|
||||||
|
/// The red component of the color, in the range 0.0 to 1.0
|
||||||
|
pub r: f32,
|
||||||
|
/// The green component of the color, in the range 0.0 to 1.0
|
||||||
|
pub g: f32,
|
||||||
|
/// The blue component of the color, in the range 0.0 to 1.0
|
||||||
|
pub b: f32,
|
||||||
|
/// The alpha component of the color, in the range 0.0 to 1.0
|
||||||
|
pub a: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for Rgba {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "rgba({:#010x})", u32::from(*self))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Rgba {
|
||||||
|
/// Create a new [`Rgba`] color by blending this and another color together
|
||||||
|
pub fn blend(&self, other: Rgba) -> Self {
|
||||||
|
if other.a >= 1.0 {
|
||||||
|
other
|
||||||
|
} else if other.a <= 0.0 {
|
||||||
|
return *self;
|
||||||
|
} else {
|
||||||
|
return Rgba {
|
||||||
|
r: (self.r * (1.0 - other.a)) + (other.r * other.a),
|
||||||
|
g: (self.g * (1.0 - other.a)) + (other.g * other.a),
|
||||||
|
b: (self.b * (1.0 - other.a)) + (other.b * other.a),
|
||||||
|
a: self.a,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Rgba> for u32 {
|
||||||
|
fn from(rgba: Rgba) -> Self {
|
||||||
|
let r = (rgba.r * 255.0) as u32;
|
||||||
|
let g = (rgba.g * 255.0) as u32;
|
||||||
|
let b = (rgba.b * 255.0) as u32;
|
||||||
|
let a = (rgba.a * 255.0) as u32;
|
||||||
|
(r << 24) | (g << 16) | (b << 8) | a
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RgbaVisitor;
|
||||||
|
|
||||||
|
impl<'de> Visitor<'de> for RgbaVisitor {
|
||||||
|
type Value = Rgba;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
formatter.write_str("a string in the format #rrggbb or #rrggbbaa")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_str<E: de::Error>(self, value: &str) -> Result<Rgba, E> {
|
||||||
|
Rgba::try_from(value).map_err(E::custom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for Rgba {
|
||||||
|
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||||
|
deserializer.deserialize_str(RgbaVisitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Hsla> for Rgba {
|
||||||
|
fn from(color: Hsla) -> Self {
|
||||||
|
let h = color.h;
|
||||||
|
let s = color.s;
|
||||||
|
let l = color.l;
|
||||||
|
|
||||||
|
let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
|
||||||
|
let x = c * (1.0 - ((h * 6.0) % 2.0 - 1.0).abs());
|
||||||
|
let m = l - c / 2.0;
|
||||||
|
let cm = c + m;
|
||||||
|
let xm = x + m;
|
||||||
|
|
||||||
|
let (r, g, b) = match (h * 6.0).floor() as i32 {
|
||||||
|
0 | 6 => (cm, xm, m),
|
||||||
|
1 => (xm, cm, m),
|
||||||
|
2 => (m, cm, xm),
|
||||||
|
3 => (m, xm, cm),
|
||||||
|
4 => (xm, m, cm),
|
||||||
|
_ => (cm, m, xm),
|
||||||
|
};
|
||||||
|
|
||||||
|
Rgba {
|
||||||
|
r,
|
||||||
|
g,
|
||||||
|
b,
|
||||||
|
a: color.a,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&'_ str> for Rgba {
|
||||||
|
type Error = anyhow::Error;
|
||||||
|
|
||||||
|
fn try_from(value: &'_ str) -> Result<Self, Self::Error> {
|
||||||
|
const RGB: usize = "rgb".len();
|
||||||
|
const RGBA: usize = "rgba".len();
|
||||||
|
const RRGGBB: usize = "rrggbb".len();
|
||||||
|
const RRGGBBAA: usize = "rrggbbaa".len();
|
||||||
|
|
||||||
|
const EXPECTED_FORMATS: &str = "Expected #rgb, #rgba, #rrggbb, or #rrggbbaa";
|
||||||
|
|
||||||
|
let Some(("", hex)) = value.trim().split_once('#') else {
|
||||||
|
bail!("invalid RGBA hex color: '{value}'. {EXPECTED_FORMATS}");
|
||||||
|
};
|
||||||
|
|
||||||
|
let (r, g, b, a) = match hex.len() {
|
||||||
|
RGB | RGBA => {
|
||||||
|
let r = u8::from_str_radix(&hex[0..1], 16)?;
|
||||||
|
let g = u8::from_str_radix(&hex[1..2], 16)?;
|
||||||
|
let b = u8::from_str_radix(&hex[2..3], 16)?;
|
||||||
|
let a = if hex.len() == RGBA {
|
||||||
|
u8::from_str_radix(&hex[3..4], 16)?
|
||||||
|
} else {
|
||||||
|
0xf
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Duplicates a given hex digit.
|
||||||
|
/// E.g., `0xf` -> `0xff`.
|
||||||
|
const fn duplicate(value: u8) -> u8 {
|
||||||
|
value << 4 | value
|
||||||
|
}
|
||||||
|
|
||||||
|
(duplicate(r), duplicate(g), duplicate(b), duplicate(a))
|
||||||
|
}
|
||||||
|
RRGGBB | RRGGBBAA => {
|
||||||
|
let r = u8::from_str_radix(&hex[0..2], 16)?;
|
||||||
|
let g = u8::from_str_radix(&hex[2..4], 16)?;
|
||||||
|
let b = u8::from_str_radix(&hex[4..6], 16)?;
|
||||||
|
let a = if hex.len() == RRGGBBAA {
|
||||||
|
u8::from_str_radix(&hex[6..8], 16)?
|
||||||
|
} else {
|
||||||
|
0xff
|
||||||
|
};
|
||||||
|
(r, g, b, a)
|
||||||
|
}
|
||||||
|
_ => bail!("invalid RGBA hex color: '{value}'. {EXPECTED_FORMATS}"),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Rgba {
|
||||||
|
r: r as f32 / 255.,
|
||||||
|
g: g as f32 / 255.,
|
||||||
|
b: b as f32 / 255.,
|
||||||
|
a: a as f32 / 255.,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An HSLA color
|
||||||
|
#[derive(Default, Copy, Clone, Debug)]
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct Hsla {
|
||||||
|
/// Hue, in a range from 0 to 1
|
||||||
|
pub h: f32,
|
||||||
|
|
||||||
|
/// Saturation, in a range from 0 to 1
|
||||||
|
pub s: f32,
|
||||||
|
|
||||||
|
/// Lightness, in a range from 0 to 1
|
||||||
|
pub l: f32,
|
||||||
|
|
||||||
|
/// Alpha, in a range from 0 to 1
|
||||||
|
pub a: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for Hsla {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.h
|
||||||
|
.total_cmp(&other.h)
|
||||||
|
.then(self.s.total_cmp(&other.s))
|
||||||
|
.then(self.l.total_cmp(&other.l).then(self.a.total_cmp(&other.a)))
|
||||||
|
.is_eq()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for Hsla {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for Hsla {
|
||||||
|
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||||
|
self.h
|
||||||
|
.total_cmp(&other.h)
|
||||||
|
.then(self.s.total_cmp(&other.s))
|
||||||
|
.then(self.l.total_cmp(&other.l).then(self.a.total_cmp(&other.a)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for Hsla {}
|
||||||
|
|
||||||
|
/// Construct an [`Hsla`] object from plain values
|
||||||
|
pub fn hsla(h: f32, s: f32, l: f32, a: f32) -> Hsla {
|
||||||
|
Hsla {
|
||||||
|
h: h.clamp(0., 1.),
|
||||||
|
s: s.clamp(0., 1.),
|
||||||
|
l: l.clamp(0., 1.),
|
||||||
|
a: a.clamp(0., 1.),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pure black in [`Hsla`]
|
||||||
|
pub fn black() -> Hsla {
|
||||||
|
Hsla {
|
||||||
|
h: 0.,
|
||||||
|
s: 0.,
|
||||||
|
l: 0.,
|
||||||
|
a: 1.,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transparent black in [`Hsla`]
|
||||||
|
pub fn transparent_black() -> Hsla {
|
||||||
|
Hsla {
|
||||||
|
h: 0.,
|
||||||
|
s: 0.,
|
||||||
|
l: 0.,
|
||||||
|
a: 0.,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Opaque grey in [`Hsla`], values will be clamped to the range [0, 1]
|
||||||
|
pub fn opaque_grey(lightness: f32, opacity: f32) -> Hsla {
|
||||||
|
Hsla {
|
||||||
|
h: 0.,
|
||||||
|
s: 0.,
|
||||||
|
l: lightness.clamp(0., 1.),
|
||||||
|
a: opacity.clamp(0., 1.),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pure white in [`Hsla`]
|
||||||
|
pub fn white() -> Hsla {
|
||||||
|
Hsla {
|
||||||
|
h: 0.,
|
||||||
|
s: 0.,
|
||||||
|
l: 1.,
|
||||||
|
a: 1.,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The color red in [`Hsla`]
|
||||||
|
pub fn red() -> Hsla {
|
||||||
|
Hsla {
|
||||||
|
h: 0.,
|
||||||
|
s: 1.,
|
||||||
|
l: 0.5,
|
||||||
|
a: 1.,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The color blue in [`Hsla`]
|
||||||
|
pub fn blue() -> Hsla {
|
||||||
|
Hsla {
|
||||||
|
h: 0.6,
|
||||||
|
s: 1.,
|
||||||
|
l: 0.5,
|
||||||
|
a: 1.,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The color green in [`Hsla`]
|
||||||
|
pub fn green() -> Hsla {
|
||||||
|
Hsla {
|
||||||
|
h: 0.33,
|
||||||
|
s: 1.,
|
||||||
|
l: 0.5,
|
||||||
|
a: 1.,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The color yellow in [`Hsla`]
|
||||||
|
pub fn yellow() -> Hsla {
|
||||||
|
Hsla {
|
||||||
|
h: 0.16,
|
||||||
|
s: 1.,
|
||||||
|
l: 0.5,
|
||||||
|
a: 1.,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hsla {
|
||||||
|
/// Converts this HSLA color to an RGBA color.
|
||||||
|
pub fn to_rgb(self) -> Rgba {
|
||||||
|
self.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The color red
|
||||||
|
pub fn red() -> Self {
|
||||||
|
red()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The color green
|
||||||
|
pub fn green() -> Self {
|
||||||
|
green()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The color blue
|
||||||
|
pub fn blue() -> Self {
|
||||||
|
blue()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The color black
|
||||||
|
pub fn black() -> Self {
|
||||||
|
black()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The color white
|
||||||
|
pub fn white() -> Self {
|
||||||
|
white()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The color transparent black
|
||||||
|
pub fn transparent_black() -> Self {
|
||||||
|
transparent_black()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the HSLA color is fully transparent, false otherwise.
|
||||||
|
pub fn is_transparent(&self) -> bool {
|
||||||
|
self.a == 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Blends `other` on top of `self` based on `other`'s alpha value. The resulting color is a combination of `self`'s and `other`'s colors.
|
||||||
|
///
|
||||||
|
/// If `other`'s alpha value is 1.0 or greater, `other` color is fully opaque, thus `other` is returned as the output color.
|
||||||
|
/// If `other`'s alpha value is 0.0 or less, `other` color is fully transparent, thus `self` is returned as the output color.
|
||||||
|
/// Else, the output color is calculated as a blend of `self` and `other` based on their weighted alpha values.
|
||||||
|
///
|
||||||
|
/// Assumptions:
|
||||||
|
/// - Alpha values are contained in the range [0, 1], with 1 as fully opaque and 0 as fully transparent.
|
||||||
|
/// - The relative contributions of `self` and `other` is based on `self`'s alpha value (`self.a`) and `other`'s alpha value (`other.a`), `self` contributing `self.a * (1.0 - other.a)` and `other` contributing its own alpha value.
|
||||||
|
/// - RGB color components are contained in the range [0, 1].
|
||||||
|
/// - If `self` and `other` colors are out of the valid range, the blend operation's output and behavior is undefined.
|
||||||
|
pub fn blend(self, other: Hsla) -> Hsla {
|
||||||
|
let alpha = other.a;
|
||||||
|
|
||||||
|
if alpha >= 1.0 {
|
||||||
|
other
|
||||||
|
} else if alpha <= 0.0 {
|
||||||
|
return self;
|
||||||
|
} else {
|
||||||
|
let converted_self = Rgba::from(self);
|
||||||
|
let converted_other = Rgba::from(other);
|
||||||
|
let blended_rgb = converted_self.blend(converted_other);
|
||||||
|
return Hsla::from(blended_rgb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new HSLA color with the same hue, and lightness, but with no saturation.
|
||||||
|
pub fn grayscale(&self) -> Self {
|
||||||
|
Hsla {
|
||||||
|
h: self.h,
|
||||||
|
s: 0.,
|
||||||
|
l: self.l,
|
||||||
|
a: self.a,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fade out the color by a given factor. This factor should be between 0.0 and 1.0.
|
||||||
|
/// Where 0.0 will leave the color unchanged, and 1.0 will completely fade out the color.
|
||||||
|
pub fn fade_out(&mut self, factor: f32) {
|
||||||
|
self.a *= 1.0 - factor.clamp(0., 1.);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Rgba> for Hsla {
|
||||||
|
fn from(color: Rgba) -> Self {
|
||||||
|
let r = color.r;
|
||||||
|
let g = color.g;
|
||||||
|
let b = color.b;
|
||||||
|
|
||||||
|
let max = r.max(g.max(b));
|
||||||
|
let min = r.min(g.min(b));
|
||||||
|
let delta = max - min;
|
||||||
|
|
||||||
|
let l = (max + min) / 2.0;
|
||||||
|
let s = if l == 0.0 || l == 1.0 {
|
||||||
|
0.0
|
||||||
|
} else if l < 0.5 {
|
||||||
|
delta / (2.0 * l)
|
||||||
|
} else {
|
||||||
|
delta / (2.0 - 2.0 * l)
|
||||||
|
};
|
||||||
|
|
||||||
|
let h = if delta == 0.0 {
|
||||||
|
0.0
|
||||||
|
} else if max == r {
|
||||||
|
((g - b) / delta).rem_euclid(6.0) / 6.0
|
||||||
|
} else if max == g {
|
||||||
|
((b - r) / delta + 2.0) / 6.0
|
||||||
|
} else {
|
||||||
|
((r - g) / delta + 4.0) / 6.0
|
||||||
|
};
|
||||||
|
|
||||||
|
Hsla {
|
||||||
|
h,
|
||||||
|
s,
|
||||||
|
l,
|
||||||
|
a: color.a,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for Hsla {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
// First, deserialize it into Rgba
|
||||||
|
let rgba = Rgba::deserialize(deserializer)?;
|
||||||
|
|
||||||
|
// Then, use the From<Rgba> for Hsla implementation to convert it
|
||||||
|
Ok(Hsla::from(rgba))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deserialize_three_value_hex_to_rgba() {
|
||||||
|
let actual: Rgba = serde_json::from_value(json!("#f09")).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(actual, rgba(0xff0099ff))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deserialize_four_value_hex_to_rgba() {
|
||||||
|
let actual: Rgba = serde_json::from_value(json!("#f09f")).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(actual, rgba(0xff0099ff))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deserialize_six_value_hex_to_rgba() {
|
||||||
|
let actual: Rgba = serde_json::from_value(json!("#ff0099")).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(actual, rgba(0xff0099ff))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deserialize_eight_value_hex_to_rgba() {
|
||||||
|
let actual: Rgba = serde_json::from_value(json!("#ff0099ff")).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(actual, rgba(0xff0099ff))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deserialize_eight_value_hex_with_padding_to_rgba() {
|
||||||
|
let actual: Rgba = serde_json::from_value(json!(" #f5f5f5ff ")).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(actual, rgba(0xf5f5f5ff))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deserialize_eight_value_hex_with_mixed_case_to_rgba() {
|
||||||
|
let actual: Rgba = serde_json::from_value(json!("#DeAdbEeF")).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(actual, rgba(0xdeadbeef))
|
||||||
|
}
|
||||||
|
}
|
627
crates/ming/src/element.rs
Normal file
627
crates/ming/src/element.rs
Normal file
|
@ -0,0 +1,627 @@
|
||||||
|
//! Elements are the workhorses of GPUI. They are responsible for laying out and painting all of
|
||||||
|
//! the contents of a window. Elements form a tree and are laid out according to the web layout
|
||||||
|
//! standards as implemented by [taffy](https://github.com/DioxusLabs/taffy). Most of the time,
|
||||||
|
//! you won't need to interact with this module or these APIs directly. Elements provide their
|
||||||
|
//! own APIs and GPUI, or other element implementation, uses the APIs in this module to convert
|
||||||
|
//! that element tree into the pixels you see on the screen.
|
||||||
|
//!
|
||||||
|
//! # Element Basics
|
||||||
|
//!
|
||||||
|
//! Elements are constructed by calling [`Render::render()`] on the root view of the window, which
|
||||||
|
//! which recursively constructs the element tree from the current state of the application,.
|
||||||
|
//! These elements are then laid out by Taffy, and painted to the screen according to their own
|
||||||
|
//! implementation of [`Element::paint()`]. Before the start of the next frame, the entire element
|
||||||
|
//! tree and any callbacks they have registered with GPUI are dropped and the process repeats.
|
||||||
|
//!
|
||||||
|
//! But some state is too simple and voluminous to store in every view that needs it, e.g.
|
||||||
|
//! whether a hover has been started or not. For this, GPUI provides the [`Element::State`], associated type.
|
||||||
|
//!
|
||||||
|
//! # Implementing your own elements
|
||||||
|
//!
|
||||||
|
//! Elements are intended to be the low level, imperative API to GPUI. They are responsible for upholding,
|
||||||
|
//! or breaking, GPUI's features as they deem necessary. As an example, most GPUI elements are expected
|
||||||
|
//! to stay in the bounds that their parent element gives them. But with [`WindowContext::break_content_mask`],
|
||||||
|
//! you can ignore this restriction and paint anywhere inside of the window's bounds. This is useful for overlays
|
||||||
|
//! and popups and anything else that shows up 'on top' of other elements.
|
||||||
|
//! With great power, comes great responsibility.
|
||||||
|
//!
|
||||||
|
//! However, most of the time, you won't need to implement your own elements. GPUI provides a number of
|
||||||
|
//! elements that should cover most common use cases out of the box and it's recommended that you use those
|
||||||
|
//! to construct `components`, using the [`RenderOnce`] trait and the `#[derive(IntoElement)]` macro. Only implement
|
||||||
|
//! elements when you need to take manual control of the layout and painting process, such as when using
|
||||||
|
//! your own custom layout algorithm or rendering a code editor.
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
util::FluentBuilder, ArenaBox, AvailableSpace, Bounds, DispatchNodeId, ElementId, LayoutId,
|
||||||
|
Pixels, Point, Size, Style, ViewContext, WindowContext, ELEMENT_ARENA,
|
||||||
|
};
|
||||||
|
use derive_more::{Deref, DerefMut};
|
||||||
|
pub(crate) use smallvec::SmallVec;
|
||||||
|
use std::{any::Any, fmt::Debug, mem};
|
||||||
|
|
||||||
|
/// Implemented by types that participate in laying out and painting the contents of a window.
|
||||||
|
/// Elements form a tree and are laid out according to web-based layout rules, as implemented by Taffy.
|
||||||
|
/// You can create custom elements by implementing this trait, see the module-level documentation
|
||||||
|
/// for more details.
|
||||||
|
pub trait Element: 'static + IntoElement {
|
||||||
|
/// The type of state returned from [`Element::request_layout`]. A mutable reference to this state is subsequently
|
||||||
|
/// provided to [`Element::prepaint`] and [`Element::paint`].
|
||||||
|
type RequestLayoutState: 'static;
|
||||||
|
|
||||||
|
/// The type of state returned from [`Element::prepaint`]. A mutable reference to this state is subsequently
|
||||||
|
/// provided to [`Element::paint`].
|
||||||
|
type PrepaintState: 'static;
|
||||||
|
|
||||||
|
/// If this element has a unique identifier, return it here. This is used to track elements across frames, and
|
||||||
|
/// will cause a GlobalElementId to be passed to the request_layout, prepaint, and paint methods.
|
||||||
|
///
|
||||||
|
/// The global id can in turn be used to access state that's connected to an element with the same id across
|
||||||
|
/// frames. This id must be unique among children of the first containing element with an id.
|
||||||
|
fn id(&self) -> Option<ElementId>;
|
||||||
|
|
||||||
|
/// Before an element can be painted, we need to know where it's going to be and how big it is.
|
||||||
|
/// Use this method to request a layout from Taffy and initialize the element's state.
|
||||||
|
fn request_layout(
|
||||||
|
&mut self,
|
||||||
|
id: Option<&GlobalElementId>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> (LayoutId, Self::RequestLayoutState);
|
||||||
|
|
||||||
|
/// After laying out an element, we need to commit its bounds to the current frame for hitbox
|
||||||
|
/// purposes. The state argument is the same state that was returned from [`Element::request_layout()`].
|
||||||
|
fn prepaint(
|
||||||
|
&mut self,
|
||||||
|
id: Option<&GlobalElementId>,
|
||||||
|
bounds: Bounds<Pixels>,
|
||||||
|
request_layout: &mut Self::RequestLayoutState,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> Self::PrepaintState;
|
||||||
|
|
||||||
|
/// Once layout has been completed, this method will be called to paint the element to the screen.
|
||||||
|
/// The state argument is the same state that was returned from [`Element::request_layout()`].
|
||||||
|
fn paint(
|
||||||
|
&mut self,
|
||||||
|
id: Option<&GlobalElementId>,
|
||||||
|
bounds: Bounds<Pixels>,
|
||||||
|
request_layout: &mut Self::RequestLayoutState,
|
||||||
|
prepaint: &mut Self::PrepaintState,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Convert this element into a dynamically-typed [`AnyElement`].
|
||||||
|
fn into_any(self) -> AnyElement {
|
||||||
|
AnyElement::new(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implemented by any type that can be converted into an element.
|
||||||
|
pub trait IntoElement: Sized {
|
||||||
|
/// The specific type of element into which the implementing type is converted.
|
||||||
|
/// Useful for converting other types into elements automatically, like Strings
|
||||||
|
type Element: Element;
|
||||||
|
|
||||||
|
/// Convert self into a type that implements [`Element`].
|
||||||
|
fn into_element(self) -> Self::Element;
|
||||||
|
|
||||||
|
/// Convert self into a dynamically-typed [`AnyElement`].
|
||||||
|
fn into_any_element(self) -> AnyElement {
|
||||||
|
self.into_element().into_any()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: IntoElement> FluentBuilder for T {}
|
||||||
|
|
||||||
|
/// An object that can be drawn to the screen. This is the trait that distinguishes `Views` from
|
||||||
|
/// models. Views are drawn to the screen and care about the current window's state, models are not and do not.
|
||||||
|
pub trait Render: 'static + Sized {
|
||||||
|
/// Render this view into an element tree.
|
||||||
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for Empty {
|
||||||
|
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
|
Empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// You can derive [`IntoElement`] on any type that implements this trait.
|
||||||
|
/// It is used to construct reusable `components` out of plain data. Think of
|
||||||
|
/// components as a recipe for a certain pattern of elements. RenderOnce allows
|
||||||
|
/// you to invoke this pattern, without breaking the fluent builder pattern of
|
||||||
|
/// the element APIs.
|
||||||
|
pub trait RenderOnce: 'static {
|
||||||
|
/// Render this component into an element tree. Note that this method
|
||||||
|
/// takes ownership of self, as compared to [`Render::render()`] method
|
||||||
|
/// which takes a mutable reference.
|
||||||
|
fn render(self, cx: &mut WindowContext) -> impl IntoElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This is a helper trait to provide a uniform interface for constructing elements that
|
||||||
|
/// can accept any number of any kind of child elements
|
||||||
|
pub trait ParentElement {
|
||||||
|
/// Extend this element's children with the given child elements.
|
||||||
|
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>);
|
||||||
|
|
||||||
|
/// Add a single child element to this element.
|
||||||
|
fn child(mut self, child: impl IntoElement) -> Self
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
self.extend(std::iter::once(child.into_element().into_any()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add multiple child elements to this element.
|
||||||
|
fn children(mut self, children: impl IntoIterator<Item = impl IntoElement>) -> Self
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
self.extend(children.into_iter().map(|child| child.into_any_element()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An element for rendering components. An implementation detail of the [`IntoElement`] derive macro
|
||||||
|
/// for [`RenderOnce`]
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub struct Component<C: RenderOnce>(Option<C>);
|
||||||
|
|
||||||
|
impl<C: RenderOnce> Component<C> {
|
||||||
|
/// Create a new component from the given RenderOnce type.
|
||||||
|
pub fn new(component: C) -> Self {
|
||||||
|
Component(Some(component))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C: RenderOnce> Element for Component<C> {
|
||||||
|
type RequestLayoutState = AnyElement;
|
||||||
|
type PrepaintState = ();
|
||||||
|
|
||||||
|
fn id(&self) -> Option<ElementId> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_layout(
|
||||||
|
&mut self,
|
||||||
|
_id: Option<&GlobalElementId>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> (LayoutId, Self::RequestLayoutState) {
|
||||||
|
let mut element = self.0.take().unwrap().render(cx).into_any_element();
|
||||||
|
let layout_id = element.request_layout(cx);
|
||||||
|
(layout_id, element)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepaint(
|
||||||
|
&mut self,
|
||||||
|
_id: Option<&GlobalElementId>,
|
||||||
|
_: Bounds<Pixels>,
|
||||||
|
element: &mut AnyElement,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) {
|
||||||
|
element.prepaint(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(
|
||||||
|
&mut self,
|
||||||
|
_id: Option<&GlobalElementId>,
|
||||||
|
_: Bounds<Pixels>,
|
||||||
|
element: &mut Self::RequestLayoutState,
|
||||||
|
_: &mut Self::PrepaintState,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) {
|
||||||
|
element.paint(cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C: RenderOnce> IntoElement for Component<C> {
|
||||||
|
type Element = Self;
|
||||||
|
|
||||||
|
fn into_element(self) -> Self::Element {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A globally unique identifier for an element, used to track state across frames.
|
||||||
|
#[derive(Deref, DerefMut, Default, Debug, Eq, PartialEq, Hash)]
|
||||||
|
pub struct GlobalElementId(pub(crate) SmallVec<[ElementId; 32]>);
|
||||||
|
|
||||||
|
trait ElementObject {
|
||||||
|
fn inner_element(&mut self) -> &mut dyn Any;
|
||||||
|
|
||||||
|
fn request_layout(&mut self, cx: &mut WindowContext) -> LayoutId;
|
||||||
|
|
||||||
|
fn prepaint(&mut self, cx: &mut WindowContext);
|
||||||
|
|
||||||
|
fn paint(&mut self, cx: &mut WindowContext);
|
||||||
|
|
||||||
|
fn layout_as_root(
|
||||||
|
&mut self,
|
||||||
|
available_space: Size<AvailableSpace>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> Size<Pixels>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A wrapper around an implementer of [`Element`] that allows it to be drawn in a window.
|
||||||
|
pub struct Drawable<E: Element> {
|
||||||
|
/// The drawn element.
|
||||||
|
pub element: E,
|
||||||
|
phase: ElementDrawPhase<E::RequestLayoutState, E::PrepaintState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
enum ElementDrawPhase<RequestLayoutState, PrepaintState> {
|
||||||
|
#[default]
|
||||||
|
Start,
|
||||||
|
RequestLayout {
|
||||||
|
layout_id: LayoutId,
|
||||||
|
global_id: Option<GlobalElementId>,
|
||||||
|
request_layout: RequestLayoutState,
|
||||||
|
},
|
||||||
|
LayoutComputed {
|
||||||
|
layout_id: LayoutId,
|
||||||
|
global_id: Option<GlobalElementId>,
|
||||||
|
available_space: Size<AvailableSpace>,
|
||||||
|
request_layout: RequestLayoutState,
|
||||||
|
},
|
||||||
|
Prepaint {
|
||||||
|
node_id: DispatchNodeId,
|
||||||
|
global_id: Option<GlobalElementId>,
|
||||||
|
bounds: Bounds<Pixels>,
|
||||||
|
request_layout: RequestLayoutState,
|
||||||
|
prepaint: PrepaintState,
|
||||||
|
},
|
||||||
|
Painted,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A wrapper around an implementer of [`Element`] that allows it to be drawn in a window.
|
||||||
|
impl<E: Element> Drawable<E> {
|
||||||
|
pub(crate) fn new(element: E) -> Self {
|
||||||
|
Drawable {
|
||||||
|
element,
|
||||||
|
phase: ElementDrawPhase::Start,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_layout(&mut self, cx: &mut WindowContext) -> LayoutId {
|
||||||
|
match mem::take(&mut self.phase) {
|
||||||
|
ElementDrawPhase::Start => {
|
||||||
|
let global_id = self.element.id().map(|element_id| {
|
||||||
|
cx.window.element_id_stack.push(element_id);
|
||||||
|
GlobalElementId(cx.window.element_id_stack.clone())
|
||||||
|
});
|
||||||
|
|
||||||
|
let (layout_id, request_layout) =
|
||||||
|
self.element.request_layout(global_id.as_ref(), cx);
|
||||||
|
|
||||||
|
if global_id.is_some() {
|
||||||
|
cx.window.element_id_stack.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.phase = ElementDrawPhase::RequestLayout {
|
||||||
|
layout_id,
|
||||||
|
global_id,
|
||||||
|
request_layout,
|
||||||
|
};
|
||||||
|
layout_id
|
||||||
|
}
|
||||||
|
_ => panic!("must call request_layout only once"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn prepaint(&mut self, cx: &mut WindowContext) {
|
||||||
|
match mem::take(&mut self.phase) {
|
||||||
|
ElementDrawPhase::RequestLayout {
|
||||||
|
layout_id,
|
||||||
|
global_id,
|
||||||
|
mut request_layout,
|
||||||
|
}
|
||||||
|
| ElementDrawPhase::LayoutComputed {
|
||||||
|
layout_id,
|
||||||
|
global_id,
|
||||||
|
mut request_layout,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
if let Some(element_id) = self.element.id() {
|
||||||
|
cx.window.element_id_stack.push(element_id);
|
||||||
|
debug_assert_eq!(global_id.as_ref().unwrap().0, cx.window.element_id_stack);
|
||||||
|
}
|
||||||
|
|
||||||
|
let bounds = cx.layout_bounds(layout_id);
|
||||||
|
let node_id = cx.window.next_frame.dispatch_tree.push_node();
|
||||||
|
let prepaint =
|
||||||
|
self.element
|
||||||
|
.prepaint(global_id.as_ref(), bounds, &mut request_layout, cx);
|
||||||
|
cx.window.next_frame.dispatch_tree.pop_node();
|
||||||
|
|
||||||
|
if global_id.is_some() {
|
||||||
|
cx.window.element_id_stack.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.phase = ElementDrawPhase::Prepaint {
|
||||||
|
node_id,
|
||||||
|
global_id,
|
||||||
|
bounds,
|
||||||
|
request_layout,
|
||||||
|
prepaint,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
_ => panic!("must call request_layout before prepaint"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn paint(
|
||||||
|
&mut self,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> (E::RequestLayoutState, E::PrepaintState) {
|
||||||
|
match mem::take(&mut self.phase) {
|
||||||
|
ElementDrawPhase::Prepaint {
|
||||||
|
node_id,
|
||||||
|
global_id,
|
||||||
|
bounds,
|
||||||
|
mut request_layout,
|
||||||
|
mut prepaint,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
if let Some(element_id) = self.element.id() {
|
||||||
|
cx.window.element_id_stack.push(element_id);
|
||||||
|
debug_assert_eq!(global_id.as_ref().unwrap().0, cx.window.element_id_stack);
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.window.next_frame.dispatch_tree.set_active_node(node_id);
|
||||||
|
self.element.paint(
|
||||||
|
global_id.as_ref(),
|
||||||
|
bounds,
|
||||||
|
&mut request_layout,
|
||||||
|
&mut prepaint,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
if global_id.is_some() {
|
||||||
|
cx.window.element_id_stack.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.phase = ElementDrawPhase::Painted;
|
||||||
|
(request_layout, prepaint)
|
||||||
|
}
|
||||||
|
_ => panic!("must call prepaint before paint"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn layout_as_root(
|
||||||
|
&mut self,
|
||||||
|
available_space: Size<AvailableSpace>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> Size<Pixels> {
|
||||||
|
if matches!(&self.phase, ElementDrawPhase::Start) {
|
||||||
|
self.request_layout(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
let layout_id = match mem::take(&mut self.phase) {
|
||||||
|
ElementDrawPhase::RequestLayout {
|
||||||
|
layout_id,
|
||||||
|
global_id,
|
||||||
|
request_layout,
|
||||||
|
} => {
|
||||||
|
cx.compute_layout(layout_id, available_space);
|
||||||
|
self.phase = ElementDrawPhase::LayoutComputed {
|
||||||
|
layout_id,
|
||||||
|
global_id,
|
||||||
|
available_space,
|
||||||
|
request_layout,
|
||||||
|
};
|
||||||
|
layout_id
|
||||||
|
}
|
||||||
|
ElementDrawPhase::LayoutComputed {
|
||||||
|
layout_id,
|
||||||
|
global_id,
|
||||||
|
available_space: prev_available_space,
|
||||||
|
request_layout,
|
||||||
|
} => {
|
||||||
|
if available_space != prev_available_space {
|
||||||
|
cx.compute_layout(layout_id, available_space);
|
||||||
|
}
|
||||||
|
self.phase = ElementDrawPhase::LayoutComputed {
|
||||||
|
layout_id,
|
||||||
|
global_id,
|
||||||
|
available_space,
|
||||||
|
request_layout,
|
||||||
|
};
|
||||||
|
layout_id
|
||||||
|
}
|
||||||
|
_ => panic!("cannot measure after painting"),
|
||||||
|
};
|
||||||
|
|
||||||
|
cx.layout_bounds(layout_id).size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E> ElementObject for Drawable<E>
|
||||||
|
where
|
||||||
|
E: Element,
|
||||||
|
E::RequestLayoutState: 'static,
|
||||||
|
{
|
||||||
|
fn inner_element(&mut self) -> &mut dyn Any {
|
||||||
|
&mut self.element
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_layout(&mut self, cx: &mut WindowContext) -> LayoutId {
|
||||||
|
Drawable::request_layout(self, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepaint(&mut self, cx: &mut WindowContext) {
|
||||||
|
Drawable::prepaint(self, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(&mut self, cx: &mut WindowContext) {
|
||||||
|
Drawable::paint(self, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout_as_root(
|
||||||
|
&mut self,
|
||||||
|
available_space: Size<AvailableSpace>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> Size<Pixels> {
|
||||||
|
Drawable::layout_as_root(self, available_space, cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A dynamically typed element that can be used to store any element type.
|
||||||
|
pub struct AnyElement(ArenaBox<dyn ElementObject>);
|
||||||
|
|
||||||
|
impl AnyElement {
|
||||||
|
pub(crate) fn new<E>(element: E) -> Self
|
||||||
|
where
|
||||||
|
E: 'static + Element,
|
||||||
|
E::RequestLayoutState: Any,
|
||||||
|
{
|
||||||
|
let element = ELEMENT_ARENA
|
||||||
|
.with_borrow_mut(|arena| arena.alloc(|| Drawable::new(element)))
|
||||||
|
.map(|element| element as &mut dyn ElementObject);
|
||||||
|
AnyElement(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempt to downcast a reference to the boxed element to a specific type.
|
||||||
|
pub fn downcast_mut<T: 'static>(&mut self) -> Option<&mut T> {
|
||||||
|
self.0.inner_element().downcast_mut::<T>()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request the layout ID of the element stored in this `AnyElement`.
|
||||||
|
/// Used for laying out child elements in a parent element.
|
||||||
|
pub fn request_layout(&mut self, cx: &mut WindowContext) -> LayoutId {
|
||||||
|
self.0.request_layout(cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prepares the element to be painted by storing its bounds, giving it a chance to draw hitboxes and
|
||||||
|
/// request autoscroll before the final paint pass is confirmed.
|
||||||
|
pub fn prepaint(&mut self, cx: &mut WindowContext) {
|
||||||
|
self.0.prepaint(cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Paints the element stored in this `AnyElement`.
|
||||||
|
pub fn paint(&mut self, cx: &mut WindowContext) {
|
||||||
|
self.0.paint(cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Performs layout for this element within the given available space and returns its size.
|
||||||
|
pub fn layout_as_root(
|
||||||
|
&mut self,
|
||||||
|
available_space: Size<AvailableSpace>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> Size<Pixels> {
|
||||||
|
self.0.layout_as_root(available_space, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prepaints this element at the given absolute origin.
|
||||||
|
pub fn prepaint_at(&mut self, origin: Point<Pixels>, cx: &mut WindowContext) {
|
||||||
|
cx.with_absolute_element_offset(origin, |cx| self.0.prepaint(cx));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Performs layout on this element in the available space, then prepaints it at the given absolute origin.
|
||||||
|
pub fn prepaint_as_root(
|
||||||
|
&mut self,
|
||||||
|
origin: Point<Pixels>,
|
||||||
|
available_space: Size<AvailableSpace>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) {
|
||||||
|
self.layout_as_root(available_space, cx);
|
||||||
|
cx.with_absolute_element_offset(origin, |cx| self.0.prepaint(cx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element for AnyElement {
|
||||||
|
type RequestLayoutState = ();
|
||||||
|
type PrepaintState = ();
|
||||||
|
|
||||||
|
fn id(&self) -> Option<ElementId> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_layout(
|
||||||
|
&mut self,
|
||||||
|
_: Option<&GlobalElementId>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> (LayoutId, Self::RequestLayoutState) {
|
||||||
|
let layout_id = self.request_layout(cx);
|
||||||
|
(layout_id, ())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepaint(
|
||||||
|
&mut self,
|
||||||
|
_: Option<&GlobalElementId>,
|
||||||
|
_: Bounds<Pixels>,
|
||||||
|
_: &mut Self::RequestLayoutState,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) {
|
||||||
|
self.prepaint(cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(
|
||||||
|
&mut self,
|
||||||
|
_: Option<&GlobalElementId>,
|
||||||
|
_: Bounds<Pixels>,
|
||||||
|
_: &mut Self::RequestLayoutState,
|
||||||
|
_: &mut Self::PrepaintState,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) {
|
||||||
|
self.paint(cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoElement for AnyElement {
|
||||||
|
type Element = Self;
|
||||||
|
|
||||||
|
fn into_element(self) -> Self::Element {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_any_element(self) -> AnyElement {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The empty element, which renders nothing.
|
||||||
|
pub struct Empty;
|
||||||
|
|
||||||
|
impl IntoElement for Empty {
|
||||||
|
type Element = Self;
|
||||||
|
|
||||||
|
fn into_element(self) -> Self::Element {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element for Empty {
|
||||||
|
type RequestLayoutState = ();
|
||||||
|
type PrepaintState = ();
|
||||||
|
|
||||||
|
fn id(&self) -> Option<ElementId> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_layout(
|
||||||
|
&mut self,
|
||||||
|
_id: Option<&GlobalElementId>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> (LayoutId, Self::RequestLayoutState) {
|
||||||
|
(cx.request_layout(Style::default(), None), ())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepaint(
|
||||||
|
&mut self,
|
||||||
|
_id: Option<&GlobalElementId>,
|
||||||
|
_bounds: Bounds<Pixels>,
|
||||||
|
_state: &mut Self::RequestLayoutState,
|
||||||
|
_cx: &mut WindowContext,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(
|
||||||
|
&mut self,
|
||||||
|
_id: Option<&GlobalElementId>,
|
||||||
|
_bounds: Bounds<Pixels>,
|
||||||
|
_request_layout: &mut Self::RequestLayoutState,
|
||||||
|
_prepaint: &mut Self::PrepaintState,
|
||||||
|
_cx: &mut WindowContext,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
312
crates/ming/src/elements/anchored.rs
Normal file
312
crates/ming/src/elements/anchored.rs
Normal file
|
@ -0,0 +1,312 @@
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
use taffy::style::{Display, Position};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
point, AnyElement, Bounds, Element, GlobalElementId, IntoElement, LayoutId, ParentElement,
|
||||||
|
Pixels, Point, Size, Style, WindowContext,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The state that the anchored element element uses to track its children.
|
||||||
|
pub struct AnchoredState {
|
||||||
|
child_layout_ids: SmallVec<[LayoutId; 4]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An anchored element that can be used to display UI that
|
||||||
|
/// will avoid overflowing the window bounds.
|
||||||
|
pub struct Anchored {
|
||||||
|
children: SmallVec<[AnyElement; 2]>,
|
||||||
|
anchor_corner: AnchorCorner,
|
||||||
|
fit_mode: AnchoredFitMode,
|
||||||
|
anchor_position: Option<Point<Pixels>>,
|
||||||
|
position_mode: AnchoredPositionMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// anchored gives you an element that will avoid overflowing the window bounds.
|
||||||
|
/// Its children should have no margin to avoid measurement issues.
|
||||||
|
pub fn anchored() -> Anchored {
|
||||||
|
Anchored {
|
||||||
|
children: SmallVec::new(),
|
||||||
|
anchor_corner: AnchorCorner::TopLeft,
|
||||||
|
fit_mode: AnchoredFitMode::SwitchAnchor,
|
||||||
|
anchor_position: None,
|
||||||
|
position_mode: AnchoredPositionMode::Window,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Anchored {
|
||||||
|
/// Sets which corner of the anchored element should be anchored to the current position.
|
||||||
|
pub fn anchor(mut self, anchor: AnchorCorner) -> Self {
|
||||||
|
self.anchor_corner = anchor;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the position in window coordinates
|
||||||
|
/// (otherwise the location the anchored element is rendered is used)
|
||||||
|
pub fn position(mut self, anchor: Point<Pixels>) -> Self {
|
||||||
|
self.anchor_position = Some(anchor);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the position mode for this anchored element. Local will have this
|
||||||
|
/// interpret its [`Anchored::position`] as relative to the parent element.
|
||||||
|
/// While Window will have it interpret the position as relative to the window.
|
||||||
|
pub fn position_mode(mut self, mode: AnchoredPositionMode) -> Self {
|
||||||
|
self.position_mode = mode;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Snap to window edge instead of switching anchor corner when an overflow would occur.
|
||||||
|
pub fn snap_to_window(mut self) -> Self {
|
||||||
|
self.fit_mode = AnchoredFitMode::SnapToWindow;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParentElement for Anchored {
|
||||||
|
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||||
|
self.children.extend(elements)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element for Anchored {
|
||||||
|
type RequestLayoutState = AnchoredState;
|
||||||
|
type PrepaintState = ();
|
||||||
|
|
||||||
|
fn id(&self) -> Option<crate::ElementId> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_layout(
|
||||||
|
&mut self,
|
||||||
|
_id: Option<&GlobalElementId>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> (crate::LayoutId, Self::RequestLayoutState) {
|
||||||
|
let child_layout_ids = self
|
||||||
|
.children
|
||||||
|
.iter_mut()
|
||||||
|
.map(|child| child.request_layout(cx))
|
||||||
|
.collect::<SmallVec<_>>();
|
||||||
|
|
||||||
|
let anchored_style = Style {
|
||||||
|
position: Position::Absolute,
|
||||||
|
display: Display::Flex,
|
||||||
|
..Style::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let layout_id = cx.request_layout(anchored_style, child_layout_ids.iter().copied());
|
||||||
|
|
||||||
|
(layout_id, AnchoredState { child_layout_ids })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepaint(
|
||||||
|
&mut self,
|
||||||
|
_id: Option<&GlobalElementId>,
|
||||||
|
bounds: Bounds<Pixels>,
|
||||||
|
request_layout: &mut Self::RequestLayoutState,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) {
|
||||||
|
if request_layout.child_layout_ids.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut child_min = point(Pixels::MAX, Pixels::MAX);
|
||||||
|
let mut child_max = Point::default();
|
||||||
|
for child_layout_id in &request_layout.child_layout_ids {
|
||||||
|
let child_bounds = cx.layout_bounds(*child_layout_id);
|
||||||
|
child_min = child_min.min(&child_bounds.origin);
|
||||||
|
child_max = child_max.max(&child_bounds.lower_right());
|
||||||
|
}
|
||||||
|
let size: Size<Pixels> = (child_max - child_min).into();
|
||||||
|
|
||||||
|
let (origin, mut desired) = self.position_mode.get_position_and_bounds(
|
||||||
|
self.anchor_position,
|
||||||
|
self.anchor_corner,
|
||||||
|
size,
|
||||||
|
bounds,
|
||||||
|
);
|
||||||
|
|
||||||
|
let limits = Bounds {
|
||||||
|
origin: Point::default(),
|
||||||
|
size: cx.viewport_size(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.fit_mode == AnchoredFitMode::SwitchAnchor {
|
||||||
|
let mut anchor_corner = self.anchor_corner;
|
||||||
|
|
||||||
|
if desired.left() < limits.left() || desired.right() > limits.right() {
|
||||||
|
let switched = anchor_corner
|
||||||
|
.switch_axis(Axis::Horizontal)
|
||||||
|
.get_bounds(origin, size);
|
||||||
|
if !(switched.left() < limits.left() || switched.right() > limits.right()) {
|
||||||
|
anchor_corner = anchor_corner.switch_axis(Axis::Horizontal);
|
||||||
|
desired = switched
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if desired.top() < limits.top() || desired.bottom() > limits.bottom() {
|
||||||
|
let switched = anchor_corner
|
||||||
|
.switch_axis(Axis::Vertical)
|
||||||
|
.get_bounds(origin, size);
|
||||||
|
if !(switched.top() < limits.top() || switched.bottom() > limits.bottom()) {
|
||||||
|
desired = switched;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snap the horizontal edges of the anchored element to the horizontal edges of the window if
|
||||||
|
// its horizontal bounds overflow, aligning to the left if it is wider than the limits.
|
||||||
|
if desired.right() > limits.right() {
|
||||||
|
desired.origin.x -= desired.right() - limits.right();
|
||||||
|
}
|
||||||
|
if desired.left() < limits.left() {
|
||||||
|
desired.origin.x = limits.origin.x;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snap the vertical edges of the anchored element to the vertical edges of the window if
|
||||||
|
// its vertical bounds overflow, aligning to the top if it is taller than the limits.
|
||||||
|
if desired.bottom() > limits.bottom() {
|
||||||
|
desired.origin.y -= desired.bottom() - limits.bottom();
|
||||||
|
}
|
||||||
|
if desired.top() < limits.top() {
|
||||||
|
desired.origin.y = limits.origin.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
let offset = desired.origin - bounds.origin;
|
||||||
|
let offset = point(offset.x.round(), offset.y.round());
|
||||||
|
|
||||||
|
cx.with_element_offset(offset, |cx| {
|
||||||
|
for child in &mut self.children {
|
||||||
|
child.prepaint(cx);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(
|
||||||
|
&mut self,
|
||||||
|
_id: Option<&GlobalElementId>,
|
||||||
|
_bounds: crate::Bounds<crate::Pixels>,
|
||||||
|
_request_layout: &mut Self::RequestLayoutState,
|
||||||
|
_prepaint: &mut Self::PrepaintState,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) {
|
||||||
|
for child in &mut self.children {
|
||||||
|
child.paint(cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoElement for Anchored {
|
||||||
|
type Element = Self;
|
||||||
|
|
||||||
|
fn into_element(self) -> Self::Element {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Axis {
|
||||||
|
Horizontal,
|
||||||
|
Vertical,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Which algorithm to use when fitting the anchored element to be inside the window.
|
||||||
|
#[derive(Copy, Clone, PartialEq)]
|
||||||
|
pub enum AnchoredFitMode {
|
||||||
|
/// Snap the anchored element to the window edge
|
||||||
|
SnapToWindow,
|
||||||
|
/// Switch which corner anchor this anchored element is attached to
|
||||||
|
SwitchAnchor,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Which algorithm to use when positioning the anchored element.
|
||||||
|
#[derive(Copy, Clone, PartialEq)]
|
||||||
|
pub enum AnchoredPositionMode {
|
||||||
|
/// Position the anchored element relative to the window
|
||||||
|
Window,
|
||||||
|
/// Position the anchored element relative to its parent
|
||||||
|
Local,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AnchoredPositionMode {
|
||||||
|
fn get_position_and_bounds(
|
||||||
|
&self,
|
||||||
|
anchor_position: Option<Point<Pixels>>,
|
||||||
|
anchor_corner: AnchorCorner,
|
||||||
|
size: Size<Pixels>,
|
||||||
|
bounds: Bounds<Pixels>,
|
||||||
|
) -> (Point<Pixels>, Bounds<Pixels>) {
|
||||||
|
match self {
|
||||||
|
AnchoredPositionMode::Window => {
|
||||||
|
let anchor_position = anchor_position.unwrap_or(bounds.origin);
|
||||||
|
let bounds = anchor_corner.get_bounds(anchor_position, size);
|
||||||
|
(anchor_position, bounds)
|
||||||
|
}
|
||||||
|
AnchoredPositionMode::Local => {
|
||||||
|
let anchor_position = anchor_position.unwrap_or_default();
|
||||||
|
let bounds = anchor_corner.get_bounds(bounds.origin + anchor_position, size);
|
||||||
|
(anchor_position, bounds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Which corner of the anchored element should be considered the anchor.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum AnchorCorner {
|
||||||
|
/// The top left corner
|
||||||
|
TopLeft,
|
||||||
|
/// The top right corner
|
||||||
|
TopRight,
|
||||||
|
/// The bottom left corner
|
||||||
|
BottomLeft,
|
||||||
|
/// The bottom right corner
|
||||||
|
BottomRight,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AnchorCorner {
|
||||||
|
fn get_bounds(&self, origin: Point<Pixels>, size: Size<Pixels>) -> Bounds<Pixels> {
|
||||||
|
let origin = match self {
|
||||||
|
Self::TopLeft => origin,
|
||||||
|
Self::TopRight => Point {
|
||||||
|
x: origin.x - size.width,
|
||||||
|
y: origin.y,
|
||||||
|
},
|
||||||
|
Self::BottomLeft => Point {
|
||||||
|
x: origin.x,
|
||||||
|
y: origin.y - size.height,
|
||||||
|
},
|
||||||
|
Self::BottomRight => Point {
|
||||||
|
x: origin.x - size.width,
|
||||||
|
y: origin.y - size.height,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Bounds { origin, size }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the point corresponding to this anchor corner in `bounds`.
|
||||||
|
pub fn corner(&self, bounds: Bounds<Pixels>) -> Point<Pixels> {
|
||||||
|
match self {
|
||||||
|
Self::TopLeft => bounds.origin,
|
||||||
|
Self::TopRight => bounds.upper_right(),
|
||||||
|
Self::BottomLeft => bounds.lower_left(),
|
||||||
|
Self::BottomRight => bounds.lower_right(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn switch_axis(self, axis: Axis) -> Self {
|
||||||
|
match axis {
|
||||||
|
Axis::Vertical => match self {
|
||||||
|
AnchorCorner::TopLeft => AnchorCorner::BottomLeft,
|
||||||
|
AnchorCorner::TopRight => AnchorCorner::BottomRight,
|
||||||
|
AnchorCorner::BottomLeft => AnchorCorner::TopLeft,
|
||||||
|
AnchorCorner::BottomRight => AnchorCorner::TopRight,
|
||||||
|
},
|
||||||
|
Axis::Horizontal => match self {
|
||||||
|
AnchorCorner::TopLeft => AnchorCorner::TopRight,
|
||||||
|
AnchorCorner::TopRight => AnchorCorner::TopLeft,
|
||||||
|
AnchorCorner::BottomLeft => AnchorCorner::BottomRight,
|
||||||
|
AnchorCorner::BottomRight => AnchorCorner::BottomLeft,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
203
crates/ming/src/elements/animation.rs
Normal file
203
crates/ming/src/elements/animation.rs
Normal file
|
@ -0,0 +1,203 @@
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use crate::{AnyElement, Element, ElementId, GlobalElementId, IntoElement};
|
||||||
|
|
||||||
|
pub use easing::*;
|
||||||
|
|
||||||
|
/// An animation that can be applied to an element.
|
||||||
|
pub struct Animation {
|
||||||
|
/// The amount of time for which this animation should run
|
||||||
|
pub duration: Duration,
|
||||||
|
/// Whether to repeat this animation when it finishes
|
||||||
|
pub oneshot: bool,
|
||||||
|
/// A function that takes a delta between 0 and 1 and returns a new delta
|
||||||
|
/// between 0 and 1 based on the given easing function.
|
||||||
|
pub easing: Box<dyn Fn(f32) -> f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Animation {
|
||||||
|
/// Create a new animation with the given duration.
|
||||||
|
/// By default the animation will only run once and will use a linear easing function.
|
||||||
|
pub fn new(duration: Duration) -> Self {
|
||||||
|
Self {
|
||||||
|
duration,
|
||||||
|
oneshot: true,
|
||||||
|
easing: Box::new(linear),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the animation to loop when it finishes.
|
||||||
|
pub fn repeat(mut self) -> Self {
|
||||||
|
self.oneshot = false;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the easing function to use for this animation.
|
||||||
|
/// The easing function will take a time delta between 0 and 1 and return a new delta
|
||||||
|
/// between 0 and 1
|
||||||
|
pub fn with_easing(mut self, easing: impl Fn(f32) -> f32 + 'static) -> Self {
|
||||||
|
self.easing = Box::new(easing);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An extension trait for adding the animation wrapper to both Elements and Components
|
||||||
|
pub trait AnimationExt {
|
||||||
|
/// Render this component or element with an animation
|
||||||
|
fn with_animation(
|
||||||
|
self,
|
||||||
|
id: impl Into<ElementId>,
|
||||||
|
animation: Animation,
|
||||||
|
animator: impl Fn(Self, f32) -> Self + 'static,
|
||||||
|
) -> AnimationElement<Self>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
AnimationElement {
|
||||||
|
id: id.into(),
|
||||||
|
element: Some(self),
|
||||||
|
animator: Box::new(animator),
|
||||||
|
animation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E> AnimationExt for E {}
|
||||||
|
|
||||||
|
/// A GPUI element that applies an animation to another element
|
||||||
|
pub struct AnimationElement<E> {
|
||||||
|
id: ElementId,
|
||||||
|
element: Option<E>,
|
||||||
|
animation: Animation,
|
||||||
|
animator: Box<dyn Fn(E, f32) -> E + 'static>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E> AnimationElement<E> {
|
||||||
|
/// Returns a new [`AnimationElement<E>`] after applying the given function
|
||||||
|
/// to the element being animated.
|
||||||
|
pub fn map_element(mut self, f: impl FnOnce(E) -> E) -> AnimationElement<E> {
|
||||||
|
self.element = self.element.map(f);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E: IntoElement + 'static> IntoElement for AnimationElement<E> {
|
||||||
|
type Element = AnimationElement<E>;
|
||||||
|
|
||||||
|
fn into_element(self) -> Self::Element {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AnimationState {
|
||||||
|
start: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E: IntoElement + 'static> Element for AnimationElement<E> {
|
||||||
|
type RequestLayoutState = AnyElement;
|
||||||
|
type PrepaintState = ();
|
||||||
|
|
||||||
|
fn id(&self) -> Option<ElementId> {
|
||||||
|
Some(self.id.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_layout(
|
||||||
|
&mut self,
|
||||||
|
global_id: Option<&GlobalElementId>,
|
||||||
|
cx: &mut crate::WindowContext,
|
||||||
|
) -> (crate::LayoutId, Self::RequestLayoutState) {
|
||||||
|
cx.with_element_state(global_id.unwrap(), |state, cx| {
|
||||||
|
let state = state.unwrap_or_else(|| AnimationState {
|
||||||
|
start: Instant::now(),
|
||||||
|
});
|
||||||
|
let mut delta =
|
||||||
|
state.start.elapsed().as_secs_f32() / self.animation.duration.as_secs_f32();
|
||||||
|
|
||||||
|
let mut done = false;
|
||||||
|
if delta > 1.0 {
|
||||||
|
if self.animation.oneshot {
|
||||||
|
done = true;
|
||||||
|
delta = 1.0;
|
||||||
|
} else {
|
||||||
|
delta = delta % 1.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let delta = (self.animation.easing)(delta);
|
||||||
|
|
||||||
|
debug_assert!(
|
||||||
|
delta >= 0.0 && delta <= 1.0,
|
||||||
|
"delta should always be between 0 and 1"
|
||||||
|
);
|
||||||
|
|
||||||
|
let element = self.element.take().expect("should only be called once");
|
||||||
|
let mut element = (self.animator)(element, delta).into_any_element();
|
||||||
|
|
||||||
|
if !done {
|
||||||
|
let parent_id = cx.parent_view_id();
|
||||||
|
cx.on_next_frame(move |cx| {
|
||||||
|
if let Some(parent_id) = parent_id {
|
||||||
|
cx.notify(parent_id)
|
||||||
|
} else {
|
||||||
|
cx.refresh()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
((element.request_layout(cx), element), state)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepaint(
|
||||||
|
&mut self,
|
||||||
|
_id: Option<&GlobalElementId>,
|
||||||
|
_bounds: crate::Bounds<crate::Pixels>,
|
||||||
|
element: &mut Self::RequestLayoutState,
|
||||||
|
cx: &mut crate::WindowContext,
|
||||||
|
) -> Self::PrepaintState {
|
||||||
|
element.prepaint(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(
|
||||||
|
&mut self,
|
||||||
|
_id: Option<&GlobalElementId>,
|
||||||
|
_bounds: crate::Bounds<crate::Pixels>,
|
||||||
|
element: &mut Self::RequestLayoutState,
|
||||||
|
_: &mut Self::PrepaintState,
|
||||||
|
cx: &mut crate::WindowContext,
|
||||||
|
) {
|
||||||
|
element.paint(cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod easing {
|
||||||
|
/// The linear easing function, or delta itself
|
||||||
|
pub fn linear(delta: f32) -> f32 {
|
||||||
|
delta
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The quadratic easing function, delta * delta
|
||||||
|
pub fn quadratic(delta: f32) -> f32 {
|
||||||
|
delta * delta
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The quadratic ease-in-out function, which starts and ends slowly but speeds up in the middle
|
||||||
|
pub fn ease_in_out(delta: f32) -> f32 {
|
||||||
|
if delta < 0.5 {
|
||||||
|
2.0 * delta * delta
|
||||||
|
} else {
|
||||||
|
let x = -2.0 * delta + 2.0;
|
||||||
|
1.0 - x * x / 2.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply the given easing function, first in the forward direction and then in the reverse direction
|
||||||
|
pub fn bounce(easing: impl Fn(f32) -> f32) -> impl Fn(f32) -> f32 {
|
||||||
|
move |delta| {
|
||||||
|
if delta < 0.5 {
|
||||||
|
easing(delta * 2.0)
|
||||||
|
} else {
|
||||||
|
easing((1.0 - delta) * 2.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
85
crates/ming/src/elements/canvas.rs
Normal file
85
crates/ming/src/elements/canvas.rs
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
use refineable::Refineable as _;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
Bounds, Element, ElementId, GlobalElementId, IntoElement, Pixels, Style, StyleRefinement,
|
||||||
|
Styled, WindowContext,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Construct a canvas element with the given paint callback.
|
||||||
|
/// Useful for adding short term custom drawing to a view.
|
||||||
|
pub fn canvas<T>(
|
||||||
|
prepaint: impl 'static + FnOnce(Bounds<Pixels>, &mut WindowContext) -> T,
|
||||||
|
paint: impl 'static + FnOnce(Bounds<Pixels>, T, &mut WindowContext),
|
||||||
|
) -> Canvas<T> {
|
||||||
|
Canvas {
|
||||||
|
prepaint: Some(Box::new(prepaint)),
|
||||||
|
paint: Some(Box::new(paint)),
|
||||||
|
style: StyleRefinement::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A canvas element, meant for accessing the low level paint API without defining a whole
|
||||||
|
/// custom element
|
||||||
|
pub struct Canvas<T> {
|
||||||
|
prepaint: Option<Box<dyn FnOnce(Bounds<Pixels>, &mut WindowContext) -> T>>,
|
||||||
|
paint: Option<Box<dyn FnOnce(Bounds<Pixels>, T, &mut WindowContext)>>,
|
||||||
|
style: StyleRefinement,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: 'static> IntoElement for Canvas<T> {
|
||||||
|
type Element = Self;
|
||||||
|
|
||||||
|
fn into_element(self) -> Self::Element {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: 'static> Element for Canvas<T> {
|
||||||
|
type RequestLayoutState = Style;
|
||||||
|
type PrepaintState = Option<T>;
|
||||||
|
|
||||||
|
fn id(&self) -> Option<ElementId> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_layout(
|
||||||
|
&mut self,
|
||||||
|
_id: Option<&GlobalElementId>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> (crate::LayoutId, Self::RequestLayoutState) {
|
||||||
|
let mut style = Style::default();
|
||||||
|
style.refine(&self.style);
|
||||||
|
let layout_id = cx.request_layout(style.clone(), []);
|
||||||
|
(layout_id, style)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepaint(
|
||||||
|
&mut self,
|
||||||
|
_id: Option<&GlobalElementId>,
|
||||||
|
bounds: Bounds<Pixels>,
|
||||||
|
_request_layout: &mut Style,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> Option<T> {
|
||||||
|
Some(self.prepaint.take().unwrap()(bounds, cx))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(
|
||||||
|
&mut self,
|
||||||
|
_id: Option<&GlobalElementId>,
|
||||||
|
bounds: Bounds<Pixels>,
|
||||||
|
style: &mut Style,
|
||||||
|
prepaint: &mut Self::PrepaintState,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) {
|
||||||
|
let prepaint = prepaint.take().unwrap();
|
||||||
|
style.paint(bounds, cx, |cx| {
|
||||||
|
(self.paint.take().unwrap())(bounds, prepaint, cx)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Styled for Canvas<T> {
|
||||||
|
fn style(&mut self) -> &mut crate::StyleRefinement {
|
||||||
|
&mut self.style
|
||||||
|
}
|
||||||
|
}
|
85
crates/ming/src/elements/deferred.rs
Normal file
85
crates/ming/src/elements/deferred.rs
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
use crate::{
|
||||||
|
AnyElement, Bounds, Element, GlobalElementId, IntoElement, LayoutId, Pixels, WindowContext,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Builds a `Deferred` element, which delays the layout and paint of its child.
|
||||||
|
pub fn deferred(child: impl IntoElement) -> Deferred {
|
||||||
|
Deferred {
|
||||||
|
child: Some(child.into_any_element()),
|
||||||
|
priority: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An element which delays the painting of its child until after all of
|
||||||
|
/// its ancestors, while keeping its layout as part of the current element tree.
|
||||||
|
pub struct Deferred {
|
||||||
|
child: Option<AnyElement>,
|
||||||
|
priority: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deferred {
|
||||||
|
/// Sets the `priority` value of the `deferred` element, which
|
||||||
|
/// determines the drawing order relative to other deferred elements,
|
||||||
|
/// with higher values being drawn on top.
|
||||||
|
pub fn with_priority(mut self, priority: usize) -> Self {
|
||||||
|
self.priority = priority;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element for Deferred {
|
||||||
|
type RequestLayoutState = ();
|
||||||
|
type PrepaintState = ();
|
||||||
|
|
||||||
|
fn id(&self) -> Option<crate::ElementId> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_layout(
|
||||||
|
&mut self,
|
||||||
|
_id: Option<&GlobalElementId>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> (LayoutId, ()) {
|
||||||
|
let layout_id = self.child.as_mut().unwrap().request_layout(cx);
|
||||||
|
(layout_id, ())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepaint(
|
||||||
|
&mut self,
|
||||||
|
_id: Option<&GlobalElementId>,
|
||||||
|
_bounds: Bounds<Pixels>,
|
||||||
|
_request_layout: &mut Self::RequestLayoutState,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) {
|
||||||
|
let child = self.child.take().unwrap();
|
||||||
|
let element_offset = cx.element_offset();
|
||||||
|
cx.defer_draw(child, element_offset, self.priority)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(
|
||||||
|
&mut self,
|
||||||
|
_id: Option<&GlobalElementId>,
|
||||||
|
_bounds: Bounds<Pixels>,
|
||||||
|
_request_layout: &mut Self::RequestLayoutState,
|
||||||
|
_prepaint: &mut Self::PrepaintState,
|
||||||
|
_cx: &mut WindowContext,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoElement for Deferred {
|
||||||
|
type Element = Self;
|
||||||
|
|
||||||
|
fn into_element(self) -> Self::Element {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deferred {
|
||||||
|
/// Sets a priority for the element. A higher priority conceptually means painting the element
|
||||||
|
/// on top of deferred draws with a lower priority (i.e. closer to the viewer).
|
||||||
|
pub fn priority(mut self, priority: usize) -> Self {
|
||||||
|
self.priority = priority;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
2546
crates/ming/src/elements/div.rs
Normal file
2546
crates/ming/src/elements/div.rs
Normal file
File diff suppressed because it is too large
Load diff
445
crates/ming/src/elements/img.rs
Normal file
445
crates/ming/src/elements/img.rs
Normal file
|
@ -0,0 +1,445 @@
|
||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
point, px, size, AbsoluteLength, Asset, Bounds, DefiniteLength, DevicePixels, Element,
|
||||||
|
ElementId, GlobalElementId, Hitbox, ImageData, InteractiveElement, Interactivity, IntoElement,
|
||||||
|
LayoutId, Length, Pixels, SharedUri, Size, StyleRefinement, Styled, SvgSize, UriOrPath,
|
||||||
|
WindowContext,
|
||||||
|
};
|
||||||
|
use futures::{AsyncReadExt, Future};
|
||||||
|
use image::{ImageBuffer, ImageError};
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
use media::core_video::CVImageBuffer;
|
||||||
|
|
||||||
|
use http;
|
||||||
|
use thiserror::Error;
|
||||||
|
use util::ResultExt;
|
||||||
|
|
||||||
|
/// A source of image content.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum ImageSource {
|
||||||
|
/// Image content will be loaded from provided URI at render time.
|
||||||
|
Uri(SharedUri),
|
||||||
|
/// Image content will be loaded from the provided file at render time.
|
||||||
|
File(Arc<PathBuf>),
|
||||||
|
/// Cached image data
|
||||||
|
Data(Arc<ImageData>),
|
||||||
|
// TODO: move surface definitions into mac platform module
|
||||||
|
/// A CoreVideo image buffer
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
Surface(CVImageBuffer),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SharedUri> for ImageSource {
|
||||||
|
fn from(value: SharedUri) -> Self {
|
||||||
|
Self::Uri(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&'static str> for ImageSource {
|
||||||
|
fn from(uri: &'static str) -> Self {
|
||||||
|
Self::Uri(uri.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for ImageSource {
|
||||||
|
fn from(uri: String) -> Self {
|
||||||
|
Self::Uri(uri.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Arc<PathBuf>> for ImageSource {
|
||||||
|
fn from(value: Arc<PathBuf>) -> Self {
|
||||||
|
Self::File(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PathBuf> for ImageSource {
|
||||||
|
fn from(value: PathBuf) -> Self {
|
||||||
|
Self::File(value.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Arc<ImageData>> for ImageSource {
|
||||||
|
fn from(value: Arc<ImageData>) -> Self {
|
||||||
|
Self::Data(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
impl From<CVImageBuffer> for ImageSource {
|
||||||
|
fn from(value: CVImageBuffer) -> Self {
|
||||||
|
Self::Surface(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An image element.
|
||||||
|
pub struct Img {
|
||||||
|
interactivity: Interactivity,
|
||||||
|
source: ImageSource,
|
||||||
|
grayscale: bool,
|
||||||
|
object_fit: ObjectFit,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new image element.
|
||||||
|
pub fn img(source: impl Into<ImageSource>) -> Img {
|
||||||
|
Img {
|
||||||
|
interactivity: Interactivity::default(),
|
||||||
|
source: source.into(),
|
||||||
|
grayscale: false,
|
||||||
|
object_fit: ObjectFit::Contain,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How to fit the image into the bounds of the element.
|
||||||
|
pub enum ObjectFit {
|
||||||
|
/// The image will be stretched to fill the bounds of the element.
|
||||||
|
Fill,
|
||||||
|
/// The image will be scaled to fit within the bounds of the element.
|
||||||
|
Contain,
|
||||||
|
/// The image will be scaled to cover the bounds of the element.
|
||||||
|
Cover,
|
||||||
|
/// The image will be scaled down to fit within the bounds of the element.
|
||||||
|
ScaleDown,
|
||||||
|
/// The image will maintain its original size.
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectFit {
|
||||||
|
/// Get the bounds of the image within the given bounds.
|
||||||
|
pub fn get_bounds(
|
||||||
|
&self,
|
||||||
|
bounds: Bounds<Pixels>,
|
||||||
|
image_size: Size<DevicePixels>,
|
||||||
|
) -> Bounds<Pixels> {
|
||||||
|
let image_size = image_size.map(|dimension| Pixels::from(u32::from(dimension)));
|
||||||
|
let image_ratio = image_size.width / image_size.height;
|
||||||
|
let bounds_ratio = bounds.size.width / bounds.size.height;
|
||||||
|
|
||||||
|
let result_bounds = match self {
|
||||||
|
ObjectFit::Fill => bounds,
|
||||||
|
ObjectFit::Contain => {
|
||||||
|
let new_size = if bounds_ratio > image_ratio {
|
||||||
|
size(
|
||||||
|
image_size.width * (bounds.size.height / image_size.height),
|
||||||
|
bounds.size.height,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
size(
|
||||||
|
bounds.size.width,
|
||||||
|
image_size.height * (bounds.size.width / image_size.width),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
Bounds {
|
||||||
|
origin: point(
|
||||||
|
bounds.origin.x + (bounds.size.width - new_size.width) / 2.0,
|
||||||
|
bounds.origin.y + (bounds.size.height - new_size.height) / 2.0,
|
||||||
|
),
|
||||||
|
size: new_size,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ObjectFit::ScaleDown => {
|
||||||
|
// Check if the image is larger than the bounds in either dimension.
|
||||||
|
if image_size.width > bounds.size.width || image_size.height > bounds.size.height {
|
||||||
|
// If the image is larger, use the same logic as Contain to scale it down.
|
||||||
|
let new_size = if bounds_ratio > image_ratio {
|
||||||
|
size(
|
||||||
|
image_size.width * (bounds.size.height / image_size.height),
|
||||||
|
bounds.size.height,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
size(
|
||||||
|
bounds.size.width,
|
||||||
|
image_size.height * (bounds.size.width / image_size.width),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
Bounds {
|
||||||
|
origin: point(
|
||||||
|
bounds.origin.x + (bounds.size.width - new_size.width) / 2.0,
|
||||||
|
bounds.origin.y + (bounds.size.height - new_size.height) / 2.0,
|
||||||
|
),
|
||||||
|
size: new_size,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If the image is smaller than or equal to the container, display it at its original size,
|
||||||
|
// centered within the container.
|
||||||
|
let original_size = size(image_size.width, image_size.height);
|
||||||
|
Bounds {
|
||||||
|
origin: point(
|
||||||
|
bounds.origin.x + (bounds.size.width - original_size.width) / 2.0,
|
||||||
|
bounds.origin.y + (bounds.size.height - original_size.height) / 2.0,
|
||||||
|
),
|
||||||
|
size: original_size,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ObjectFit::Cover => {
|
||||||
|
let new_size = if bounds_ratio > image_ratio {
|
||||||
|
size(
|
||||||
|
bounds.size.width,
|
||||||
|
image_size.height * (bounds.size.width / image_size.width),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
size(
|
||||||
|
image_size.width * (bounds.size.height / image_size.height),
|
||||||
|
bounds.size.height,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
Bounds {
|
||||||
|
origin: point(
|
||||||
|
bounds.origin.x + (bounds.size.width - new_size.width) / 2.0,
|
||||||
|
bounds.origin.y + (bounds.size.height - new_size.height) / 2.0,
|
||||||
|
),
|
||||||
|
size: new_size,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ObjectFit::None => Bounds {
|
||||||
|
origin: bounds.origin,
|
||||||
|
size: image_size,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
result_bounds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Img {
|
||||||
|
/// A list of all format extensions currently supported by this img element
|
||||||
|
pub fn extensions() -> &'static [&'static str] {
|
||||||
|
// This is the list in [image::ImageFormat::from_extension] + `svg`
|
||||||
|
&[
|
||||||
|
"avif", "jpg", "jpeg", "png", "gif", "webp", "tif", "tiff", "tga", "dds", "bmp", "ico",
|
||||||
|
"hdr", "exr", "pbm", "pam", "ppm", "pgm", "ff", "farbfeld", "qoi", "svg",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the image to be displayed in grayscale.
|
||||||
|
pub fn grayscale(mut self, grayscale: bool) -> Self {
|
||||||
|
self.grayscale = grayscale;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
/// Set the object fit for the image.
|
||||||
|
pub fn object_fit(mut self, object_fit: ObjectFit) -> Self {
|
||||||
|
self.object_fit = object_fit;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element for Img {
|
||||||
|
type RequestLayoutState = ();
|
||||||
|
type PrepaintState = Option<Hitbox>;
|
||||||
|
|
||||||
|
fn id(&self) -> Option<ElementId> {
|
||||||
|
self.interactivity.element_id.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_layout(
|
||||||
|
&mut self,
|
||||||
|
global_id: Option<&GlobalElementId>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> (LayoutId, Self::RequestLayoutState) {
|
||||||
|
let layout_id = self
|
||||||
|
.interactivity
|
||||||
|
.request_layout(global_id, cx, |mut style, cx| {
|
||||||
|
if let Some(data) = self.source.data(cx) {
|
||||||
|
let image_size = data.size();
|
||||||
|
match (style.size.width, style.size.height) {
|
||||||
|
(Length::Auto, Length::Auto) => {
|
||||||
|
style.size = Size {
|
||||||
|
width: Length::Definite(DefiniteLength::Absolute(
|
||||||
|
AbsoluteLength::Pixels(px(image_size.width.0 as f32)),
|
||||||
|
)),
|
||||||
|
height: Length::Definite(DefiniteLength::Absolute(
|
||||||
|
AbsoluteLength::Pixels(px(image_size.height.0 as f32)),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.request_layout(style, [])
|
||||||
|
});
|
||||||
|
(layout_id, ())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepaint(
|
||||||
|
&mut self,
|
||||||
|
global_id: Option<&GlobalElementId>,
|
||||||
|
bounds: Bounds<Pixels>,
|
||||||
|
_request_layout: &mut Self::RequestLayoutState,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> Option<Hitbox> {
|
||||||
|
self.interactivity
|
||||||
|
.prepaint(global_id, bounds, bounds.size, cx, |_, _, hitbox, _| hitbox)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(
|
||||||
|
&mut self,
|
||||||
|
global_id: Option<&GlobalElementId>,
|
||||||
|
bounds: Bounds<Pixels>,
|
||||||
|
_: &mut Self::RequestLayoutState,
|
||||||
|
hitbox: &mut Self::PrepaintState,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) {
|
||||||
|
let source = self.source.clone();
|
||||||
|
self.interactivity
|
||||||
|
.paint(global_id, bounds, hitbox.as_ref(), cx, |style, cx| {
|
||||||
|
let corner_radii = style.corner_radii.to_pixels(bounds.size, cx.rem_size());
|
||||||
|
|
||||||
|
if let Some(data) = source.data(cx) {
|
||||||
|
let new_bounds = self.object_fit.get_bounds(bounds, data.size());
|
||||||
|
cx.paint_image(new_bounds, corner_radii, data.clone(), self.grayscale)
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
|
|
||||||
|
match source {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
ImageSource::Surface(surface) => {
|
||||||
|
let size = size(surface.width().into(), surface.height().into());
|
||||||
|
let new_bounds = self.object_fit.get_bounds(bounds, size);
|
||||||
|
// TODO: Add support for corner_radii and grayscale.
|
||||||
|
cx.paint_surface(new_bounds, surface);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoElement for Img {
|
||||||
|
type Element = Self;
|
||||||
|
|
||||||
|
fn into_element(self) -> Self::Element {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Styled for Img {
|
||||||
|
fn style(&mut self) -> &mut StyleRefinement {
|
||||||
|
&mut self.interactivity.base_style
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InteractiveElement for Img {
|
||||||
|
fn interactivity(&mut self) -> &mut Interactivity {
|
||||||
|
&mut self.interactivity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImageSource {
|
||||||
|
fn data(&self, cx: &mut WindowContext) -> Option<Arc<ImageData>> {
|
||||||
|
match self {
|
||||||
|
ImageSource::Uri(_) | ImageSource::File(_) => {
|
||||||
|
let uri_or_path: UriOrPath = match self {
|
||||||
|
ImageSource::Uri(uri) => uri.clone().into(),
|
||||||
|
ImageSource::File(path) => path.clone().into(),
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
cx.use_cached_asset::<Image>(&uri_or_path)?.log_err()
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageSource::Data(data) => Some(data.to_owned()),
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
ImageSource::Surface(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
enum Image {}
|
||||||
|
|
||||||
|
impl Asset for Image {
|
||||||
|
type Source = UriOrPath;
|
||||||
|
type Output = Result<Arc<ImageData>, ImageCacheError>;
|
||||||
|
|
||||||
|
fn load(
|
||||||
|
source: Self::Source,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> impl Future<Output = Self::Output> + Send + 'static {
|
||||||
|
let client = cx.http_client();
|
||||||
|
let scale_factor = cx.scale_factor();
|
||||||
|
let svg_renderer = cx.svg_renderer();
|
||||||
|
async move {
|
||||||
|
let bytes = match source.clone() {
|
||||||
|
UriOrPath::Path(uri) => fs::read(uri.as_ref())?,
|
||||||
|
UriOrPath::Uri(uri) => {
|
||||||
|
let mut response = client.get(uri.as_ref(), ().into(), true).await?;
|
||||||
|
let mut body = Vec::new();
|
||||||
|
response.body_mut().read_to_end(&mut body).await?;
|
||||||
|
if !response.status().is_success() {
|
||||||
|
return Err(ImageCacheError::BadStatus {
|
||||||
|
status: response.status(),
|
||||||
|
body: String::from_utf8_lossy(&body).into_owned(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
body
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let data = if let Ok(format) = image::guess_format(&bytes) {
|
||||||
|
let data = image::load_from_memory_with_format(&bytes, format)?.into_bgra8();
|
||||||
|
ImageData::new(data)
|
||||||
|
} else {
|
||||||
|
let pixmap =
|
||||||
|
svg_renderer.render_pixmap(&bytes, SvgSize::ScaleFactor(scale_factor))?;
|
||||||
|
|
||||||
|
let buffer =
|
||||||
|
ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take()).unwrap();
|
||||||
|
|
||||||
|
ImageData::new(buffer)
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Arc::new(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An error that can occur when interacting with the image cache.
|
||||||
|
#[derive(Debug, Error, Clone)]
|
||||||
|
pub enum ImageCacheError {
|
||||||
|
/// An error that occurred while fetching an image from a remote source.
|
||||||
|
#[error("http error: {0}")]
|
||||||
|
Client(#[from] http::Error),
|
||||||
|
/// An error that occurred while reading the image from disk.
|
||||||
|
#[error("IO error: {0}")]
|
||||||
|
Io(Arc<std::io::Error>),
|
||||||
|
/// An error that occurred while processing an image.
|
||||||
|
#[error("unexpected http status: {status}, body: {body}")]
|
||||||
|
BadStatus {
|
||||||
|
/// The HTTP status code.
|
||||||
|
status: http::StatusCode,
|
||||||
|
/// The HTTP response body.
|
||||||
|
body: String,
|
||||||
|
},
|
||||||
|
/// An error that occurred while processing an image.
|
||||||
|
#[error("image error: {0}")]
|
||||||
|
Image(Arc<ImageError>),
|
||||||
|
/// An error that occurred while processing an SVG.
|
||||||
|
#[error("svg error: {0}")]
|
||||||
|
Usvg(Arc<usvg::Error>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::io::Error> for ImageCacheError {
|
||||||
|
fn from(error: std::io::Error) -> Self {
|
||||||
|
Self::Io(Arc::new(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ImageError> for ImageCacheError {
|
||||||
|
fn from(error: ImageError) -> Self {
|
||||||
|
Self::Image(Arc::new(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<usvg::Error> for ImageCacheError {
|
||||||
|
fn from(error: usvg::Error) -> Self {
|
||||||
|
Self::Usvg(Arc::new(error))
|
||||||
|
}
|
||||||
|
}
|
979
crates/ming/src/elements/list.rs
Normal file
979
crates/ming/src/elements/list.rs
Normal file
|
@ -0,0 +1,979 @@
|
||||||
|
//! A list element that can be used to render a large number of differently sized elements
|
||||||
|
//! efficiently. Clients of this API need to ensure that elements outside of the scrolled
|
||||||
|
//! area do not change their height for this element to function correctly. In order to minimize
|
||||||
|
//! re-renders, this element's state is stored intrusively on your own views, so that your code
|
||||||
|
//! can coordinate directly with the list element's cached state.
|
||||||
|
//!
|
||||||
|
//! If all of your elements are the same height, see [`UniformList`] for a simpler API
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
point, px, size, AnyElement, AvailableSpace, Bounds, ContentMask, DispatchPhase, Edges,
|
||||||
|
Element, FocusHandle, GlobalElementId, Hitbox, IntoElement, Pixels, Point, ScrollWheelEvent,
|
||||||
|
Size, Style, StyleRefinement, Styled, WindowContext,
|
||||||
|
};
|
||||||
|
use collections::VecDeque;
|
||||||
|
use refineable::Refineable as _;
|
||||||
|
use std::{cell::RefCell, ops::Range, rc::Rc};
|
||||||
|
use sum_tree::{Bias, SumTree};
|
||||||
|
use taffy::style::Overflow;
|
||||||
|
|
||||||
|
/// Construct a new list element
|
||||||
|
pub fn list(state: ListState) -> List {
|
||||||
|
List {
|
||||||
|
state,
|
||||||
|
style: StyleRefinement::default(),
|
||||||
|
sizing_behavior: ListSizingBehavior::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A list element
|
||||||
|
pub struct List {
|
||||||
|
state: ListState,
|
||||||
|
style: StyleRefinement,
|
||||||
|
sizing_behavior: ListSizingBehavior,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl List {
|
||||||
|
/// Set the sizing behavior for the list.
|
||||||
|
pub fn with_sizing_behavior(mut self, behavior: ListSizingBehavior) -> Self {
|
||||||
|
self.sizing_behavior = behavior;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list state that views must hold on behalf of the list element.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ListState(Rc<RefCell<StateInner>>);
|
||||||
|
|
||||||
|
struct StateInner {
|
||||||
|
last_layout_bounds: Option<Bounds<Pixels>>,
|
||||||
|
last_padding: Option<Edges<Pixels>>,
|
||||||
|
render_item: Box<dyn FnMut(usize, &mut WindowContext) -> AnyElement>,
|
||||||
|
items: SumTree<ListItem>,
|
||||||
|
logical_scroll_top: Option<ListOffset>,
|
||||||
|
alignment: ListAlignment,
|
||||||
|
overdraw: Pixels,
|
||||||
|
reset: bool,
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
scroll_handler: Option<Box<dyn FnMut(&ListScrollEvent, &mut WindowContext)>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the list is scrolling from top to bottom or bottom to top.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum ListAlignment {
|
||||||
|
/// The list is scrolling from top to bottom, like most lists.
|
||||||
|
Top,
|
||||||
|
/// The list is scrolling from bottom to top, like a chat log.
|
||||||
|
Bottom,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A scroll event that has been converted to be in terms of the list's items.
|
||||||
|
pub struct ListScrollEvent {
|
||||||
|
/// The range of items currently visible in the list, after applying the scroll event.
|
||||||
|
pub visible_range: Range<usize>,
|
||||||
|
|
||||||
|
/// The number of items that are currently visible in the list, after applying the scroll event.
|
||||||
|
pub count: usize,
|
||||||
|
|
||||||
|
/// Whether the list has been scrolled.
|
||||||
|
pub is_scrolled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The sizing behavior to apply during layout.
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||||
|
pub enum ListSizingBehavior {
|
||||||
|
/// The list should calculate its size based on the size of its items.
|
||||||
|
Infer,
|
||||||
|
/// The list should not calculate a fixed size.
|
||||||
|
#[default]
|
||||||
|
Auto,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LayoutItemsResponse {
|
||||||
|
max_item_width: Pixels,
|
||||||
|
scroll_top: ListOffset,
|
||||||
|
item_layouts: VecDeque<ItemLayout>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ItemLayout {
|
||||||
|
index: usize,
|
||||||
|
element: AnyElement,
|
||||||
|
size: Size<Pixels>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Frame state used by the [List] element after layout.
|
||||||
|
pub struct ListPrepaintState {
|
||||||
|
hitbox: Hitbox,
|
||||||
|
layout: LayoutItemsResponse,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
enum ListItem {
|
||||||
|
Unmeasured {
|
||||||
|
focus_handle: Option<FocusHandle>,
|
||||||
|
},
|
||||||
|
Measured {
|
||||||
|
size: Size<Pixels>,
|
||||||
|
focus_handle: Option<FocusHandle>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ListItem {
|
||||||
|
fn size(&self) -> Option<Size<Pixels>> {
|
||||||
|
if let ListItem::Measured { size, .. } = self {
|
||||||
|
Some(*size)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn focus_handle(&self) -> Option<FocusHandle> {
|
||||||
|
match self {
|
||||||
|
ListItem::Unmeasured { focus_handle } | ListItem::Measured { focus_handle, .. } => {
|
||||||
|
focus_handle.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn contains_focused(&self, cx: &WindowContext) -> bool {
|
||||||
|
match self {
|
||||||
|
ListItem::Unmeasured { focus_handle } | ListItem::Measured { focus_handle, .. } => {
|
||||||
|
focus_handle
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|handle| handle.contains_focused(cx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq)]
|
||||||
|
struct ListItemSummary {
|
||||||
|
count: usize,
|
||||||
|
rendered_count: usize,
|
||||||
|
unrendered_count: usize,
|
||||||
|
height: Pixels,
|
||||||
|
has_focus_handles: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
struct Count(usize);
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
struct RenderedCount(usize);
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
struct UnrenderedCount(usize);
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
struct Height(Pixels);
|
||||||
|
|
||||||
|
impl ListState {
|
||||||
|
/// Construct a new list state, for storage on a view.
|
||||||
|
///
|
||||||
|
/// The overdraw parameter controls how much extra space is rendered
|
||||||
|
/// above and below the visible area. Elements within this area will
|
||||||
|
/// be measured even though they are not visible. This can help ensure
|
||||||
|
/// that the list doesn't flicker or pop in when scrolling.
|
||||||
|
pub fn new<R>(
|
||||||
|
item_count: usize,
|
||||||
|
alignment: ListAlignment,
|
||||||
|
overdraw: Pixels,
|
||||||
|
render_item: R,
|
||||||
|
) -> Self
|
||||||
|
where
|
||||||
|
R: 'static + FnMut(usize, &mut WindowContext) -> AnyElement,
|
||||||
|
{
|
||||||
|
let this = Self(Rc::new(RefCell::new(StateInner {
|
||||||
|
last_layout_bounds: None,
|
||||||
|
last_padding: None,
|
||||||
|
render_item: Box::new(render_item),
|
||||||
|
items: SumTree::new(),
|
||||||
|
logical_scroll_top: None,
|
||||||
|
alignment,
|
||||||
|
overdraw,
|
||||||
|
scroll_handler: None,
|
||||||
|
reset: false,
|
||||||
|
})));
|
||||||
|
this.splice(0..0, item_count);
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset this instantiation of the list state.
|
||||||
|
///
|
||||||
|
/// Note that this will cause scroll events to be dropped until the next paint.
|
||||||
|
pub fn reset(&self, element_count: usize) {
|
||||||
|
let old_count = {
|
||||||
|
let state = &mut *self.0.borrow_mut();
|
||||||
|
state.reset = true;
|
||||||
|
state.logical_scroll_top = None;
|
||||||
|
state.items.summary().count
|
||||||
|
};
|
||||||
|
|
||||||
|
self.splice(0..old_count, element_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The number of items in this list.
|
||||||
|
pub fn item_count(&self) -> usize {
|
||||||
|
self.0.borrow().items.summary().count
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Inform the list state that the items in `old_range` have been replaced
|
||||||
|
/// by `count` new items that must be recalculated.
|
||||||
|
pub fn splice(&self, old_range: Range<usize>, count: usize) {
|
||||||
|
self.splice_focusable(old_range, (0..count).map(|_| None))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Register with the list state that the items in `old_range` have been replaced
|
||||||
|
/// by new items. As opposed to [`splice`], this method allows an iterator of optional focus handles
|
||||||
|
/// to be supplied to properly integrate with items in the list that can be focused. If a focused item
|
||||||
|
/// is scrolled out of view, the list will continue to render it to allow keyboard interaction.
|
||||||
|
pub fn splice_focusable(
|
||||||
|
&self,
|
||||||
|
old_range: Range<usize>,
|
||||||
|
focus_handles: impl IntoIterator<Item = Option<FocusHandle>>,
|
||||||
|
) {
|
||||||
|
let state = &mut *self.0.borrow_mut();
|
||||||
|
|
||||||
|
let mut old_items = state.items.cursor::<Count>();
|
||||||
|
let mut new_items = old_items.slice(&Count(old_range.start), Bias::Right, &());
|
||||||
|
old_items.seek_forward(&Count(old_range.end), Bias::Right, &());
|
||||||
|
|
||||||
|
let mut spliced_count = 0;
|
||||||
|
new_items.extend(
|
||||||
|
focus_handles.into_iter().map(|focus_handle| {
|
||||||
|
spliced_count += 1;
|
||||||
|
ListItem::Unmeasured { focus_handle }
|
||||||
|
}),
|
||||||
|
&(),
|
||||||
|
);
|
||||||
|
new_items.append(old_items.suffix(&()), &());
|
||||||
|
drop(old_items);
|
||||||
|
state.items = new_items;
|
||||||
|
|
||||||
|
if let Some(ListOffset {
|
||||||
|
item_ix,
|
||||||
|
offset_in_item,
|
||||||
|
}) = state.logical_scroll_top.as_mut()
|
||||||
|
{
|
||||||
|
if old_range.contains(item_ix) {
|
||||||
|
*item_ix = old_range.start;
|
||||||
|
*offset_in_item = px(0.);
|
||||||
|
} else if old_range.end <= *item_ix {
|
||||||
|
*item_ix = *item_ix - (old_range.end - old_range.start) + spliced_count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set a handler that will be called when the list is scrolled.
|
||||||
|
pub fn set_scroll_handler(
|
||||||
|
&self,
|
||||||
|
handler: impl FnMut(&ListScrollEvent, &mut WindowContext) + 'static,
|
||||||
|
) {
|
||||||
|
self.0.borrow_mut().scroll_handler = Some(Box::new(handler))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current scroll offset, in terms of the list's items.
|
||||||
|
pub fn logical_scroll_top(&self) -> ListOffset {
|
||||||
|
self.0.borrow().logical_scroll_top()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scroll the list to the given offset
|
||||||
|
pub fn scroll_to(&self, mut scroll_top: ListOffset) {
|
||||||
|
let state = &mut *self.0.borrow_mut();
|
||||||
|
let item_count = state.items.summary().count;
|
||||||
|
if scroll_top.item_ix >= item_count {
|
||||||
|
scroll_top.item_ix = item_count;
|
||||||
|
scroll_top.offset_in_item = px(0.);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.logical_scroll_top = Some(scroll_top);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scroll the list to the given item, such that the item is fully visible.
|
||||||
|
pub fn scroll_to_reveal_item(&self, ix: usize) {
|
||||||
|
let state = &mut *self.0.borrow_mut();
|
||||||
|
|
||||||
|
let mut scroll_top = state.logical_scroll_top();
|
||||||
|
let height = state
|
||||||
|
.last_layout_bounds
|
||||||
|
.map_or(px(0.), |bounds| bounds.size.height);
|
||||||
|
let padding = state.last_padding.unwrap_or_default();
|
||||||
|
|
||||||
|
if ix <= scroll_top.item_ix {
|
||||||
|
scroll_top.item_ix = ix;
|
||||||
|
scroll_top.offset_in_item = px(0.);
|
||||||
|
} else {
|
||||||
|
let mut cursor = state.items.cursor::<ListItemSummary>();
|
||||||
|
cursor.seek(&Count(ix + 1), Bias::Right, &());
|
||||||
|
let bottom = cursor.start().height + padding.top;
|
||||||
|
let goal_top = px(0.).max(bottom - height + padding.bottom);
|
||||||
|
|
||||||
|
cursor.seek(&Height(goal_top), Bias::Left, &());
|
||||||
|
let start_ix = cursor.start().count;
|
||||||
|
let start_item_top = cursor.start().height;
|
||||||
|
|
||||||
|
if start_ix >= scroll_top.item_ix {
|
||||||
|
scroll_top.item_ix = start_ix;
|
||||||
|
scroll_top.offset_in_item = goal_top - start_item_top;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.logical_scroll_top = Some(scroll_top);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the bounds for the given item in window coordinates, if it's
|
||||||
|
/// been rendered.
|
||||||
|
pub fn bounds_for_item(&self, ix: usize) -> Option<Bounds<Pixels>> {
|
||||||
|
let state = &*self.0.borrow();
|
||||||
|
|
||||||
|
let bounds = state.last_layout_bounds.unwrap_or_default();
|
||||||
|
let scroll_top = state.logical_scroll_top();
|
||||||
|
if ix < scroll_top.item_ix {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut cursor = state.items.cursor::<(Count, Height)>();
|
||||||
|
cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
|
||||||
|
|
||||||
|
let scroll_top = cursor.start().1 .0 + scroll_top.offset_in_item;
|
||||||
|
|
||||||
|
cursor.seek_forward(&Count(ix), Bias::Right, &());
|
||||||
|
if let Some(&ListItem::Measured { size, .. }) = cursor.item() {
|
||||||
|
let &(Count(count), Height(top)) = cursor.start();
|
||||||
|
if count == ix {
|
||||||
|
let top = bounds.top() + top - scroll_top;
|
||||||
|
return Some(Bounds::from_corners(
|
||||||
|
point(bounds.left(), top),
|
||||||
|
point(bounds.right(), top + size.height),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StateInner {
|
||||||
|
fn visible_range(&self, height: Pixels, scroll_top: &ListOffset) -> Range<usize> {
|
||||||
|
let mut cursor = self.items.cursor::<ListItemSummary>();
|
||||||
|
cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
|
||||||
|
let start_y = cursor.start().height + scroll_top.offset_in_item;
|
||||||
|
cursor.seek_forward(&Height(start_y + height), Bias::Left, &());
|
||||||
|
scroll_top.item_ix..cursor.start().count + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scroll(
|
||||||
|
&mut self,
|
||||||
|
scroll_top: &ListOffset,
|
||||||
|
height: Pixels,
|
||||||
|
delta: Point<Pixels>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) {
|
||||||
|
// Drop scroll events after a reset, since we can't calculate
|
||||||
|
// the new logical scroll top without the item heights
|
||||||
|
if self.reset {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let padding = self.last_padding.unwrap_or_default();
|
||||||
|
let scroll_max =
|
||||||
|
(self.items.summary().height + padding.top + padding.bottom - height).max(px(0.));
|
||||||
|
let new_scroll_top = (self.scroll_top(scroll_top) - delta.y)
|
||||||
|
.max(px(0.))
|
||||||
|
.min(scroll_max);
|
||||||
|
|
||||||
|
if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max {
|
||||||
|
self.logical_scroll_top = None;
|
||||||
|
} else {
|
||||||
|
let mut cursor = self.items.cursor::<ListItemSummary>();
|
||||||
|
cursor.seek(&Height(new_scroll_top), Bias::Right, &());
|
||||||
|
let item_ix = cursor.start().count;
|
||||||
|
let offset_in_item = new_scroll_top - cursor.start().height;
|
||||||
|
self.logical_scroll_top = Some(ListOffset {
|
||||||
|
item_ix,
|
||||||
|
offset_in_item,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.scroll_handler.is_some() {
|
||||||
|
let visible_range = self.visible_range(height, scroll_top);
|
||||||
|
self.scroll_handler.as_mut().unwrap()(
|
||||||
|
&ListScrollEvent {
|
||||||
|
visible_range,
|
||||||
|
count: self.items.summary().count,
|
||||||
|
is_scrolled: self.logical_scroll_top.is_some(),
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn logical_scroll_top(&self) -> ListOffset {
|
||||||
|
self.logical_scroll_top
|
||||||
|
.unwrap_or_else(|| match self.alignment {
|
||||||
|
ListAlignment::Top => ListOffset {
|
||||||
|
item_ix: 0,
|
||||||
|
offset_in_item: px(0.),
|
||||||
|
},
|
||||||
|
ListAlignment::Bottom => ListOffset {
|
||||||
|
item_ix: self.items.summary().count,
|
||||||
|
offset_in_item: px(0.),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scroll_top(&self, logical_scroll_top: &ListOffset) -> Pixels {
|
||||||
|
let mut cursor = self.items.cursor::<ListItemSummary>();
|
||||||
|
cursor.seek(&Count(logical_scroll_top.item_ix), Bias::Right, &());
|
||||||
|
cursor.start().height + logical_scroll_top.offset_in_item
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout_items(
|
||||||
|
&mut self,
|
||||||
|
available_width: Option<Pixels>,
|
||||||
|
available_height: Pixels,
|
||||||
|
padding: &Edges<Pixels>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> LayoutItemsResponse {
|
||||||
|
let old_items = self.items.clone();
|
||||||
|
let mut measured_items = VecDeque::new();
|
||||||
|
let mut item_layouts = VecDeque::new();
|
||||||
|
let mut rendered_height = padding.top;
|
||||||
|
let mut max_item_width = px(0.);
|
||||||
|
let mut scroll_top = self.logical_scroll_top();
|
||||||
|
let mut rendered_focused_item = false;
|
||||||
|
|
||||||
|
let available_item_space = size(
|
||||||
|
available_width.map_or(AvailableSpace::MinContent, |width| {
|
||||||
|
AvailableSpace::Definite(width)
|
||||||
|
}),
|
||||||
|
AvailableSpace::MinContent,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut cursor = old_items.cursor::<Count>();
|
||||||
|
|
||||||
|
// Render items after the scroll top, including those in the trailing overdraw
|
||||||
|
cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
|
||||||
|
for (ix, item) in cursor.by_ref().enumerate() {
|
||||||
|
let visible_height = rendered_height - scroll_top.offset_in_item;
|
||||||
|
if visible_height >= available_height + self.overdraw {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the previously cached height and focus handle if available
|
||||||
|
let mut size = item.size();
|
||||||
|
|
||||||
|
// If we're within the visible area or the height wasn't cached, render and measure the item's element
|
||||||
|
if visible_height < available_height || size.is_none() {
|
||||||
|
let item_index = scroll_top.item_ix + ix;
|
||||||
|
let mut element = (self.render_item)(item_index, cx);
|
||||||
|
let element_size = element.layout_as_root(available_item_space, cx);
|
||||||
|
size = Some(element_size);
|
||||||
|
if visible_height < available_height {
|
||||||
|
item_layouts.push_back(ItemLayout {
|
||||||
|
index: item_index,
|
||||||
|
element,
|
||||||
|
size: element_size,
|
||||||
|
});
|
||||||
|
if item.contains_focused(cx) {
|
||||||
|
rendered_focused_item = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let size = size.unwrap();
|
||||||
|
rendered_height += size.height;
|
||||||
|
max_item_width = max_item_width.max(size.width);
|
||||||
|
measured_items.push_back(ListItem::Measured {
|
||||||
|
size,
|
||||||
|
focus_handle: item.focus_handle(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
rendered_height += padding.bottom;
|
||||||
|
|
||||||
|
// Prepare to start walking upward from the item at the scroll top.
|
||||||
|
cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
|
||||||
|
|
||||||
|
// If the rendered items do not fill the visible region, then adjust
|
||||||
|
// the scroll top upward.
|
||||||
|
if rendered_height - scroll_top.offset_in_item < available_height {
|
||||||
|
while rendered_height < available_height {
|
||||||
|
cursor.prev(&());
|
||||||
|
if let Some(item) = cursor.item() {
|
||||||
|
let item_index = cursor.start().0;
|
||||||
|
let mut element = (self.render_item)(item_index, cx);
|
||||||
|
let element_size = element.layout_as_root(available_item_space, cx);
|
||||||
|
let focus_handle = item.focus_handle();
|
||||||
|
rendered_height += element_size.height;
|
||||||
|
measured_items.push_front(ListItem::Measured {
|
||||||
|
size: element_size,
|
||||||
|
focus_handle,
|
||||||
|
});
|
||||||
|
item_layouts.push_front(ItemLayout {
|
||||||
|
index: item_index,
|
||||||
|
element,
|
||||||
|
size: element_size,
|
||||||
|
});
|
||||||
|
if item.contains_focused(cx) {
|
||||||
|
rendered_focused_item = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scroll_top = ListOffset {
|
||||||
|
item_ix: cursor.start().0,
|
||||||
|
offset_in_item: rendered_height - available_height,
|
||||||
|
};
|
||||||
|
|
||||||
|
match self.alignment {
|
||||||
|
ListAlignment::Top => {
|
||||||
|
scroll_top.offset_in_item = scroll_top.offset_in_item.max(px(0.));
|
||||||
|
self.logical_scroll_top = Some(scroll_top);
|
||||||
|
}
|
||||||
|
ListAlignment::Bottom => {
|
||||||
|
scroll_top = ListOffset {
|
||||||
|
item_ix: cursor.start().0,
|
||||||
|
offset_in_item: rendered_height - available_height,
|
||||||
|
};
|
||||||
|
self.logical_scroll_top = None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Measure items in the leading overdraw
|
||||||
|
let mut leading_overdraw = scroll_top.offset_in_item;
|
||||||
|
while leading_overdraw < self.overdraw {
|
||||||
|
cursor.prev(&());
|
||||||
|
if let Some(item) = cursor.item() {
|
||||||
|
let size = if let ListItem::Measured { size, .. } = item {
|
||||||
|
*size
|
||||||
|
} else {
|
||||||
|
let mut element = (self.render_item)(cursor.start().0, cx);
|
||||||
|
element.layout_as_root(available_item_space, cx)
|
||||||
|
};
|
||||||
|
|
||||||
|
leading_overdraw += size.height;
|
||||||
|
measured_items.push_front(ListItem::Measured {
|
||||||
|
size,
|
||||||
|
focus_handle: item.focus_handle(),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let measured_range = cursor.start().0..(cursor.start().0 + measured_items.len());
|
||||||
|
let mut cursor = old_items.cursor::<Count>();
|
||||||
|
let mut new_items = cursor.slice(&Count(measured_range.start), Bias::Right, &());
|
||||||
|
new_items.extend(measured_items, &());
|
||||||
|
cursor.seek(&Count(measured_range.end), Bias::Right, &());
|
||||||
|
new_items.append(cursor.suffix(&()), &());
|
||||||
|
self.items = new_items;
|
||||||
|
|
||||||
|
// If none of the visible items are focused, check if an off-screen item is focused
|
||||||
|
// and include it to be rendered after the visible items so keyboard interaction continues
|
||||||
|
// to work for it.
|
||||||
|
if !rendered_focused_item {
|
||||||
|
let mut cursor = self
|
||||||
|
.items
|
||||||
|
.filter::<_, Count>(|summary| summary.has_focus_handles);
|
||||||
|
cursor.next(&());
|
||||||
|
while let Some(item) = cursor.item() {
|
||||||
|
if item.contains_focused(cx) {
|
||||||
|
let item_index = cursor.start().0;
|
||||||
|
let mut element = (self.render_item)(cursor.start().0, cx);
|
||||||
|
let size = element.layout_as_root(available_item_space, cx);
|
||||||
|
item_layouts.push_back(ItemLayout {
|
||||||
|
index: item_index,
|
||||||
|
element,
|
||||||
|
size,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
cursor.next(&());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LayoutItemsResponse {
|
||||||
|
max_item_width,
|
||||||
|
scroll_top,
|
||||||
|
item_layouts,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepaint_items(
|
||||||
|
&mut self,
|
||||||
|
bounds: Bounds<Pixels>,
|
||||||
|
padding: Edges<Pixels>,
|
||||||
|
autoscroll: bool,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> Result<LayoutItemsResponse, ListOffset> {
|
||||||
|
cx.transact(|cx| {
|
||||||
|
let mut layout_response =
|
||||||
|
self.layout_items(Some(bounds.size.width), bounds.size.height, &padding, cx);
|
||||||
|
|
||||||
|
// Avoid honoring autoscroll requests from elements other than our children.
|
||||||
|
cx.take_autoscroll();
|
||||||
|
|
||||||
|
// Only paint the visible items, if there is actually any space for them (taking padding into account)
|
||||||
|
if bounds.size.height > padding.top + padding.bottom {
|
||||||
|
let mut item_origin = bounds.origin + Point::new(px(0.), padding.top);
|
||||||
|
item_origin.y -= layout_response.scroll_top.offset_in_item;
|
||||||
|
for item in &mut layout_response.item_layouts {
|
||||||
|
cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
|
||||||
|
item.element.prepaint_at(item_origin, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(autoscroll_bounds) = cx.take_autoscroll() {
|
||||||
|
if autoscroll {
|
||||||
|
if autoscroll_bounds.top() < bounds.top() {
|
||||||
|
return Err(ListOffset {
|
||||||
|
item_ix: item.index,
|
||||||
|
offset_in_item: autoscroll_bounds.top() - item_origin.y,
|
||||||
|
});
|
||||||
|
} else if autoscroll_bounds.bottom() > bounds.bottom() {
|
||||||
|
let mut cursor = self.items.cursor::<Count>();
|
||||||
|
cursor.seek(&Count(item.index), Bias::Right, &());
|
||||||
|
let mut height = bounds.size.height - padding.top - padding.bottom;
|
||||||
|
|
||||||
|
// Account for the height of the element down until the autoscroll bottom.
|
||||||
|
height -= autoscroll_bounds.bottom() - item_origin.y;
|
||||||
|
|
||||||
|
// Keep decreasing the scroll top until we fill all the available space.
|
||||||
|
while height > Pixels::ZERO {
|
||||||
|
cursor.prev(&());
|
||||||
|
let Some(item) = cursor.item() else { break };
|
||||||
|
|
||||||
|
let size = item.size().unwrap_or_else(|| {
|
||||||
|
let mut item = (self.render_item)(cursor.start().0, cx);
|
||||||
|
let item_available_size = size(
|
||||||
|
bounds.size.width.into(),
|
||||||
|
AvailableSpace::MinContent,
|
||||||
|
);
|
||||||
|
item.layout_as_root(item_available_size, cx)
|
||||||
|
});
|
||||||
|
height -= size.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Err(ListOffset {
|
||||||
|
item_ix: cursor.start().0,
|
||||||
|
offset_in_item: if height < Pixels::ZERO {
|
||||||
|
-height
|
||||||
|
} else {
|
||||||
|
Pixels::ZERO
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item_origin.y += item.size.height;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
layout_response.item_layouts.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(layout_response)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for ListItem {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Unmeasured { .. } => write!(f, "Unrendered"),
|
||||||
|
Self::Measured { size, .. } => f.debug_struct("Rendered").field("size", size).finish(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An offset into the list's items, in terms of the item index and the number
|
||||||
|
/// of pixels off the top left of the item.
|
||||||
|
#[derive(Debug, Clone, Copy, Default)]
|
||||||
|
pub struct ListOffset {
|
||||||
|
/// The index of an item in the list
|
||||||
|
pub item_ix: usize,
|
||||||
|
/// The number of pixels to offset from the item index.
|
||||||
|
pub offset_in_item: Pixels,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element for List {
|
||||||
|
type RequestLayoutState = ();
|
||||||
|
type PrepaintState = ListPrepaintState;
|
||||||
|
|
||||||
|
fn id(&self) -> Option<crate::ElementId> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_layout(
|
||||||
|
&mut self,
|
||||||
|
_id: Option<&GlobalElementId>,
|
||||||
|
cx: &mut crate::WindowContext,
|
||||||
|
) -> (crate::LayoutId, Self::RequestLayoutState) {
|
||||||
|
let layout_id = match self.sizing_behavior {
|
||||||
|
ListSizingBehavior::Infer => {
|
||||||
|
let mut style = Style::default();
|
||||||
|
style.overflow.y = Overflow::Scroll;
|
||||||
|
style.refine(&self.style);
|
||||||
|
cx.with_text_style(style.text_style().cloned(), |cx| {
|
||||||
|
let state = &mut *self.state.0.borrow_mut();
|
||||||
|
|
||||||
|
let available_height = if let Some(last_bounds) = state.last_layout_bounds {
|
||||||
|
last_bounds.size.height
|
||||||
|
} else {
|
||||||
|
// If we don't have the last layout bounds (first render),
|
||||||
|
// we might just use the overdraw value as the available height to layout enough items.
|
||||||
|
state.overdraw
|
||||||
|
};
|
||||||
|
let padding = style.padding.to_pixels(
|
||||||
|
state.last_layout_bounds.unwrap_or_default().size.into(),
|
||||||
|
cx.rem_size(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let layout_response = state.layout_items(None, available_height, &padding, cx);
|
||||||
|
let max_element_width = layout_response.max_item_width;
|
||||||
|
|
||||||
|
let summary = state.items.summary();
|
||||||
|
let total_height = summary.height;
|
||||||
|
|
||||||
|
cx.request_measured_layout(
|
||||||
|
style,
|
||||||
|
move |known_dimensions, available_space, _cx| {
|
||||||
|
let width =
|
||||||
|
known_dimensions
|
||||||
|
.width
|
||||||
|
.unwrap_or(match available_space.width {
|
||||||
|
AvailableSpace::Definite(x) => x,
|
||||||
|
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
|
||||||
|
max_element_width
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let height = match available_space.height {
|
||||||
|
AvailableSpace::Definite(height) => total_height.min(height),
|
||||||
|
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
|
||||||
|
total_height
|
||||||
|
}
|
||||||
|
};
|
||||||
|
size(width, height)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ListSizingBehavior::Auto => {
|
||||||
|
let mut style = Style::default();
|
||||||
|
style.refine(&self.style);
|
||||||
|
cx.with_text_style(style.text_style().cloned(), |cx| {
|
||||||
|
cx.request_layout(style, None)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
(layout_id, ())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepaint(
|
||||||
|
&mut self,
|
||||||
|
_id: Option<&GlobalElementId>,
|
||||||
|
bounds: Bounds<Pixels>,
|
||||||
|
_: &mut Self::RequestLayoutState,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> ListPrepaintState {
|
||||||
|
let state = &mut *self.state.0.borrow_mut();
|
||||||
|
state.reset = false;
|
||||||
|
|
||||||
|
let mut style = Style::default();
|
||||||
|
style.refine(&self.style);
|
||||||
|
|
||||||
|
let hitbox = cx.insert_hitbox(bounds, false);
|
||||||
|
|
||||||
|
// If the width of the list has changed, invalidate all cached item heights
|
||||||
|
if state.last_layout_bounds.map_or(true, |last_bounds| {
|
||||||
|
last_bounds.size.width != bounds.size.width
|
||||||
|
}) {
|
||||||
|
let new_items = SumTree::from_iter(
|
||||||
|
state.items.iter().map(|item| ListItem::Unmeasured {
|
||||||
|
focus_handle: item.focus_handle(),
|
||||||
|
}),
|
||||||
|
&(),
|
||||||
|
);
|
||||||
|
|
||||||
|
state.items = new_items;
|
||||||
|
}
|
||||||
|
|
||||||
|
let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size());
|
||||||
|
let layout = match state.prepaint_items(bounds, padding, true, cx) {
|
||||||
|
Ok(layout) => layout,
|
||||||
|
Err(autoscroll_request) => {
|
||||||
|
state.logical_scroll_top = Some(autoscroll_request);
|
||||||
|
state.prepaint_items(bounds, padding, false, cx).unwrap()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
state.last_layout_bounds = Some(bounds);
|
||||||
|
state.last_padding = Some(padding);
|
||||||
|
ListPrepaintState { hitbox, layout }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(
|
||||||
|
&mut self,
|
||||||
|
_id: Option<&GlobalElementId>,
|
||||||
|
bounds: Bounds<crate::Pixels>,
|
||||||
|
_: &mut Self::RequestLayoutState,
|
||||||
|
prepaint: &mut Self::PrepaintState,
|
||||||
|
cx: &mut crate::WindowContext,
|
||||||
|
) {
|
||||||
|
cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
|
||||||
|
for item in &mut prepaint.layout.item_layouts {
|
||||||
|
item.element.paint(cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let list_state = self.state.clone();
|
||||||
|
let height = bounds.size.height;
|
||||||
|
let scroll_top = prepaint.layout.scroll_top;
|
||||||
|
let hitbox_id = prepaint.hitbox.id;
|
||||||
|
cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| {
|
||||||
|
if phase == DispatchPhase::Bubble && hitbox_id.is_hovered(cx) {
|
||||||
|
list_state.0.borrow_mut().scroll(
|
||||||
|
&scroll_top,
|
||||||
|
height,
|
||||||
|
event.delta.pixel_delta(px(20.)),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoElement for List {
|
||||||
|
type Element = Self;
|
||||||
|
|
||||||
|
fn into_element(self) -> Self::Element {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Styled for List {
|
||||||
|
fn style(&mut self) -> &mut StyleRefinement {
|
||||||
|
&mut self.style
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl sum_tree::Item for ListItem {
|
||||||
|
type Summary = ListItemSummary;
|
||||||
|
|
||||||
|
fn summary(&self) -> Self::Summary {
|
||||||
|
match self {
|
||||||
|
ListItem::Unmeasured { focus_handle } => ListItemSummary {
|
||||||
|
count: 1,
|
||||||
|
rendered_count: 0,
|
||||||
|
unrendered_count: 1,
|
||||||
|
height: px(0.),
|
||||||
|
has_focus_handles: focus_handle.is_some(),
|
||||||
|
},
|
||||||
|
ListItem::Measured {
|
||||||
|
size, focus_handle, ..
|
||||||
|
} => ListItemSummary {
|
||||||
|
count: 1,
|
||||||
|
rendered_count: 1,
|
||||||
|
unrendered_count: 0,
|
||||||
|
height: size.height,
|
||||||
|
has_focus_handles: focus_handle.is_some(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl sum_tree::Summary for ListItemSummary {
|
||||||
|
type Context = ();
|
||||||
|
|
||||||
|
fn add_summary(&mut self, summary: &Self, _: &()) {
|
||||||
|
self.count += summary.count;
|
||||||
|
self.rendered_count += summary.rendered_count;
|
||||||
|
self.unrendered_count += summary.unrendered_count;
|
||||||
|
self.height += summary.height;
|
||||||
|
self.has_focus_handles |= summary.has_focus_handles;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Count {
|
||||||
|
fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
|
||||||
|
self.0 += summary.count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> sum_tree::Dimension<'a, ListItemSummary> for RenderedCount {
|
||||||
|
fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
|
||||||
|
self.0 += summary.rendered_count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> sum_tree::Dimension<'a, ListItemSummary> for UnrenderedCount {
|
||||||
|
fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
|
||||||
|
self.0 += summary.unrendered_count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Height {
|
||||||
|
fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
|
||||||
|
self.0 += summary.height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Count {
|
||||||
|
fn cmp(&self, other: &ListItemSummary, _: &()) -> std::cmp::Ordering {
|
||||||
|
self.0.partial_cmp(&other.count).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Height {
|
||||||
|
fn cmp(&self, other: &ListItemSummary, _: &()) -> std::cmp::Ordering {
|
||||||
|
self.0.partial_cmp(&other.height).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
|
||||||
|
use gpui::{ScrollDelta, ScrollWheelEvent};
|
||||||
|
|
||||||
|
use crate::{self as gpui, TestAppContext};
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn test_reset_after_paint_before_scroll(cx: &mut TestAppContext) {
|
||||||
|
use crate::{div, list, point, px, size, Element, ListState, Styled};
|
||||||
|
|
||||||
|
let cx = cx.add_empty_window();
|
||||||
|
|
||||||
|
let state = ListState::new(5, crate::ListAlignment::Top, px(10.), |_, _| {
|
||||||
|
div().h(px(10.)).w_full().into_any()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure that the list is scrolled to the top
|
||||||
|
state.scroll_to(gpui::ListOffset {
|
||||||
|
item_ix: 0,
|
||||||
|
offset_in_item: px(0.0),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Paint
|
||||||
|
cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_| {
|
||||||
|
list(state.clone()).w_full().h_full()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset
|
||||||
|
state.reset(5);
|
||||||
|
|
||||||
|
// And then receive a scroll event _before_ the next paint
|
||||||
|
cx.simulate_event(ScrollWheelEvent {
|
||||||
|
position: point(px(1.), px(1.)),
|
||||||
|
delta: ScrollDelta::Pixels(point(px(0.), px(-500.))),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Scroll position should stay at the top of the list
|
||||||
|
assert_eq!(state.logical_scroll_top().item_ix, 0);
|
||||||
|
assert_eq!(state.logical_scroll_top().offset_in_item, px(0.));
|
||||||
|
}
|
||||||
|
}
|
21
crates/ming/src/elements/mod.rs
Normal file
21
crates/ming/src/elements/mod.rs
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
mod anchored;
|
||||||
|
mod animation;
|
||||||
|
mod canvas;
|
||||||
|
mod deferred;
|
||||||
|
mod div;
|
||||||
|
mod img;
|
||||||
|
mod list;
|
||||||
|
mod svg;
|
||||||
|
mod text;
|
||||||
|
mod uniform_list;
|
||||||
|
|
||||||
|
pub use anchored::*;
|
||||||
|
pub use animation::*;
|
||||||
|
pub use canvas::*;
|
||||||
|
pub use deferred::*;
|
||||||
|
pub use div::*;
|
||||||
|
pub use img::*;
|
||||||
|
pub use list::*;
|
||||||
|
pub use svg::*;
|
||||||
|
pub use text::*;
|
||||||
|
pub use uniform_list::*;
|
190
crates/ming/src/elements/svg.rs
Normal file
190
crates/ming/src/elements/svg.rs
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
use crate::{
|
||||||
|
geometry::Negate as _, point, px, radians, size, Bounds, Element, GlobalElementId, Hitbox,
|
||||||
|
InteractiveElement, Interactivity, IntoElement, LayoutId, Pixels, Point, Radians, SharedString,
|
||||||
|
Size, StyleRefinement, Styled, TransformationMatrix, WindowContext,
|
||||||
|
};
|
||||||
|
use util::ResultExt;
|
||||||
|
|
||||||
|
/// An SVG element.
|
||||||
|
pub struct Svg {
|
||||||
|
interactivity: Interactivity,
|
||||||
|
transformation: Option<Transformation>,
|
||||||
|
path: Option<SharedString>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new SVG element.
|
||||||
|
pub fn svg() -> Svg {
|
||||||
|
Svg {
|
||||||
|
interactivity: Interactivity::default(),
|
||||||
|
transformation: None,
|
||||||
|
path: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Svg {
|
||||||
|
/// Set the path to the SVG file for this element.
|
||||||
|
pub fn path(mut self, path: impl Into<SharedString>) -> Self {
|
||||||
|
self.path = Some(path.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transform the SVG element with the given transformation.
|
||||||
|
/// Note that this won't effect the hitbox or layout of the element, only the rendering.
|
||||||
|
pub fn with_transformation(mut self, transformation: Transformation) -> Self {
|
||||||
|
self.transformation = Some(transformation);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element for Svg {
|
||||||
|
type RequestLayoutState = ();
|
||||||
|
type PrepaintState = Option<Hitbox>;
|
||||||
|
|
||||||
|
fn id(&self) -> Option<crate::ElementId> {
|
||||||
|
self.interactivity.element_id.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_layout(
|
||||||
|
&mut self,
|
||||||
|
global_id: Option<&GlobalElementId>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> (LayoutId, Self::RequestLayoutState) {
|
||||||
|
let layout_id = self
|
||||||
|
.interactivity
|
||||||
|
.request_layout(global_id, cx, |style, cx| cx.request_layout(style, None));
|
||||||
|
(layout_id, ())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepaint(
|
||||||
|
&mut self,
|
||||||
|
global_id: Option<&GlobalElementId>,
|
||||||
|
bounds: Bounds<Pixels>,
|
||||||
|
_request_layout: &mut Self::RequestLayoutState,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> Option<Hitbox> {
|
||||||
|
self.interactivity
|
||||||
|
.prepaint(global_id, bounds, bounds.size, cx, |_, _, hitbox, _| hitbox)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(
|
||||||
|
&mut self,
|
||||||
|
global_id: Option<&GlobalElementId>,
|
||||||
|
bounds: Bounds<Pixels>,
|
||||||
|
_request_layout: &mut Self::RequestLayoutState,
|
||||||
|
hitbox: &mut Option<Hitbox>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
self.interactivity
|
||||||
|
.paint(global_id, bounds, hitbox.as_ref(), cx, |style, cx| {
|
||||||
|
if let Some((path, color)) = self.path.as_ref().zip(style.text.color) {
|
||||||
|
let transformation = self
|
||||||
|
.transformation
|
||||||
|
.as_ref()
|
||||||
|
.map(|transformation| {
|
||||||
|
transformation.into_matrix(bounds.center(), cx.scale_factor())
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
cx.paint_svg(bounds, path.clone(), transformation, color)
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoElement for Svg {
|
||||||
|
type Element = Self;
|
||||||
|
|
||||||
|
fn into_element(self) -> Self::Element {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Styled for Svg {
|
||||||
|
fn style(&mut self) -> &mut StyleRefinement {
|
||||||
|
&mut self.interactivity.base_style
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InteractiveElement for Svg {
|
||||||
|
fn interactivity(&mut self) -> &mut Interactivity {
|
||||||
|
&mut self.interactivity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A transformation to apply to an SVG element.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct Transformation {
|
||||||
|
scale: Size<f32>,
|
||||||
|
translate: Point<Pixels>,
|
||||||
|
rotate: Radians,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Transformation {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
scale: size(1.0, 1.0),
|
||||||
|
translate: point(px(0.0), px(0.0)),
|
||||||
|
rotate: radians(0.0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Transformation {
|
||||||
|
/// Create a new Transformation with the specified scale along each axis.
|
||||||
|
pub fn scale(scale: Size<f32>) -> Self {
|
||||||
|
Self {
|
||||||
|
scale,
|
||||||
|
translate: point(px(0.0), px(0.0)),
|
||||||
|
rotate: radians(0.0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new Transformation with the specified translation.
|
||||||
|
pub fn translate(translate: Point<Pixels>) -> Self {
|
||||||
|
Self {
|
||||||
|
scale: size(1.0, 1.0),
|
||||||
|
translate,
|
||||||
|
rotate: radians(0.0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new Transformation with the specified rotation in radians.
|
||||||
|
pub fn rotate(rotate: impl Into<Radians>) -> Self {
|
||||||
|
let rotate = rotate.into();
|
||||||
|
Self {
|
||||||
|
scale: size(1.0, 1.0),
|
||||||
|
translate: point(px(0.0), px(0.0)),
|
||||||
|
rotate,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the scaling factor of this transformation.
|
||||||
|
pub fn with_scaling(mut self, scale: Size<f32>) -> Self {
|
||||||
|
self.scale = scale;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the translation value of this transformation.
|
||||||
|
pub fn with_translation(mut self, translate: Point<Pixels>) -> Self {
|
||||||
|
self.translate = translate;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the rotation angle of this transformation.
|
||||||
|
pub fn with_rotation(mut self, rotate: impl Into<Radians>) -> Self {
|
||||||
|
self.rotate = rotate.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_matrix(self, center: Point<Pixels>, scale_factor: f32) -> TransformationMatrix {
|
||||||
|
//Note: if you read this as a sequence of matrix mulitplications, start from the bottom
|
||||||
|
TransformationMatrix::unit()
|
||||||
|
.translate(center.scale(scale_factor) + self.translate.scale(scale_factor))
|
||||||
|
.rotate(self.rotate)
|
||||||
|
.scale(self.scale)
|
||||||
|
.translate(center.scale(scale_factor).negate())
|
||||||
|
}
|
||||||
|
}
|
715
crates/ming/src/elements/text.rs
Normal file
715
crates/ming/src/elements/text.rs
Normal file
|
@ -0,0 +1,715 @@
|
||||||
|
use crate::{
|
||||||
|
ActiveTooltip, AnyTooltip, AnyView, Bounds, DispatchPhase, Element, ElementId, GlobalElementId,
|
||||||
|
HighlightStyle, Hitbox, IntoElement, LayoutId, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
|
||||||
|
Pixels, Point, SharedString, Size, TextRun, TextStyle, WhiteSpace, WindowContext, WrappedLine,
|
||||||
|
TOOLTIP_DELAY,
|
||||||
|
};
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use parking_lot::{Mutex, MutexGuard};
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
use std::{
|
||||||
|
cell::{Cell, RefCell},
|
||||||
|
mem,
|
||||||
|
ops::Range,
|
||||||
|
rc::Rc,
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
use util::ResultExt;
|
||||||
|
|
||||||
|
impl Element for &'static str {
|
||||||
|
type RequestLayoutState = TextLayout;
|
||||||
|
type PrepaintState = ();
|
||||||
|
|
||||||
|
fn id(&self) -> Option<ElementId> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_layout(
|
||||||
|
&mut self,
|
||||||
|
_id: Option<&GlobalElementId>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> (LayoutId, Self::RequestLayoutState) {
|
||||||
|
let mut state = TextLayout::default();
|
||||||
|
let layout_id = state.layout(SharedString::from(*self), None, cx);
|
||||||
|
(layout_id, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepaint(
|
||||||
|
&mut self,
|
||||||
|
_id: Option<&GlobalElementId>,
|
||||||
|
bounds: Bounds<Pixels>,
|
||||||
|
text_layout: &mut Self::RequestLayoutState,
|
||||||
|
_cx: &mut WindowContext,
|
||||||
|
) {
|
||||||
|
text_layout.prepaint(bounds, self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(
|
||||||
|
&mut self,
|
||||||
|
_id: Option<&GlobalElementId>,
|
||||||
|
_bounds: Bounds<Pixels>,
|
||||||
|
text_layout: &mut TextLayout,
|
||||||
|
_: &mut (),
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) {
|
||||||
|
text_layout.paint(self, cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoElement for &'static str {
|
||||||
|
type Element = Self;
|
||||||
|
|
||||||
|
fn into_element(self) -> Self::Element {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoElement for String {
|
||||||
|
type Element = SharedString;
|
||||||
|
|
||||||
|
fn into_element(self) -> Self::Element {
|
||||||
|
self.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element for SharedString {
|
||||||
|
type RequestLayoutState = TextLayout;
|
||||||
|
type PrepaintState = ();
|
||||||
|
|
||||||
|
fn id(&self) -> Option<ElementId> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_layout(
|
||||||
|
&mut self,
|
||||||
|
|
||||||
|
_id: Option<&GlobalElementId>,
|
||||||
|
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> (LayoutId, Self::RequestLayoutState) {
|
||||||
|
let mut state = TextLayout::default();
|
||||||
|
let layout_id = state.layout(self.clone(), None, cx);
|
||||||
|
(layout_id, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepaint(
|
||||||
|
&mut self,
|
||||||
|
_id: Option<&GlobalElementId>,
|
||||||
|
bounds: Bounds<Pixels>,
|
||||||
|
text_layout: &mut Self::RequestLayoutState,
|
||||||
|
_cx: &mut WindowContext,
|
||||||
|
) {
|
||||||
|
text_layout.prepaint(bounds, self.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(
|
||||||
|
&mut self,
|
||||||
|
_id: Option<&GlobalElementId>,
|
||||||
|
_bounds: Bounds<Pixels>,
|
||||||
|
text_layout: &mut Self::RequestLayoutState,
|
||||||
|
_: &mut Self::PrepaintState,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) {
|
||||||
|
text_layout.paint(self.as_ref(), cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoElement for SharedString {
|
||||||
|
type Element = Self;
|
||||||
|
|
||||||
|
fn into_element(self) -> Self::Element {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders text with runs of different styles.
|
||||||
|
///
|
||||||
|
/// Callers are responsible for setting the correct style for each run.
|
||||||
|
/// For text with a uniform style, you can usually avoid calling this constructor
|
||||||
|
/// and just pass text directly.
|
||||||
|
pub struct StyledText {
|
||||||
|
text: SharedString,
|
||||||
|
runs: Option<Vec<TextRun>>,
|
||||||
|
layout: TextLayout,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StyledText {
|
||||||
|
/// Construct a new styled text element from the given string.
|
||||||
|
pub fn new(text: impl Into<SharedString>) -> Self {
|
||||||
|
StyledText {
|
||||||
|
text: text.into(),
|
||||||
|
runs: None,
|
||||||
|
layout: TextLayout::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// todo!()
|
||||||
|
pub fn layout(&self) -> &TextLayout {
|
||||||
|
&self.layout
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the styling attributes for the given text, as well as
|
||||||
|
/// as any ranges of text that have had their style customized.
|
||||||
|
pub fn with_highlights(
|
||||||
|
mut self,
|
||||||
|
default_style: &TextStyle,
|
||||||
|
highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
|
||||||
|
) -> Self {
|
||||||
|
let mut runs = Vec::new();
|
||||||
|
let mut ix = 0;
|
||||||
|
for (range, highlight) in highlights {
|
||||||
|
if ix < range.start {
|
||||||
|
runs.push(default_style.clone().to_run(range.start - ix));
|
||||||
|
}
|
||||||
|
runs.push(
|
||||||
|
default_style
|
||||||
|
.clone()
|
||||||
|
.highlight(highlight)
|
||||||
|
.to_run(range.len()),
|
||||||
|
);
|
||||||
|
ix = range.end;
|
||||||
|
}
|
||||||
|
if ix < self.text.len() {
|
||||||
|
runs.push(default_style.to_run(self.text.len() - ix));
|
||||||
|
}
|
||||||
|
self.runs = Some(runs);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the text runs for this piece of text.
|
||||||
|
pub fn with_runs(mut self, runs: Vec<TextRun>) -> Self {
|
||||||
|
self.runs = Some(runs);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element for StyledText {
|
||||||
|
type RequestLayoutState = ();
|
||||||
|
type PrepaintState = ();
|
||||||
|
|
||||||
|
fn id(&self) -> Option<ElementId> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_layout(
|
||||||
|
&mut self,
|
||||||
|
|
||||||
|
_id: Option<&GlobalElementId>,
|
||||||
|
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> (LayoutId, Self::RequestLayoutState) {
|
||||||
|
let layout_id = self.layout.layout(self.text.clone(), self.runs.take(), cx);
|
||||||
|
(layout_id, ())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepaint(
|
||||||
|
&mut self,
|
||||||
|
_id: Option<&GlobalElementId>,
|
||||||
|
bounds: Bounds<Pixels>,
|
||||||
|
_: &mut Self::RequestLayoutState,
|
||||||
|
_cx: &mut WindowContext,
|
||||||
|
) {
|
||||||
|
self.layout.prepaint(bounds, &self.text)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(
|
||||||
|
&mut self,
|
||||||
|
_id: Option<&GlobalElementId>,
|
||||||
|
_bounds: Bounds<Pixels>,
|
||||||
|
_: &mut Self::RequestLayoutState,
|
||||||
|
_: &mut Self::PrepaintState,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) {
|
||||||
|
self.layout.paint(&self.text, cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoElement for StyledText {
|
||||||
|
type Element = Self;
|
||||||
|
|
||||||
|
fn into_element(self) -> Self::Element {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// todo!()
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
pub struct TextLayout(Arc<Mutex<Option<TextLayoutInner>>>);
|
||||||
|
|
||||||
|
struct TextLayoutInner {
|
||||||
|
lines: SmallVec<[WrappedLine; 1]>,
|
||||||
|
line_height: Pixels,
|
||||||
|
wrap_width: Option<Pixels>,
|
||||||
|
size: Option<Size<Pixels>>,
|
||||||
|
bounds: Option<Bounds<Pixels>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextLayout {
|
||||||
|
fn lock(&self) -> MutexGuard<Option<TextLayoutInner>> {
|
||||||
|
self.0.lock()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
text: SharedString,
|
||||||
|
runs: Option<Vec<TextRun>>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> LayoutId {
|
||||||
|
let text_style = cx.text_style();
|
||||||
|
let font_size = text_style.font_size.to_pixels(cx.rem_size());
|
||||||
|
let line_height = text_style
|
||||||
|
.line_height
|
||||||
|
.to_pixels(font_size.into(), cx.rem_size());
|
||||||
|
|
||||||
|
let runs = if let Some(runs) = runs {
|
||||||
|
runs
|
||||||
|
} else {
|
||||||
|
vec![text_style.to_run(text.len())]
|
||||||
|
};
|
||||||
|
|
||||||
|
let layout_id = cx.request_measured_layout(Default::default(), {
|
||||||
|
let element_state = self.clone();
|
||||||
|
|
||||||
|
move |known_dimensions, available_space, cx| {
|
||||||
|
let wrap_width = if text_style.white_space == WhiteSpace::Normal {
|
||||||
|
known_dimensions.width.or(match available_space.width {
|
||||||
|
crate::AvailableSpace::Definite(x) => Some(x),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(text_layout) = element_state.0.lock().as_ref() {
|
||||||
|
if text_layout.size.is_some()
|
||||||
|
&& (wrap_width.is_none() || wrap_width == text_layout.wrap_width)
|
||||||
|
{
|
||||||
|
return text_layout.size.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(lines) = cx
|
||||||
|
.text_system()
|
||||||
|
.shape_text(
|
||||||
|
text.clone(),
|
||||||
|
font_size,
|
||||||
|
&runs,
|
||||||
|
wrap_width, // Wrap if we know the width.
|
||||||
|
)
|
||||||
|
.log_err()
|
||||||
|
else {
|
||||||
|
element_state.lock().replace(TextLayoutInner {
|
||||||
|
lines: Default::default(),
|
||||||
|
line_height,
|
||||||
|
wrap_width,
|
||||||
|
size: Some(Size::default()),
|
||||||
|
bounds: None,
|
||||||
|
});
|
||||||
|
return Size::default();
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut size: Size<Pixels> = Size::default();
|
||||||
|
for line in &lines {
|
||||||
|
let line_size = line.size(line_height);
|
||||||
|
size.height += line_size.height;
|
||||||
|
size.width = size.width.max(line_size.width).ceil();
|
||||||
|
}
|
||||||
|
|
||||||
|
element_state.lock().replace(TextLayoutInner {
|
||||||
|
lines,
|
||||||
|
line_height,
|
||||||
|
wrap_width,
|
||||||
|
size: Some(size),
|
||||||
|
bounds: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
size
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
layout_id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepaint(&mut self, bounds: Bounds<Pixels>, text: &str) {
|
||||||
|
let mut element_state = self.lock();
|
||||||
|
let element_state = element_state
|
||||||
|
.as_mut()
|
||||||
|
.ok_or_else(|| anyhow!("measurement has not been performed on {}", text))
|
||||||
|
.unwrap();
|
||||||
|
element_state.bounds = Some(bounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(&mut self, text: &str, cx: &mut WindowContext) {
|
||||||
|
let element_state = self.lock();
|
||||||
|
let element_state = element_state
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| anyhow!("measurement has not been performed on {}", text))
|
||||||
|
.unwrap();
|
||||||
|
let bounds = element_state
|
||||||
|
.bounds
|
||||||
|
.ok_or_else(|| anyhow!("prepaint has not been performed on {:?}", text))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let line_height = element_state.line_height;
|
||||||
|
let mut line_origin = bounds.origin;
|
||||||
|
for line in &element_state.lines {
|
||||||
|
line.paint(line_origin, line_height, cx).log_err();
|
||||||
|
line_origin.y += line.size(line_height).height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// todo!()
|
||||||
|
pub fn index_for_position(&self, mut position: Point<Pixels>) -> Result<usize, usize> {
|
||||||
|
let element_state = self.lock();
|
||||||
|
let element_state = element_state
|
||||||
|
.as_ref()
|
||||||
|
.expect("measurement has not been performed");
|
||||||
|
let bounds = element_state
|
||||||
|
.bounds
|
||||||
|
.expect("prepaint has not been performed");
|
||||||
|
|
||||||
|
if position.y < bounds.top() {
|
||||||
|
return Err(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let line_height = element_state.line_height;
|
||||||
|
let mut line_origin = bounds.origin;
|
||||||
|
let mut line_start_ix = 0;
|
||||||
|
for line in &element_state.lines {
|
||||||
|
let line_bottom = line_origin.y + line.size(line_height).height;
|
||||||
|
if position.y > line_bottom {
|
||||||
|
line_origin.y = line_bottom;
|
||||||
|
line_start_ix += line.len() + 1;
|
||||||
|
} else {
|
||||||
|
let position_within_line = position - line_origin;
|
||||||
|
match line.index_for_position(position_within_line, line_height) {
|
||||||
|
Ok(index_within_line) => return Ok(line_start_ix + index_within_line),
|
||||||
|
Err(index_within_line) => return Err(line_start_ix + index_within_line),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(line_start_ix.saturating_sub(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// todo!()
|
||||||
|
pub fn position_for_index(&self, index: usize) -> Option<Point<Pixels>> {
|
||||||
|
let element_state = self.lock();
|
||||||
|
let element_state = element_state
|
||||||
|
.as_ref()
|
||||||
|
.expect("measurement has not been performed");
|
||||||
|
let bounds = element_state
|
||||||
|
.bounds
|
||||||
|
.expect("prepaint has not been performed");
|
||||||
|
let line_height = element_state.line_height;
|
||||||
|
|
||||||
|
let mut line_origin = bounds.origin;
|
||||||
|
let mut line_start_ix = 0;
|
||||||
|
|
||||||
|
for line in &element_state.lines {
|
||||||
|
let line_end_ix = line_start_ix + line.len();
|
||||||
|
if index < line_start_ix {
|
||||||
|
break;
|
||||||
|
} else if index > line_end_ix {
|
||||||
|
line_origin.y += line.size(line_height).height;
|
||||||
|
line_start_ix = line_end_ix + 1;
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
let ix_within_line = index - line_start_ix;
|
||||||
|
return Some(line_origin + line.position_for_index(ix_within_line, line_height)?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// todo!()
|
||||||
|
pub fn bounds(&self) -> Bounds<Pixels> {
|
||||||
|
self.0.lock().as_ref().unwrap().bounds.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// todo!()
|
||||||
|
pub fn line_height(&self) -> Pixels {
|
||||||
|
self.0.lock().as_ref().unwrap().line_height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A text element that can be interacted with.
|
||||||
|
pub struct InteractiveText {
|
||||||
|
element_id: ElementId,
|
||||||
|
text: StyledText,
|
||||||
|
click_listener:
|
||||||
|
Option<Box<dyn Fn(&[Range<usize>], InteractiveTextClickEvent, &mut WindowContext<'_>)>>,
|
||||||
|
hover_listener: Option<Box<dyn Fn(Option<usize>, MouseMoveEvent, &mut WindowContext<'_>)>>,
|
||||||
|
tooltip_builder: Option<Rc<dyn Fn(usize, &mut WindowContext<'_>) -> Option<AnyView>>>,
|
||||||
|
clickable_ranges: Vec<Range<usize>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct InteractiveTextClickEvent {
|
||||||
|
mouse_down_index: usize,
|
||||||
|
mouse_up_index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct InteractiveTextState {
|
||||||
|
mouse_down_index: Rc<Cell<Option<usize>>>,
|
||||||
|
hovered_index: Rc<Cell<Option<usize>>>,
|
||||||
|
active_tooltip: Rc<RefCell<Option<ActiveTooltip>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// InteractiveTest is a wrapper around StyledText that adds mouse interactions.
|
||||||
|
impl InteractiveText {
|
||||||
|
/// Creates a new InteractiveText from the given text.
|
||||||
|
pub fn new(id: impl Into<ElementId>, text: StyledText) -> Self {
|
||||||
|
Self {
|
||||||
|
element_id: id.into(),
|
||||||
|
text,
|
||||||
|
click_listener: None,
|
||||||
|
hover_listener: None,
|
||||||
|
tooltip_builder: None,
|
||||||
|
clickable_ranges: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// on_click is called when the user clicks on one of the given ranges, passing the index of
|
||||||
|
/// the clicked range.
|
||||||
|
pub fn on_click(
|
||||||
|
mut self,
|
||||||
|
ranges: Vec<Range<usize>>,
|
||||||
|
listener: impl Fn(usize, &mut WindowContext<'_>) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.click_listener = Some(Box::new(move |ranges, event, cx| {
|
||||||
|
for (range_ix, range) in ranges.iter().enumerate() {
|
||||||
|
if range.contains(&event.mouse_down_index) && range.contains(&event.mouse_up_index)
|
||||||
|
{
|
||||||
|
listener(range_ix, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
self.clickable_ranges = ranges;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// on_hover is called when the mouse moves over a character within the text, passing the
|
||||||
|
/// index of the hovered character, or None if the mouse leaves the text.
|
||||||
|
pub fn on_hover(
|
||||||
|
mut self,
|
||||||
|
listener: impl Fn(Option<usize>, MouseMoveEvent, &mut WindowContext<'_>) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.hover_listener = Some(Box::new(listener));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// tooltip lets you specify a tooltip for a given character index in the string.
|
||||||
|
pub fn tooltip(
|
||||||
|
mut self,
|
||||||
|
builder: impl Fn(usize, &mut WindowContext<'_>) -> Option<AnyView> + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.tooltip_builder = Some(Rc::new(builder));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element for InteractiveText {
|
||||||
|
type RequestLayoutState = ();
|
||||||
|
type PrepaintState = Hitbox;
|
||||||
|
|
||||||
|
fn id(&self) -> Option<ElementId> {
|
||||||
|
Some(self.element_id.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_layout(
|
||||||
|
&mut self,
|
||||||
|
_id: Option<&GlobalElementId>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> (LayoutId, Self::RequestLayoutState) {
|
||||||
|
self.text.request_layout(None, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepaint(
|
||||||
|
&mut self,
|
||||||
|
global_id: Option<&GlobalElementId>,
|
||||||
|
bounds: Bounds<Pixels>,
|
||||||
|
state: &mut Self::RequestLayoutState,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> Hitbox {
|
||||||
|
cx.with_optional_element_state::<InteractiveTextState, _>(
|
||||||
|
global_id,
|
||||||
|
|interactive_state, cx| {
|
||||||
|
let interactive_state = interactive_state
|
||||||
|
.map(|interactive_state| interactive_state.unwrap_or_default());
|
||||||
|
|
||||||
|
if let Some(interactive_state) = interactive_state.as_ref() {
|
||||||
|
if let Some(active_tooltip) = interactive_state.active_tooltip.borrow().as_ref()
|
||||||
|
{
|
||||||
|
if let Some(tooltip) = active_tooltip.tooltip.clone() {
|
||||||
|
cx.set_tooltip(tooltip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.text.prepaint(None, bounds, state, cx);
|
||||||
|
let hitbox = cx.insert_hitbox(bounds, false);
|
||||||
|
(hitbox, interactive_state)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(
|
||||||
|
&mut self,
|
||||||
|
global_id: Option<&GlobalElementId>,
|
||||||
|
bounds: Bounds<Pixels>,
|
||||||
|
_: &mut Self::RequestLayoutState,
|
||||||
|
hitbox: &mut Hitbox,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) {
|
||||||
|
let text_layout = self.text.layout().clone();
|
||||||
|
cx.with_element_state::<InteractiveTextState, _>(
|
||||||
|
global_id.unwrap(),
|
||||||
|
|interactive_state, cx| {
|
||||||
|
let mut interactive_state = interactive_state.unwrap_or_default();
|
||||||
|
if let Some(click_listener) = self.click_listener.take() {
|
||||||
|
let mouse_position = cx.mouse_position();
|
||||||
|
if let Some(ix) = text_layout.index_for_position(mouse_position).ok() {
|
||||||
|
if self
|
||||||
|
.clickable_ranges
|
||||||
|
.iter()
|
||||||
|
.any(|range| range.contains(&ix))
|
||||||
|
{
|
||||||
|
cx.set_cursor_style(crate::CursorStyle::PointingHand, hitbox)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let text_layout = text_layout.clone();
|
||||||
|
let mouse_down = interactive_state.mouse_down_index.clone();
|
||||||
|
if let Some(mouse_down_index) = mouse_down.get() {
|
||||||
|
let hitbox = hitbox.clone();
|
||||||
|
let clickable_ranges = mem::take(&mut self.clickable_ranges);
|
||||||
|
cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| {
|
||||||
|
if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) {
|
||||||
|
if let Some(mouse_up_index) =
|
||||||
|
text_layout.index_for_position(event.position).ok()
|
||||||
|
{
|
||||||
|
click_listener(
|
||||||
|
&clickable_ranges,
|
||||||
|
InteractiveTextClickEvent {
|
||||||
|
mouse_down_index,
|
||||||
|
mouse_up_index,
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
mouse_down.take();
|
||||||
|
cx.refresh();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let hitbox = hitbox.clone();
|
||||||
|
cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
|
||||||
|
if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) {
|
||||||
|
if let Some(mouse_down_index) =
|
||||||
|
text_layout.index_for_position(event.position).ok()
|
||||||
|
{
|
||||||
|
mouse_down.set(Some(mouse_down_index));
|
||||||
|
cx.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.on_mouse_event({
|
||||||
|
let mut hover_listener = self.hover_listener.take();
|
||||||
|
let hitbox = hitbox.clone();
|
||||||
|
let text_layout = text_layout.clone();
|
||||||
|
let hovered_index = interactive_state.hovered_index.clone();
|
||||||
|
move |event: &MouseMoveEvent, phase, cx| {
|
||||||
|
if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) {
|
||||||
|
let current = hovered_index.get();
|
||||||
|
let updated = text_layout.index_for_position(event.position).ok();
|
||||||
|
if current != updated {
|
||||||
|
hovered_index.set(updated);
|
||||||
|
if let Some(hover_listener) = hover_listener.as_ref() {
|
||||||
|
hover_listener(updated, event.clone(), cx);
|
||||||
|
}
|
||||||
|
cx.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(tooltip_builder) = self.tooltip_builder.clone() {
|
||||||
|
let hitbox = hitbox.clone();
|
||||||
|
let active_tooltip = interactive_state.active_tooltip.clone();
|
||||||
|
let pending_mouse_down = interactive_state.mouse_down_index.clone();
|
||||||
|
let text_layout = text_layout.clone();
|
||||||
|
|
||||||
|
cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
|
||||||
|
let position = text_layout.index_for_position(event.position).ok();
|
||||||
|
let is_hovered = position.is_some()
|
||||||
|
&& hitbox.is_hovered(cx)
|
||||||
|
&& pending_mouse_down.get().is_none();
|
||||||
|
if !is_hovered {
|
||||||
|
active_tooltip.take();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let position = position.unwrap();
|
||||||
|
|
||||||
|
if phase != DispatchPhase::Bubble {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if active_tooltip.borrow().is_none() {
|
||||||
|
let task = cx.spawn({
|
||||||
|
let active_tooltip = active_tooltip.clone();
|
||||||
|
let tooltip_builder = tooltip_builder.clone();
|
||||||
|
|
||||||
|
move |mut cx| async move {
|
||||||
|
cx.background_executor().timer(TOOLTIP_DELAY).await;
|
||||||
|
cx.update(|cx| {
|
||||||
|
let new_tooltip =
|
||||||
|
tooltip_builder(position, cx).map(|tooltip| {
|
||||||
|
ActiveTooltip {
|
||||||
|
tooltip: Some(AnyTooltip {
|
||||||
|
view: tooltip,
|
||||||
|
mouse_position: cx.mouse_position(),
|
||||||
|
}),
|
||||||
|
_task: None,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
*active_tooltip.borrow_mut() = new_tooltip;
|
||||||
|
cx.refresh();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
*active_tooltip.borrow_mut() = Some(ActiveTooltip {
|
||||||
|
tooltip: None,
|
||||||
|
_task: Some(task),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let active_tooltip = interactive_state.active_tooltip.clone();
|
||||||
|
cx.on_mouse_event(move |_: &MouseDownEvent, _, _| {
|
||||||
|
active_tooltip.take();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
self.text.paint(None, bounds, &mut (), &mut (), cx);
|
||||||
|
|
||||||
|
((), interactive_state)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoElement for InteractiveText {
|
||||||
|
type Element = Self;
|
||||||
|
|
||||||
|
fn into_element(self) -> Self::Element {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
312
crates/ming/src/elements/uniform_list.rs
Normal file
312
crates/ming/src/elements/uniform_list.rs
Normal file
|
@ -0,0 +1,312 @@
|
||||||
|
//! A scrollable list of elements with uniform height, optimized for large lists.
|
||||||
|
//! Rather than use the full taffy layout system, uniform_list simply measures
|
||||||
|
//! the first element and then lays out all remaining elements in a line based on that
|
||||||
|
//! measurement. This is much faster than the full layout system, but only works for
|
||||||
|
//! elements with uniform height.
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
point, px, size, AnyElement, AvailableSpace, Bounds, ContentMask, Element, ElementId,
|
||||||
|
GlobalElementId, Hitbox, InteractiveElement, Interactivity, IntoElement, LayoutId, Pixels,
|
||||||
|
Render, ScrollHandle, Size, StyleRefinement, Styled, View, ViewContext, WindowContext,
|
||||||
|
};
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
|
||||||
|
use taffy::style::Overflow;
|
||||||
|
|
||||||
|
/// uniform_list provides lazy rendering for a set of items that are of uniform height.
|
||||||
|
/// When rendered into a container with overflow-y: hidden and a fixed (or max) height,
|
||||||
|
/// uniform_list will only render the visible subset of items.
|
||||||
|
#[track_caller]
|
||||||
|
pub fn uniform_list<I, R, V>(
|
||||||
|
view: View<V>,
|
||||||
|
id: I,
|
||||||
|
item_count: usize,
|
||||||
|
f: impl 'static + Fn(&mut V, Range<usize>, &mut ViewContext<V>) -> Vec<R>,
|
||||||
|
) -> UniformList
|
||||||
|
where
|
||||||
|
I: Into<ElementId>,
|
||||||
|
R: IntoElement,
|
||||||
|
V: Render,
|
||||||
|
{
|
||||||
|
let id = id.into();
|
||||||
|
let mut base_style = StyleRefinement::default();
|
||||||
|
base_style.overflow.y = Some(Overflow::Scroll);
|
||||||
|
|
||||||
|
let render_range = move |range, cx: &mut WindowContext| {
|
||||||
|
view.update(cx, |this, cx| {
|
||||||
|
f(this, range, cx)
|
||||||
|
.into_iter()
|
||||||
|
.map(|component| component.into_any_element())
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
UniformList {
|
||||||
|
item_count,
|
||||||
|
item_to_measure_index: 0,
|
||||||
|
render_items: Box::new(render_range),
|
||||||
|
interactivity: Interactivity {
|
||||||
|
element_id: Some(id),
|
||||||
|
base_style: Box::new(base_style),
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
location: Some(*core::panic::Location::caller()),
|
||||||
|
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
scroll_handle: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A list element for efficiently laying out and displaying a list of uniform-height elements.
|
||||||
|
pub struct UniformList {
|
||||||
|
item_count: usize,
|
||||||
|
item_to_measure_index: usize,
|
||||||
|
render_items:
|
||||||
|
Box<dyn for<'a> Fn(Range<usize>, &'a mut WindowContext) -> SmallVec<[AnyElement; 64]>>,
|
||||||
|
interactivity: Interactivity,
|
||||||
|
scroll_handle: Option<UniformListScrollHandle>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Frame state used by the [UniformList].
|
||||||
|
pub struct UniformListFrameState {
|
||||||
|
item_size: Size<Pixels>,
|
||||||
|
items: SmallVec<[AnyElement; 32]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A handle for controlling the scroll position of a uniform list.
|
||||||
|
/// This should be stored in your view and passed to the uniform_list on each frame.
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
pub struct UniformListScrollHandle {
|
||||||
|
base_handle: ScrollHandle,
|
||||||
|
deferred_scroll_to_item: Rc<RefCell<Option<usize>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UniformListScrollHandle {
|
||||||
|
/// Create a new scroll handle to bind to a uniform list.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
base_handle: ScrollHandle::new(),
|
||||||
|
deferred_scroll_to_item: Rc::new(RefCell::new(None)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scroll the list to the given item index.
|
||||||
|
pub fn scroll_to_item(&mut self, ix: usize) {
|
||||||
|
self.deferred_scroll_to_item.replace(Some(ix));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Styled for UniformList {
|
||||||
|
fn style(&mut self) -> &mut StyleRefinement {
|
||||||
|
&mut self.interactivity.base_style
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element for UniformList {
|
||||||
|
type RequestLayoutState = UniformListFrameState;
|
||||||
|
type PrepaintState = Option<Hitbox>;
|
||||||
|
|
||||||
|
fn id(&self) -> Option<ElementId> {
|
||||||
|
self.interactivity.element_id.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_layout(
|
||||||
|
&mut self,
|
||||||
|
global_id: Option<&GlobalElementId>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> (LayoutId, Self::RequestLayoutState) {
|
||||||
|
let max_items = self.item_count;
|
||||||
|
let item_size = self.measure_item(None, cx);
|
||||||
|
let layout_id = self
|
||||||
|
.interactivity
|
||||||
|
.request_layout(global_id, cx, |style, cx| {
|
||||||
|
cx.request_measured_layout(style, move |known_dimensions, available_space, _cx| {
|
||||||
|
let desired_height = item_size.height * max_items;
|
||||||
|
let width = known_dimensions
|
||||||
|
.width
|
||||||
|
.unwrap_or(match available_space.width {
|
||||||
|
AvailableSpace::Definite(x) => x,
|
||||||
|
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
|
||||||
|
item_size.width
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let height = match available_space.height {
|
||||||
|
AvailableSpace::Definite(height) => desired_height.min(height),
|
||||||
|
AvailableSpace::MinContent | AvailableSpace::MaxContent => desired_height,
|
||||||
|
};
|
||||||
|
size(width, height)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
(
|
||||||
|
layout_id,
|
||||||
|
UniformListFrameState {
|
||||||
|
item_size,
|
||||||
|
items: SmallVec::new(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepaint(
|
||||||
|
&mut self,
|
||||||
|
global_id: Option<&GlobalElementId>,
|
||||||
|
bounds: Bounds<Pixels>,
|
||||||
|
frame_state: &mut Self::RequestLayoutState,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> Option<Hitbox> {
|
||||||
|
let style = self.interactivity.compute_style(global_id, None, cx);
|
||||||
|
let border = style.border_widths.to_pixels(cx.rem_size());
|
||||||
|
let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size());
|
||||||
|
|
||||||
|
let padded_bounds = Bounds::from_corners(
|
||||||
|
bounds.origin + point(border.left + padding.left, border.top + padding.top),
|
||||||
|
bounds.lower_right()
|
||||||
|
- point(border.right + padding.right, border.bottom + padding.bottom),
|
||||||
|
);
|
||||||
|
|
||||||
|
let content_size = Size {
|
||||||
|
width: padded_bounds.size.width,
|
||||||
|
height: frame_state.item_size.height * self.item_count + padding.top + padding.bottom,
|
||||||
|
};
|
||||||
|
|
||||||
|
let shared_scroll_offset = self.interactivity.scroll_offset.clone().unwrap();
|
||||||
|
|
||||||
|
let item_height = self.measure_item(Some(padded_bounds.size.width), cx).height;
|
||||||
|
let shared_scroll_to_item = self
|
||||||
|
.scroll_handle
|
||||||
|
.as_mut()
|
||||||
|
.and_then(|handle| handle.deferred_scroll_to_item.take());
|
||||||
|
|
||||||
|
self.interactivity.prepaint(
|
||||||
|
global_id,
|
||||||
|
bounds,
|
||||||
|
content_size,
|
||||||
|
cx,
|
||||||
|
|style, mut scroll_offset, hitbox, cx| {
|
||||||
|
let border = style.border_widths.to_pixels(cx.rem_size());
|
||||||
|
let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size());
|
||||||
|
|
||||||
|
let padded_bounds = Bounds::from_corners(
|
||||||
|
bounds.origin + point(border.left + padding.left, border.top),
|
||||||
|
bounds.lower_right() - point(border.right + padding.right, border.bottom),
|
||||||
|
);
|
||||||
|
|
||||||
|
if self.item_count > 0 {
|
||||||
|
let content_height =
|
||||||
|
item_height * self.item_count + padding.top + padding.bottom;
|
||||||
|
let min_scroll_offset = padded_bounds.size.height - content_height;
|
||||||
|
let is_scrolled = scroll_offset.y != px(0.);
|
||||||
|
|
||||||
|
if is_scrolled && scroll_offset.y < min_scroll_offset {
|
||||||
|
shared_scroll_offset.borrow_mut().y = min_scroll_offset;
|
||||||
|
scroll_offset.y = min_scroll_offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ix) = shared_scroll_to_item {
|
||||||
|
let list_height = padded_bounds.size.height;
|
||||||
|
let mut updated_scroll_offset = shared_scroll_offset.borrow_mut();
|
||||||
|
let item_top = item_height * ix + padding.top;
|
||||||
|
let item_bottom = item_top + item_height;
|
||||||
|
let scroll_top = -updated_scroll_offset.y;
|
||||||
|
if item_top < scroll_top + padding.top {
|
||||||
|
updated_scroll_offset.y = -(item_top) + padding.top;
|
||||||
|
} else if item_bottom > scroll_top + list_height - padding.bottom {
|
||||||
|
updated_scroll_offset.y = -(item_bottom - list_height) - padding.bottom;
|
||||||
|
}
|
||||||
|
scroll_offset = *updated_scroll_offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
let first_visible_element_ix =
|
||||||
|
(-(scroll_offset.y + padding.top) / item_height).floor() as usize;
|
||||||
|
let last_visible_element_ix = ((-scroll_offset.y + padded_bounds.size.height)
|
||||||
|
/ item_height)
|
||||||
|
.ceil() as usize;
|
||||||
|
let visible_range = first_visible_element_ix
|
||||||
|
..cmp::min(last_visible_element_ix, self.item_count);
|
||||||
|
|
||||||
|
let mut items = (self.render_items)(visible_range.clone(), cx);
|
||||||
|
let content_mask = ContentMask { bounds };
|
||||||
|
cx.with_content_mask(Some(content_mask), |cx| {
|
||||||
|
for (mut item, ix) in items.into_iter().zip(visible_range) {
|
||||||
|
let item_origin = padded_bounds.origin
|
||||||
|
+ point(px(0.), item_height * ix + scroll_offset.y + padding.top);
|
||||||
|
let available_space = size(
|
||||||
|
AvailableSpace::Definite(padded_bounds.size.width),
|
||||||
|
AvailableSpace::Definite(item_height),
|
||||||
|
);
|
||||||
|
item.layout_as_root(available_space, cx);
|
||||||
|
item.prepaint_at(item_origin, cx);
|
||||||
|
frame_state.items.push(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
hitbox
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(
|
||||||
|
&mut self,
|
||||||
|
global_id: Option<&GlobalElementId>,
|
||||||
|
bounds: Bounds<crate::Pixels>,
|
||||||
|
request_layout: &mut Self::RequestLayoutState,
|
||||||
|
hitbox: &mut Option<Hitbox>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) {
|
||||||
|
self.interactivity
|
||||||
|
.paint(global_id, bounds, hitbox.as_ref(), cx, |_, cx| {
|
||||||
|
for item in &mut request_layout.items {
|
||||||
|
item.paint(cx);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoElement for UniformList {
|
||||||
|
type Element = Self;
|
||||||
|
|
||||||
|
fn into_element(self) -> Self::Element {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UniformList {
|
||||||
|
/// Selects a specific list item for measurement.
|
||||||
|
pub fn with_width_from_item(mut self, item_index: Option<usize>) -> Self {
|
||||||
|
self.item_to_measure_index = item_index.unwrap_or(0);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn measure_item(&self, list_width: Option<Pixels>, cx: &mut WindowContext) -> Size<Pixels> {
|
||||||
|
if self.item_count == 0 {
|
||||||
|
return Size::default();
|
||||||
|
}
|
||||||
|
|
||||||
|
let item_ix = cmp::min(self.item_to_measure_index, self.item_count - 1);
|
||||||
|
let mut items = (self.render_items)(item_ix..item_ix + 1, cx);
|
||||||
|
let mut item_to_measure = items.pop().unwrap();
|
||||||
|
let available_space = size(
|
||||||
|
list_width.map_or(AvailableSpace::MinContent, |width| {
|
||||||
|
AvailableSpace::Definite(width)
|
||||||
|
}),
|
||||||
|
AvailableSpace::MinContent,
|
||||||
|
);
|
||||||
|
item_to_measure.layout_as_root(available_space, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Track and render scroll state of this list with reference to the given scroll handle.
|
||||||
|
pub fn track_scroll(mut self, handle: UniformListScrollHandle) -> Self {
|
||||||
|
self.interactivity.tracked_scroll_handle = Some(handle.base_handle.clone());
|
||||||
|
self.scroll_handle = Some(handle);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InteractiveElement for UniformList {
|
||||||
|
fn interactivity(&mut self) -> &mut crate::Interactivity {
|
||||||
|
&mut self.interactivity
|
||||||
|
}
|
||||||
|
}
|
501
crates/ming/src/executor.rs
Normal file
501
crates/ming/src/executor.rs
Normal file
|
@ -0,0 +1,501 @@
|
||||||
|
use crate::{AppContext, PlatformDispatcher};
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
use tokio::spawn;
|
||||||
|
use std::{
|
||||||
|
fmt::Debug,
|
||||||
|
marker::PhantomData,
|
||||||
|
mem,
|
||||||
|
num::NonZeroUsize,
|
||||||
|
pin::Pin,
|
||||||
|
rc::Rc,
|
||||||
|
sync::{
|
||||||
|
atomic::{AtomicUsize, Ordering::SeqCst},
|
||||||
|
Arc,
|
||||||
|
},
|
||||||
|
task::{Context, Poll},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
use util::TryFutureExt;
|
||||||
|
use waker_fn::waker_fn;
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
use rand::rngs::StdRng;
|
||||||
|
|
||||||
|
/// A pointer to the executor that is currently running,
|
||||||
|
/// for spawning background tasks.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct BackgroundExecutor {
|
||||||
|
dispatcher: Arc<dyn PlatformDispatcher>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A pointer to the executor that is currently running,
|
||||||
|
/// for spawning tasks on the main thread.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ForegroundExecutor {
|
||||||
|
dispatcher: Arc<dyn PlatformDispatcher>,
|
||||||
|
not_send: PhantomData<Rc<()>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Task is a primitive that allows work to happen in the background.
|
||||||
|
///
|
||||||
|
/// It implements [`Future`] so you can `.await` on it.
|
||||||
|
///
|
||||||
|
/// If you drop a task it will be cancelled immediately. Calling [`Task::detach`] allows
|
||||||
|
/// the task to continue running, but with no way to return a value.
|
||||||
|
#[must_use]
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Task<T> {
|
||||||
|
/// A task that is ready to return a value
|
||||||
|
Ready(Option<T>),
|
||||||
|
|
||||||
|
/// A task that is currently running.
|
||||||
|
Spawned(async_task::Task<T>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Task<T> {
|
||||||
|
/// Creates a new task that will resolve with the value
|
||||||
|
pub fn ready(val: T) -> Self {
|
||||||
|
Task::Ready(Some(val))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Detaching a task runs it to completion in the background
|
||||||
|
pub fn detach(self) {
|
||||||
|
match self {
|
||||||
|
Task::Ready(_) => {}
|
||||||
|
Task::Spawned(task) => task.detach(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E, T> Task<Result<T, E>>
|
||||||
|
where
|
||||||
|
T: 'static,
|
||||||
|
E: 'static + Debug,
|
||||||
|
{
|
||||||
|
/// Run the task to completion in the background and log any
|
||||||
|
/// errors that occur.
|
||||||
|
#[track_caller]
|
||||||
|
pub fn detach_and_log_err(self, cx: &AppContext) {
|
||||||
|
let location = core::panic::Location::caller();
|
||||||
|
cx.foreground_executor()
|
||||||
|
.spawn(self.log_tracked_err(*location))
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Future for Task<T> {
|
||||||
|
type Output = T;
|
||||||
|
|
||||||
|
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
|
||||||
|
match unsafe { self.get_unchecked_mut() } {
|
||||||
|
Task::Ready(val) => Poll::Ready(val.take().unwrap()),
|
||||||
|
Task::Spawned(task) => task.poll(cx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A task label is an opaque identifier that you can use to
|
||||||
|
/// refer to a task in tests.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||||
|
pub struct TaskLabel(NonZeroUsize);
|
||||||
|
|
||||||
|
impl Default for TaskLabel {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TaskLabel {
|
||||||
|
/// Construct a new task label.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
static NEXT_TASK_LABEL: AtomicUsize = AtomicUsize::new(1);
|
||||||
|
Self(NEXT_TASK_LABEL.fetch_add(1, SeqCst).try_into().unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnyLocalFuture<R> = Pin<Box<dyn 'static + Future<Output = R>>>;
|
||||||
|
|
||||||
|
type AnyFuture<R> = Pin<Box<dyn 'static + Send + Future<Output = R>>>;
|
||||||
|
|
||||||
|
/// BackgroundExecutor lets you run things on background threads.
|
||||||
|
/// In production this is a thread pool with no ordering guarantees.
|
||||||
|
/// In tests this is simulated by running tasks one by one in a deterministic
|
||||||
|
/// (but arbitrary) order controlled by the `SEED` environment variable.
|
||||||
|
impl BackgroundExecutor {
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub fn new(dispatcher: Arc<dyn PlatformDispatcher>) -> Self {
|
||||||
|
Self { dispatcher }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enqueues the given future to be run to completion on a background thread.
|
||||||
|
pub fn spawn<R>(&self, future: impl Future<Output = R> + Send + 'static) -> Task<R>
|
||||||
|
where
|
||||||
|
R: Send + 'static,
|
||||||
|
{
|
||||||
|
self.spawn_internal::<R>(Box::pin(future), None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enqueues the given future to be run to completion on a background thread.
|
||||||
|
/// The given label can be used to control the priority of the task in tests.
|
||||||
|
pub fn spawn_labeled<R>(
|
||||||
|
&self,
|
||||||
|
label: TaskLabel,
|
||||||
|
future: impl Future<Output = R> + Send + 'static,
|
||||||
|
) -> Task<R>
|
||||||
|
where
|
||||||
|
R: Send + 'static,
|
||||||
|
{
|
||||||
|
self.spawn_internal::<R>(Box::pin(future), Some(label))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_internal<R: Send + 'static>(
|
||||||
|
&self,
|
||||||
|
future: AnyFuture<R>,
|
||||||
|
label: Option<TaskLabel>,
|
||||||
|
) -> Task<R> {
|
||||||
|
let dispatcher = self.dispatcher.clone();
|
||||||
|
let (runnable, task) =
|
||||||
|
async_task::spawn(future, move |runnable| dispatcher.dispatch(runnable, label));
|
||||||
|
runnable.schedule();
|
||||||
|
Task::Spawned(task)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Used by the test harness to run an async test in a synchronous fashion.
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
#[track_caller]
|
||||||
|
pub fn block_test<R>(&self, future: impl Future<Output = R>) -> R {
|
||||||
|
if let Ok(value) = self.block_internal(false, future, None) {
|
||||||
|
value
|
||||||
|
} else {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Block the current thread until the given future resolves.
|
||||||
|
/// Consider using `block_with_timeout` instead.
|
||||||
|
pub fn block<R>(&self, future: impl Future<Output = R>) -> R {
|
||||||
|
if let Ok(value) = self.block_internal(true, future, None) {
|
||||||
|
value
|
||||||
|
} else {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(test, feature = "test-support")))]
|
||||||
|
pub(crate) fn block_internal<R>(
|
||||||
|
&self,
|
||||||
|
_background_only: bool,
|
||||||
|
future: impl Future<Output = R>,
|
||||||
|
timeout: Option<Duration>,
|
||||||
|
) -> Result<R, impl Future<Output = R>> {
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
let mut future = Box::pin(future);
|
||||||
|
if timeout == Some(Duration::ZERO) {
|
||||||
|
return Err(future);
|
||||||
|
}
|
||||||
|
let deadline = timeout.map(|timeout| Instant::now() + timeout);
|
||||||
|
|
||||||
|
let unparker = self.dispatcher.unparker();
|
||||||
|
let waker = waker_fn(move || {
|
||||||
|
unparker.unpark();
|
||||||
|
});
|
||||||
|
let mut cx = std::task::Context::from_waker(&waker);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match future.as_mut().poll(&mut cx) {
|
||||||
|
Poll::Ready(result) => return Ok(result),
|
||||||
|
Poll::Pending => {
|
||||||
|
let timeout =
|
||||||
|
deadline.map(|deadline| deadline.saturating_duration_since(Instant::now()));
|
||||||
|
if !self.dispatcher.park(timeout) {
|
||||||
|
if deadline.is_some_and(|deadline| deadline < Instant::now()) {
|
||||||
|
return Err(future);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
#[track_caller]
|
||||||
|
pub(crate) fn block_internal<R>(
|
||||||
|
&self,
|
||||||
|
background_only: bool,
|
||||||
|
future: impl Future<Output = R>,
|
||||||
|
timeout: Option<Duration>,
|
||||||
|
) -> Result<R, impl Future<Output = R>> {
|
||||||
|
use std::sync::atomic::AtomicBool;
|
||||||
|
|
||||||
|
let mut future = Box::pin(future);
|
||||||
|
if timeout == Some(Duration::ZERO) {
|
||||||
|
return Err(future);
|
||||||
|
}
|
||||||
|
let Some(dispatcher) = self.dispatcher.as_test() else {
|
||||||
|
return Err(future);
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut max_ticks = if timeout.is_some() {
|
||||||
|
dispatcher.gen_block_on_ticks()
|
||||||
|
} else {
|
||||||
|
usize::MAX
|
||||||
|
};
|
||||||
|
let unparker = self.dispatcher.unparker();
|
||||||
|
let awoken = Arc::new(AtomicBool::new(false));
|
||||||
|
let waker = waker_fn({
|
||||||
|
let awoken = awoken.clone();
|
||||||
|
move || {
|
||||||
|
awoken.store(true, SeqCst);
|
||||||
|
unparker.unpark();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let mut cx = std::task::Context::from_waker(&waker);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match future.as_mut().poll(&mut cx) {
|
||||||
|
Poll::Ready(result) => return Ok(result),
|
||||||
|
Poll::Pending => {
|
||||||
|
if max_ticks == 0 {
|
||||||
|
return Err(future);
|
||||||
|
}
|
||||||
|
max_ticks -= 1;
|
||||||
|
|
||||||
|
if !dispatcher.tick(background_only) {
|
||||||
|
if awoken.swap(false, SeqCst) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !dispatcher.parking_allowed() {
|
||||||
|
let mut backtrace_message = String::new();
|
||||||
|
let mut waiting_message = String::new();
|
||||||
|
if let Some(backtrace) = dispatcher.waiting_backtrace() {
|
||||||
|
backtrace_message =
|
||||||
|
format!("\nbacktrace of waiting future:\n{:?}", backtrace);
|
||||||
|
}
|
||||||
|
if let Some(waiting_hint) = dispatcher.waiting_hint() {
|
||||||
|
waiting_message = format!("\n waiting on: {}\n", waiting_hint);
|
||||||
|
}
|
||||||
|
panic!(
|
||||||
|
"parked with nothing left to run{waiting_message}{backtrace_message}",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
self.dispatcher.park(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Block the current thread until the given future resolves
|
||||||
|
/// or `duration` has elapsed.
|
||||||
|
pub fn block_with_timeout<R>(
|
||||||
|
&self,
|
||||||
|
duration: Duration,
|
||||||
|
future: impl Future<Output = R>,
|
||||||
|
) -> Result<R, impl Future<Output = R>> {
|
||||||
|
self.block_internal(true, future, Some(duration))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scoped lets you start a number of tasks and waits
|
||||||
|
/// for all of them to complete before returning.
|
||||||
|
pub async fn scoped<'scope, F>(&self, scheduler: F)
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut Scope<'scope>),
|
||||||
|
{
|
||||||
|
let mut scope = Scope::new(self.clone());
|
||||||
|
(scheduler)(&mut scope);
|
||||||
|
let spawned = mem::take(&mut scope.futures)
|
||||||
|
.into_iter()
|
||||||
|
.map(|f| self.spawn(f))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
for task in spawned {
|
||||||
|
task.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a task that will complete after the given duration.
|
||||||
|
/// Depending on other concurrent tasks the elapsed duration may be longer
|
||||||
|
/// than requested.
|
||||||
|
pub fn timer(&self, duration: Duration) -> Task<()> {
|
||||||
|
let (runnable, task) = async_task::spawn(async move {}, {
|
||||||
|
let dispatcher = self.dispatcher.clone();
|
||||||
|
move |runnable| dispatcher.dispatch_after(duration, runnable)
|
||||||
|
});
|
||||||
|
runnable.schedule();
|
||||||
|
Task::Spawned(task)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// in tests, start_waiting lets you indicate which task is waiting (for debugging only)
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub fn start_waiting(&self) {
|
||||||
|
self.dispatcher.as_test().unwrap().start_waiting();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// in tests, removes the debugging data added by start_waiting
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub fn finish_waiting(&self) {
|
||||||
|
self.dispatcher.as_test().unwrap().finish_waiting();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// in tests, run an arbitrary number of tasks (determined by the SEED environment variable)
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub fn simulate_random_delay(&self) -> impl Future<Output = ()> {
|
||||||
|
self.dispatcher.as_test().unwrap().simulate_random_delay()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// in tests, indicate that a given task from `spawn_labeled` should run after everything else
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub fn deprioritize(&self, task_label: TaskLabel) {
|
||||||
|
self.dispatcher.as_test().unwrap().deprioritize(task_label)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// in tests, move time forward. This does not run any tasks, but does make `timer`s ready.
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub fn advance_clock(&self, duration: Duration) {
|
||||||
|
self.dispatcher.as_test().unwrap().advance_clock(duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// in tests, run one task.
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub fn tick(&self) -> bool {
|
||||||
|
self.dispatcher.as_test().unwrap().tick(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// in tests, run all tasks that are ready to run. If after doing so
|
||||||
|
/// the test still has outstanding tasks, this will panic. (See also `allow_parking`)
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub fn run_until_parked(&self) {
|
||||||
|
self.dispatcher.as_test().unwrap().run_until_parked()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// in tests, prevents `run_until_parked` from panicking if there are outstanding tasks.
|
||||||
|
/// This is useful when you are integrating other (non-GPUI) futures, like disk access, that
|
||||||
|
/// do take real async time to run.
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub fn allow_parking(&self) {
|
||||||
|
self.dispatcher.as_test().unwrap().allow_parking();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// undoes the effect of [`allow_parking`].
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub fn forbid_parking(&self) {
|
||||||
|
self.dispatcher.as_test().unwrap().forbid_parking();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// adds detail to the "parked with nothing let to run" message.
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub fn set_waiting_hint(&self, msg: Option<String>) {
|
||||||
|
self.dispatcher.as_test().unwrap().set_waiting_hint(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// in tests, returns the rng used by the dispatcher and seeded by the `SEED` environment variable
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub fn rng(&self) -> StdRng {
|
||||||
|
self.dispatcher.as_test().unwrap().rng()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How many CPUs are available to the dispatcher.
|
||||||
|
pub fn num_cpus(&self) -> usize {
|
||||||
|
num_cpus::get()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether we're on the main thread.
|
||||||
|
pub fn is_main_thread(&self) -> bool {
|
||||||
|
self.dispatcher.is_main_thread()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
/// in tests, control the number of ticks that `block_with_timeout` will run before timing out.
|
||||||
|
pub fn set_block_on_ticks(&self, range: std::ops::RangeInclusive<usize>) {
|
||||||
|
self.dispatcher.as_test().unwrap().set_block_on_ticks(range);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ForegroundExecutor runs things on the main thread.
|
||||||
|
impl ForegroundExecutor {
|
||||||
|
/// Creates a new ForegroundExecutor from the given PlatformDispatcher.
|
||||||
|
pub fn new(dispatcher: Arc<dyn PlatformDispatcher>) -> Self {
|
||||||
|
Self {
|
||||||
|
dispatcher,
|
||||||
|
not_send: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enqueues the given Task to run on the main thread at some point in the future.
|
||||||
|
pub fn spawn<R>(&self, future: impl Future<Output = R> + 'static) -> Task<R>
|
||||||
|
where
|
||||||
|
R: 'static,
|
||||||
|
{
|
||||||
|
let dispatcher = self.dispatcher.clone();
|
||||||
|
fn inner<R: 'static>(
|
||||||
|
dispatcher: Arc<dyn PlatformDispatcher>,
|
||||||
|
future: AnyLocalFuture<R>,
|
||||||
|
) -> Task<R> {
|
||||||
|
let (runnable, task) = async_task::spawn_local(future, move |runnable| {
|
||||||
|
dispatcher.dispatch_on_main_thread(runnable)
|
||||||
|
});
|
||||||
|
runnable.schedule();
|
||||||
|
Task::Spawned(task)
|
||||||
|
}
|
||||||
|
inner::<R>(dispatcher, Box::pin(future))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scope manages a set of tasks that are enqueued and waited on together. See [`BackgroundExecutor::scoped`].
|
||||||
|
pub struct Scope<'a> {
|
||||||
|
executor: BackgroundExecutor,
|
||||||
|
futures: Vec<Pin<Box<dyn Future<Output = ()> + Send + 'static>>>,
|
||||||
|
tx: Option<mpsc::Sender<()>>,
|
||||||
|
rx: mpsc::Receiver<()>,
|
||||||
|
lifetime: PhantomData<&'a ()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Scope<'a> {
|
||||||
|
fn new(executor: BackgroundExecutor) -> Self {
|
||||||
|
let (tx, rx) = mpsc::channel(1);
|
||||||
|
Self {
|
||||||
|
executor,
|
||||||
|
tx: Some(tx),
|
||||||
|
rx,
|
||||||
|
futures: Default::default(),
|
||||||
|
lifetime: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How many CPUs are available to the dispatcher.
|
||||||
|
pub fn num_cpus(&self) -> usize {
|
||||||
|
self.executor.num_cpus()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spawn a future into this scope.
|
||||||
|
pub fn spawn<F>(&mut self, f: F)
|
||||||
|
where
|
||||||
|
F: Future<Output = ()> + Send + 'a,
|
||||||
|
{
|
||||||
|
let tx = self.tx.clone().unwrap();
|
||||||
|
|
||||||
|
// SAFETY: The 'a lifetime is guaranteed to outlive any of these futures because
|
||||||
|
// dropping this `Scope` blocks until all of the futures have resolved.
|
||||||
|
let f = unsafe {
|
||||||
|
mem::transmute::<
|
||||||
|
Pin<Box<dyn Future<Output = ()> + Send + 'a>>,
|
||||||
|
Pin<Box<dyn Future<Output = ()> + Send + 'static>>,
|
||||||
|
>(Box::pin(async move {
|
||||||
|
f.await;
|
||||||
|
drop(tx);
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
self.futures.push(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Drop for Scope<'a> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.tx.take().unwrap();
|
||||||
|
|
||||||
|
// Wait until the channel is closed, which means that all of the spawned
|
||||||
|
// futures have resolved.
|
||||||
|
self.executor.block(self.rx.next());
|
||||||
|
}
|
||||||
|
}
|
3037
crates/ming/src/geometry.rs
Normal file
3037
crates/ming/src/geometry.rs
Normal file
File diff suppressed because it is too large
Load diff
341
crates/ming/src/gpui.rs
Normal file
341
crates/ming/src/gpui.rs
Normal file
|
@ -0,0 +1,341 @@
|
||||||
|
//! # Welcome to GPUI!
|
||||||
|
//!
|
||||||
|
//! GPUI is a hybrid immediate and retained mode, GPU accelerated, UI framework
|
||||||
|
//! for Rust, designed to support a wide variety of applications.
|
||||||
|
//!
|
||||||
|
//! ## Getting Started
|
||||||
|
//!
|
||||||
|
//! GPUI is still in active development as we work on the Zed code editor and isn't yet on crates.io.
|
||||||
|
//! You'll also need to use the latest version of stable rust. Add the following to your Cargo.toml:
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! gpui = { git = "https://github.com/zed-industries/zed" }
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! Everything in GPUI starts with an [`App`]. You can create one with [`App::new`], and
|
||||||
|
//! kick off your application by passing a callback to [`App::run`]. Inside this callback,
|
||||||
|
//! you can create a new window with [`AppContext::open_window`], and register your first root
|
||||||
|
//! view. See [gpui.rs](https://www.gpui.rs/) for a complete example.
|
||||||
|
//!
|
||||||
|
//! ## The Big Picture
|
||||||
|
//!
|
||||||
|
//! GPUI offers three different [registers](https://en.wikipedia.org/wiki/Register_(sociolinguistics)) depending on your needs:
|
||||||
|
//!
|
||||||
|
//! - State management and communication with Models. Whenever you need to store application state
|
||||||
|
//! that communicates between different parts of your application, you'll want to use GPUI's
|
||||||
|
//! models. Models are owned by GPUI and are only accessible through an owned smart pointer
|
||||||
|
//! similar to an [`Rc`]. See the [`app::model_context`] module for more information.
|
||||||
|
//!
|
||||||
|
//! - High level, declarative UI with Views. All UI in GPUI starts with a View. A view is simply
|
||||||
|
//! a model that can be rendered, via the [`Render`] trait. At the start of each frame, GPUI
|
||||||
|
//! will call this render method on the root view of a given window. Views build a tree of
|
||||||
|
//! `elements`, lay them out and style them with a tailwind-style API, and then give them to
|
||||||
|
//! GPUI to turn into pixels. See the [`elements::Div`] element for an all purpose swiss-army
|
||||||
|
//! knife for UI.
|
||||||
|
//!
|
||||||
|
//! - Low level, imperative UI with Elements. Elements are the building blocks of UI in GPUI, and they
|
||||||
|
//! provide a nice wrapper around an imperative API that provides as much flexibility and control as
|
||||||
|
//! you need. Elements have total control over how they and their child elements are rendered and
|
||||||
|
//! can be used for making efficient views into large lists, implement custom layouting for a code editor,
|
||||||
|
//! and anything else you can think of. See the [`element`] module for more information.
|
||||||
|
//!
|
||||||
|
//! Each of these registers has one or more corresponding contexts that can be accessed from all GPUI services.
|
||||||
|
//! This context is your main interface to GPUI, and is used extensively throughout the framework.
|
||||||
|
//!
|
||||||
|
//! ## Other Resources
|
||||||
|
//!
|
||||||
|
//! In addition to the systems above, GPUI provides a range of smaller services that are useful for building
|
||||||
|
//! complex applications:
|
||||||
|
//!
|
||||||
|
//! - Actions are user-defined structs that are used for converting keystrokes into logical operations in your UI.
|
||||||
|
//! Use this for implementing keyboard shortcuts, such as cmd-q. See the [`action`] module for more information.
|
||||||
|
//! - Platform services, such as `quit the app` or `open a URL` are available as methods on the [`app::AppContext`].
|
||||||
|
//! - An async executor that is integrated with the platform's event loop. See the [`executor`] module for more information.,
|
||||||
|
//! - The [gpui::test] macro provides a convenient way to write tests for your GPUI applications. Tests also have their
|
||||||
|
//! own kind of context, a [`TestAppContext`] which provides ways of simulating common platform input. See [`app::test_context`]
|
||||||
|
//! and [`test`] modules for more details.
|
||||||
|
//!
|
||||||
|
//! Currently, the best way to learn about these APIs is to read the Zed source code, ask us about it at a fireside hack, or drop
|
||||||
|
//! a question in the [Zed Discord](https://discord.gg/U4qhCEhMXP). We're working on improving the documentation, creating more examples,
|
||||||
|
//! and will be publishing more guides to GPUI on our [blog](https://zed.dev/blog).
|
||||||
|
|
||||||
|
#![deny(missing_docs)]
|
||||||
|
#![allow(clippy::type_complexity)] // Not useful, GPUI makes heavy use of callbacks
|
||||||
|
#![allow(clippy::collapsible_else_if)] // False positives in platform specific code
|
||||||
|
#![allow(unused_mut)] // False positives in platform specific code
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
mod action;
|
||||||
|
mod app;
|
||||||
|
|
||||||
|
mod arena;
|
||||||
|
mod asset_cache;
|
||||||
|
mod assets;
|
||||||
|
mod bounds_tree;
|
||||||
|
mod color;
|
||||||
|
mod element;
|
||||||
|
mod elements;
|
||||||
|
mod executor;
|
||||||
|
mod geometry;
|
||||||
|
mod input;
|
||||||
|
mod interactive;
|
||||||
|
mod key_dispatch;
|
||||||
|
mod keymap;
|
||||||
|
mod platform;
|
||||||
|
pub mod prelude;
|
||||||
|
mod scene;
|
||||||
|
mod shared_string;
|
||||||
|
mod shared_uri;
|
||||||
|
mod style;
|
||||||
|
mod styled;
|
||||||
|
mod subscription;
|
||||||
|
mod svg_renderer;
|
||||||
|
mod taffy;
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub mod test;
|
||||||
|
mod text_system;
|
||||||
|
mod util;
|
||||||
|
mod view;
|
||||||
|
mod window;
|
||||||
|
|
||||||
|
/// Do not touch, here be dragons for use by gpui_macros and such.
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub mod private {
|
||||||
|
pub use linkme;
|
||||||
|
pub use serde;
|
||||||
|
pub use serde_derive;
|
||||||
|
pub use serde_json;
|
||||||
|
}
|
||||||
|
|
||||||
|
mod seal {
|
||||||
|
/// A mechanism for restricting implementations of a trait to only those in GPUI.
|
||||||
|
/// See: https://predr.ag/blog/definitive-guide-to-sealed-traits-in-rust/
|
||||||
|
pub trait Sealed {}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub use action::*;
|
||||||
|
pub use anyhow::Result;
|
||||||
|
pub use app::*;
|
||||||
|
pub(crate) use arena::*;
|
||||||
|
pub use asset_cache::*;
|
||||||
|
pub use assets::*;
|
||||||
|
pub use color::*;
|
||||||
|
pub use ctor::ctor;
|
||||||
|
pub use element::*;
|
||||||
|
pub use elements::*;
|
||||||
|
pub use executor::*;
|
||||||
|
pub use geometry::*;
|
||||||
|
pub use gpui_macros::{register_action, test, IntoElement, Render};
|
||||||
|
pub use input::*;
|
||||||
|
pub use interactive::*;
|
||||||
|
use key_dispatch::*;
|
||||||
|
pub use keymap::*;
|
||||||
|
pub use platform::*;
|
||||||
|
pub use refineable::*;
|
||||||
|
pub use scene::*;
|
||||||
|
use seal::Sealed;
|
||||||
|
pub use shared_string::*;
|
||||||
|
pub use shared_uri::*;
|
||||||
|
pub use smol::Timer;
|
||||||
|
pub use style::*;
|
||||||
|
pub use styled::*;
|
||||||
|
pub use subscription::*;
|
||||||
|
use svg_renderer::*;
|
||||||
|
pub use taffy::{AvailableSpace, LayoutId};
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub use test::*;
|
||||||
|
pub use text_system::*;
|
||||||
|
pub use util::arc_cow::ArcCow;
|
||||||
|
pub use view::*;
|
||||||
|
pub use window::*;
|
||||||
|
|
||||||
|
use std::{any::Any, borrow::BorrowMut};
|
||||||
|
use taffy::TaffyLayoutEngine;
|
||||||
|
|
||||||
|
/// The context trait, allows the different contexts in GPUI to be used
|
||||||
|
/// interchangeably for certain operations.
|
||||||
|
pub trait Context {
|
||||||
|
/// The result type for this context, used for async contexts that
|
||||||
|
/// can't hold a direct reference to the application context.
|
||||||
|
type Result<T>;
|
||||||
|
|
||||||
|
/// Create a new model in the app context.
|
||||||
|
fn new_model<T: 'static>(
|
||||||
|
&mut self,
|
||||||
|
build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T,
|
||||||
|
) -> Self::Result<Model<T>>;
|
||||||
|
|
||||||
|
/// Reserve a slot for a model to be inserted later.
|
||||||
|
/// The returned [Reservation] allows you to obtain the [EntityId] for the future model.
|
||||||
|
fn reserve_model<T: 'static>(&mut self) -> Self::Result<Reservation<T>>;
|
||||||
|
|
||||||
|
/// Insert a new model in the app context based on a [Reservation] previously obtained from [`reserve_model`].
|
||||||
|
///
|
||||||
|
/// [`reserve_model`]: Self::reserve_model
|
||||||
|
fn insert_model<T: 'static>(
|
||||||
|
&mut self,
|
||||||
|
reservation: Reservation<T>,
|
||||||
|
build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T,
|
||||||
|
) -> Self::Result<Model<T>>;
|
||||||
|
|
||||||
|
/// Update a model in the app context.
|
||||||
|
fn update_model<T, R>(
|
||||||
|
&mut self,
|
||||||
|
handle: &Model<T>,
|
||||||
|
update: impl FnOnce(&mut T, &mut ModelContext<'_, T>) -> R,
|
||||||
|
) -> Self::Result<R>
|
||||||
|
where
|
||||||
|
T: 'static;
|
||||||
|
|
||||||
|
/// Read a model from the app context.
|
||||||
|
fn read_model<T, R>(
|
||||||
|
&self,
|
||||||
|
handle: &Model<T>,
|
||||||
|
read: impl FnOnce(&T, &AppContext) -> R,
|
||||||
|
) -> Self::Result<R>
|
||||||
|
where
|
||||||
|
T: 'static;
|
||||||
|
|
||||||
|
/// Update a window for the given handle.
|
||||||
|
fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Result<T>
|
||||||
|
where
|
||||||
|
F: FnOnce(AnyView, &mut WindowContext<'_>) -> T;
|
||||||
|
|
||||||
|
/// Read a window off of the application context.
|
||||||
|
fn read_window<T, R>(
|
||||||
|
&self,
|
||||||
|
window: &WindowHandle<T>,
|
||||||
|
read: impl FnOnce(View<T>, &AppContext) -> R,
|
||||||
|
) -> Result<R>
|
||||||
|
where
|
||||||
|
T: 'static;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returned by [Context::reserve_model] to later be passed to [Context::insert_model].
|
||||||
|
/// Allows you to obtain the [EntityId] for a model before it is created.
|
||||||
|
pub struct Reservation<T>(pub(crate) Slot<T>);
|
||||||
|
|
||||||
|
impl<T: 'static> Reservation<T> {
|
||||||
|
/// Returns the [EntityId] that will be associated with the model once it is inserted.
|
||||||
|
pub fn entity_id(&self) -> EntityId {
|
||||||
|
self.0.entity_id()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This trait is used for the different visual contexts in GPUI that
|
||||||
|
/// require a window to be present.
|
||||||
|
pub trait VisualContext: Context {
|
||||||
|
/// Construct a new view in the window referenced by this context.
|
||||||
|
fn new_view<V>(
|
||||||
|
&mut self,
|
||||||
|
build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V,
|
||||||
|
) -> Self::Result<View<V>>
|
||||||
|
where
|
||||||
|
V: 'static + Render;
|
||||||
|
|
||||||
|
/// Update a view with the given callback
|
||||||
|
fn update_view<V: 'static, R>(
|
||||||
|
&mut self,
|
||||||
|
view: &View<V>,
|
||||||
|
update: impl FnOnce(&mut V, &mut ViewContext<'_, V>) -> R,
|
||||||
|
) -> Self::Result<R>;
|
||||||
|
|
||||||
|
/// Replace the root view of a window with a new view.
|
||||||
|
fn replace_root_view<V>(
|
||||||
|
&mut self,
|
||||||
|
build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V,
|
||||||
|
) -> Self::Result<View<V>>
|
||||||
|
where
|
||||||
|
V: 'static + Render;
|
||||||
|
|
||||||
|
/// Focus a view in the window, if it implements the [`FocusableView`] trait.
|
||||||
|
fn focus_view<V>(&mut self, view: &View<V>) -> Self::Result<()>
|
||||||
|
where
|
||||||
|
V: FocusableView;
|
||||||
|
|
||||||
|
/// Dismiss a view in the window, if it implements the [`ManagedView`] trait.
|
||||||
|
fn dismiss_view<V>(&mut self, view: &View<V>) -> Self::Result<()>
|
||||||
|
where
|
||||||
|
V: ManagedView;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A trait that allows models and views to be interchangeable in certain operations
|
||||||
|
pub trait Entity<T>: Sealed {
|
||||||
|
/// The weak reference type for this entity.
|
||||||
|
type Weak: 'static;
|
||||||
|
|
||||||
|
/// The ID for this entity
|
||||||
|
fn entity_id(&self) -> EntityId;
|
||||||
|
|
||||||
|
/// Downgrade this entity to a weak reference.
|
||||||
|
fn downgrade(&self) -> Self::Weak;
|
||||||
|
|
||||||
|
/// Upgrade this entity from a weak reference.
|
||||||
|
fn upgrade_from(weak: &Self::Weak) -> Option<Self>
|
||||||
|
where
|
||||||
|
Self: Sized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A trait for tying together the types of a GPUI entity and the events it can
|
||||||
|
/// emit.
|
||||||
|
pub trait EventEmitter<E: Any>: 'static {}
|
||||||
|
|
||||||
|
/// A helper trait for auto-implementing certain methods on contexts that
|
||||||
|
/// can be used interchangeably.
|
||||||
|
pub trait BorrowAppContext {
|
||||||
|
/// Set a global value on the context.
|
||||||
|
fn set_global<T: Global>(&mut self, global: T);
|
||||||
|
/// Updates the global state of the given type.
|
||||||
|
fn update_global<G, R>(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R
|
||||||
|
where
|
||||||
|
G: Global;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C> BorrowAppContext for C
|
||||||
|
where
|
||||||
|
C: BorrowMut<AppContext>,
|
||||||
|
{
|
||||||
|
fn set_global<G: Global>(&mut self, global: G) {
|
||||||
|
self.borrow_mut().set_global(global)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_global<G, R>(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R
|
||||||
|
where
|
||||||
|
G: Global,
|
||||||
|
{
|
||||||
|
let mut global = self.borrow_mut().lease_global::<G>();
|
||||||
|
let result = f(&mut global, self);
|
||||||
|
self.borrow_mut().end_global_lease(global);
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A flatten equivalent for anyhow `Result`s.
|
||||||
|
pub trait Flatten<T> {
|
||||||
|
/// Convert this type into a simple `Result<T>`.
|
||||||
|
fn flatten(self) -> Result<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Flatten<T> for Result<Result<T>> {
|
||||||
|
fn flatten(self) -> Result<T> {
|
||||||
|
self?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Flatten<T> for Result<T> {
|
||||||
|
fn flatten(self) -> Result<T> {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A marker trait for types that can be stored in GPUI's global state.
|
||||||
|
///
|
||||||
|
/// This trait exists to provide type-safe access to globals by restricting
|
||||||
|
/// the scope from which they can be accessed. For instance, the actual type
|
||||||
|
/// that implements [`Global`] can be private, with public accessor functions
|
||||||
|
/// that enforce correct usage.
|
||||||
|
///
|
||||||
|
/// Implement this on types you want to store in the context as a global.
|
||||||
|
pub trait Global: 'static {
|
||||||
|
// This trait is intentionally left empty, by virtue of being a marker trait.
|
||||||
|
}
|
125
crates/ming/src/input.rs
Normal file
125
crates/ming/src/input.rs
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
use crate::{Bounds, InputHandler, Pixels, View, ViewContext, WindowContext};
|
||||||
|
use std::ops::Range;
|
||||||
|
|
||||||
|
/// Implement this trait to allow views to handle textual input when implementing an editor, field, etc.
|
||||||
|
///
|
||||||
|
/// Once your view implements this trait, you can use it to construct an [`ElementInputHandler<V>`].
|
||||||
|
/// This input handler can then be assigned during paint by calling [`WindowContext::handle_input`].
|
||||||
|
///
|
||||||
|
/// See [`InputHandler`] for details on how to implement each method.
|
||||||
|
pub trait ViewInputHandler: 'static + Sized {
|
||||||
|
/// See [`InputHandler::text_for_range`] for details
|
||||||
|
fn text_for_range(&mut self, range: Range<usize>, cx: &mut ViewContext<Self>)
|
||||||
|
-> Option<String>;
|
||||||
|
|
||||||
|
/// See [`InputHandler::selected_text_range`] for details
|
||||||
|
fn selected_text_range(&mut self, cx: &mut ViewContext<Self>) -> Option<Range<usize>>;
|
||||||
|
|
||||||
|
/// See [`InputHandler::marked_text_range`] for details
|
||||||
|
fn marked_text_range(&self, cx: &mut ViewContext<Self>) -> Option<Range<usize>>;
|
||||||
|
|
||||||
|
/// See [`InputHandler::unmark_text`] for details
|
||||||
|
fn unmark_text(&mut self, cx: &mut ViewContext<Self>);
|
||||||
|
|
||||||
|
/// See [`InputHandler::replace_text_in_range`] for details
|
||||||
|
fn replace_text_in_range(
|
||||||
|
&mut self,
|
||||||
|
range: Option<Range<usize>>,
|
||||||
|
text: &str,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// See [`InputHandler::replace_and_mark_text_in_range`] for details
|
||||||
|
fn replace_and_mark_text_in_range(
|
||||||
|
&mut self,
|
||||||
|
range: Option<Range<usize>>,
|
||||||
|
new_text: &str,
|
||||||
|
new_selected_range: Option<Range<usize>>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// See [`InputHandler::bounds_for_range`] for details
|
||||||
|
fn bounds_for_range(
|
||||||
|
&mut self,
|
||||||
|
range_utf16: Range<usize>,
|
||||||
|
element_bounds: Bounds<Pixels>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Option<Bounds<Pixels>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The canonical implementation of [`PlatformInputHandler`]. Call [`WindowContext::handle_input`]
|
||||||
|
/// with an instance during your element's paint.
|
||||||
|
pub struct ElementInputHandler<V> {
|
||||||
|
view: View<V>,
|
||||||
|
element_bounds: Bounds<Pixels>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V: 'static> ElementInputHandler<V> {
|
||||||
|
/// Used in [`Element::paint`][element_paint] with the element's bounds and a view context for its
|
||||||
|
/// containing view.
|
||||||
|
///
|
||||||
|
/// [element_paint]: crate::Element::paint
|
||||||
|
pub fn new(element_bounds: Bounds<Pixels>, view: View<V>) -> Self {
|
||||||
|
ElementInputHandler {
|
||||||
|
view,
|
||||||
|
element_bounds,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V: ViewInputHandler> InputHandler for ElementInputHandler<V> {
|
||||||
|
fn selected_text_range(&mut self, cx: &mut WindowContext) -> Option<Range<usize>> {
|
||||||
|
self.view
|
||||||
|
.update(cx, |view, cx| view.selected_text_range(cx))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn marked_text_range(&mut self, cx: &mut WindowContext) -> Option<Range<usize>> {
|
||||||
|
self.view.update(cx, |view, cx| view.marked_text_range(cx))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn text_for_range(
|
||||||
|
&mut self,
|
||||||
|
range_utf16: Range<usize>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> Option<String> {
|
||||||
|
self.view
|
||||||
|
.update(cx, |view, cx| view.text_for_range(range_utf16, cx))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replace_text_in_range(
|
||||||
|
&mut self,
|
||||||
|
replacement_range: Option<Range<usize>>,
|
||||||
|
text: &str,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) {
|
||||||
|
self.view.update(cx, |view, cx| {
|
||||||
|
view.replace_text_in_range(replacement_range, text, cx)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replace_and_mark_text_in_range(
|
||||||
|
&mut self,
|
||||||
|
range_utf16: Option<Range<usize>>,
|
||||||
|
new_text: &str,
|
||||||
|
new_selected_range: Option<Range<usize>>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) {
|
||||||
|
self.view.update(cx, |view, cx| {
|
||||||
|
view.replace_and_mark_text_in_range(range_utf16, new_text, new_selected_range, cx)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unmark_text(&mut self, cx: &mut WindowContext) {
|
||||||
|
self.view.update(cx, |view, cx| view.unmark_text(cx));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bounds_for_range(
|
||||||
|
&mut self,
|
||||||
|
range_utf16: Range<usize>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> Option<Bounds<Pixels>> {
|
||||||
|
self.view.update(cx, |view, cx| {
|
||||||
|
view.bounds_for_range(range_utf16, self.element_bounds, cx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
510
crates/ming/src/interactive.rs
Normal file
510
crates/ming/src/interactive.rs
Normal file
|
@ -0,0 +1,510 @@
|
||||||
|
use crate::{
|
||||||
|
point, seal::Sealed, Empty, IntoElement, Keystroke, Modifiers, Pixels, Point, Render,
|
||||||
|
ViewContext,
|
||||||
|
};
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
use std::{any::Any, fmt::Debug, ops::Deref, path::PathBuf};
|
||||||
|
|
||||||
|
/// An event from a platform input source.
|
||||||
|
pub trait InputEvent: Sealed + 'static {
|
||||||
|
/// Convert this event into the platform input enum.
|
||||||
|
fn to_platform_input(self) -> PlatformInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A key event from the platform.
|
||||||
|
pub trait KeyEvent: InputEvent {}
|
||||||
|
|
||||||
|
/// A mouse event from the platform.
|
||||||
|
pub trait MouseEvent: InputEvent {}
|
||||||
|
|
||||||
|
/// The key down event equivalent for the platform.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct KeyDownEvent {
|
||||||
|
/// The keystroke that was generated.
|
||||||
|
pub keystroke: Keystroke,
|
||||||
|
|
||||||
|
/// Whether the key is currently held down.
|
||||||
|
pub is_held: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sealed for KeyDownEvent {}
|
||||||
|
impl InputEvent for KeyDownEvent {
|
||||||
|
fn to_platform_input(self) -> PlatformInput {
|
||||||
|
PlatformInput::KeyDown(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl KeyEvent for KeyDownEvent {}
|
||||||
|
|
||||||
|
/// The key up event equivalent for the platform.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct KeyUpEvent {
|
||||||
|
/// The keystroke that was released.
|
||||||
|
pub keystroke: Keystroke,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sealed for KeyUpEvent {}
|
||||||
|
impl InputEvent for KeyUpEvent {
|
||||||
|
fn to_platform_input(self) -> PlatformInput {
|
||||||
|
PlatformInput::KeyUp(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl KeyEvent for KeyUpEvent {}
|
||||||
|
|
||||||
|
/// The modifiers changed event equivalent for the platform.
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub struct ModifiersChangedEvent {
|
||||||
|
/// The new state of the modifier keys
|
||||||
|
pub modifiers: Modifiers,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sealed for ModifiersChangedEvent {}
|
||||||
|
impl InputEvent for ModifiersChangedEvent {
|
||||||
|
fn to_platform_input(self) -> PlatformInput {
|
||||||
|
PlatformInput::ModifiersChanged(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl KeyEvent for ModifiersChangedEvent {}
|
||||||
|
|
||||||
|
impl Deref for ModifiersChangedEvent {
|
||||||
|
type Target = Modifiers;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.modifiers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The phase of a touch motion event.
|
||||||
|
/// Based on the winit enum of the same name.
|
||||||
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
|
pub enum TouchPhase {
|
||||||
|
/// The touch started.
|
||||||
|
Started,
|
||||||
|
/// The touch event is moving.
|
||||||
|
#[default]
|
||||||
|
Moved,
|
||||||
|
/// The touch phase has ended
|
||||||
|
Ended,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A mouse down event from the platform
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub struct MouseDownEvent {
|
||||||
|
/// Which mouse button was pressed.
|
||||||
|
pub button: MouseButton,
|
||||||
|
|
||||||
|
/// The position of the mouse on the window.
|
||||||
|
pub position: Point<Pixels>,
|
||||||
|
|
||||||
|
/// The modifiers that were held down when the mouse was pressed.
|
||||||
|
pub modifiers: Modifiers,
|
||||||
|
|
||||||
|
/// The number of times the button has been clicked.
|
||||||
|
pub click_count: usize,
|
||||||
|
|
||||||
|
/// Whether this is the first, focusing click.
|
||||||
|
pub first_mouse: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sealed for MouseDownEvent {}
|
||||||
|
impl InputEvent for MouseDownEvent {
|
||||||
|
fn to_platform_input(self) -> PlatformInput {
|
||||||
|
PlatformInput::MouseDown(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl MouseEvent for MouseDownEvent {}
|
||||||
|
|
||||||
|
/// A mouse up event from the platform
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub struct MouseUpEvent {
|
||||||
|
/// Which mouse button was released.
|
||||||
|
pub button: MouseButton,
|
||||||
|
|
||||||
|
/// The position of the mouse on the window.
|
||||||
|
pub position: Point<Pixels>,
|
||||||
|
|
||||||
|
/// The modifiers that were held down when the mouse was released.
|
||||||
|
pub modifiers: Modifiers,
|
||||||
|
|
||||||
|
/// The number of times the button has been clicked.
|
||||||
|
pub click_count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sealed for MouseUpEvent {}
|
||||||
|
impl InputEvent for MouseUpEvent {
|
||||||
|
fn to_platform_input(self) -> PlatformInput {
|
||||||
|
PlatformInput::MouseUp(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl MouseEvent for MouseUpEvent {}
|
||||||
|
|
||||||
|
/// A click event, generated when a mouse button is pressed and released.
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub struct ClickEvent {
|
||||||
|
/// The mouse event when the button was pressed.
|
||||||
|
pub down: MouseDownEvent,
|
||||||
|
|
||||||
|
/// The mouse event when the button was released.
|
||||||
|
pub up: MouseUpEvent,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An enum representing the mouse button that was pressed.
|
||||||
|
#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)]
|
||||||
|
pub enum MouseButton {
|
||||||
|
/// The left mouse button.
|
||||||
|
Left,
|
||||||
|
|
||||||
|
/// The right mouse button.
|
||||||
|
Right,
|
||||||
|
|
||||||
|
/// The middle mouse button.
|
||||||
|
Middle,
|
||||||
|
|
||||||
|
/// A navigation button, such as back or forward.
|
||||||
|
Navigate(NavigationDirection),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MouseButton {
|
||||||
|
/// Get all the mouse buttons in a list.
|
||||||
|
pub fn all() -> Vec<Self> {
|
||||||
|
vec![
|
||||||
|
MouseButton::Left,
|
||||||
|
MouseButton::Right,
|
||||||
|
MouseButton::Middle,
|
||||||
|
MouseButton::Navigate(NavigationDirection::Back),
|
||||||
|
MouseButton::Navigate(NavigationDirection::Forward),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MouseButton {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Left
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A navigation direction, such as back or forward.
|
||||||
|
#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)]
|
||||||
|
pub enum NavigationDirection {
|
||||||
|
/// The back button.
|
||||||
|
Back,
|
||||||
|
|
||||||
|
/// The forward button.
|
||||||
|
Forward,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for NavigationDirection {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Back
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A mouse move event from the platform
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub struct MouseMoveEvent {
|
||||||
|
/// The position of the mouse on the window.
|
||||||
|
pub position: Point<Pixels>,
|
||||||
|
|
||||||
|
/// The mouse button that was pressed, if any.
|
||||||
|
pub pressed_button: Option<MouseButton>,
|
||||||
|
|
||||||
|
/// The modifiers that were held down when the mouse was moved.
|
||||||
|
pub modifiers: Modifiers,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sealed for MouseMoveEvent {}
|
||||||
|
impl InputEvent for MouseMoveEvent {
|
||||||
|
fn to_platform_input(self) -> PlatformInput {
|
||||||
|
PlatformInput::MouseMove(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl MouseEvent for MouseMoveEvent {}
|
||||||
|
|
||||||
|
impl MouseMoveEvent {
|
||||||
|
/// Returns true if the left mouse button is currently held down.
|
||||||
|
pub fn dragging(&self) -> bool {
|
||||||
|
self.pressed_button == Some(MouseButton::Left)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A mouse wheel event from the platform
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub struct ScrollWheelEvent {
|
||||||
|
/// The position of the mouse on the window.
|
||||||
|
pub position: Point<Pixels>,
|
||||||
|
|
||||||
|
/// The change in scroll wheel position for this event.
|
||||||
|
pub delta: ScrollDelta,
|
||||||
|
|
||||||
|
/// The modifiers that were held down when the mouse was moved.
|
||||||
|
pub modifiers: Modifiers,
|
||||||
|
|
||||||
|
/// The phase of the touch event.
|
||||||
|
pub touch_phase: TouchPhase,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sealed for ScrollWheelEvent {}
|
||||||
|
impl InputEvent for ScrollWheelEvent {
|
||||||
|
fn to_platform_input(self) -> PlatformInput {
|
||||||
|
PlatformInput::ScrollWheel(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl MouseEvent for ScrollWheelEvent {}
|
||||||
|
|
||||||
|
impl Deref for ScrollWheelEvent {
|
||||||
|
type Target = Modifiers;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.modifiers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The scroll delta for a scroll wheel event.
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub enum ScrollDelta {
|
||||||
|
/// An exact scroll delta in pixels.
|
||||||
|
Pixels(Point<Pixels>),
|
||||||
|
/// An inexact scroll delta in lines.
|
||||||
|
Lines(Point<f32>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ScrollDelta {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Lines(Default::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScrollDelta {
|
||||||
|
/// Returns true if this is a precise scroll delta in pixels.
|
||||||
|
pub fn precise(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
ScrollDelta::Pixels(_) => true,
|
||||||
|
ScrollDelta::Lines(_) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts this scroll event into exact pixels.
|
||||||
|
pub fn pixel_delta(&self, line_height: Pixels) -> Point<Pixels> {
|
||||||
|
match self {
|
||||||
|
ScrollDelta::Pixels(delta) => *delta,
|
||||||
|
ScrollDelta::Lines(delta) => point(line_height * delta.x, line_height * delta.y),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Combines two scroll deltas into one.
|
||||||
|
pub fn coalesce(self, other: ScrollDelta) -> ScrollDelta {
|
||||||
|
match (self, other) {
|
||||||
|
(ScrollDelta::Pixels(px_a), ScrollDelta::Pixels(px_b)) => {
|
||||||
|
ScrollDelta::Pixels(px_a + px_b)
|
||||||
|
}
|
||||||
|
|
||||||
|
(ScrollDelta::Lines(lines_a), ScrollDelta::Lines(lines_b)) => {
|
||||||
|
ScrollDelta::Lines(lines_a + lines_b)
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => other,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A mouse exit event from the platform, generated when the mouse leaves the window.
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub struct MouseExitEvent {
|
||||||
|
/// The position of the mouse relative to the window.
|
||||||
|
pub position: Point<Pixels>,
|
||||||
|
/// The mouse button that was pressed, if any.
|
||||||
|
pub pressed_button: Option<MouseButton>,
|
||||||
|
/// The modifiers that were held down when the mouse was moved.
|
||||||
|
pub modifiers: Modifiers,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sealed for MouseExitEvent {}
|
||||||
|
impl InputEvent for MouseExitEvent {
|
||||||
|
fn to_platform_input(self) -> PlatformInput {
|
||||||
|
PlatformInput::MouseExited(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl MouseEvent for MouseExitEvent {}
|
||||||
|
|
||||||
|
impl Deref for MouseExitEvent {
|
||||||
|
type Target = Modifiers;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.modifiers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A collection of paths from the platform, such as from a file drop.
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct ExternalPaths(pub(crate) SmallVec<[PathBuf; 2]>);
|
||||||
|
|
||||||
|
impl ExternalPaths {
|
||||||
|
/// Convert this collection of paths into a slice.
|
||||||
|
pub fn paths(&self) -> &[PathBuf] {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for ExternalPaths {
|
||||||
|
fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
|
// the platform will render icons for the dragged files
|
||||||
|
Empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A file drop event from the platform, generated when files are dragged and dropped onto the window.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum FileDropEvent {
|
||||||
|
/// The files have entered the window.
|
||||||
|
Entered {
|
||||||
|
/// The position of the mouse relative to the window.
|
||||||
|
position: Point<Pixels>,
|
||||||
|
/// The paths of the files that are being dragged.
|
||||||
|
paths: ExternalPaths,
|
||||||
|
},
|
||||||
|
/// The files are being dragged over the window
|
||||||
|
Pending {
|
||||||
|
/// The position of the mouse relative to the window.
|
||||||
|
position: Point<Pixels>,
|
||||||
|
},
|
||||||
|
/// The files have been dropped onto the window.
|
||||||
|
Submit {
|
||||||
|
/// The position of the mouse relative to the window.
|
||||||
|
position: Point<Pixels>,
|
||||||
|
},
|
||||||
|
/// The user has stopped dragging the files over the window.
|
||||||
|
Exited,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sealed for FileDropEvent {}
|
||||||
|
impl InputEvent for FileDropEvent {
|
||||||
|
fn to_platform_input(self) -> PlatformInput {
|
||||||
|
PlatformInput::FileDrop(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl MouseEvent for FileDropEvent {}
|
||||||
|
|
||||||
|
/// An enum corresponding to all kinds of platform input events.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum PlatformInput {
|
||||||
|
/// A key was pressed.
|
||||||
|
KeyDown(KeyDownEvent),
|
||||||
|
/// A key was released.
|
||||||
|
KeyUp(KeyUpEvent),
|
||||||
|
/// The keyboard modifiers were changed.
|
||||||
|
ModifiersChanged(ModifiersChangedEvent),
|
||||||
|
/// The mouse was pressed.
|
||||||
|
MouseDown(MouseDownEvent),
|
||||||
|
/// The mouse was released.
|
||||||
|
MouseUp(MouseUpEvent),
|
||||||
|
/// The mouse was moved.
|
||||||
|
MouseMove(MouseMoveEvent),
|
||||||
|
/// The mouse exited the window.
|
||||||
|
MouseExited(MouseExitEvent),
|
||||||
|
/// The scroll wheel was used.
|
||||||
|
ScrollWheel(ScrollWheelEvent),
|
||||||
|
/// Files were dragged and dropped onto the window.
|
||||||
|
FileDrop(FileDropEvent),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlatformInput {
|
||||||
|
pub(crate) fn mouse_event(&self) -> Option<&dyn Any> {
|
||||||
|
match self {
|
||||||
|
PlatformInput::KeyDown { .. } => None,
|
||||||
|
PlatformInput::KeyUp { .. } => None,
|
||||||
|
PlatformInput::ModifiersChanged { .. } => None,
|
||||||
|
PlatformInput::MouseDown(event) => Some(event),
|
||||||
|
PlatformInput::MouseUp(event) => Some(event),
|
||||||
|
PlatformInput::MouseMove(event) => Some(event),
|
||||||
|
PlatformInput::MouseExited(event) => Some(event),
|
||||||
|
PlatformInput::ScrollWheel(event) => Some(event),
|
||||||
|
PlatformInput::FileDrop(event) => Some(event),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn keyboard_event(&self) -> Option<&dyn Any> {
|
||||||
|
match self {
|
||||||
|
PlatformInput::KeyDown(event) => Some(event),
|
||||||
|
PlatformInput::KeyUp(event) => Some(event),
|
||||||
|
PlatformInput::ModifiersChanged(event) => Some(event),
|
||||||
|
PlatformInput::MouseDown(_) => None,
|
||||||
|
PlatformInput::MouseUp(_) => None,
|
||||||
|
PlatformInput::MouseMove(_) => None,
|
||||||
|
PlatformInput::MouseExited(_) => None,
|
||||||
|
PlatformInput::ScrollWheel(_) => None,
|
||||||
|
PlatformInput::FileDrop(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
self as gpui, div, Element, FocusHandle, InteractiveElement, IntoElement, KeyBinding,
|
||||||
|
Keystroke, ParentElement, Render, TestAppContext, VisualContext,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct TestView {
|
||||||
|
saw_key_down: bool,
|
||||||
|
saw_action: bool,
|
||||||
|
focus_handle: FocusHandle,
|
||||||
|
}
|
||||||
|
|
||||||
|
actions!(test, [TestAction]);
|
||||||
|
|
||||||
|
impl Render for TestView {
|
||||||
|
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl Element {
|
||||||
|
div().id("testview").child(
|
||||||
|
div()
|
||||||
|
.key_context("parent")
|
||||||
|
.on_key_down(cx.listener(|this, _, cx| {
|
||||||
|
cx.stop_propagation();
|
||||||
|
this.saw_key_down = true
|
||||||
|
}))
|
||||||
|
.on_action(
|
||||||
|
cx.listener(|this: &mut TestView, _: &TestAction, _| {
|
||||||
|
this.saw_action = true
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.key_context("nested")
|
||||||
|
.track_focus(&self.focus_handle)
|
||||||
|
.into_element(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn test_on_events(cx: &mut TestAppContext) {
|
||||||
|
let window = cx.update(|cx| {
|
||||||
|
cx.open_window(Default::default(), |cx| {
|
||||||
|
cx.new_view(|cx| TestView {
|
||||||
|
saw_key_down: false,
|
||||||
|
saw_action: false,
|
||||||
|
focus_handle: cx.focus_handle(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.update(|cx| {
|
||||||
|
cx.bind_keys(vec![KeyBinding::new("ctrl-g", TestAction, Some("parent"))]);
|
||||||
|
});
|
||||||
|
|
||||||
|
window
|
||||||
|
.update(cx, |test_view, cx| cx.focus(&test_view.focus_handle))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
cx.dispatch_keystroke(*window, Keystroke::parse("a").unwrap());
|
||||||
|
cx.dispatch_keystroke(*window, Keystroke::parse("ctrl-g").unwrap());
|
||||||
|
|
||||||
|
window
|
||||||
|
.update(cx, |test_view, _| {
|
||||||
|
assert!(test_view.saw_key_down || test_view.saw_action);
|
||||||
|
assert!(test_view.saw_key_down);
|
||||||
|
assert!(test_view.saw_action);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
608
crates/ming/src/key_dispatch.rs
Normal file
608
crates/ming/src/key_dispatch.rs
Normal file
|
@ -0,0 +1,608 @@
|
||||||
|
/// KeyDispatch is where GPUI deals with binding actions to key events.
|
||||||
|
///
|
||||||
|
/// The key pieces to making a key binding work are to define an action,
|
||||||
|
/// implement a method that takes that action as a type parameter,
|
||||||
|
/// and then to register the action during render on a focused node
|
||||||
|
/// with a keymap context:
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// actions!(editor,[Undo, Redo]);;
|
||||||
|
///
|
||||||
|
/// impl Editor {
|
||||||
|
/// fn undo(&mut self, _: &Undo, _cx: &mut ViewContext<Self>) { ... }
|
||||||
|
/// fn redo(&mut self, _: &Redo, _cx: &mut ViewContext<Self>) { ... }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// impl Render for Editor {
|
||||||
|
/// fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
|
/// div()
|
||||||
|
/// .track_focus(&self.focus_handle)
|
||||||
|
/// .keymap_context("Editor")
|
||||||
|
/// .on_action(cx.listener(Editor::undo))
|
||||||
|
/// .on_action(cx.listener(Editor::redo))
|
||||||
|
/// ...
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///```
|
||||||
|
///
|
||||||
|
/// The keybindings themselves are managed independently by calling cx.bind_keys().
|
||||||
|
/// (Though mostly when developing Zed itself, you just need to add a new line to
|
||||||
|
/// assets/keymaps/default.json).
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// cx.bind_keys([
|
||||||
|
/// KeyBinding::new("cmd-z", Editor::undo, Some("Editor")),
|
||||||
|
/// KeyBinding::new("cmd-shift-z", Editor::redo, Some("Editor")),
|
||||||
|
/// ])
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// With all of this in place, GPUI will ensure that if you have an Editor that contains
|
||||||
|
/// the focus, hitting cmd-z will Undo.
|
||||||
|
///
|
||||||
|
/// In real apps, it is a little more complicated than this, because typically you have
|
||||||
|
/// several nested views that each register keyboard handlers. In this case action matching
|
||||||
|
/// bubbles up from the bottom. For example in Zed, the Workspace is the top-level view, which contains Pane's, which contain Editors. If there are conflicting keybindings defined
|
||||||
|
/// then the Editor's bindings take precedence over the Pane's bindings, which take precedence over the Workspace.
|
||||||
|
///
|
||||||
|
/// In GPUI, keybindings are not limited to just single keystrokes, you can define
|
||||||
|
/// sequences by separating the keys with a space:
|
||||||
|
///
|
||||||
|
/// KeyBinding::new("cmd-k left", pane::SplitLeft, Some("Pane"))
|
||||||
|
///
|
||||||
|
use crate::{
|
||||||
|
Action, ActionRegistry, DispatchPhase, EntityId, FocusId, KeyBinding, KeyContext, Keymap,
|
||||||
|
KeymatchResult, Keystroke, KeystrokeMatcher, ModifiersChangedEvent, WindowContext,
|
||||||
|
};
|
||||||
|
use collections::FxHashMap;
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
use std::{
|
||||||
|
any::{Any, TypeId},
|
||||||
|
cell::RefCell,
|
||||||
|
mem,
|
||||||
|
ops::Range,
|
||||||
|
rc::Rc,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
|
||||||
|
pub(crate) struct DispatchNodeId(usize);
|
||||||
|
|
||||||
|
pub(crate) struct DispatchTree {
|
||||||
|
node_stack: Vec<DispatchNodeId>,
|
||||||
|
pub(crate) context_stack: Vec<KeyContext>,
|
||||||
|
view_stack: Vec<EntityId>,
|
||||||
|
nodes: Vec<DispatchNode>,
|
||||||
|
focusable_node_ids: FxHashMap<FocusId, DispatchNodeId>,
|
||||||
|
view_node_ids: FxHashMap<EntityId, DispatchNodeId>,
|
||||||
|
keystroke_matchers: FxHashMap<SmallVec<[KeyContext; 4]>, KeystrokeMatcher>,
|
||||||
|
keymap: Rc<RefCell<Keymap>>,
|
||||||
|
action_registry: Rc<ActionRegistry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub(crate) struct DispatchNode {
|
||||||
|
pub key_listeners: Vec<KeyListener>,
|
||||||
|
pub action_listeners: Vec<DispatchActionListener>,
|
||||||
|
pub modifiers_changed_listeners: Vec<ModifiersChangedListener>,
|
||||||
|
pub context: Option<KeyContext>,
|
||||||
|
pub focus_id: Option<FocusId>,
|
||||||
|
view_id: Option<EntityId>,
|
||||||
|
parent: Option<DispatchNodeId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct ReusedSubtree {
|
||||||
|
old_range: Range<usize>,
|
||||||
|
new_range: Range<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReusedSubtree {
|
||||||
|
pub fn refresh_node_id(&self, node_id: DispatchNodeId) -> DispatchNodeId {
|
||||||
|
debug_assert!(
|
||||||
|
self.old_range.contains(&node_id.0),
|
||||||
|
"node {} was not part of the reused subtree {:?}",
|
||||||
|
node_id.0,
|
||||||
|
self.old_range
|
||||||
|
);
|
||||||
|
DispatchNodeId((node_id.0 - self.old_range.start) + self.new_range.start)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type KeyListener = Rc<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext)>;
|
||||||
|
type ModifiersChangedListener = Rc<dyn Fn(&ModifiersChangedEvent, &mut WindowContext)>;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct DispatchActionListener {
|
||||||
|
pub(crate) action_type: TypeId,
|
||||||
|
pub(crate) listener: Rc<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DispatchTree {
|
||||||
|
pub fn new(keymap: Rc<RefCell<Keymap>>, action_registry: Rc<ActionRegistry>) -> Self {
|
||||||
|
Self {
|
||||||
|
node_stack: Vec::new(),
|
||||||
|
context_stack: Vec::new(),
|
||||||
|
view_stack: Vec::new(),
|
||||||
|
nodes: Vec::new(),
|
||||||
|
focusable_node_ids: FxHashMap::default(),
|
||||||
|
view_node_ids: FxHashMap::default(),
|
||||||
|
keystroke_matchers: FxHashMap::default(),
|
||||||
|
keymap,
|
||||||
|
action_registry,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.node_stack.clear();
|
||||||
|
self.context_stack.clear();
|
||||||
|
self.view_stack.clear();
|
||||||
|
self.nodes.clear();
|
||||||
|
self.focusable_node_ids.clear();
|
||||||
|
self.view_node_ids.clear();
|
||||||
|
self.keystroke_matchers.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.nodes.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push_node(&mut self) -> DispatchNodeId {
|
||||||
|
let parent = self.node_stack.last().copied();
|
||||||
|
let node_id = DispatchNodeId(self.nodes.len());
|
||||||
|
|
||||||
|
self.nodes.push(DispatchNode {
|
||||||
|
parent,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
self.node_stack.push(node_id);
|
||||||
|
node_id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_active_node(&mut self, node_id: DispatchNodeId) {
|
||||||
|
let next_node_parent = self.nodes[node_id.0].parent;
|
||||||
|
while self.node_stack.last().copied() != next_node_parent && !self.node_stack.is_empty() {
|
||||||
|
self.pop_node();
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.node_stack.last().copied() == next_node_parent {
|
||||||
|
self.node_stack.push(node_id);
|
||||||
|
let active_node = &self.nodes[node_id.0];
|
||||||
|
if let Some(view_id) = active_node.view_id {
|
||||||
|
self.view_stack.push(view_id)
|
||||||
|
}
|
||||||
|
if let Some(context) = active_node.context.clone() {
|
||||||
|
self.context_stack.push(context);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debug_assert_eq!(self.node_stack.len(), 0);
|
||||||
|
|
||||||
|
let mut current_node_id = Some(node_id);
|
||||||
|
while let Some(node_id) = current_node_id {
|
||||||
|
let node = &self.nodes[node_id.0];
|
||||||
|
if let Some(context) = node.context.clone() {
|
||||||
|
self.context_stack.push(context);
|
||||||
|
}
|
||||||
|
if node.view_id.is_some() {
|
||||||
|
self.view_stack.push(node.view_id.unwrap());
|
||||||
|
}
|
||||||
|
self.node_stack.push(node_id);
|
||||||
|
current_node_id = node.parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.context_stack.reverse();
|
||||||
|
self.view_stack.reverse();
|
||||||
|
self.node_stack.reverse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_key_context(&mut self, context: KeyContext) {
|
||||||
|
self.active_node().context = Some(context.clone());
|
||||||
|
self.context_stack.push(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_focus_id(&mut self, focus_id: FocusId) {
|
||||||
|
let node_id = *self.node_stack.last().unwrap();
|
||||||
|
self.nodes[node_id.0].focus_id = Some(focus_id);
|
||||||
|
self.focusable_node_ids.insert(focus_id, node_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parent_view_id(&mut self) -> Option<EntityId> {
|
||||||
|
self.view_stack.last().copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_view_id(&mut self, view_id: EntityId) {
|
||||||
|
if self.view_stack.last().copied() != Some(view_id) {
|
||||||
|
let node_id = *self.node_stack.last().unwrap();
|
||||||
|
self.nodes[node_id.0].view_id = Some(view_id);
|
||||||
|
self.view_node_ids.insert(view_id, node_id);
|
||||||
|
self.view_stack.push(view_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pop_node(&mut self) {
|
||||||
|
let node = &self.nodes[self.active_node_id().unwrap().0];
|
||||||
|
if node.context.is_some() {
|
||||||
|
self.context_stack.pop();
|
||||||
|
}
|
||||||
|
if node.view_id.is_some() {
|
||||||
|
self.view_stack.pop();
|
||||||
|
}
|
||||||
|
self.node_stack.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn move_node(&mut self, source: &mut DispatchNode) {
|
||||||
|
self.push_node();
|
||||||
|
if let Some(context) = source.context.clone() {
|
||||||
|
self.set_key_context(context);
|
||||||
|
}
|
||||||
|
if let Some(focus_id) = source.focus_id {
|
||||||
|
self.set_focus_id(focus_id);
|
||||||
|
}
|
||||||
|
if let Some(view_id) = source.view_id {
|
||||||
|
self.set_view_id(view_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
let target = self.active_node();
|
||||||
|
target.key_listeners = mem::take(&mut source.key_listeners);
|
||||||
|
target.action_listeners = mem::take(&mut source.action_listeners);
|
||||||
|
target.modifiers_changed_listeners = mem::take(&mut source.modifiers_changed_listeners);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reuse_subtree(&mut self, old_range: Range<usize>, source: &mut Self) -> ReusedSubtree {
|
||||||
|
let new_range = self.nodes.len()..self.nodes.len() + old_range.len();
|
||||||
|
|
||||||
|
let mut source_stack = vec![];
|
||||||
|
for (source_node_id, source_node) in source
|
||||||
|
.nodes
|
||||||
|
.iter_mut()
|
||||||
|
.enumerate()
|
||||||
|
.skip(old_range.start)
|
||||||
|
.take(old_range.len())
|
||||||
|
{
|
||||||
|
let source_node_id = DispatchNodeId(source_node_id);
|
||||||
|
while let Some(source_ancestor) = source_stack.last() {
|
||||||
|
if source_node.parent == Some(*source_ancestor) {
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
source_stack.pop();
|
||||||
|
self.pop_node();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
source_stack.push(source_node_id);
|
||||||
|
self.move_node(source_node);
|
||||||
|
}
|
||||||
|
|
||||||
|
while !source_stack.is_empty() {
|
||||||
|
source_stack.pop();
|
||||||
|
self.pop_node();
|
||||||
|
}
|
||||||
|
|
||||||
|
ReusedSubtree {
|
||||||
|
old_range,
|
||||||
|
new_range,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn truncate(&mut self, index: usize) {
|
||||||
|
for node in &self.nodes[index..] {
|
||||||
|
if let Some(focus_id) = node.focus_id {
|
||||||
|
self.focusable_node_ids.remove(&focus_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(view_id) = node.view_id {
|
||||||
|
self.view_node_ids.remove(&view_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.nodes.truncate(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_pending_keystrokes(&mut self) {
|
||||||
|
self.keystroke_matchers.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Preserve keystroke matchers from previous frames to support multi-stroke
|
||||||
|
/// bindings across multiple frames.
|
||||||
|
pub fn preserve_pending_keystrokes(&mut self, old_tree: &mut Self, focus_id: Option<FocusId>) {
|
||||||
|
if let Some(node_id) = focus_id.and_then(|focus_id| self.focusable_node_id(focus_id)) {
|
||||||
|
let dispatch_path = self.dispatch_path(node_id);
|
||||||
|
|
||||||
|
self.context_stack.clear();
|
||||||
|
for node_id in dispatch_path {
|
||||||
|
let node = self.node(node_id);
|
||||||
|
if let Some(context) = node.context.clone() {
|
||||||
|
self.context_stack.push(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((context_stack, matcher)) = old_tree
|
||||||
|
.keystroke_matchers
|
||||||
|
.remove_entry(self.context_stack.as_slice())
|
||||||
|
{
|
||||||
|
self.keystroke_matchers.insert(context_stack, matcher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_key_event(&mut self, listener: KeyListener) {
|
||||||
|
self.active_node().key_listeners.push(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_modifiers_changed(&mut self, listener: ModifiersChangedListener) {
|
||||||
|
self.active_node()
|
||||||
|
.modifiers_changed_listeners
|
||||||
|
.push(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_action(
|
||||||
|
&mut self,
|
||||||
|
action_type: TypeId,
|
||||||
|
listener: Rc<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext)>,
|
||||||
|
) {
|
||||||
|
self.active_node()
|
||||||
|
.action_listeners
|
||||||
|
.push(DispatchActionListener {
|
||||||
|
action_type,
|
||||||
|
listener,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn focus_contains(&self, parent: FocusId, child: FocusId) -> bool {
|
||||||
|
if parent == child {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(parent_node_id) = self.focusable_node_ids.get(&parent) {
|
||||||
|
let mut current_node_id = self.focusable_node_ids.get(&child).copied();
|
||||||
|
while let Some(node_id) = current_node_id {
|
||||||
|
if node_id == *parent_node_id {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
current_node_id = self.nodes[node_id.0].parent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn available_actions(&self, target: DispatchNodeId) -> Vec<Box<dyn Action>> {
|
||||||
|
let mut actions = Vec::<Box<dyn Action>>::new();
|
||||||
|
for node_id in self.dispatch_path(target) {
|
||||||
|
let node = &self.nodes[node_id.0];
|
||||||
|
for DispatchActionListener { action_type, .. } in &node.action_listeners {
|
||||||
|
if let Err(ix) = actions.binary_search_by_key(action_type, |a| a.as_any().type_id())
|
||||||
|
{
|
||||||
|
// Intentionally silence these errors without logging.
|
||||||
|
// If an action cannot be built by default, it's not available.
|
||||||
|
let action = self.action_registry.build_action_type(action_type).ok();
|
||||||
|
if let Some(action) = action {
|
||||||
|
actions.insert(ix, action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
actions
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_action_available(&self, action: &dyn Action, target: DispatchNodeId) -> bool {
|
||||||
|
for node_id in self.dispatch_path(target) {
|
||||||
|
let node = &self.nodes[node_id.0];
|
||||||
|
if node
|
||||||
|
.action_listeners
|
||||||
|
.iter()
|
||||||
|
.any(|listener| listener.action_type == action.as_any().type_id())
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bindings_for_action(
|
||||||
|
&self,
|
||||||
|
action: &dyn Action,
|
||||||
|
context_stack: &[KeyContext],
|
||||||
|
) -> Vec<KeyBinding> {
|
||||||
|
let keymap = self.keymap.borrow();
|
||||||
|
keymap
|
||||||
|
.bindings_for_action(action)
|
||||||
|
.filter(|binding| {
|
||||||
|
for i in 0..context_stack.len() {
|
||||||
|
let context = &context_stack[0..=i];
|
||||||
|
if keymap.binding_enabled(binding, context) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
// dispatch_key pushes the next keystroke into any key binding matchers.
|
||||||
|
// any matching bindings are returned in the order that they should be dispatched:
|
||||||
|
// * First by length of binding (so if you have a binding for "b" and "ab", the "ab" binding fires first)
|
||||||
|
// * Secondly by depth in the tree (so if Editor has a binding for "b" and workspace a
|
||||||
|
// binding for "b", the Editor action fires first).
|
||||||
|
pub fn dispatch_key(
|
||||||
|
&mut self,
|
||||||
|
keystroke: &Keystroke,
|
||||||
|
dispatch_path: &SmallVec<[DispatchNodeId; 32]>,
|
||||||
|
) -> KeymatchResult {
|
||||||
|
let mut bindings = SmallVec::<[KeyBinding; 1]>::new();
|
||||||
|
let mut pending = false;
|
||||||
|
|
||||||
|
let mut context_stack: SmallVec<[KeyContext; 4]> = SmallVec::new();
|
||||||
|
for node_id in dispatch_path {
|
||||||
|
let node = self.node(*node_id);
|
||||||
|
|
||||||
|
if let Some(context) = node.context.clone() {
|
||||||
|
context_stack.push(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while !context_stack.is_empty() {
|
||||||
|
let keystroke_matcher = self
|
||||||
|
.keystroke_matchers
|
||||||
|
.entry(context_stack.clone())
|
||||||
|
.or_insert_with(|| KeystrokeMatcher::new(self.keymap.clone()));
|
||||||
|
|
||||||
|
let result = keystroke_matcher.match_keystroke(keystroke, &context_stack);
|
||||||
|
if result.pending && !pending && !bindings.is_empty() {
|
||||||
|
context_stack.pop();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
pending = result.pending || pending;
|
||||||
|
for new_binding in result.bindings {
|
||||||
|
match bindings
|
||||||
|
.iter()
|
||||||
|
.position(|el| el.keystrokes.len() < new_binding.keystrokes.len())
|
||||||
|
{
|
||||||
|
Some(idx) => {
|
||||||
|
bindings.insert(idx, new_binding);
|
||||||
|
}
|
||||||
|
None => bindings.push(new_binding),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
context_stack.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
KeymatchResult { bindings, pending }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_pending_keystrokes(&self) -> bool {
|
||||||
|
self.keystroke_matchers
|
||||||
|
.iter()
|
||||||
|
.any(|(_, matcher)| matcher.has_pending_keystrokes())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dispatch_path(&self, target: DispatchNodeId) -> SmallVec<[DispatchNodeId; 32]> {
|
||||||
|
let mut dispatch_path: SmallVec<[DispatchNodeId; 32]> = SmallVec::new();
|
||||||
|
let mut current_node_id = Some(target);
|
||||||
|
while let Some(node_id) = current_node_id {
|
||||||
|
dispatch_path.push(node_id);
|
||||||
|
current_node_id = self.nodes[node_id.0].parent;
|
||||||
|
}
|
||||||
|
dispatch_path.reverse(); // Reverse the path so it goes from the root to the focused node.
|
||||||
|
dispatch_path
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn focus_path(&self, focus_id: FocusId) -> SmallVec<[FocusId; 8]> {
|
||||||
|
let mut focus_path: SmallVec<[FocusId; 8]> = SmallVec::new();
|
||||||
|
let mut current_node_id = self.focusable_node_ids.get(&focus_id).copied();
|
||||||
|
while let Some(node_id) = current_node_id {
|
||||||
|
let node = self.node(node_id);
|
||||||
|
if let Some(focus_id) = node.focus_id {
|
||||||
|
focus_path.push(focus_id);
|
||||||
|
}
|
||||||
|
current_node_id = node.parent;
|
||||||
|
}
|
||||||
|
focus_path.reverse(); // Reverse the path so it goes from the root to the focused node.
|
||||||
|
focus_path
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn view_path(&self, view_id: EntityId) -> SmallVec<[EntityId; 8]> {
|
||||||
|
let mut view_path: SmallVec<[EntityId; 8]> = SmallVec::new();
|
||||||
|
let mut current_node_id = self.view_node_ids.get(&view_id).copied();
|
||||||
|
while let Some(node_id) = current_node_id {
|
||||||
|
let node = self.node(node_id);
|
||||||
|
if let Some(view_id) = node.view_id {
|
||||||
|
view_path.push(view_id);
|
||||||
|
}
|
||||||
|
current_node_id = node.parent;
|
||||||
|
}
|
||||||
|
view_path.reverse(); // Reverse the path so it goes from the root to the view node.
|
||||||
|
view_path
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn node(&self, node_id: DispatchNodeId) -> &DispatchNode {
|
||||||
|
&self.nodes[node_id.0]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn active_node(&mut self) -> &mut DispatchNode {
|
||||||
|
let active_node_id = self.active_node_id().unwrap();
|
||||||
|
&mut self.nodes[active_node_id.0]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn focusable_node_id(&self, target: FocusId) -> Option<DispatchNodeId> {
|
||||||
|
self.focusable_node_ids.get(&target).copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn root_node_id(&self) -> DispatchNodeId {
|
||||||
|
debug_assert!(!self.nodes.is_empty());
|
||||||
|
DispatchNodeId(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn active_node_id(&self) -> Option<DispatchNodeId> {
|
||||||
|
self.node_stack.last().copied()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
|
use crate::{Action, ActionRegistry, DispatchTree, KeyBinding, KeyContext, Keymap};
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq)]
|
||||||
|
struct TestAction;
|
||||||
|
|
||||||
|
impl Action for TestAction {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"test::TestAction"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn debug_name() -> &'static str
|
||||||
|
where
|
||||||
|
Self: ::std::marker::Sized,
|
||||||
|
{
|
||||||
|
"test::TestAction"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn partial_eq(&self, action: &dyn Action) -> bool {
|
||||||
|
action
|
||||||
|
.as_any()
|
||||||
|
.downcast_ref::<Self>()
|
||||||
|
.map_or(false, |a| self == a)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn boxed_clone(&self) -> std::boxed::Box<dyn Action> {
|
||||||
|
Box::new(TestAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_any(&self) -> &dyn ::std::any::Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build(_value: serde_json::Value) -> anyhow::Result<Box<dyn Action>>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
Ok(Box::new(TestAction))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_keybinding_for_action_bounds() {
|
||||||
|
let keymap = Keymap::new(vec![KeyBinding::new(
|
||||||
|
"cmd-n",
|
||||||
|
TestAction,
|
||||||
|
Some("ProjectPanel"),
|
||||||
|
)]);
|
||||||
|
|
||||||
|
let mut registry = ActionRegistry::default();
|
||||||
|
|
||||||
|
registry.load_action::<TestAction>();
|
||||||
|
|
||||||
|
let keymap = Rc::new(RefCell::new(keymap));
|
||||||
|
|
||||||
|
let tree = DispatchTree::new(keymap, Rc::new(registry));
|
||||||
|
|
||||||
|
let contexts = vec![
|
||||||
|
KeyContext::parse("Workspace").unwrap(),
|
||||||
|
KeyContext::parse("ProjectPanel").unwrap(),
|
||||||
|
];
|
||||||
|
|
||||||
|
let keybinding = tree.bindings_for_action(&TestAction, &contexts);
|
||||||
|
|
||||||
|
assert!(keybinding[0].action.partial_eq(&TestAction))
|
||||||
|
}
|
||||||
|
}
|
183
crates/ming/src/keymap.rs
Normal file
183
crates/ming/src/keymap.rs
Normal file
|
@ -0,0 +1,183 @@
|
||||||
|
mod binding;
|
||||||
|
mod context;
|
||||||
|
mod matcher;
|
||||||
|
|
||||||
|
pub use binding::*;
|
||||||
|
pub use context::*;
|
||||||
|
pub(crate) use matcher::*;
|
||||||
|
|
||||||
|
use crate::{Action, Keystroke, NoAction};
|
||||||
|
use collections::{HashMap, HashSet};
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
use std::any::{Any, TypeId};
|
||||||
|
|
||||||
|
/// An opaque identifier of which version of the keymap is currently active.
|
||||||
|
/// The keymap's version is changed whenever bindings are added or removed.
|
||||||
|
#[derive(Copy, Clone, Eq, PartialEq, Default)]
|
||||||
|
pub struct KeymapVersion(usize);
|
||||||
|
|
||||||
|
/// A collection of key bindings for the user's application.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Keymap {
|
||||||
|
bindings: Vec<KeyBinding>,
|
||||||
|
binding_indices_by_action_id: HashMap<TypeId, SmallVec<[usize; 3]>>,
|
||||||
|
disabled_keystrokes:
|
||||||
|
HashMap<SmallVec<[Keystroke; 2]>, HashSet<Option<KeyBindingContextPredicate>>>,
|
||||||
|
version: KeymapVersion,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Keymap {
|
||||||
|
/// Create a new keymap with the given bindings.
|
||||||
|
pub fn new(bindings: Vec<KeyBinding>) -> Self {
|
||||||
|
let mut this = Self::default();
|
||||||
|
this.add_bindings(bindings);
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current version of the keymap.
|
||||||
|
pub fn version(&self) -> KeymapVersion {
|
||||||
|
self.version
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add more bindings to the keymap.
|
||||||
|
pub fn add_bindings<T: IntoIterator<Item = KeyBinding>>(&mut self, bindings: T) {
|
||||||
|
let no_action_id = (NoAction {}).type_id();
|
||||||
|
|
||||||
|
for binding in bindings {
|
||||||
|
let action_id = binding.action().as_any().type_id();
|
||||||
|
if action_id == no_action_id {
|
||||||
|
self.disabled_keystrokes
|
||||||
|
.entry(binding.keystrokes)
|
||||||
|
.or_default()
|
||||||
|
.insert(binding.context_predicate);
|
||||||
|
} else {
|
||||||
|
self.binding_indices_by_action_id
|
||||||
|
.entry(action_id)
|
||||||
|
.or_default()
|
||||||
|
.push(self.bindings.len());
|
||||||
|
self.bindings.push(binding);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.version.0 += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset this keymap to its initial state.
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.bindings.clear();
|
||||||
|
self.binding_indices_by_action_id.clear();
|
||||||
|
self.disabled_keystrokes.clear();
|
||||||
|
self.version.0 += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterate over all bindings, in the order they were added.
|
||||||
|
pub fn bindings(&self) -> impl DoubleEndedIterator<Item = &KeyBinding> {
|
||||||
|
self.bindings.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterate over all bindings for the given action, in the order they were added.
|
||||||
|
pub fn bindings_for_action<'a>(
|
||||||
|
&'a self,
|
||||||
|
action: &'a dyn Action,
|
||||||
|
) -> impl 'a + DoubleEndedIterator<Item = &'a KeyBinding> {
|
||||||
|
let action_id = action.type_id();
|
||||||
|
self.binding_indices_by_action_id
|
||||||
|
.get(&action_id)
|
||||||
|
.map_or(&[] as _, SmallVec::as_slice)
|
||||||
|
.iter()
|
||||||
|
.map(|ix| &self.bindings[*ix])
|
||||||
|
.filter(move |binding| binding.action().partial_eq(action))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the given binding is enabled, given a certain key context.
|
||||||
|
pub fn binding_enabled(&self, binding: &KeyBinding, context: &[KeyContext]) -> bool {
|
||||||
|
// If binding has a context predicate, it must match the current context,
|
||||||
|
if let Some(predicate) = &binding.context_predicate {
|
||||||
|
if !predicate.eval(context) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(disabled_predicates) = self.disabled_keystrokes.get(&binding.keystrokes) {
|
||||||
|
for disabled_predicate in disabled_predicates {
|
||||||
|
match disabled_predicate {
|
||||||
|
// The binding must not be globally disabled.
|
||||||
|
None => return false,
|
||||||
|
|
||||||
|
// The binding must not be disabled in the current context.
|
||||||
|
Some(predicate) => {
|
||||||
|
if predicate.eval(context) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate as gpui;
|
||||||
|
use gpui::actions;
|
||||||
|
|
||||||
|
actions!(
|
||||||
|
keymap_test,
|
||||||
|
[ActionAlpha, ActionBeta, ActionGamma, ActionDelta,]
|
||||||
|
);
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_keymap() {
|
||||||
|
let bindings = [
|
||||||
|
KeyBinding::new("ctrl-a", ActionAlpha {}, None),
|
||||||
|
KeyBinding::new("ctrl-a", ActionBeta {}, Some("pane")),
|
||||||
|
KeyBinding::new("ctrl-a", ActionGamma {}, Some("editor && mode==full")),
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut keymap = Keymap::default();
|
||||||
|
keymap.add_bindings(bindings.clone());
|
||||||
|
|
||||||
|
// global bindings are enabled in all contexts
|
||||||
|
assert!(keymap.binding_enabled(&bindings[0], &[]));
|
||||||
|
assert!(keymap.binding_enabled(&bindings[0], &[KeyContext::parse("terminal").unwrap()]));
|
||||||
|
|
||||||
|
// contextual bindings are enabled in contexts that match their predicate
|
||||||
|
assert!(!keymap.binding_enabled(&bindings[1], &[KeyContext::parse("barf x=y").unwrap()]));
|
||||||
|
assert!(keymap.binding_enabled(&bindings[1], &[KeyContext::parse("pane x=y").unwrap()]));
|
||||||
|
|
||||||
|
assert!(!keymap.binding_enabled(&bindings[2], &[KeyContext::parse("editor").unwrap()]));
|
||||||
|
assert!(keymap.binding_enabled(
|
||||||
|
&bindings[2],
|
||||||
|
&[KeyContext::parse("editor mode=full").unwrap()]
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_keymap_disabled() {
|
||||||
|
let bindings = [
|
||||||
|
KeyBinding::new("ctrl-a", ActionAlpha {}, Some("editor")),
|
||||||
|
KeyBinding::new("ctrl-b", ActionAlpha {}, Some("editor")),
|
||||||
|
KeyBinding::new("ctrl-a", NoAction {}, Some("editor && mode==full")),
|
||||||
|
KeyBinding::new("ctrl-b", NoAction {}, None),
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut keymap = Keymap::default();
|
||||||
|
keymap.add_bindings(bindings.clone());
|
||||||
|
|
||||||
|
// binding is only enabled in a specific context
|
||||||
|
assert!(!keymap.binding_enabled(&bindings[0], &[KeyContext::parse("barf").unwrap()]));
|
||||||
|
assert!(keymap.binding_enabled(&bindings[0], &[KeyContext::parse("editor").unwrap()]));
|
||||||
|
|
||||||
|
// binding is disabled in a more specific context
|
||||||
|
assert!(!keymap.binding_enabled(
|
||||||
|
&bindings[0],
|
||||||
|
&[KeyContext::parse("editor mode=full").unwrap()]
|
||||||
|
));
|
||||||
|
|
||||||
|
// binding is globally disabled
|
||||||
|
assert!(!keymap.binding_enabled(&bindings[1], &[KeyContext::parse("barf").unwrap()]));
|
||||||
|
}
|
||||||
|
}
|
81
crates/ming/src/keymap/binding.rs
Normal file
81
crates/ming/src/keymap/binding.rs
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
use crate::{Action, KeyBindingContextPredicate, KeyMatch, Keystroke};
|
||||||
|
use anyhow::Result;
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
|
/// A keybinding and its associated metadata, from the keymap.
|
||||||
|
pub struct KeyBinding {
|
||||||
|
pub(crate) action: Box<dyn Action>,
|
||||||
|
pub(crate) keystrokes: SmallVec<[Keystroke; 2]>,
|
||||||
|
pub(crate) context_predicate: Option<KeyBindingContextPredicate>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for KeyBinding {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
KeyBinding {
|
||||||
|
action: self.action.boxed_clone(),
|
||||||
|
keystrokes: self.keystrokes.clone(),
|
||||||
|
context_predicate: self.context_predicate.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeyBinding {
|
||||||
|
/// Construct a new keybinding from the given data.
|
||||||
|
pub fn new<A: Action>(keystrokes: &str, action: A, context_predicate: Option<&str>) -> Self {
|
||||||
|
Self::load(keystrokes, Box::new(action), context_predicate).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load a keybinding from the given raw data.
|
||||||
|
pub fn load(keystrokes: &str, action: Box<dyn Action>, context: Option<&str>) -> Result<Self> {
|
||||||
|
let context = if let Some(context) = context {
|
||||||
|
Some(KeyBindingContextPredicate::parse(context)?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let keystrokes = keystrokes
|
||||||
|
.split_whitespace()
|
||||||
|
.map(Keystroke::parse)
|
||||||
|
.collect::<Result<_>>()?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
keystrokes,
|
||||||
|
action,
|
||||||
|
context_predicate: context,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the given keystrokes match this binding.
|
||||||
|
pub fn match_keystrokes(&self, pending_keystrokes: &[Keystroke]) -> KeyMatch {
|
||||||
|
if self.keystrokes.as_ref().starts_with(pending_keystrokes) {
|
||||||
|
// If the binding is completed, push it onto the matches list
|
||||||
|
if self.keystrokes.as_ref().len() == pending_keystrokes.len() {
|
||||||
|
KeyMatch::Matched
|
||||||
|
} else {
|
||||||
|
KeyMatch::Pending
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
KeyMatch::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the keystrokes associated with this binding
|
||||||
|
pub fn keystrokes(&self) -> &[Keystroke] {
|
||||||
|
self.keystrokes.as_slice()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the action associated with this binding
|
||||||
|
pub fn action(&self) -> &dyn Action {
|
||||||
|
self.action.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for KeyBinding {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("KeyBinding")
|
||||||
|
.field("keystrokes", &self.keystrokes)
|
||||||
|
.field("context_predicate", &self.context_predicate)
|
||||||
|
.field("action", &self.action.name())
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
522
crates/ming/src/keymap/context.rs
Normal file
522
crates/ming/src/keymap/context.rs
Normal file
|
@ -0,0 +1,522 @@
|
||||||
|
use crate::SharedString;
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
/// A datastructure for resolving whether an action should be dispatched
|
||||||
|
/// at this point in the element tree. Contains a set of identifiers
|
||||||
|
/// and/or key value pairs representing the current context for the
|
||||||
|
/// keymap.
|
||||||
|
#[derive(Clone, Default, Eq, PartialEq, Hash)]
|
||||||
|
pub struct KeyContext(SmallVec<[ContextEntry; 1]>);
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||||
|
struct ContextEntry {
|
||||||
|
key: SharedString,
|
||||||
|
value: Option<SharedString>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> TryFrom<&'a str> for KeyContext {
|
||||||
|
type Error = anyhow::Error;
|
||||||
|
|
||||||
|
fn try_from(value: &'a str) -> Result<Self> {
|
||||||
|
Self::parse(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeyContext {
|
||||||
|
/// Initialize a new [`KeyContext`] that contains an `os` key set to either `macos`, `linux`, `windows` or `unknown`.
|
||||||
|
pub fn new_with_defaults() -> Self {
|
||||||
|
let mut context = Self::default();
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
context.set("os", "macos");
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
context.set("os", "linux");
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
context.set("os", "windows");
|
||||||
|
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
|
||||||
|
context.set("os", "unknown");
|
||||||
|
context
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a key context from a string.
|
||||||
|
/// The key context format is very simple:
|
||||||
|
/// - either a single identifier, such as `StatusBar`
|
||||||
|
/// - or a key value pair, such as `mode = visible`
|
||||||
|
/// - separated by whitespace, such as `StatusBar mode = visible`
|
||||||
|
pub fn parse(source: &str) -> Result<Self> {
|
||||||
|
let mut context = Self::default();
|
||||||
|
let source = skip_whitespace(source);
|
||||||
|
Self::parse_expr(source, &mut context)?;
|
||||||
|
Ok(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_expr(mut source: &str, context: &mut Self) -> Result<()> {
|
||||||
|
if source.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = source
|
||||||
|
.chars()
|
||||||
|
.take_while(|c| is_identifier_char(*c))
|
||||||
|
.collect::<String>();
|
||||||
|
source = skip_whitespace(&source[key.len()..]);
|
||||||
|
if let Some(suffix) = source.strip_prefix('=') {
|
||||||
|
source = skip_whitespace(suffix);
|
||||||
|
let value = source
|
||||||
|
.chars()
|
||||||
|
.take_while(|c| is_identifier_char(*c))
|
||||||
|
.collect::<String>();
|
||||||
|
source = skip_whitespace(&source[value.len()..]);
|
||||||
|
context.set(key, value);
|
||||||
|
} else {
|
||||||
|
context.add(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
Self::parse_expr(source, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if this context is empty.
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.0.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear this context.
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.0.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extend this context with another context.
|
||||||
|
pub fn extend(&mut self, other: &Self) {
|
||||||
|
for entry in &other.0 {
|
||||||
|
if !self.contains(&entry.key) {
|
||||||
|
self.0.push(entry.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add an identifier to this context, if it's not already in this context.
|
||||||
|
pub fn add<I: Into<SharedString>>(&mut self, identifier: I) {
|
||||||
|
let key = identifier.into();
|
||||||
|
|
||||||
|
if !self.contains(&key) {
|
||||||
|
self.0.push(ContextEntry { key, value: None })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set a key value pair in this context, if it's not already set.
|
||||||
|
pub fn set<S1: Into<SharedString>, S2: Into<SharedString>>(&mut self, key: S1, value: S2) {
|
||||||
|
let key = key.into();
|
||||||
|
if !self.contains(&key) {
|
||||||
|
self.0.push(ContextEntry {
|
||||||
|
key,
|
||||||
|
value: Some(value.into()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if this context contains a given identifier or key.
|
||||||
|
pub fn contains(&self, key: &str) -> bool {
|
||||||
|
self.0.iter().any(|entry| entry.key.as_ref() == key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the associated value for a given identifier or key.
|
||||||
|
pub fn get(&self, key: &str) -> Option<&SharedString> {
|
||||||
|
self.0
|
||||||
|
.iter()
|
||||||
|
.find(|entry| entry.key.as_ref() == key)?
|
||||||
|
.value
|
||||||
|
.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for KeyContext {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let mut entries = self.0.iter().peekable();
|
||||||
|
while let Some(entry) = entries.next() {
|
||||||
|
if let Some(ref value) = entry.value {
|
||||||
|
write!(f, "{}={}", entry.key, value)?;
|
||||||
|
} else {
|
||||||
|
write!(f, "{}", entry.key)?;
|
||||||
|
}
|
||||||
|
if entries.peek().is_some() {
|
||||||
|
write!(f, " ")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A datastructure for resolving whether an action should be dispatched
|
||||||
|
/// Representing a small language for describing which contexts correspond
|
||||||
|
/// to which actions.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||||
|
pub enum KeyBindingContextPredicate {
|
||||||
|
/// A predicate that will match a given identifier.
|
||||||
|
Identifier(SharedString),
|
||||||
|
/// A predicate that will match a given key-value pair.
|
||||||
|
Equal(SharedString, SharedString),
|
||||||
|
/// A predicate that will match a given key-value pair not being present.
|
||||||
|
NotEqual(SharedString, SharedString),
|
||||||
|
/// A predicate that will match a given predicate appearing below another predicate.
|
||||||
|
/// in the element tree
|
||||||
|
Child(
|
||||||
|
Box<KeyBindingContextPredicate>,
|
||||||
|
Box<KeyBindingContextPredicate>,
|
||||||
|
),
|
||||||
|
/// Predicate that will invert another predicate.
|
||||||
|
Not(Box<KeyBindingContextPredicate>),
|
||||||
|
/// A predicate that will match if both of its children match.
|
||||||
|
And(
|
||||||
|
Box<KeyBindingContextPredicate>,
|
||||||
|
Box<KeyBindingContextPredicate>,
|
||||||
|
),
|
||||||
|
/// A predicate that will match if either of its children match.
|
||||||
|
Or(
|
||||||
|
Box<KeyBindingContextPredicate>,
|
||||||
|
Box<KeyBindingContextPredicate>,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeyBindingContextPredicate {
|
||||||
|
/// Parse a string in the same format as the keymap's context field.
|
||||||
|
///
|
||||||
|
/// A basic equivalence check against a set of identifiers can performed by
|
||||||
|
/// simply writing a string:
|
||||||
|
///
|
||||||
|
/// `StatusBar` -> A predicate that will match a context with the identifier `StatusBar`
|
||||||
|
///
|
||||||
|
/// You can also specify a key-value pair:
|
||||||
|
///
|
||||||
|
/// `mode == visible` -> A predicate that will match a context with the key `mode`
|
||||||
|
/// with the value `visible`
|
||||||
|
///
|
||||||
|
/// And a logical operations combining these two checks:
|
||||||
|
///
|
||||||
|
/// `StatusBar && mode == visible` -> A predicate that will match a context with the
|
||||||
|
/// identifier `StatusBar` and the key `mode`
|
||||||
|
/// with the value `visible`
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// There is also a special child `>` operator that will match a predicate that is
|
||||||
|
/// below another predicate:
|
||||||
|
///
|
||||||
|
/// `StatusBar > mode == visible` -> A predicate that will match a context identifier `StatusBar`
|
||||||
|
/// and a child context that has the key `mode` with the
|
||||||
|
/// value `visible`
|
||||||
|
///
|
||||||
|
/// This syntax supports `!=`, `||` and `&&` as logical operators.
|
||||||
|
/// You can also preface an operation or check with a `!` to negate it.
|
||||||
|
pub fn parse(source: &str) -> Result<Self> {
|
||||||
|
let source = skip_whitespace(source);
|
||||||
|
let (predicate, rest) = Self::parse_expr(source, 0)?;
|
||||||
|
if let Some(next) = rest.chars().next() {
|
||||||
|
Err(anyhow!("unexpected character {next:?}"))
|
||||||
|
} else {
|
||||||
|
Ok(predicate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Eval a predicate against a set of contexts, arranged from lowest to highest.
|
||||||
|
pub fn eval(&self, contexts: &[KeyContext]) -> bool {
|
||||||
|
let Some(context) = contexts.last() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
match self {
|
||||||
|
Self::Identifier(name) => context.contains(name),
|
||||||
|
Self::Equal(left, right) => context
|
||||||
|
.get(left)
|
||||||
|
.map(|value| value == right)
|
||||||
|
.unwrap_or(false),
|
||||||
|
Self::NotEqual(left, right) => context
|
||||||
|
.get(left)
|
||||||
|
.map(|value| value != right)
|
||||||
|
.unwrap_or(true),
|
||||||
|
Self::Not(pred) => !pred.eval(contexts),
|
||||||
|
Self::Child(parent, child) => {
|
||||||
|
parent.eval(&contexts[..contexts.len() - 1]) && child.eval(contexts)
|
||||||
|
}
|
||||||
|
Self::And(left, right) => left.eval(contexts) && right.eval(contexts),
|
||||||
|
Self::Or(left, right) => left.eval(contexts) || right.eval(contexts),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_expr(mut source: &str, min_precedence: u32) -> anyhow::Result<(Self, &str)> {
|
||||||
|
type Op = fn(
|
||||||
|
KeyBindingContextPredicate,
|
||||||
|
KeyBindingContextPredicate,
|
||||||
|
) -> Result<KeyBindingContextPredicate>;
|
||||||
|
|
||||||
|
let (mut predicate, rest) = Self::parse_primary(source)?;
|
||||||
|
source = rest;
|
||||||
|
|
||||||
|
'parse: loop {
|
||||||
|
for (operator, precedence, constructor) in [
|
||||||
|
(">", PRECEDENCE_CHILD, Self::new_child as Op),
|
||||||
|
("&&", PRECEDENCE_AND, Self::new_and as Op),
|
||||||
|
("||", PRECEDENCE_OR, Self::new_or as Op),
|
||||||
|
("==", PRECEDENCE_EQ, Self::new_eq as Op),
|
||||||
|
("!=", PRECEDENCE_EQ, Self::new_neq as Op),
|
||||||
|
] {
|
||||||
|
if source.starts_with(operator) && precedence >= min_precedence {
|
||||||
|
source = skip_whitespace(&source[operator.len()..]);
|
||||||
|
let (right, rest) = Self::parse_expr(source, precedence + 1)?;
|
||||||
|
predicate = constructor(predicate, right)?;
|
||||||
|
source = rest;
|
||||||
|
continue 'parse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((predicate, source))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_primary(mut source: &str) -> anyhow::Result<(Self, &str)> {
|
||||||
|
let next = source
|
||||||
|
.chars()
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| anyhow!("unexpected eof"))?;
|
||||||
|
match next {
|
||||||
|
'(' => {
|
||||||
|
source = skip_whitespace(&source[1..]);
|
||||||
|
let (predicate, rest) = Self::parse_expr(source, 0)?;
|
||||||
|
if let Some(stripped) = rest.strip_prefix(')') {
|
||||||
|
source = skip_whitespace(stripped);
|
||||||
|
Ok((predicate, source))
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("expected a ')'"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'!' => {
|
||||||
|
let source = skip_whitespace(&source[1..]);
|
||||||
|
let (predicate, source) = Self::parse_expr(source, PRECEDENCE_NOT)?;
|
||||||
|
Ok((KeyBindingContextPredicate::Not(Box::new(predicate)), source))
|
||||||
|
}
|
||||||
|
_ if is_identifier_char(next) => {
|
||||||
|
let len = source
|
||||||
|
.find(|c: char| !is_identifier_char(c))
|
||||||
|
.unwrap_or(source.len());
|
||||||
|
let (identifier, rest) = source.split_at(len);
|
||||||
|
source = skip_whitespace(rest);
|
||||||
|
Ok((
|
||||||
|
KeyBindingContextPredicate::Identifier(identifier.to_string().into()),
|
||||||
|
source,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
_ => Err(anyhow!("unexpected character {next:?}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_or(self, other: Self) -> Result<Self> {
|
||||||
|
Ok(Self::Or(Box::new(self), Box::new(other)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_and(self, other: Self) -> Result<Self> {
|
||||||
|
Ok(Self::And(Box::new(self), Box::new(other)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_child(self, other: Self) -> Result<Self> {
|
||||||
|
Ok(Self::Child(Box::new(self), Box::new(other)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_eq(self, other: Self) -> Result<Self> {
|
||||||
|
if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) {
|
||||||
|
Ok(Self::Equal(left, right))
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("operands must be identifiers"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_neq(self, other: Self) -> Result<Self> {
|
||||||
|
if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) {
|
||||||
|
Ok(Self::NotEqual(left, right))
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("operands must be identifiers"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const PRECEDENCE_CHILD: u32 = 1;
|
||||||
|
const PRECEDENCE_OR: u32 = 2;
|
||||||
|
const PRECEDENCE_AND: u32 = 3;
|
||||||
|
const PRECEDENCE_EQ: u32 = 4;
|
||||||
|
const PRECEDENCE_NOT: u32 = 5;
|
||||||
|
|
||||||
|
fn is_identifier_char(c: char) -> bool {
|
||||||
|
c.is_alphanumeric() || c == '_' || c == '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
fn skip_whitespace(source: &str) -> &str {
|
||||||
|
let len = source
|
||||||
|
.find(|c: char| !c.is_whitespace())
|
||||||
|
.unwrap_or(source.len());
|
||||||
|
&source[len..]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate as gpui;
|
||||||
|
use KeyBindingContextPredicate::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_actions_definition() {
|
||||||
|
{
|
||||||
|
actions!(test, [A, B, C, D, E, F, G]);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
actions!(
|
||||||
|
test,
|
||||||
|
[
|
||||||
|
A,
|
||||||
|
B,
|
||||||
|
C,
|
||||||
|
D,
|
||||||
|
E,
|
||||||
|
F,
|
||||||
|
G, // Don't wrap, test the trailing comma
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_context() {
|
||||||
|
let mut expected = KeyContext::default();
|
||||||
|
expected.add("baz");
|
||||||
|
expected.set("foo", "bar");
|
||||||
|
assert_eq!(KeyContext::parse("baz foo=bar").unwrap(), expected);
|
||||||
|
assert_eq!(KeyContext::parse("baz foo = bar").unwrap(), expected);
|
||||||
|
assert_eq!(
|
||||||
|
KeyContext::parse(" baz foo = bar baz").unwrap(),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(KeyContext::parse(" baz foo = bar").unwrap(), expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_identifiers() {
|
||||||
|
// Identifiers
|
||||||
|
assert_eq!(
|
||||||
|
KeyBindingContextPredicate::parse("abc12").unwrap(),
|
||||||
|
Identifier("abc12".into())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
KeyBindingContextPredicate::parse("_1a").unwrap(),
|
||||||
|
Identifier("_1a".into())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_negations() {
|
||||||
|
assert_eq!(
|
||||||
|
KeyBindingContextPredicate::parse("!abc").unwrap(),
|
||||||
|
Not(Box::new(Identifier("abc".into())))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
KeyBindingContextPredicate::parse(" ! ! abc").unwrap(),
|
||||||
|
Not(Box::new(Not(Box::new(Identifier("abc".into())))))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_equality_operators() {
|
||||||
|
assert_eq!(
|
||||||
|
KeyBindingContextPredicate::parse("a == b").unwrap(),
|
||||||
|
Equal("a".into(), "b".into())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
KeyBindingContextPredicate::parse("c!=d").unwrap(),
|
||||||
|
NotEqual("c".into(), "d".into())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
KeyBindingContextPredicate::parse("c == !d")
|
||||||
|
.unwrap_err()
|
||||||
|
.to_string(),
|
||||||
|
"operands must be identifiers"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_boolean_operators() {
|
||||||
|
assert_eq!(
|
||||||
|
KeyBindingContextPredicate::parse("a || b").unwrap(),
|
||||||
|
Or(
|
||||||
|
Box::new(Identifier("a".into())),
|
||||||
|
Box::new(Identifier("b".into()))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
KeyBindingContextPredicate::parse("a || !b && c").unwrap(),
|
||||||
|
Or(
|
||||||
|
Box::new(Identifier("a".into())),
|
||||||
|
Box::new(And(
|
||||||
|
Box::new(Not(Box::new(Identifier("b".into())))),
|
||||||
|
Box::new(Identifier("c".into()))
|
||||||
|
))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
KeyBindingContextPredicate::parse("a && b || c&&d").unwrap(),
|
||||||
|
Or(
|
||||||
|
Box::new(And(
|
||||||
|
Box::new(Identifier("a".into())),
|
||||||
|
Box::new(Identifier("b".into()))
|
||||||
|
)),
|
||||||
|
Box::new(And(
|
||||||
|
Box::new(Identifier("c".into())),
|
||||||
|
Box::new(Identifier("d".into()))
|
||||||
|
))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
KeyBindingContextPredicate::parse("a == b && c || d == e && f").unwrap(),
|
||||||
|
Or(
|
||||||
|
Box::new(And(
|
||||||
|
Box::new(Equal("a".into(), "b".into())),
|
||||||
|
Box::new(Identifier("c".into()))
|
||||||
|
)),
|
||||||
|
Box::new(And(
|
||||||
|
Box::new(Equal("d".into(), "e".into())),
|
||||||
|
Box::new(Identifier("f".into()))
|
||||||
|
))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
KeyBindingContextPredicate::parse("a && b && c && d").unwrap(),
|
||||||
|
And(
|
||||||
|
Box::new(And(
|
||||||
|
Box::new(And(
|
||||||
|
Box::new(Identifier("a".into())),
|
||||||
|
Box::new(Identifier("b".into()))
|
||||||
|
)),
|
||||||
|
Box::new(Identifier("c".into())),
|
||||||
|
)),
|
||||||
|
Box::new(Identifier("d".into()))
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_parenthesized_expressions() {
|
||||||
|
assert_eq!(
|
||||||
|
KeyBindingContextPredicate::parse("a && (b == c || d != e)").unwrap(),
|
||||||
|
And(
|
||||||
|
Box::new(Identifier("a".into())),
|
||||||
|
Box::new(Or(
|
||||||
|
Box::new(Equal("b".into(), "c".into())),
|
||||||
|
Box::new(NotEqual("d".into(), "e".into())),
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
KeyBindingContextPredicate::parse(" ( a || b ) ").unwrap(),
|
||||||
|
Or(
|
||||||
|
Box::new(Identifier("a".into())),
|
||||||
|
Box::new(Identifier("b".into())),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
102
crates/ming/src/keymap/matcher.rs
Normal file
102
crates/ming/src/keymap/matcher.rs
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
use crate::{KeyBinding, KeyContext, Keymap, KeymapVersion, Keystroke};
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
|
pub(crate) struct KeystrokeMatcher {
|
||||||
|
pending_keystrokes: Vec<Keystroke>,
|
||||||
|
keymap: Rc<RefCell<Keymap>>,
|
||||||
|
keymap_version: KeymapVersion,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct KeymatchResult {
|
||||||
|
pub bindings: SmallVec<[KeyBinding; 1]>,
|
||||||
|
pub pending: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeystrokeMatcher {
|
||||||
|
pub fn new(keymap: Rc<RefCell<Keymap>>) -> Self {
|
||||||
|
let keymap_version = keymap.borrow().version();
|
||||||
|
Self {
|
||||||
|
pending_keystrokes: Vec::new(),
|
||||||
|
keymap_version,
|
||||||
|
keymap,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_pending_keystrokes(&self) -> bool {
|
||||||
|
!self.pending_keystrokes.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pushes a keystroke onto the matcher.
|
||||||
|
/// The result of the new keystroke is returned:
|
||||||
|
/// - KeyMatch::None =>
|
||||||
|
/// No match is valid for this key given any pending keystrokes.
|
||||||
|
/// - KeyMatch::Pending =>
|
||||||
|
/// There exist bindings which are still waiting for more keys.
|
||||||
|
/// - KeyMatch::Complete(matches) =>
|
||||||
|
/// One or more bindings have received the necessary key presses.
|
||||||
|
/// Bindings added later will take precedence over earlier bindings.
|
||||||
|
pub(crate) fn match_keystroke(
|
||||||
|
&mut self,
|
||||||
|
keystroke: &Keystroke,
|
||||||
|
context_stack: &[KeyContext],
|
||||||
|
) -> KeymatchResult {
|
||||||
|
let keymap = self.keymap.borrow();
|
||||||
|
|
||||||
|
// Clear pending keystrokes if the keymap has changed since the last matched keystroke.
|
||||||
|
if keymap.version() != self.keymap_version {
|
||||||
|
self.keymap_version = keymap.version();
|
||||||
|
self.pending_keystrokes.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut pending_key = None;
|
||||||
|
let mut bindings = SmallVec::new();
|
||||||
|
|
||||||
|
for binding in keymap.bindings().rev() {
|
||||||
|
if !keymap.binding_enabled(binding, context_stack) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for candidate in keystroke.match_candidates() {
|
||||||
|
self.pending_keystrokes.push(candidate.clone());
|
||||||
|
match binding.match_keystrokes(&self.pending_keystrokes) {
|
||||||
|
KeyMatch::Matched => {
|
||||||
|
bindings.push(binding.clone());
|
||||||
|
}
|
||||||
|
KeyMatch::Pending => {
|
||||||
|
pending_key.get_or_insert(candidate);
|
||||||
|
}
|
||||||
|
KeyMatch::None => {}
|
||||||
|
}
|
||||||
|
self.pending_keystrokes.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if bindings.is_empty() && pending_key.is_none() && !self.pending_keystrokes.is_empty() {
|
||||||
|
drop(keymap);
|
||||||
|
self.pending_keystrokes.remove(0);
|
||||||
|
return self.match_keystroke(keystroke, context_stack);
|
||||||
|
}
|
||||||
|
|
||||||
|
let pending = if let Some(pending_key) = pending_key {
|
||||||
|
self.pending_keystrokes.push(pending_key);
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
self.pending_keystrokes.clear();
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
KeymatchResult { bindings, pending }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The result of matching a keystroke against a given keybinding.
|
||||||
|
/// - KeyMatch::None => No match is valid for this key given any pending keystrokes.
|
||||||
|
/// - KeyMatch::Pending => There exist bindings that is still waiting for more keys.
|
||||||
|
/// - KeyMatch::Some(matches) => One or more bindings have received the necessary key presses.
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub enum KeyMatch {
|
||||||
|
None,
|
||||||
|
Pending,
|
||||||
|
Matched,
|
||||||
|
}
|
854
crates/ming/src/platform.rs
Normal file
854
crates/ming/src/platform.rs
Normal file
|
@ -0,0 +1,854 @@
|
||||||
|
// todo(linux): remove
|
||||||
|
#![cfg_attr(target_os = "linux", allow(dead_code))]
|
||||||
|
// todo(windows): remove
|
||||||
|
#![cfg_attr(windows, allow(dead_code))]
|
||||||
|
|
||||||
|
mod app_menu;
|
||||||
|
mod keystroke;
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
mod cosmic_text;
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
mod linux;
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
mod mac;
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "windows", feature = "macos-blade"))]
|
||||||
|
mod blade;
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
mod test;
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
mod windows;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
Action, AnyWindowHandle, AsyncWindowContext, BackgroundExecutor, Bounds, DevicePixels,
|
||||||
|
DispatchEventResult, Font, FontId, FontMetrics, FontRun, ForegroundExecutor, GlyphId, Keymap,
|
||||||
|
LineLayout, Pixels, PlatformInput, Point, RenderGlyphParams, RenderImageParams,
|
||||||
|
RenderSvgParams, Scene, SharedString, Size, Task, TaskLabel, WindowContext,
|
||||||
|
};
|
||||||
|
use anyhow::Result;
|
||||||
|
use async_task::Runnable;
|
||||||
|
use futures::channel::oneshot;
|
||||||
|
use parking::Unparker;
|
||||||
|
use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
|
||||||
|
use seahash::SeaHasher;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
use std::time::Duration;
|
||||||
|
use std::{
|
||||||
|
fmt::{self, Debug},
|
||||||
|
ops::Range,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
rc::Rc,
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
pub use app_menu::*;
|
||||||
|
pub use keystroke::*;
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
pub(crate) use cosmic_text::*;
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub(crate) use linux::*;
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
pub(crate) use mac::*;
|
||||||
|
pub use semantic_version::SemanticVersion;
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub(crate) use test::*;
|
||||||
|
use time::UtcOffset;
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub(crate) use windows::*;
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
pub(crate) fn current_platform() -> Rc<dyn Platform> {
|
||||||
|
Rc::new(MacPlatform::new())
|
||||||
|
}
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub(crate) fn current_platform() -> Rc<dyn Platform> {
|
||||||
|
let wayland_display = std::env::var_os("WAYLAND_DISPLAY");
|
||||||
|
let x11_display = std::env::var_os("DISPLAY");
|
||||||
|
|
||||||
|
let use_wayland = wayland_display.is_some_and(|display| !display.is_empty());
|
||||||
|
let use_x11 = x11_display.is_some_and(|display| !display.is_empty());
|
||||||
|
|
||||||
|
if use_wayland {
|
||||||
|
Rc::new(WaylandClient::new())
|
||||||
|
} else if use_x11 {
|
||||||
|
Rc::new(X11Client::new())
|
||||||
|
} else {
|
||||||
|
Rc::new(HeadlessClient::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// todo("windows")
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub(crate) fn current_platform() -> Rc<dyn Platform> {
|
||||||
|
Rc::new(WindowsPlatform::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) trait Platform: 'static {
|
||||||
|
fn background_executor(&self) -> BackgroundExecutor;
|
||||||
|
fn foreground_executor(&self) -> ForegroundExecutor;
|
||||||
|
fn text_system(&self) -> Arc<dyn PlatformTextSystem>;
|
||||||
|
|
||||||
|
fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>);
|
||||||
|
fn quit(&self);
|
||||||
|
fn restart(&self, binary_path: Option<PathBuf>);
|
||||||
|
fn activate(&self, ignoring_other_apps: bool);
|
||||||
|
fn hide(&self);
|
||||||
|
fn hide_other_apps(&self);
|
||||||
|
fn unhide_other_apps(&self);
|
||||||
|
|
||||||
|
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
|
||||||
|
fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>>;
|
||||||
|
fn active_window(&self) -> Option<AnyWindowHandle>;
|
||||||
|
fn open_window(
|
||||||
|
&self,
|
||||||
|
handle: AnyWindowHandle,
|
||||||
|
options: WindowParams,
|
||||||
|
) -> Box<dyn PlatformWindow>;
|
||||||
|
|
||||||
|
/// Returns the appearance of the application's windows.
|
||||||
|
fn window_appearance(&self) -> WindowAppearance;
|
||||||
|
|
||||||
|
fn open_url(&self, url: &str);
|
||||||
|
fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>);
|
||||||
|
fn register_url_scheme(&self, url: &str) -> Task<Result<()>>;
|
||||||
|
|
||||||
|
fn prompt_for_paths(
|
||||||
|
&self,
|
||||||
|
options: PathPromptOptions,
|
||||||
|
) -> oneshot::Receiver<Option<Vec<PathBuf>>>;
|
||||||
|
fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>>;
|
||||||
|
fn reveal_path(&self, path: &Path);
|
||||||
|
|
||||||
|
fn on_quit(&self, callback: Box<dyn FnMut()>);
|
||||||
|
fn on_reopen(&self, callback: Box<dyn FnMut()>);
|
||||||
|
|
||||||
|
fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
|
||||||
|
fn add_recent_document(&self, _path: &Path) {}
|
||||||
|
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
|
||||||
|
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
|
||||||
|
fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
|
||||||
|
|
||||||
|
fn os_name(&self) -> &'static str;
|
||||||
|
fn os_version(&self) -> Result<SemanticVersion>;
|
||||||
|
fn app_version(&self) -> Result<SemanticVersion>;
|
||||||
|
fn app_path(&self) -> Result<PathBuf>;
|
||||||
|
fn local_timezone(&self) -> UtcOffset;
|
||||||
|
fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
|
||||||
|
|
||||||
|
fn set_cursor_style(&self, style: CursorStyle);
|
||||||
|
fn should_auto_hide_scrollbars(&self) -> bool;
|
||||||
|
|
||||||
|
fn write_to_primary(&self, item: ClipboardItem);
|
||||||
|
fn write_to_clipboard(&self, item: ClipboardItem);
|
||||||
|
fn read_from_primary(&self) -> Option<ClipboardItem>;
|
||||||
|
fn read_from_clipboard(&self) -> Option<ClipboardItem>;
|
||||||
|
|
||||||
|
fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;
|
||||||
|
fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>;
|
||||||
|
fn delete_credentials(&self, url: &str) -> Task<Result<()>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A handle to a platform's display, e.g. a monitor or laptop screen.
|
||||||
|
pub trait PlatformDisplay: Send + Sync + Debug {
|
||||||
|
/// Get the ID for this display
|
||||||
|
fn id(&self) -> DisplayId;
|
||||||
|
|
||||||
|
/// Returns a stable identifier for this display that can be persisted and used
|
||||||
|
/// across system restarts.
|
||||||
|
fn uuid(&self) -> Result<Uuid>;
|
||||||
|
|
||||||
|
/// Get the bounds for this display
|
||||||
|
fn bounds(&self) -> Bounds<DevicePixels>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An opaque identifier for a hardware display
|
||||||
|
#[derive(PartialEq, Eq, Hash, Copy, Clone)]
|
||||||
|
pub struct DisplayId(pub(crate) u32);
|
||||||
|
|
||||||
|
impl Debug for DisplayId {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "DisplayId({})", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl Send for DisplayId {}
|
||||||
|
|
||||||
|
pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
|
||||||
|
fn bounds(&self) -> Bounds<DevicePixels>;
|
||||||
|
fn is_maximized(&self) -> bool;
|
||||||
|
fn window_bounds(&self) -> WindowBounds;
|
||||||
|
fn content_size(&self) -> Size<Pixels>;
|
||||||
|
fn scale_factor(&self) -> f32;
|
||||||
|
fn appearance(&self) -> WindowAppearance;
|
||||||
|
fn display(&self) -> Rc<dyn PlatformDisplay>;
|
||||||
|
fn mouse_position(&self) -> Point<Pixels>;
|
||||||
|
fn modifiers(&self) -> Modifiers;
|
||||||
|
fn set_input_handler(&mut self, input_handler: PlatformInputHandler);
|
||||||
|
fn take_input_handler(&mut self) -> Option<PlatformInputHandler>;
|
||||||
|
fn prompt(
|
||||||
|
&self,
|
||||||
|
level: PromptLevel,
|
||||||
|
msg: &str,
|
||||||
|
detail: Option<&str>,
|
||||||
|
answers: &[&str],
|
||||||
|
) -> Option<oneshot::Receiver<usize>>;
|
||||||
|
fn activate(&self);
|
||||||
|
fn is_active(&self) -> bool;
|
||||||
|
fn set_title(&mut self, title: &str);
|
||||||
|
fn set_app_id(&mut self, app_id: &str);
|
||||||
|
fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance);
|
||||||
|
fn set_edited(&mut self, edited: bool);
|
||||||
|
fn show_character_palette(&self);
|
||||||
|
fn minimize(&self);
|
||||||
|
fn zoom(&self);
|
||||||
|
fn toggle_fullscreen(&self);
|
||||||
|
fn is_fullscreen(&self) -> bool;
|
||||||
|
fn on_request_frame(&self, callback: Box<dyn FnMut()>);
|
||||||
|
fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> DispatchEventResult>);
|
||||||
|
fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>);
|
||||||
|
fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>);
|
||||||
|
fn on_moved(&self, callback: Box<dyn FnMut()>);
|
||||||
|
fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>);
|
||||||
|
fn on_close(&self, callback: Box<dyn FnOnce()>);
|
||||||
|
fn on_appearance_changed(&self, callback: Box<dyn FnMut()>);
|
||||||
|
fn draw(&self, scene: &Scene);
|
||||||
|
fn completed_frame(&self) {}
|
||||||
|
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn get_raw_handle(&self) -> windows::HWND;
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
fn as_test(&mut self) -> Option<&mut TestWindow> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This type is public so that our test macro can generate and use it, but it should not
|
||||||
|
/// be considered part of our public API.
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub trait PlatformDispatcher: Send + Sync {
|
||||||
|
fn is_main_thread(&self) -> bool;
|
||||||
|
fn dispatch(&self, runnable: Runnable, label: Option<TaskLabel>);
|
||||||
|
fn dispatch_on_main_thread(&self, runnable: Runnable);
|
||||||
|
fn dispatch_after(&self, duration: Duration, runnable: Runnable);
|
||||||
|
fn park(&self, timeout: Option<Duration>) -> bool;
|
||||||
|
fn unparker(&self) -> Unparker;
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
fn as_test(&self) -> Option<&TestDispatcher> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) trait PlatformTextSystem: Send + Sync {
|
||||||
|
fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()>;
|
||||||
|
fn all_font_names(&self) -> Vec<String>;
|
||||||
|
fn all_font_families(&self) -> Vec<String>;
|
||||||
|
fn font_id(&self, descriptor: &Font) -> Result<FontId>;
|
||||||
|
fn font_metrics(&self, font_id: FontId) -> FontMetrics;
|
||||||
|
fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>>;
|
||||||
|
fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>>;
|
||||||
|
fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId>;
|
||||||
|
fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>>;
|
||||||
|
fn rasterize_glyph(
|
||||||
|
&self,
|
||||||
|
params: &RenderGlyphParams,
|
||||||
|
raster_bounds: Bounds<DevicePixels>,
|
||||||
|
) -> Result<(Size<DevicePixels>, Vec<u8>)>;
|
||||||
|
fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Basic metadata about the current application and operating system.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct AppMetadata {
|
||||||
|
/// The name of the current operating system
|
||||||
|
pub os_name: &'static str,
|
||||||
|
|
||||||
|
/// The operating system's version
|
||||||
|
pub os_version: Option<SemanticVersion>,
|
||||||
|
|
||||||
|
/// The current version of the application
|
||||||
|
pub app_version: Option<SemanticVersion>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Hash, Clone)]
|
||||||
|
pub(crate) enum AtlasKey {
|
||||||
|
Glyph(RenderGlyphParams),
|
||||||
|
Svg(RenderSvgParams),
|
||||||
|
Image(RenderImageParams),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AtlasKey {
|
||||||
|
pub(crate) fn texture_kind(&self) -> AtlasTextureKind {
|
||||||
|
match self {
|
||||||
|
AtlasKey::Glyph(params) => {
|
||||||
|
if params.is_emoji {
|
||||||
|
AtlasTextureKind::Polychrome
|
||||||
|
} else {
|
||||||
|
AtlasTextureKind::Monochrome
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AtlasKey::Svg(_) => AtlasTextureKind::Monochrome,
|
||||||
|
AtlasKey::Image(_) => AtlasTextureKind::Polychrome,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RenderGlyphParams> for AtlasKey {
|
||||||
|
fn from(params: RenderGlyphParams) -> Self {
|
||||||
|
Self::Glyph(params)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RenderSvgParams> for AtlasKey {
|
||||||
|
fn from(params: RenderSvgParams) -> Self {
|
||||||
|
Self::Svg(params)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RenderImageParams> for AtlasKey {
|
||||||
|
fn from(params: RenderImageParams) -> Self {
|
||||||
|
Self::Image(params)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) trait PlatformAtlas: Send + Sync {
|
||||||
|
fn get_or_insert_with<'a>(
|
||||||
|
&self,
|
||||||
|
key: &AtlasKey,
|
||||||
|
build: &mut dyn FnMut() -> Result<(Size<DevicePixels>, Cow<'a, [u8]>)>,
|
||||||
|
) -> Result<AtlasTile>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
#[repr(C)]
|
||||||
|
pub(crate) struct AtlasTile {
|
||||||
|
pub(crate) texture_id: AtlasTextureId,
|
||||||
|
pub(crate) tile_id: TileId,
|
||||||
|
pub(crate) padding: u32,
|
||||||
|
pub(crate) bounds: Bounds<DevicePixels>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||||
|
#[repr(C)]
|
||||||
|
pub(crate) struct AtlasTextureId {
|
||||||
|
// We use u32 instead of usize for Metal Shader Language compatibility
|
||||||
|
pub(crate) index: u32,
|
||||||
|
pub(crate) kind: AtlasTextureKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||||
|
#[repr(C)]
|
||||||
|
pub(crate) enum AtlasTextureKind {
|
||||||
|
Monochrome = 0,
|
||||||
|
Polychrome = 1,
|
||||||
|
Path = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
#[repr(C)]
|
||||||
|
pub(crate) struct TileId(pub(crate) u32);
|
||||||
|
|
||||||
|
impl From<etagere::AllocId> for TileId {
|
||||||
|
fn from(id: etagere::AllocId) -> Self {
|
||||||
|
Self(id.serialize())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TileId> for etagere::AllocId {
|
||||||
|
fn from(id: TileId) -> Self {
|
||||||
|
Self::deserialize(id.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct PlatformInputHandler {
|
||||||
|
cx: AsyncWindowContext,
|
||||||
|
handler: Box<dyn InputHandler>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlatformInputHandler {
|
||||||
|
pub fn new(cx: AsyncWindowContext, handler: Box<dyn InputHandler>) -> Self {
|
||||||
|
Self { cx, handler }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selected_text_range(&mut self) -> Option<Range<usize>> {
|
||||||
|
self.cx
|
||||||
|
.update(|cx| self.handler.selected_text_range(cx))
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn marked_text_range(&mut self) -> Option<Range<usize>> {
|
||||||
|
self.cx
|
||||||
|
.update(|cx| self.handler.marked_text_range(cx))
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn text_for_range(&mut self, range_utf16: Range<usize>) -> Option<String> {
|
||||||
|
self.cx
|
||||||
|
.update(|cx| self.handler.text_for_range(range_utf16, cx))
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replace_text_in_range(&mut self, replacement_range: Option<Range<usize>>, text: &str) {
|
||||||
|
self.cx
|
||||||
|
.update(|cx| {
|
||||||
|
self.handler
|
||||||
|
.replace_text_in_range(replacement_range, text, cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replace_and_mark_text_in_range(
|
||||||
|
&mut self,
|
||||||
|
range_utf16: Option<Range<usize>>,
|
||||||
|
new_text: &str,
|
||||||
|
new_selected_range: Option<Range<usize>>,
|
||||||
|
) {
|
||||||
|
self.cx
|
||||||
|
.update(|cx| {
|
||||||
|
self.handler.replace_and_mark_text_in_range(
|
||||||
|
range_utf16,
|
||||||
|
new_text,
|
||||||
|
new_selected_range,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unmark_text(&mut self) {
|
||||||
|
self.cx.update(|cx| self.handler.unmark_text(cx)).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bounds_for_range(&mut self, range_utf16: Range<usize>) -> Option<Bounds<Pixels>> {
|
||||||
|
self.cx
|
||||||
|
.update(|cx| self.handler.bounds_for_range(range_utf16, cx))
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn dispatch_input(&mut self, input: &str, cx: &mut WindowContext) {
|
||||||
|
self.handler.replace_text_in_range(None, input, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Zed's interface for handling text input from the platform's IME system
|
||||||
|
/// This is currently a 1:1 exposure of the NSTextInputClient API:
|
||||||
|
///
|
||||||
|
/// <https://developer.apple.com/documentation/appkit/nstextinputclient>
|
||||||
|
pub trait InputHandler: 'static {
|
||||||
|
/// Get the range of the user's currently selected text, if any
|
||||||
|
/// Corresponds to [selectedRange()](https://developer.apple.com/documentation/appkit/nstextinputclient/1438242-selectedrange)
|
||||||
|
///
|
||||||
|
/// Return value is in terms of UTF-16 characters, from 0 to the length of the document
|
||||||
|
fn selected_text_range(&mut self, cx: &mut WindowContext) -> Option<Range<usize>>;
|
||||||
|
|
||||||
|
/// Get the range of the currently marked text, if any
|
||||||
|
/// Corresponds to [markedRange()](https://developer.apple.com/documentation/appkit/nstextinputclient/1438250-markedrange)
|
||||||
|
///
|
||||||
|
/// Return value is in terms of UTF-16 characters, from 0 to the length of the document
|
||||||
|
fn marked_text_range(&mut self, cx: &mut WindowContext) -> Option<Range<usize>>;
|
||||||
|
|
||||||
|
/// Get the text for the given document range in UTF-16 characters
|
||||||
|
/// Corresponds to [attributedSubstring(forProposedRange: actualRange:)](https://developer.apple.com/documentation/appkit/nstextinputclient/1438238-attributedsubstring)
|
||||||
|
///
|
||||||
|
/// range_utf16 is in terms of UTF-16 characters
|
||||||
|
fn text_for_range(
|
||||||
|
&mut self,
|
||||||
|
range_utf16: Range<usize>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> Option<String>;
|
||||||
|
|
||||||
|
/// Replace the text in the given document range with the given text
|
||||||
|
/// Corresponds to [insertText(_:replacementRange:)](https://developer.apple.com/documentation/appkit/nstextinputclient/1438258-inserttext)
|
||||||
|
///
|
||||||
|
/// replacement_range is in terms of UTF-16 characters
|
||||||
|
fn replace_text_in_range(
|
||||||
|
&mut self,
|
||||||
|
replacement_range: Option<Range<usize>>,
|
||||||
|
text: &str,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Replace the text in the given document range with the given text,
|
||||||
|
/// and mark the given text as part of of an IME 'composing' state
|
||||||
|
/// Corresponds to [setMarkedText(_:selectedRange:replacementRange:)](https://developer.apple.com/documentation/appkit/nstextinputclient/1438246-setmarkedtext)
|
||||||
|
///
|
||||||
|
/// range_utf16 is in terms of UTF-16 characters
|
||||||
|
/// new_selected_range is in terms of UTF-16 characters
|
||||||
|
fn replace_and_mark_text_in_range(
|
||||||
|
&mut self,
|
||||||
|
range_utf16: Option<Range<usize>>,
|
||||||
|
new_text: &str,
|
||||||
|
new_selected_range: Option<Range<usize>>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Remove the IME 'composing' state from the document
|
||||||
|
/// Corresponds to [unmarkText()](https://developer.apple.com/documentation/appkit/nstextinputclient/1438239-unmarktext)
|
||||||
|
fn unmark_text(&mut self, cx: &mut WindowContext);
|
||||||
|
|
||||||
|
/// Get the bounds of the given document range in screen coordinates
|
||||||
|
/// Corresponds to [firstRect(forCharacterRange:actualRange:)](https://developer.apple.com/documentation/appkit/nstextinputclient/1438240-firstrect)
|
||||||
|
///
|
||||||
|
/// This is used for positioning the IME candidate window
|
||||||
|
fn bounds_for_range(
|
||||||
|
&mut self,
|
||||||
|
range_utf16: Range<usize>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> Option<Bounds<Pixels>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The variables that can be configured when creating a new window
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct WindowOptions {
|
||||||
|
/// Specifies the state and bounds of the window in screen coordinates.
|
||||||
|
/// - `None`: Inherit the bounds.
|
||||||
|
/// - `Some(WindowBounds)`: Open a window with corresponding state and its restore size.
|
||||||
|
pub window_bounds: Option<WindowBounds>,
|
||||||
|
|
||||||
|
/// The titlebar configuration of the window
|
||||||
|
pub titlebar: Option<TitlebarOptions>,
|
||||||
|
|
||||||
|
/// Whether the window should be focused when created
|
||||||
|
pub focus: bool,
|
||||||
|
|
||||||
|
/// Whether the window should be shown when created
|
||||||
|
pub show: bool,
|
||||||
|
|
||||||
|
/// The kind of window to create
|
||||||
|
pub kind: WindowKind,
|
||||||
|
|
||||||
|
/// Whether the window should be movable by the user
|
||||||
|
pub is_movable: bool,
|
||||||
|
|
||||||
|
/// The display to create the window on, if this is None,
|
||||||
|
/// the window will be created on the main display
|
||||||
|
pub display_id: Option<DisplayId>,
|
||||||
|
|
||||||
|
/// The appearance of the window background.
|
||||||
|
pub window_background: WindowBackgroundAppearance,
|
||||||
|
|
||||||
|
/// Application identifier of the window. Can by used by desktop environments to group applications together.
|
||||||
|
pub app_id: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The variables that can be configured when creating a new window
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct WindowParams {
|
||||||
|
pub bounds: Bounds<DevicePixels>,
|
||||||
|
|
||||||
|
/// The titlebar configuration of the window
|
||||||
|
pub titlebar: Option<TitlebarOptions>,
|
||||||
|
|
||||||
|
/// The kind of window to create
|
||||||
|
pub kind: WindowKind,
|
||||||
|
|
||||||
|
/// Whether the window should be movable by the user
|
||||||
|
pub is_movable: bool,
|
||||||
|
|
||||||
|
pub focus: bool,
|
||||||
|
|
||||||
|
pub show: bool,
|
||||||
|
|
||||||
|
pub display_id: Option<DisplayId>,
|
||||||
|
|
||||||
|
pub window_background: WindowBackgroundAppearance,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents the status of how a window should be opened.
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||||
|
pub enum WindowBounds {
|
||||||
|
/// Indicates that the window should open in a windowed state with the given bounds.
|
||||||
|
Windowed(Bounds<DevicePixels>),
|
||||||
|
/// Indicates that the window should open in a maximized state.
|
||||||
|
/// The bounds provided here represent the restore size of the window.
|
||||||
|
Maximized(Bounds<DevicePixels>),
|
||||||
|
/// Indicates that the window should open in fullscreen mode.
|
||||||
|
/// The bounds provided here represent the restore size of the window.
|
||||||
|
Fullscreen(Bounds<DevicePixels>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for WindowBounds {
|
||||||
|
fn default() -> Self {
|
||||||
|
WindowBounds::Windowed(Bounds::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WindowBounds {
|
||||||
|
/// Retrieve the inner bounds
|
||||||
|
pub fn get_bounds(&self) -> Bounds<DevicePixels> {
|
||||||
|
match self {
|
||||||
|
WindowBounds::Windowed(bounds) => *bounds,
|
||||||
|
WindowBounds::Maximized(bounds) => *bounds,
|
||||||
|
WindowBounds::Fullscreen(bounds) => *bounds,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for WindowOptions {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
window_bounds: None,
|
||||||
|
titlebar: Some(TitlebarOptions {
|
||||||
|
title: Default::default(),
|
||||||
|
appears_transparent: Default::default(),
|
||||||
|
traffic_light_position: Default::default(),
|
||||||
|
}),
|
||||||
|
focus: true,
|
||||||
|
show: true,
|
||||||
|
kind: WindowKind::Normal,
|
||||||
|
is_movable: true,
|
||||||
|
display_id: None,
|
||||||
|
window_background: WindowBackgroundAppearance::default(),
|
||||||
|
app_id: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The options that can be configured for a window's titlebar
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct TitlebarOptions {
|
||||||
|
/// The initial title of the window
|
||||||
|
pub title: Option<SharedString>,
|
||||||
|
|
||||||
|
/// Whether the titlebar should appear transparent
|
||||||
|
pub appears_transparent: bool,
|
||||||
|
|
||||||
|
/// The position of the macOS traffic light buttons
|
||||||
|
pub traffic_light_position: Option<Point<Pixels>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The kind of window to create
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum WindowKind {
|
||||||
|
/// A normal application window
|
||||||
|
Normal,
|
||||||
|
|
||||||
|
/// A window that appears above all other windows, usually used for alerts or popups
|
||||||
|
/// use sparingly!
|
||||||
|
PopUp,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The appearance of the window, as defined by the operating system.
|
||||||
|
///
|
||||||
|
/// On macOS, this corresponds to named [`NSAppearance`](https://developer.apple.com/documentation/appkit/nsappearance)
|
||||||
|
/// values.
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
pub enum WindowAppearance {
|
||||||
|
/// A light appearance.
|
||||||
|
///
|
||||||
|
/// On macOS, this corresponds to the `aqua` appearance.
|
||||||
|
Light,
|
||||||
|
|
||||||
|
/// A light appearance with vibrant colors.
|
||||||
|
///
|
||||||
|
/// On macOS, this corresponds to the `NSAppearanceNameVibrantLight` appearance.
|
||||||
|
VibrantLight,
|
||||||
|
|
||||||
|
/// A dark appearance.
|
||||||
|
///
|
||||||
|
/// On macOS, this corresponds to the `darkAqua` appearance.
|
||||||
|
Dark,
|
||||||
|
|
||||||
|
/// A dark appearance with vibrant colors.
|
||||||
|
///
|
||||||
|
/// On macOS, this corresponds to the `NSAppearanceNameVibrantDark` appearance.
|
||||||
|
VibrantDark,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for WindowAppearance {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Light
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The appearance of the background of the window itself, when there is
|
||||||
|
/// no content or the content is transparent.
|
||||||
|
#[derive(Copy, Clone, Debug, Default, PartialEq)]
|
||||||
|
pub enum WindowBackgroundAppearance {
|
||||||
|
/// Opaque.
|
||||||
|
///
|
||||||
|
/// This lets the window manager know that content behind this
|
||||||
|
/// window does not need to be drawn.
|
||||||
|
///
|
||||||
|
/// Actual color depends on the system and themes should define a fully
|
||||||
|
/// opaque background color instead.
|
||||||
|
#[default]
|
||||||
|
Opaque,
|
||||||
|
/// Plain alpha transparency.
|
||||||
|
Transparent,
|
||||||
|
/// Transparency, but the contents behind the window are blurred.
|
||||||
|
///
|
||||||
|
/// Not always supported.
|
||||||
|
Blurred,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The options that can be configured for a file dialog prompt
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
pub struct PathPromptOptions {
|
||||||
|
/// Should the prompt allow files to be selected?
|
||||||
|
pub files: bool,
|
||||||
|
/// Should the prompt allow directories to be selected?
|
||||||
|
pub directories: bool,
|
||||||
|
/// Should the prompt allow multiple files to be selected?
|
||||||
|
pub multiple: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What kind of prompt styling to show
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||||
|
pub enum PromptLevel {
|
||||||
|
/// A prompt that is shown when the user should be notified of something
|
||||||
|
Info,
|
||||||
|
|
||||||
|
/// A prompt that is shown when the user needs to be warned of a potential problem
|
||||||
|
Warning,
|
||||||
|
|
||||||
|
/// A prompt that is shown when a critical problem has occurred
|
||||||
|
Critical,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The style of the cursor (pointer)
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
|
pub enum CursorStyle {
|
||||||
|
/// The default cursor
|
||||||
|
Arrow,
|
||||||
|
|
||||||
|
/// A text input cursor
|
||||||
|
/// corresponds to the CSS cursor value `text`
|
||||||
|
IBeam,
|
||||||
|
|
||||||
|
/// A crosshair cursor
|
||||||
|
/// corresponds to the CSS cursor value `crosshair`
|
||||||
|
Crosshair,
|
||||||
|
|
||||||
|
/// A closed hand cursor
|
||||||
|
/// corresponds to the CSS cursor value `grabbing`
|
||||||
|
ClosedHand,
|
||||||
|
|
||||||
|
/// An open hand cursor
|
||||||
|
/// corresponds to the CSS cursor value `grab`
|
||||||
|
OpenHand,
|
||||||
|
|
||||||
|
/// A pointing hand cursor
|
||||||
|
/// corresponds to the CSS cursor value `pointer`
|
||||||
|
PointingHand,
|
||||||
|
|
||||||
|
/// A resize left cursor
|
||||||
|
/// corresponds to the CSS cursor value `w-resize`
|
||||||
|
ResizeLeft,
|
||||||
|
|
||||||
|
/// A resize right cursor
|
||||||
|
/// corresponds to the CSS cursor value `e-resize`
|
||||||
|
ResizeRight,
|
||||||
|
|
||||||
|
/// A resize cursor to the left and right
|
||||||
|
/// corresponds to the CSS cursor value `ew-resize`
|
||||||
|
ResizeLeftRight,
|
||||||
|
|
||||||
|
/// A resize up cursor
|
||||||
|
/// corresponds to the CSS cursor value `n-resize`
|
||||||
|
ResizeUp,
|
||||||
|
|
||||||
|
/// A resize down cursor
|
||||||
|
/// corresponds to the CSS cursor value `s-resize`
|
||||||
|
ResizeDown,
|
||||||
|
|
||||||
|
/// A resize cursor directing up and down
|
||||||
|
/// corresponds to the CSS cursor value `ns-resize`
|
||||||
|
ResizeUpDown,
|
||||||
|
|
||||||
|
/// A cursor indicating that the item/column can be resized horizontally.
|
||||||
|
/// corresponds to the CSS curosr value `col-resize`
|
||||||
|
ResizeColumn,
|
||||||
|
|
||||||
|
/// A cursor indicating that the item/row can be resized vertically.
|
||||||
|
/// corresponds to the CSS curosr value `row-resize`
|
||||||
|
ResizeRow,
|
||||||
|
|
||||||
|
/// A cursor indicating that something will disappear if moved here
|
||||||
|
/// Does not correspond to a CSS cursor value
|
||||||
|
DisappearingItem,
|
||||||
|
|
||||||
|
/// A text input cursor for vertical layout
|
||||||
|
/// corresponds to the CSS cursor value `vertical-text`
|
||||||
|
IBeamCursorForVerticalLayout,
|
||||||
|
|
||||||
|
/// A cursor indicating that the operation is not allowed
|
||||||
|
/// corresponds to the CSS cursor value `not-allowed`
|
||||||
|
OperationNotAllowed,
|
||||||
|
|
||||||
|
/// A cursor indicating that the operation will result in a link
|
||||||
|
/// corresponds to the CSS cursor value `alias`
|
||||||
|
DragLink,
|
||||||
|
|
||||||
|
/// A cursor indicating that the operation will result in a copy
|
||||||
|
/// corresponds to the CSS cursor value `copy`
|
||||||
|
DragCopy,
|
||||||
|
|
||||||
|
/// A cursor indicating that the operation will result in a context menu
|
||||||
|
/// corresponds to the CSS cursor value `context-menu`
|
||||||
|
ContextualMenu,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CursorStyle {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Arrow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A clipboard item that should be copied to the clipboard
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct ClipboardItem {
|
||||||
|
pub(crate) text: String,
|
||||||
|
pub(crate) metadata: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClipboardItem {
|
||||||
|
/// Create a new clipboard item with the given text
|
||||||
|
pub fn new(text: String) -> Self {
|
||||||
|
Self {
|
||||||
|
text,
|
||||||
|
metadata: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new clipboard item with the given text and metadata
|
||||||
|
pub fn with_metadata<T: Serialize>(mut self, metadata: T) -> Self {
|
||||||
|
self.metadata = Some(serde_json::to_string(&metadata).unwrap());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the text of the clipboard item
|
||||||
|
pub fn text(&self) -> &String {
|
||||||
|
&self.text
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the metadata of the clipboard item
|
||||||
|
pub fn metadata<T>(&self) -> Option<T>
|
||||||
|
where
|
||||||
|
T: for<'a> Deserialize<'a>,
|
||||||
|
{
|
||||||
|
self.metadata
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|m| serde_json::from_str(m).ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn text_hash(text: &str) -> u64 {
|
||||||
|
let mut hasher = SeaHasher::new();
|
||||||
|
text.hash(&mut hasher);
|
||||||
|
hasher.finish()
|
||||||
|
}
|
||||||
|
}
|
115
crates/ming/src/platform/app_menu.rs
Normal file
115
crates/ming/src/platform/app_menu.rs
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
use crate::{Action, AppContext, Platform};
|
||||||
|
use util::ResultExt;
|
||||||
|
|
||||||
|
/// A menu of the application, either a main menu or a submenu
|
||||||
|
pub struct Menu<'a> {
|
||||||
|
/// The name of the menu
|
||||||
|
pub name: &'a str,
|
||||||
|
|
||||||
|
/// The items in the menu
|
||||||
|
pub items: Vec<MenuItem<'a>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The different kinds of items that can be in a menu
|
||||||
|
pub enum MenuItem<'a> {
|
||||||
|
/// A separator between items
|
||||||
|
Separator,
|
||||||
|
|
||||||
|
/// A submenu
|
||||||
|
Submenu(Menu<'a>),
|
||||||
|
|
||||||
|
/// An action that can be performed
|
||||||
|
Action {
|
||||||
|
/// The name of this menu item
|
||||||
|
name: &'a str,
|
||||||
|
|
||||||
|
/// the action to perform when this menu item is selected
|
||||||
|
action: Box<dyn Action>,
|
||||||
|
|
||||||
|
/// The OS Action that corresponds to this action, if any
|
||||||
|
/// See [`OsAction`] for more information
|
||||||
|
os_action: Option<OsAction>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> MenuItem<'a> {
|
||||||
|
/// Creates a new menu item that is a separator
|
||||||
|
pub fn separator() -> Self {
|
||||||
|
Self::Separator
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new menu item that is a submenu
|
||||||
|
pub fn submenu(menu: Menu<'a>) -> Self {
|
||||||
|
Self::Submenu(menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new menu item that invokes an action
|
||||||
|
pub fn action(name: &'a str, action: impl Action) -> Self {
|
||||||
|
Self::Action {
|
||||||
|
name,
|
||||||
|
action: Box::new(action),
|
||||||
|
os_action: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new menu item that invokes an action and has an OS action
|
||||||
|
pub fn os_action(name: &'a str, action: impl Action, os_action: OsAction) -> Self {
|
||||||
|
Self::Action {
|
||||||
|
name,
|
||||||
|
action: Box::new(action),
|
||||||
|
os_action: Some(os_action),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: As part of the global selections refactor, these should
|
||||||
|
// be moved to GPUI-provided actions that make this association
|
||||||
|
// without leaking the platform details to GPUI users
|
||||||
|
|
||||||
|
/// OS actions are actions that are recognized by the operating system
|
||||||
|
/// This allows the operating system to provide specialized behavior for
|
||||||
|
/// these actions
|
||||||
|
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||||
|
pub enum OsAction {
|
||||||
|
/// The 'cut' action
|
||||||
|
Cut,
|
||||||
|
|
||||||
|
/// The 'copy' action
|
||||||
|
Copy,
|
||||||
|
|
||||||
|
/// The 'paste' action
|
||||||
|
Paste,
|
||||||
|
|
||||||
|
/// The 'select all' action
|
||||||
|
SelectAll,
|
||||||
|
|
||||||
|
/// The 'undo' action
|
||||||
|
Undo,
|
||||||
|
|
||||||
|
/// The 'redo' action
|
||||||
|
Redo,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn init_app_menus(platform: &dyn Platform, cx: &mut AppContext) {
|
||||||
|
platform.on_will_open_app_menu(Box::new({
|
||||||
|
let cx = cx.to_async();
|
||||||
|
move || {
|
||||||
|
cx.update(|cx| cx.clear_pending_keystrokes()).ok();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
platform.on_validate_app_menu_command(Box::new({
|
||||||
|
let cx = cx.to_async();
|
||||||
|
move |action| {
|
||||||
|
cx.update(|cx| cx.is_action_available(action))
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
platform.on_app_menu_action(Box::new({
|
||||||
|
let cx = cx.to_async();
|
||||||
|
move |action| {
|
||||||
|
cx.update(|cx| cx.dispatch_action(action)).log_err();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
8
crates/ming/src/platform/blade.rs
Normal file
8
crates/ming/src/platform/blade.rs
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
mod blade_atlas;
|
||||||
|
mod blade_belt;
|
||||||
|
mod blade_renderer;
|
||||||
|
|
||||||
|
pub(crate) use blade_atlas::*;
|
||||||
|
pub(crate) use blade_renderer::*;
|
||||||
|
|
||||||
|
use blade_belt::*;
|
377
crates/ming/src/platform/blade/blade_atlas.rs
Normal file
377
crates/ming/src/platform/blade/blade_atlas.rs
Normal file
|
@ -0,0 +1,377 @@
|
||||||
|
use super::{BladeBelt, BladeBeltDescriptor};
|
||||||
|
use crate::{
|
||||||
|
AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds, DevicePixels, PlatformAtlas,
|
||||||
|
Point, Size,
|
||||||
|
};
|
||||||
|
use anyhow::Result;
|
||||||
|
use blade_graphics as gpu;
|
||||||
|
use collections::FxHashMap;
|
||||||
|
use etagere::BucketedAtlasAllocator;
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use std::{borrow::Cow, ops, sync::Arc};
|
||||||
|
|
||||||
|
pub(crate) const PATH_TEXTURE_FORMAT: gpu::TextureFormat = gpu::TextureFormat::R16Float;
|
||||||
|
|
||||||
|
pub(crate) struct BladeAtlas(Mutex<BladeAtlasState>);
|
||||||
|
|
||||||
|
struct PendingUpload {
|
||||||
|
id: AtlasTextureId,
|
||||||
|
bounds: Bounds<DevicePixels>,
|
||||||
|
data: gpu::BufferPiece,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct BladeAtlasState {
|
||||||
|
gpu: Arc<gpu::Context>,
|
||||||
|
upload_belt: BladeBelt,
|
||||||
|
storage: BladeAtlasStorage,
|
||||||
|
tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
|
||||||
|
initializations: Vec<AtlasTextureId>,
|
||||||
|
uploads: Vec<PendingUpload>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(gles)]
|
||||||
|
unsafe impl Send for BladeAtlasState {}
|
||||||
|
|
||||||
|
impl BladeAtlasState {
|
||||||
|
fn destroy(&mut self) {
|
||||||
|
self.storage.destroy(&self.gpu);
|
||||||
|
self.upload_belt.destroy(&self.gpu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct BladeTextureInfo {
|
||||||
|
pub size: gpu::Extent,
|
||||||
|
pub raw_view: gpu::TextureView,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BladeAtlas {
|
||||||
|
pub(crate) fn new(gpu: &Arc<gpu::Context>) -> Self {
|
||||||
|
BladeAtlas(Mutex::new(BladeAtlasState {
|
||||||
|
gpu: Arc::clone(gpu),
|
||||||
|
upload_belt: BladeBelt::new(BladeBeltDescriptor {
|
||||||
|
memory: gpu::Memory::Upload,
|
||||||
|
min_chunk_size: 0x10000,
|
||||||
|
alignment: 64, // Vulkan `optimalBufferCopyOffsetAlignment` on Intel XE
|
||||||
|
}),
|
||||||
|
storage: BladeAtlasStorage::default(),
|
||||||
|
tiles_by_key: Default::default(),
|
||||||
|
initializations: Vec::new(),
|
||||||
|
uploads: Vec::new(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn destroy(&self) {
|
||||||
|
self.0.lock().destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn clear_textures(&self, texture_kind: AtlasTextureKind) {
|
||||||
|
let mut lock = self.0.lock();
|
||||||
|
let textures = &mut lock.storage[texture_kind];
|
||||||
|
for texture in textures {
|
||||||
|
texture.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Allocate a rectangle and make it available for rendering immediately (without waiting for `before_frame`)
|
||||||
|
pub fn allocate_for_rendering(
|
||||||
|
&self,
|
||||||
|
size: Size<DevicePixels>,
|
||||||
|
texture_kind: AtlasTextureKind,
|
||||||
|
gpu_encoder: &mut gpu::CommandEncoder,
|
||||||
|
) -> AtlasTile {
|
||||||
|
let mut lock = self.0.lock();
|
||||||
|
let tile = lock.allocate(size, texture_kind);
|
||||||
|
lock.flush_initializations(gpu_encoder);
|
||||||
|
tile
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn before_frame(&self, gpu_encoder: &mut gpu::CommandEncoder) {
|
||||||
|
let mut lock = self.0.lock();
|
||||||
|
lock.flush(gpu_encoder);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn after_frame(&self, sync_point: &gpu::SyncPoint) {
|
||||||
|
let mut lock = self.0.lock();
|
||||||
|
lock.upload_belt.flush(sync_point);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_texture_info(&self, id: AtlasTextureId) -> BladeTextureInfo {
|
||||||
|
let lock = self.0.lock();
|
||||||
|
let texture = &lock.storage[id];
|
||||||
|
let size = texture.allocator.size();
|
||||||
|
BladeTextureInfo {
|
||||||
|
size: gpu::Extent {
|
||||||
|
width: size.width as u32,
|
||||||
|
height: size.height as u32,
|
||||||
|
depth: 1,
|
||||||
|
},
|
||||||
|
raw_view: texture.raw_view,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlatformAtlas for BladeAtlas {
|
||||||
|
fn get_or_insert_with<'a>(
|
||||||
|
&self,
|
||||||
|
key: &AtlasKey,
|
||||||
|
build: &mut dyn FnMut() -> Result<(Size<DevicePixels>, Cow<'a, [u8]>)>,
|
||||||
|
) -> Result<AtlasTile> {
|
||||||
|
let mut lock = self.0.lock();
|
||||||
|
if let Some(tile) = lock.tiles_by_key.get(key) {
|
||||||
|
Ok(tile.clone())
|
||||||
|
} else {
|
||||||
|
profiling::scope!("new tile");
|
||||||
|
let (size, bytes) = build()?;
|
||||||
|
let tile = lock.allocate(size, key.texture_kind());
|
||||||
|
lock.upload_texture(tile.texture_id, tile.bounds, &bytes);
|
||||||
|
lock.tiles_by_key.insert(key.clone(), tile.clone());
|
||||||
|
Ok(tile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BladeAtlasState {
|
||||||
|
fn allocate(&mut self, size: Size<DevicePixels>, texture_kind: AtlasTextureKind) -> AtlasTile {
|
||||||
|
let textures = &mut self.storage[texture_kind];
|
||||||
|
textures
|
||||||
|
.iter_mut()
|
||||||
|
.rev()
|
||||||
|
.find_map(|texture| texture.allocate(size))
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
let texture = self.push_texture(size, texture_kind);
|
||||||
|
texture.allocate(size).unwrap()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_texture(
|
||||||
|
&mut self,
|
||||||
|
min_size: Size<DevicePixels>,
|
||||||
|
kind: AtlasTextureKind,
|
||||||
|
) -> &mut BladeAtlasTexture {
|
||||||
|
const DEFAULT_ATLAS_SIZE: Size<DevicePixels> = Size {
|
||||||
|
width: DevicePixels(1024),
|
||||||
|
height: DevicePixels(1024),
|
||||||
|
};
|
||||||
|
|
||||||
|
let size = min_size.max(&DEFAULT_ATLAS_SIZE);
|
||||||
|
let format;
|
||||||
|
let usage;
|
||||||
|
match kind {
|
||||||
|
AtlasTextureKind::Monochrome => {
|
||||||
|
format = gpu::TextureFormat::R8Unorm;
|
||||||
|
usage = gpu::TextureUsage::COPY | gpu::TextureUsage::RESOURCE;
|
||||||
|
}
|
||||||
|
AtlasTextureKind::Polychrome => {
|
||||||
|
format = gpu::TextureFormat::Bgra8UnormSrgb;
|
||||||
|
usage = gpu::TextureUsage::COPY | gpu::TextureUsage::RESOURCE;
|
||||||
|
}
|
||||||
|
AtlasTextureKind::Path => {
|
||||||
|
format = PATH_TEXTURE_FORMAT;
|
||||||
|
usage = gpu::TextureUsage::COPY
|
||||||
|
| gpu::TextureUsage::RESOURCE
|
||||||
|
| gpu::TextureUsage::TARGET;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let raw = self.gpu.create_texture(gpu::TextureDesc {
|
||||||
|
name: "atlas",
|
||||||
|
format,
|
||||||
|
size: gpu::Extent {
|
||||||
|
width: size.width.into(),
|
||||||
|
height: size.height.into(),
|
||||||
|
depth: 1,
|
||||||
|
},
|
||||||
|
array_layer_count: 1,
|
||||||
|
mip_level_count: 1,
|
||||||
|
dimension: gpu::TextureDimension::D2,
|
||||||
|
usage,
|
||||||
|
});
|
||||||
|
let raw_view = self.gpu.create_texture_view(gpu::TextureViewDesc {
|
||||||
|
name: "",
|
||||||
|
texture: raw,
|
||||||
|
format,
|
||||||
|
dimension: gpu::ViewDimension::D2,
|
||||||
|
subresources: &Default::default(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let textures = &mut self.storage[kind];
|
||||||
|
let atlas_texture = BladeAtlasTexture {
|
||||||
|
id: AtlasTextureId {
|
||||||
|
index: textures.len() as u32,
|
||||||
|
kind,
|
||||||
|
},
|
||||||
|
allocator: etagere::BucketedAtlasAllocator::new(size.into()),
|
||||||
|
format,
|
||||||
|
raw,
|
||||||
|
raw_view,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.initializations.push(atlas_texture.id);
|
||||||
|
textures.push(atlas_texture);
|
||||||
|
textures.last_mut().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn upload_texture(&mut self, id: AtlasTextureId, bounds: Bounds<DevicePixels>, bytes: &[u8]) {
|
||||||
|
let data = unsafe { self.upload_belt.alloc_data(bytes, &self.gpu) };
|
||||||
|
self.uploads.push(PendingUpload { id, bounds, data });
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush_initializations(&mut self, encoder: &mut gpu::CommandEncoder) {
|
||||||
|
for id in self.initializations.drain(..) {
|
||||||
|
let texture = &self.storage[id];
|
||||||
|
encoder.init_texture(texture.raw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self, encoder: &mut gpu::CommandEncoder) {
|
||||||
|
self.flush_initializations(encoder);
|
||||||
|
|
||||||
|
let mut transfers = encoder.transfer();
|
||||||
|
for upload in self.uploads.drain(..) {
|
||||||
|
let texture = &self.storage[upload.id];
|
||||||
|
transfers.copy_buffer_to_texture(
|
||||||
|
upload.data,
|
||||||
|
upload.bounds.size.width.to_bytes(texture.bytes_per_pixel()),
|
||||||
|
gpu::TexturePiece {
|
||||||
|
texture: texture.raw,
|
||||||
|
mip_level: 0,
|
||||||
|
array_layer: 0,
|
||||||
|
origin: [
|
||||||
|
upload.bounds.origin.x.into(),
|
||||||
|
upload.bounds.origin.y.into(),
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
gpu::Extent {
|
||||||
|
width: upload.bounds.size.width.into(),
|
||||||
|
height: upload.bounds.size.height.into(),
|
||||||
|
depth: 1,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct BladeAtlasStorage {
|
||||||
|
monochrome_textures: Vec<BladeAtlasTexture>,
|
||||||
|
polychrome_textures: Vec<BladeAtlasTexture>,
|
||||||
|
path_textures: Vec<BladeAtlasTexture>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ops::Index<AtlasTextureKind> for BladeAtlasStorage {
|
||||||
|
type Output = Vec<BladeAtlasTexture>;
|
||||||
|
fn index(&self, kind: AtlasTextureKind) -> &Self::Output {
|
||||||
|
match kind {
|
||||||
|
crate::AtlasTextureKind::Monochrome => &self.monochrome_textures,
|
||||||
|
crate::AtlasTextureKind::Polychrome => &self.polychrome_textures,
|
||||||
|
crate::AtlasTextureKind::Path => &self.path_textures,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ops::IndexMut<AtlasTextureKind> for BladeAtlasStorage {
|
||||||
|
fn index_mut(&mut self, kind: AtlasTextureKind) -> &mut Self::Output {
|
||||||
|
match kind {
|
||||||
|
crate::AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
|
||||||
|
crate::AtlasTextureKind::Polychrome => &mut self.polychrome_textures,
|
||||||
|
crate::AtlasTextureKind::Path => &mut self.path_textures,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ops::Index<AtlasTextureId> for BladeAtlasStorage {
|
||||||
|
type Output = BladeAtlasTexture;
|
||||||
|
fn index(&self, id: AtlasTextureId) -> &Self::Output {
|
||||||
|
let textures = match id.kind {
|
||||||
|
crate::AtlasTextureKind::Monochrome => &self.monochrome_textures,
|
||||||
|
crate::AtlasTextureKind::Polychrome => &self.polychrome_textures,
|
||||||
|
crate::AtlasTextureKind::Path => &self.path_textures,
|
||||||
|
};
|
||||||
|
&textures[id.index as usize]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BladeAtlasStorage {
|
||||||
|
fn destroy(&mut self, gpu: &gpu::Context) {
|
||||||
|
for mut texture in self.monochrome_textures.drain(..) {
|
||||||
|
texture.destroy(gpu);
|
||||||
|
}
|
||||||
|
for mut texture in self.polychrome_textures.drain(..) {
|
||||||
|
texture.destroy(gpu);
|
||||||
|
}
|
||||||
|
for mut texture in self.path_textures.drain(..) {
|
||||||
|
texture.destroy(gpu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct BladeAtlasTexture {
|
||||||
|
id: AtlasTextureId,
|
||||||
|
allocator: BucketedAtlasAllocator,
|
||||||
|
raw: gpu::Texture,
|
||||||
|
raw_view: gpu::TextureView,
|
||||||
|
format: gpu::TextureFormat,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BladeAtlasTexture {
|
||||||
|
fn clear(&mut self) {
|
||||||
|
self.allocator.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn allocate(&mut self, size: Size<DevicePixels>) -> Option<AtlasTile> {
|
||||||
|
let allocation = self.allocator.allocate(size.into())?;
|
||||||
|
let tile = AtlasTile {
|
||||||
|
texture_id: self.id,
|
||||||
|
tile_id: allocation.id.into(),
|
||||||
|
padding: 0,
|
||||||
|
bounds: Bounds {
|
||||||
|
origin: allocation.rectangle.min.into(),
|
||||||
|
size,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
Some(tile)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn destroy(&mut self, gpu: &gpu::Context) {
|
||||||
|
gpu.destroy_texture(self.raw);
|
||||||
|
gpu.destroy_texture_view(self.raw_view);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bytes_per_pixel(&self) -> u8 {
|
||||||
|
self.format.block_info().size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Size<DevicePixels>> for etagere::Size {
|
||||||
|
fn from(size: Size<DevicePixels>) -> Self {
|
||||||
|
etagere::Size::new(size.width.into(), size.height.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<etagere::Point> for Point<DevicePixels> {
|
||||||
|
fn from(value: etagere::Point) -> Self {
|
||||||
|
Point {
|
||||||
|
x: DevicePixels::from(value.x),
|
||||||
|
y: DevicePixels::from(value.y),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<etagere::Size> for Size<DevicePixels> {
|
||||||
|
fn from(size: etagere::Size) -> Self {
|
||||||
|
Size {
|
||||||
|
width: DevicePixels::from(size.width),
|
||||||
|
height: DevicePixels::from(size.height),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<etagere::Rectangle> for Bounds<DevicePixels> {
|
||||||
|
fn from(rectangle: etagere::Rectangle) -> Self {
|
||||||
|
Bounds {
|
||||||
|
origin: rectangle.min.into(),
|
||||||
|
size: rectangle.size().into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
101
crates/ming/src/platform/blade/blade_belt.rs
Normal file
101
crates/ming/src/platform/blade/blade_belt.rs
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
use blade_graphics as gpu;
|
||||||
|
use std::mem;
|
||||||
|
|
||||||
|
struct ReusableBuffer {
|
||||||
|
raw: gpu::Buffer,
|
||||||
|
size: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct BladeBeltDescriptor {
|
||||||
|
pub memory: gpu::Memory,
|
||||||
|
pub min_chunk_size: u64,
|
||||||
|
pub alignment: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A belt of buffers, used by the BladeAtlas to cheaply
|
||||||
|
/// find staging space for uploads.
|
||||||
|
pub struct BladeBelt {
|
||||||
|
desc: BladeBeltDescriptor,
|
||||||
|
buffers: Vec<(ReusableBuffer, gpu::SyncPoint)>,
|
||||||
|
active: Vec<(ReusableBuffer, u64)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BladeBelt {
|
||||||
|
pub fn new(desc: BladeBeltDescriptor) -> Self {
|
||||||
|
assert_ne!(desc.alignment, 0);
|
||||||
|
Self {
|
||||||
|
desc,
|
||||||
|
buffers: Vec::new(),
|
||||||
|
active: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn destroy(&mut self, gpu: &gpu::Context) {
|
||||||
|
for (buffer, _) in self.buffers.drain(..) {
|
||||||
|
gpu.destroy_buffer(buffer.raw);
|
||||||
|
}
|
||||||
|
for (buffer, _) in self.active.drain(..) {
|
||||||
|
gpu.destroy_buffer(buffer.raw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[profiling::function]
|
||||||
|
pub fn alloc(&mut self, size: u64, gpu: &gpu::Context) -> gpu::BufferPiece {
|
||||||
|
for &mut (ref rb, ref mut offset) in self.active.iter_mut() {
|
||||||
|
let aligned = offset.next_multiple_of(self.desc.alignment);
|
||||||
|
if aligned + size <= rb.size {
|
||||||
|
let piece = rb.raw.at(aligned);
|
||||||
|
*offset = aligned + size;
|
||||||
|
return piece;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let index_maybe = self
|
||||||
|
.buffers
|
||||||
|
.iter()
|
||||||
|
.position(|(rb, sp)| size <= rb.size && gpu.wait_for(sp, 0));
|
||||||
|
if let Some(index) = index_maybe {
|
||||||
|
let (rb, _) = self.buffers.remove(index);
|
||||||
|
let piece = rb.raw.into();
|
||||||
|
self.active.push((rb, size));
|
||||||
|
return piece;
|
||||||
|
}
|
||||||
|
|
||||||
|
let chunk_index = self.buffers.len() + self.active.len();
|
||||||
|
let chunk_size = size.max(self.desc.min_chunk_size);
|
||||||
|
let chunk = gpu.create_buffer(gpu::BufferDesc {
|
||||||
|
name: &format!("chunk-{}", chunk_index),
|
||||||
|
size: chunk_size,
|
||||||
|
memory: self.desc.memory,
|
||||||
|
});
|
||||||
|
let rb = ReusableBuffer {
|
||||||
|
raw: chunk,
|
||||||
|
size: chunk_size,
|
||||||
|
};
|
||||||
|
self.active.push((rb, size));
|
||||||
|
chunk.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAFETY: T should be zeroable and ordinary data, no references, pointers, cells or other complicated data type.
|
||||||
|
pub unsafe fn alloc_data<T>(&mut self, data: &[T], gpu: &gpu::Context) -> gpu::BufferPiece {
|
||||||
|
assert!(!data.is_empty());
|
||||||
|
let type_alignment = mem::align_of::<T>() as u64;
|
||||||
|
debug_assert_eq!(
|
||||||
|
self.desc.alignment % type_alignment,
|
||||||
|
0,
|
||||||
|
"Type alignment {} is too big",
|
||||||
|
type_alignment
|
||||||
|
);
|
||||||
|
let total_bytes = std::mem::size_of_val(data);
|
||||||
|
let bp = self.alloc(total_bytes as u64, gpu);
|
||||||
|
unsafe {
|
||||||
|
std::ptr::copy_nonoverlapping(data.as_ptr() as *const u8, bp.data(), total_bytes);
|
||||||
|
}
|
||||||
|
bp
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn flush(&mut self, sp: &gpu::SyncPoint) {
|
||||||
|
self.buffers
|
||||||
|
.extend(self.active.drain(..).map(|(rb, _)| (rb, sp.clone())));
|
||||||
|
}
|
||||||
|
}
|
753
crates/ming/src/platform/blade/blade_renderer.rs
Normal file
753
crates/ming/src/platform/blade/blade_renderer.rs
Normal file
|
@ -0,0 +1,753 @@
|
||||||
|
// Doing `if let` gives you nice scoping with passes/encoders
|
||||||
|
#![allow(irrefutable_let_patterns)]
|
||||||
|
|
||||||
|
use super::{BladeAtlas, BladeBelt, BladeBeltDescriptor, PATH_TEXTURE_FORMAT};
|
||||||
|
use crate::{
|
||||||
|
AtlasTextureKind, AtlasTile, Bounds, ContentMask, Hsla, MonochromeSprite, Path, PathId,
|
||||||
|
PathVertex, PolychromeSprite, PrimitiveBatch, Quad, ScaledPixels, Scene, Shadow, Size,
|
||||||
|
Underline,
|
||||||
|
};
|
||||||
|
use bytemuck::{Pod, Zeroable};
|
||||||
|
use collections::HashMap;
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
use media::core_video::CVMetalTextureCache;
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
use std::{ffi::c_void, ptr::NonNull};
|
||||||
|
|
||||||
|
use blade_graphics as gpu;
|
||||||
|
use std::{mem, sync::Arc};
|
||||||
|
|
||||||
|
const MAX_FRAME_TIME_MS: u32 = 1000;
|
||||||
|
|
||||||
|
pub type Context = ();
|
||||||
|
pub type Renderer = BladeRenderer;
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
pub unsafe fn new_renderer(
|
||||||
|
_context: self::Context,
|
||||||
|
_native_window: *mut c_void,
|
||||||
|
native_view: *mut c_void,
|
||||||
|
bounds: crate::Size<f32>,
|
||||||
|
transparent: bool,
|
||||||
|
) -> Renderer {
|
||||||
|
use raw_window_handle as rwh;
|
||||||
|
struct RawWindow {
|
||||||
|
view: *mut c_void,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl rwh::HasWindowHandle for RawWindow {
|
||||||
|
fn window_handle(&self) -> Result<rwh::WindowHandle, rwh::HandleError> {
|
||||||
|
let view = NonNull::new(self.view).unwrap();
|
||||||
|
let handle = rwh::AppKitWindowHandle::new(view);
|
||||||
|
Ok(unsafe { rwh::WindowHandle::borrow_raw(handle.into()) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl rwh::HasDisplayHandle for RawWindow {
|
||||||
|
fn display_handle(&self) -> Result<rwh::DisplayHandle, rwh::HandleError> {
|
||||||
|
let handle = rwh::AppKitDisplayHandle::new();
|
||||||
|
Ok(unsafe { rwh::DisplayHandle::borrow_raw(handle.into()) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let gpu = Arc::new(
|
||||||
|
gpu::Context::init_windowed(
|
||||||
|
&RawWindow {
|
||||||
|
view: native_view as *mut _,
|
||||||
|
},
|
||||||
|
gpu::ContextDesc {
|
||||||
|
validation: cfg!(debug_assertions),
|
||||||
|
capture: false,
|
||||||
|
overlay: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
BladeRenderer::new(
|
||||||
|
gpu,
|
||||||
|
BladeSurfaceConfig {
|
||||||
|
size: gpu::Extent {
|
||||||
|
width: bounds.width as u32,
|
||||||
|
height: bounds.height as u32,
|
||||||
|
depth: 1,
|
||||||
|
},
|
||||||
|
transparent,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Clone, Copy, Pod, Zeroable)]
|
||||||
|
struct GlobalParams {
|
||||||
|
viewport_size: [f32; 2],
|
||||||
|
premultiplied_alpha: u32,
|
||||||
|
pad: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
//Note: we can't use `Bounds` directly here because
|
||||||
|
// it doesn't implement Pod + Zeroable
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Clone, Copy, Pod, Zeroable)]
|
||||||
|
struct PodBounds {
|
||||||
|
origin: [f32; 2],
|
||||||
|
size: [f32; 2],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Bounds<ScaledPixels>> for PodBounds {
|
||||||
|
fn from(bounds: Bounds<ScaledPixels>) -> Self {
|
||||||
|
Self {
|
||||||
|
origin: [bounds.origin.x.0, bounds.origin.y.0],
|
||||||
|
size: [bounds.size.width.0, bounds.size.height.0],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Clone, Copy, Pod, Zeroable)]
|
||||||
|
struct SurfaceParams {
|
||||||
|
bounds: PodBounds,
|
||||||
|
content_mask: PodBounds,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(blade_macros::ShaderData)]
|
||||||
|
struct ShaderQuadsData {
|
||||||
|
globals: GlobalParams,
|
||||||
|
b_quads: gpu::BufferPiece,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(blade_macros::ShaderData)]
|
||||||
|
struct ShaderShadowsData {
|
||||||
|
globals: GlobalParams,
|
||||||
|
b_shadows: gpu::BufferPiece,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(blade_macros::ShaderData)]
|
||||||
|
struct ShaderPathRasterizationData {
|
||||||
|
globals: GlobalParams,
|
||||||
|
b_path_vertices: gpu::BufferPiece,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(blade_macros::ShaderData)]
|
||||||
|
struct ShaderPathsData {
|
||||||
|
globals: GlobalParams,
|
||||||
|
t_sprite: gpu::TextureView,
|
||||||
|
s_sprite: gpu::Sampler,
|
||||||
|
b_path_sprites: gpu::BufferPiece,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(blade_macros::ShaderData)]
|
||||||
|
struct ShaderUnderlinesData {
|
||||||
|
globals: GlobalParams,
|
||||||
|
b_underlines: gpu::BufferPiece,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(blade_macros::ShaderData)]
|
||||||
|
struct ShaderMonoSpritesData {
|
||||||
|
globals: GlobalParams,
|
||||||
|
t_sprite: gpu::TextureView,
|
||||||
|
s_sprite: gpu::Sampler,
|
||||||
|
b_mono_sprites: gpu::BufferPiece,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(blade_macros::ShaderData)]
|
||||||
|
struct ShaderPolySpritesData {
|
||||||
|
globals: GlobalParams,
|
||||||
|
t_sprite: gpu::TextureView,
|
||||||
|
s_sprite: gpu::Sampler,
|
||||||
|
b_poly_sprites: gpu::BufferPiece,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(blade_macros::ShaderData)]
|
||||||
|
struct ShaderSurfacesData {
|
||||||
|
globals: GlobalParams,
|
||||||
|
surface_locals: SurfaceParams,
|
||||||
|
t_y: gpu::TextureView,
|
||||||
|
t_cb_cr: gpu::TextureView,
|
||||||
|
s_surface: gpu::Sampler,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
#[repr(C)]
|
||||||
|
struct PathSprite {
|
||||||
|
bounds: Bounds<ScaledPixels>,
|
||||||
|
color: Hsla,
|
||||||
|
tile: AtlasTile,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct BladePipelines {
|
||||||
|
quads: gpu::RenderPipeline,
|
||||||
|
shadows: gpu::RenderPipeline,
|
||||||
|
path_rasterization: gpu::RenderPipeline,
|
||||||
|
paths: gpu::RenderPipeline,
|
||||||
|
underlines: gpu::RenderPipeline,
|
||||||
|
mono_sprites: gpu::RenderPipeline,
|
||||||
|
poly_sprites: gpu::RenderPipeline,
|
||||||
|
surfaces: gpu::RenderPipeline,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BladePipelines {
|
||||||
|
fn new(gpu: &gpu::Context, surface_info: gpu::SurfaceInfo) -> Self {
|
||||||
|
use gpu::ShaderData as _;
|
||||||
|
|
||||||
|
log::info!(
|
||||||
|
"Initializing Blade pipelines for surface {:?}",
|
||||||
|
surface_info
|
||||||
|
);
|
||||||
|
let shader = gpu.create_shader(gpu::ShaderDesc {
|
||||||
|
source: include_str!("shaders.wgsl"),
|
||||||
|
});
|
||||||
|
shader.check_struct_size::<GlobalParams>();
|
||||||
|
shader.check_struct_size::<SurfaceParams>();
|
||||||
|
shader.check_struct_size::<Quad>();
|
||||||
|
shader.check_struct_size::<Shadow>();
|
||||||
|
assert_eq!(
|
||||||
|
mem::size_of::<PathVertex<ScaledPixels>>(),
|
||||||
|
shader.get_struct_size("PathVertex") as usize,
|
||||||
|
);
|
||||||
|
shader.check_struct_size::<PathSprite>();
|
||||||
|
shader.check_struct_size::<Underline>();
|
||||||
|
shader.check_struct_size::<MonochromeSprite>();
|
||||||
|
shader.check_struct_size::<PolychromeSprite>();
|
||||||
|
|
||||||
|
// See https://apoorvaj.io/alpha-compositing-opengl-blending-and-premultiplied-alpha/
|
||||||
|
let blend_mode = match surface_info.alpha {
|
||||||
|
gpu::AlphaMode::Ignored => gpu::BlendState::ALPHA_BLENDING,
|
||||||
|
gpu::AlphaMode::PreMultiplied => gpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING,
|
||||||
|
gpu::AlphaMode::PostMultiplied => gpu::BlendState::ALPHA_BLENDING,
|
||||||
|
};
|
||||||
|
let color_targets = &[gpu::ColorTargetState {
|
||||||
|
format: surface_info.format,
|
||||||
|
blend: Some(blend_mode),
|
||||||
|
write_mask: gpu::ColorWrites::default(),
|
||||||
|
}];
|
||||||
|
|
||||||
|
Self {
|
||||||
|
quads: gpu.create_render_pipeline(gpu::RenderPipelineDesc {
|
||||||
|
name: "quads",
|
||||||
|
data_layouts: &[&ShaderQuadsData::layout()],
|
||||||
|
vertex: shader.at("vs_quad"),
|
||||||
|
vertex_fetches: &[],
|
||||||
|
primitive: gpu::PrimitiveState {
|
||||||
|
topology: gpu::PrimitiveTopology::TriangleStrip,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
depth_stencil: None,
|
||||||
|
fragment: shader.at("fs_quad"),
|
||||||
|
color_targets,
|
||||||
|
}),
|
||||||
|
shadows: gpu.create_render_pipeline(gpu::RenderPipelineDesc {
|
||||||
|
name: "shadows",
|
||||||
|
data_layouts: &[&ShaderShadowsData::layout()],
|
||||||
|
vertex: shader.at("vs_shadow"),
|
||||||
|
vertex_fetches: &[],
|
||||||
|
primitive: gpu::PrimitiveState {
|
||||||
|
topology: gpu::PrimitiveTopology::TriangleStrip,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
depth_stencil: None,
|
||||||
|
fragment: shader.at("fs_shadow"),
|
||||||
|
color_targets,
|
||||||
|
}),
|
||||||
|
path_rasterization: gpu.create_render_pipeline(gpu::RenderPipelineDesc {
|
||||||
|
name: "path_rasterization",
|
||||||
|
data_layouts: &[&ShaderPathRasterizationData::layout()],
|
||||||
|
vertex: shader.at("vs_path_rasterization"),
|
||||||
|
vertex_fetches: &[],
|
||||||
|
primitive: gpu::PrimitiveState {
|
||||||
|
topology: gpu::PrimitiveTopology::TriangleList,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
depth_stencil: None,
|
||||||
|
fragment: shader.at("fs_path_rasterization"),
|
||||||
|
color_targets: &[gpu::ColorTargetState {
|
||||||
|
format: PATH_TEXTURE_FORMAT,
|
||||||
|
blend: Some(gpu::BlendState::ADDITIVE),
|
||||||
|
write_mask: gpu::ColorWrites::default(),
|
||||||
|
}],
|
||||||
|
}),
|
||||||
|
paths: gpu.create_render_pipeline(gpu::RenderPipelineDesc {
|
||||||
|
name: "paths",
|
||||||
|
data_layouts: &[&ShaderPathsData::layout()],
|
||||||
|
vertex: shader.at("vs_path"),
|
||||||
|
vertex_fetches: &[],
|
||||||
|
primitive: gpu::PrimitiveState {
|
||||||
|
topology: gpu::PrimitiveTopology::TriangleStrip,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
depth_stencil: None,
|
||||||
|
fragment: shader.at("fs_path"),
|
||||||
|
color_targets,
|
||||||
|
}),
|
||||||
|
underlines: gpu.create_render_pipeline(gpu::RenderPipelineDesc {
|
||||||
|
name: "underlines",
|
||||||
|
data_layouts: &[&ShaderUnderlinesData::layout()],
|
||||||
|
vertex: shader.at("vs_underline"),
|
||||||
|
vertex_fetches: &[],
|
||||||
|
primitive: gpu::PrimitiveState {
|
||||||
|
topology: gpu::PrimitiveTopology::TriangleStrip,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
depth_stencil: None,
|
||||||
|
fragment: shader.at("fs_underline"),
|
||||||
|
color_targets,
|
||||||
|
}),
|
||||||
|
mono_sprites: gpu.create_render_pipeline(gpu::RenderPipelineDesc {
|
||||||
|
name: "mono-sprites",
|
||||||
|
data_layouts: &[&ShaderMonoSpritesData::layout()],
|
||||||
|
vertex: shader.at("vs_mono_sprite"),
|
||||||
|
vertex_fetches: &[],
|
||||||
|
primitive: gpu::PrimitiveState {
|
||||||
|
topology: gpu::PrimitiveTopology::TriangleStrip,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
depth_stencil: None,
|
||||||
|
fragment: shader.at("fs_mono_sprite"),
|
||||||
|
color_targets,
|
||||||
|
}),
|
||||||
|
poly_sprites: gpu.create_render_pipeline(gpu::RenderPipelineDesc {
|
||||||
|
name: "poly-sprites",
|
||||||
|
data_layouts: &[&ShaderPolySpritesData::layout()],
|
||||||
|
vertex: shader.at("vs_poly_sprite"),
|
||||||
|
vertex_fetches: &[],
|
||||||
|
primitive: gpu::PrimitiveState {
|
||||||
|
topology: gpu::PrimitiveTopology::TriangleStrip,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
depth_stencil: None,
|
||||||
|
fragment: shader.at("fs_poly_sprite"),
|
||||||
|
color_targets,
|
||||||
|
}),
|
||||||
|
surfaces: gpu.create_render_pipeline(gpu::RenderPipelineDesc {
|
||||||
|
name: "surfaces",
|
||||||
|
data_layouts: &[&ShaderSurfacesData::layout()],
|
||||||
|
vertex: shader.at("vs_surface"),
|
||||||
|
vertex_fetches: &[],
|
||||||
|
primitive: gpu::PrimitiveState {
|
||||||
|
topology: gpu::PrimitiveTopology::TriangleStrip,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
depth_stencil: None,
|
||||||
|
fragment: shader.at("fs_surface"),
|
||||||
|
color_targets,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct BladeSurfaceConfig {
|
||||||
|
pub size: gpu::Extent,
|
||||||
|
pub transparent: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct BladeRenderer {
|
||||||
|
gpu: Arc<gpu::Context>,
|
||||||
|
surface_config: gpu::SurfaceConfig,
|
||||||
|
alpha_mode: gpu::AlphaMode,
|
||||||
|
command_encoder: gpu::CommandEncoder,
|
||||||
|
last_sync_point: Option<gpu::SyncPoint>,
|
||||||
|
pipelines: BladePipelines,
|
||||||
|
instance_belt: BladeBelt,
|
||||||
|
path_tiles: HashMap<PathId, AtlasTile>,
|
||||||
|
atlas: Arc<BladeAtlas>,
|
||||||
|
atlas_sampler: gpu::Sampler,
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
core_video_texture_cache: CVMetalTextureCache,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BladeRenderer {
|
||||||
|
pub fn new(gpu: Arc<gpu::Context>, config: BladeSurfaceConfig) -> Self {
|
||||||
|
let surface_config = gpu::SurfaceConfig {
|
||||||
|
size: config.size,
|
||||||
|
usage: gpu::TextureUsage::TARGET,
|
||||||
|
display_sync: gpu::DisplaySync::Recent,
|
||||||
|
color_space: gpu::ColorSpace::Linear,
|
||||||
|
allow_exclusive_full_screen: false,
|
||||||
|
transparent: config.transparent,
|
||||||
|
};
|
||||||
|
let surface_info = gpu.resize(surface_config);
|
||||||
|
|
||||||
|
let command_encoder = gpu.create_command_encoder(gpu::CommandEncoderDesc {
|
||||||
|
name: "main",
|
||||||
|
buffer_count: 2,
|
||||||
|
});
|
||||||
|
let pipelines = BladePipelines::new(&gpu, surface_info);
|
||||||
|
let instance_belt = BladeBelt::new(BladeBeltDescriptor {
|
||||||
|
memory: gpu::Memory::Shared,
|
||||||
|
min_chunk_size: 0x1000,
|
||||||
|
alignment: 0x40, // Vulkan `minStorageBufferOffsetAlignment` on Intel Xe
|
||||||
|
});
|
||||||
|
let atlas = Arc::new(BladeAtlas::new(&gpu));
|
||||||
|
let atlas_sampler = gpu.create_sampler(gpu::SamplerDesc {
|
||||||
|
name: "atlas",
|
||||||
|
mag_filter: gpu::FilterMode::Linear,
|
||||||
|
min_filter: gpu::FilterMode::Linear,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
let core_video_texture_cache = unsafe {
|
||||||
|
use foreign_types::ForeignType as _;
|
||||||
|
CVMetalTextureCache::new(gpu.metal_device().as_ptr()).unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
Self {
|
||||||
|
gpu,
|
||||||
|
surface_config,
|
||||||
|
alpha_mode: surface_info.alpha,
|
||||||
|
command_encoder,
|
||||||
|
last_sync_point: None,
|
||||||
|
pipelines,
|
||||||
|
instance_belt,
|
||||||
|
path_tiles: HashMap::default(),
|
||||||
|
atlas,
|
||||||
|
atlas_sampler,
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
core_video_texture_cache,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wait_for_gpu(&mut self) {
|
||||||
|
if let Some(last_sp) = self.last_sync_point.take() {
|
||||||
|
if !self.gpu.wait_for(&last_sp, MAX_FRAME_TIME_MS) {
|
||||||
|
panic!("GPU hung");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_drawable_size(&mut self, size: Size<f64>) {
|
||||||
|
let gpu_size = gpu::Extent {
|
||||||
|
width: size.width as u32,
|
||||||
|
height: size.height as u32,
|
||||||
|
depth: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
if gpu_size != self.surface_config.size {
|
||||||
|
self.wait_for_gpu();
|
||||||
|
self.surface_config.size = gpu_size;
|
||||||
|
self.gpu.resize(self.surface_config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_transparency(&mut self, transparent: bool) {
|
||||||
|
if transparent != self.surface_config.transparent {
|
||||||
|
self.wait_for_gpu();
|
||||||
|
self.surface_config.transparent = transparent;
|
||||||
|
let surface_info = self.gpu.resize(self.surface_config);
|
||||||
|
self.pipelines = BladePipelines::new(&self.gpu, surface_info);
|
||||||
|
self.alpha_mode = surface_info.alpha;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(target_os = "macos", allow(dead_code))]
|
||||||
|
pub fn viewport_size(&self) -> gpu::Extent {
|
||||||
|
self.surface_config.size
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sprite_atlas(&self) -> &Arc<BladeAtlas> {
|
||||||
|
&self.atlas
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
pub fn layer(&self) -> metal::MetalLayer {
|
||||||
|
self.gpu.metal_layer().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
pub fn layer_ptr(&self) -> *mut metal::CAMetalLayer {
|
||||||
|
use metal::foreign_types::ForeignType as _;
|
||||||
|
self.gpu.metal_layer().unwrap().as_ptr()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[profiling::function]
|
||||||
|
fn rasterize_paths(&mut self, paths: &[Path<ScaledPixels>]) {
|
||||||
|
self.path_tiles.clear();
|
||||||
|
let mut vertices_by_texture_id = HashMap::default();
|
||||||
|
|
||||||
|
for path in paths {
|
||||||
|
let clipped_bounds = path.bounds.intersect(&path.content_mask.bounds);
|
||||||
|
let tile = self.atlas.allocate_for_rendering(
|
||||||
|
clipped_bounds.size.map(Into::into),
|
||||||
|
AtlasTextureKind::Path,
|
||||||
|
&mut self.command_encoder,
|
||||||
|
);
|
||||||
|
vertices_by_texture_id
|
||||||
|
.entry(tile.texture_id)
|
||||||
|
.or_insert(Vec::new())
|
||||||
|
.extend(path.vertices.iter().map(|vertex| PathVertex {
|
||||||
|
xy_position: vertex.xy_position - clipped_bounds.origin
|
||||||
|
+ tile.bounds.origin.map(Into::into),
|
||||||
|
st_position: vertex.st_position,
|
||||||
|
content_mask: ContentMask {
|
||||||
|
bounds: tile.bounds.map(Into::into),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
self.path_tiles.insert(path.id, tile);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (texture_id, vertices) in vertices_by_texture_id {
|
||||||
|
let tex_info = self.atlas.get_texture_info(texture_id);
|
||||||
|
let globals = GlobalParams {
|
||||||
|
viewport_size: [tex_info.size.width as f32, tex_info.size.height as f32],
|
||||||
|
premultiplied_alpha: 0,
|
||||||
|
pad: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let vertex_buf = unsafe { self.instance_belt.alloc_data(&vertices, &self.gpu) };
|
||||||
|
let mut pass = self.command_encoder.render(gpu::RenderTargetSet {
|
||||||
|
colors: &[gpu::RenderTarget {
|
||||||
|
view: tex_info.raw_view,
|
||||||
|
init_op: gpu::InitOp::Clear(gpu::TextureColor::OpaqueBlack),
|
||||||
|
finish_op: gpu::FinishOp::Store,
|
||||||
|
}],
|
||||||
|
depth_stencil: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut encoder = pass.with(&self.pipelines.path_rasterization);
|
||||||
|
encoder.bind(
|
||||||
|
0,
|
||||||
|
&ShaderPathRasterizationData {
|
||||||
|
globals,
|
||||||
|
b_path_vertices: vertex_buf,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
encoder.draw(0, vertices.len() as u32, 0, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn destroy(&mut self) {
|
||||||
|
self.wait_for_gpu();
|
||||||
|
self.atlas.destroy();
|
||||||
|
self.instance_belt.destroy(&self.gpu);
|
||||||
|
self.gpu.destroy_command_encoder(&mut self.command_encoder);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw(&mut self, scene: &Scene) {
|
||||||
|
self.command_encoder.start();
|
||||||
|
self.atlas.before_frame(&mut self.command_encoder);
|
||||||
|
self.rasterize_paths(scene.paths());
|
||||||
|
|
||||||
|
let frame = {
|
||||||
|
profiling::scope!("acquire frame");
|
||||||
|
self.gpu.acquire_frame()
|
||||||
|
};
|
||||||
|
self.command_encoder.init_texture(frame.texture());
|
||||||
|
|
||||||
|
let globals = GlobalParams {
|
||||||
|
viewport_size: [
|
||||||
|
self.surface_config.size.width as f32,
|
||||||
|
self.surface_config.size.height as f32,
|
||||||
|
],
|
||||||
|
premultiplied_alpha: match self.alpha_mode {
|
||||||
|
gpu::AlphaMode::Ignored | gpu::AlphaMode::PostMultiplied => 0,
|
||||||
|
gpu::AlphaMode::PreMultiplied => 1,
|
||||||
|
},
|
||||||
|
pad: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let mut pass = self.command_encoder.render(gpu::RenderTargetSet {
|
||||||
|
colors: &[gpu::RenderTarget {
|
||||||
|
view: frame.texture_view(),
|
||||||
|
init_op: gpu::InitOp::Clear(gpu::TextureColor::TransparentBlack),
|
||||||
|
finish_op: gpu::FinishOp::Store,
|
||||||
|
}],
|
||||||
|
depth_stencil: None,
|
||||||
|
}) {
|
||||||
|
profiling::scope!("render pass");
|
||||||
|
for batch in scene.batches() {
|
||||||
|
match batch {
|
||||||
|
PrimitiveBatch::Quads(quads) => {
|
||||||
|
let instance_buf =
|
||||||
|
unsafe { self.instance_belt.alloc_data(quads, &self.gpu) };
|
||||||
|
let mut encoder = pass.with(&self.pipelines.quads);
|
||||||
|
encoder.bind(
|
||||||
|
0,
|
||||||
|
&ShaderQuadsData {
|
||||||
|
globals,
|
||||||
|
b_quads: instance_buf,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
encoder.draw(0, 4, 0, quads.len() as u32);
|
||||||
|
}
|
||||||
|
PrimitiveBatch::Shadows(shadows) => {
|
||||||
|
let instance_buf =
|
||||||
|
unsafe { self.instance_belt.alloc_data(shadows, &self.gpu) };
|
||||||
|
let mut encoder = pass.with(&self.pipelines.shadows);
|
||||||
|
encoder.bind(
|
||||||
|
0,
|
||||||
|
&ShaderShadowsData {
|
||||||
|
globals,
|
||||||
|
b_shadows: instance_buf,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
encoder.draw(0, 4, 0, shadows.len() as u32);
|
||||||
|
}
|
||||||
|
PrimitiveBatch::Paths(paths) => {
|
||||||
|
let mut encoder = pass.with(&self.pipelines.paths);
|
||||||
|
// todo(linux): group by texture ID
|
||||||
|
for path in paths {
|
||||||
|
let tile = &self.path_tiles[&path.id];
|
||||||
|
let tex_info = self.atlas.get_texture_info(tile.texture_id);
|
||||||
|
let origin = path.bounds.intersect(&path.content_mask.bounds).origin;
|
||||||
|
let sprites = [PathSprite {
|
||||||
|
bounds: Bounds {
|
||||||
|
origin: origin.map(|p| p.floor()),
|
||||||
|
size: tile.bounds.size.map(Into::into),
|
||||||
|
},
|
||||||
|
color: path.color,
|
||||||
|
tile: (*tile).clone(),
|
||||||
|
}];
|
||||||
|
|
||||||
|
let instance_buf =
|
||||||
|
unsafe { self.instance_belt.alloc_data(&sprites, &self.gpu) };
|
||||||
|
encoder.bind(
|
||||||
|
0,
|
||||||
|
&ShaderPathsData {
|
||||||
|
globals,
|
||||||
|
t_sprite: tex_info.raw_view,
|
||||||
|
s_sprite: self.atlas_sampler,
|
||||||
|
b_path_sprites: instance_buf,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
encoder.draw(0, 4, 0, sprites.len() as u32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PrimitiveBatch::Underlines(underlines) => {
|
||||||
|
let instance_buf =
|
||||||
|
unsafe { self.instance_belt.alloc_data(underlines, &self.gpu) };
|
||||||
|
let mut encoder = pass.with(&self.pipelines.underlines);
|
||||||
|
encoder.bind(
|
||||||
|
0,
|
||||||
|
&ShaderUnderlinesData {
|
||||||
|
globals,
|
||||||
|
b_underlines: instance_buf,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
encoder.draw(0, 4, 0, underlines.len() as u32);
|
||||||
|
}
|
||||||
|
PrimitiveBatch::MonochromeSprites {
|
||||||
|
texture_id,
|
||||||
|
sprites,
|
||||||
|
} => {
|
||||||
|
let tex_info = self.atlas.get_texture_info(texture_id);
|
||||||
|
let instance_buf =
|
||||||
|
unsafe { self.instance_belt.alloc_data(sprites, &self.gpu) };
|
||||||
|
let mut encoder = pass.with(&self.pipelines.mono_sprites);
|
||||||
|
encoder.bind(
|
||||||
|
0,
|
||||||
|
&ShaderMonoSpritesData {
|
||||||
|
globals,
|
||||||
|
t_sprite: tex_info.raw_view,
|
||||||
|
s_sprite: self.atlas_sampler,
|
||||||
|
b_mono_sprites: instance_buf,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
encoder.draw(0, 4, 0, sprites.len() as u32);
|
||||||
|
}
|
||||||
|
PrimitiveBatch::PolychromeSprites {
|
||||||
|
texture_id,
|
||||||
|
sprites,
|
||||||
|
} => {
|
||||||
|
let tex_info = self.atlas.get_texture_info(texture_id);
|
||||||
|
let instance_buf =
|
||||||
|
unsafe { self.instance_belt.alloc_data(sprites, &self.gpu) };
|
||||||
|
let mut encoder = pass.with(&self.pipelines.poly_sprites);
|
||||||
|
encoder.bind(
|
||||||
|
0,
|
||||||
|
&ShaderPolySpritesData {
|
||||||
|
globals,
|
||||||
|
t_sprite: tex_info.raw_view,
|
||||||
|
s_sprite: self.atlas_sampler,
|
||||||
|
b_poly_sprites: instance_buf,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
encoder.draw(0, 4, 0, sprites.len() as u32);
|
||||||
|
}
|
||||||
|
PrimitiveBatch::Surfaces(surfaces) => {
|
||||||
|
let mut _encoder = pass.with(&self.pipelines.surfaces);
|
||||||
|
|
||||||
|
for surface in surfaces {
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
{
|
||||||
|
let _ = surface;
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
let (t_y, t_cb_cr) = {
|
||||||
|
use core_foundation::base::TCFType as _;
|
||||||
|
use std::ptr;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
surface.image_buffer.pixel_format_type(),
|
||||||
|
media::core_video::kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
|
||||||
|
);
|
||||||
|
|
||||||
|
let y_texture = unsafe {
|
||||||
|
self.core_video_texture_cache
|
||||||
|
.create_texture_from_image(
|
||||||
|
surface.image_buffer.as_concrete_TypeRef(),
|
||||||
|
ptr::null(),
|
||||||
|
metal::MTLPixelFormat::R8Unorm,
|
||||||
|
surface.image_buffer.plane_width(0),
|
||||||
|
surface.image_buffer.plane_height(0),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
|
let cb_cr_texture = unsafe {
|
||||||
|
self.core_video_texture_cache
|
||||||
|
.create_texture_from_image(
|
||||||
|
surface.image_buffer.as_concrete_TypeRef(),
|
||||||
|
ptr::null(),
|
||||||
|
metal::MTLPixelFormat::RG8Unorm,
|
||||||
|
surface.image_buffer.plane_width(1),
|
||||||
|
surface.image_buffer.plane_height(1),
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
|
(
|
||||||
|
gpu::TextureView::from_metal_texture(
|
||||||
|
y_texture.as_texture_ref(),
|
||||||
|
),
|
||||||
|
gpu::TextureView::from_metal_texture(
|
||||||
|
cb_cr_texture.as_texture_ref(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
_encoder.bind(
|
||||||
|
0,
|
||||||
|
&ShaderSurfacesData {
|
||||||
|
globals,
|
||||||
|
surface_locals: SurfaceParams {
|
||||||
|
bounds: surface.bounds.into(),
|
||||||
|
content_mask: surface.content_mask.bounds.into(),
|
||||||
|
},
|
||||||
|
t_y,
|
||||||
|
t_cb_cr,
|
||||||
|
s_surface: self.atlas_sampler,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
_encoder.draw(0, 4, 0, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.command_encoder.present(frame);
|
||||||
|
let sync_point = self.gpu.submit(&mut self.command_encoder);
|
||||||
|
|
||||||
|
profiling::scope!("finish");
|
||||||
|
self.instance_belt.flush(&sync_point);
|
||||||
|
self.atlas.after_frame(&sync_point);
|
||||||
|
self.atlas.clear_textures(AtlasTextureKind::Path);
|
||||||
|
|
||||||
|
self.wait_for_gpu();
|
||||||
|
self.last_sync_point = Some(sync_point);
|
||||||
|
}
|
||||||
|
}
|
647
crates/ming/src/platform/blade/shaders.wgsl
Normal file
647
crates/ming/src/platform/blade/shaders.wgsl
Normal file
|
@ -0,0 +1,647 @@
|
||||||
|
struct GlobalParams {
|
||||||
|
viewport_size: vec2<f32>,
|
||||||
|
premultiplied_alpha: u32,
|
||||||
|
pad: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
var<uniform> globals: GlobalParams;
|
||||||
|
var t_sprite: texture_2d<f32>;
|
||||||
|
var s_sprite: sampler;
|
||||||
|
|
||||||
|
const M_PI_F: f32 = 3.1415926;
|
||||||
|
const GRAYSCALE_FACTORS: vec3<f32> = vec3<f32>(0.2126, 0.7152, 0.0722);
|
||||||
|
|
||||||
|
struct Bounds {
|
||||||
|
origin: vec2<f32>,
|
||||||
|
size: vec2<f32>,
|
||||||
|
}
|
||||||
|
struct Corners {
|
||||||
|
top_left: f32,
|
||||||
|
top_right: f32,
|
||||||
|
bottom_right: f32,
|
||||||
|
bottom_left: f32,
|
||||||
|
}
|
||||||
|
struct Edges {
|
||||||
|
top: f32,
|
||||||
|
right: f32,
|
||||||
|
bottom: f32,
|
||||||
|
left: f32,
|
||||||
|
}
|
||||||
|
struct Hsla {
|
||||||
|
h: f32,
|
||||||
|
s: f32,
|
||||||
|
l: f32,
|
||||||
|
a: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AtlasTextureId {
|
||||||
|
index: u32,
|
||||||
|
kind: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AtlasBounds {
|
||||||
|
origin: vec2<i32>,
|
||||||
|
size: vec2<i32>,
|
||||||
|
}
|
||||||
|
struct AtlasTile {
|
||||||
|
texture_id: AtlasTextureId,
|
||||||
|
tile_id: u32,
|
||||||
|
padding: u32,
|
||||||
|
bounds: AtlasBounds,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TransformationMatrix {
|
||||||
|
rotation_scale: mat2x2<f32>,
|
||||||
|
translation: vec2<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_device_position_impl(position: vec2<f32>) -> vec4<f32> {
|
||||||
|
let device_position = position / globals.viewport_size * vec2<f32>(2.0, -2.0) + vec2<f32>(-1.0, 1.0);
|
||||||
|
return vec4<f32>(device_position, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_device_position(unit_vertex: vec2<f32>, bounds: Bounds) -> vec4<f32> {
|
||||||
|
let position = unit_vertex * vec2<f32>(bounds.size) + bounds.origin;
|
||||||
|
return to_device_position_impl(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_device_position_transformed(unit_vertex: vec2<f32>, bounds: Bounds, transform: TransformationMatrix) -> vec4<f32> {
|
||||||
|
let position = unit_vertex * vec2<f32>(bounds.size) + bounds.origin;
|
||||||
|
//Note: Rust side stores it as row-major, so transposing here
|
||||||
|
let transformed = transpose(transform.rotation_scale) * position + transform.translation;
|
||||||
|
return to_device_position_impl(transformed);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_tile_position(unit_vertex: vec2<f32>, tile: AtlasTile) -> vec2<f32> {
|
||||||
|
let atlas_size = vec2<f32>(textureDimensions(t_sprite, 0));
|
||||||
|
return (vec2<f32>(tile.bounds.origin) + unit_vertex * vec2<f32>(tile.bounds.size)) / atlas_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn distance_from_clip_rect_impl(position: vec2<f32>, clip_bounds: Bounds) -> vec4<f32> {
|
||||||
|
let tl = position - clip_bounds.origin;
|
||||||
|
let br = clip_bounds.origin + clip_bounds.size - position;
|
||||||
|
return vec4<f32>(tl.x, br.x, tl.y, br.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn distance_from_clip_rect(unit_vertex: vec2<f32>, bounds: Bounds, clip_bounds: Bounds) -> vec4<f32> {
|
||||||
|
let position = unit_vertex * vec2<f32>(bounds.size) + bounds.origin;
|
||||||
|
return distance_from_clip_rect_impl(position, clip_bounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://gamedev.stackexchange.com/questions/92015/optimized-linear-to-srgb-glsl
|
||||||
|
fn srgb_to_linear(srgb: vec3<f32>) -> vec3<f32> {
|
||||||
|
let cutoff = srgb < vec3<f32>(0.04045);
|
||||||
|
let higher = pow((srgb + vec3<f32>(0.055)) / vec3<f32>(1.055), vec3<f32>(2.4));
|
||||||
|
let lower = srgb / vec3<f32>(12.92);
|
||||||
|
return select(higher, lower, cutoff);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hsla_to_rgba(hsla: Hsla) -> vec4<f32> {
|
||||||
|
let h = hsla.h * 6.0; // Now, it's an angle but scaled in [0, 6) range
|
||||||
|
let s = hsla.s;
|
||||||
|
let l = hsla.l;
|
||||||
|
let a = hsla.a;
|
||||||
|
|
||||||
|
let c = (1.0 - abs(2.0 * l - 1.0)) * s;
|
||||||
|
let x = c * (1.0 - abs(h % 2.0 - 1.0));
|
||||||
|
let m = l - c / 2.0;
|
||||||
|
var color = vec3<f32>(m);
|
||||||
|
|
||||||
|
if (h >= 0.0 && h < 1.0) {
|
||||||
|
color.r += c;
|
||||||
|
color.g += x;
|
||||||
|
} else if (h >= 1.0 && h < 2.0) {
|
||||||
|
color.r += x;
|
||||||
|
color.g += c;
|
||||||
|
} else if (h >= 2.0 && h < 3.0) {
|
||||||
|
color.g += c;
|
||||||
|
color.b += x;
|
||||||
|
} else if (h >= 3.0 && h < 4.0) {
|
||||||
|
color.g += x;
|
||||||
|
color.b += c;
|
||||||
|
} else if (h >= 4.0 && h < 5.0) {
|
||||||
|
color.r += x;
|
||||||
|
color.b += c;
|
||||||
|
} else {
|
||||||
|
color.r += c;
|
||||||
|
color.b += x;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Input colors are assumed to be in sRGB space,
|
||||||
|
// but blending and rendering needs to happen in linear space.
|
||||||
|
// The output will be converted to sRGB by either the target
|
||||||
|
// texture format or the swapchain color space.
|
||||||
|
let linear = srgb_to_linear(color);
|
||||||
|
return vec4<f32>(linear, a);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn over(below: vec4<f32>, above: vec4<f32>) -> vec4<f32> {
|
||||||
|
let alpha = above.a + below.a * (1.0 - above.a);
|
||||||
|
let color = (above.rgb * above.a + below.rgb * below.a * (1.0 - above.a)) / alpha;
|
||||||
|
return vec4<f32>(color, alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A standard gaussian function, used for weighting samples
|
||||||
|
fn gaussian(x: f32, sigma: f32) -> f32{
|
||||||
|
return exp(-(x * x) / (2.0 * sigma * sigma)) / (sqrt(2.0 * M_PI_F) * sigma);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This approximates the error function, needed for the gaussian integral
|
||||||
|
fn erf(v: vec2<f32>) -> vec2<f32> {
|
||||||
|
let s = sign(v);
|
||||||
|
let a = abs(v);
|
||||||
|
let r1 = 1.0 + (0.278393 + (0.230389 + 0.078108 * (a * a)) * a) * a;
|
||||||
|
let r2 = r1 * r1;
|
||||||
|
return s - s / (r2 * r2);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn blur_along_x(x: f32, y: f32, sigma: f32, corner: f32, half_size: vec2<f32>) -> f32 {
|
||||||
|
let delta = min(half_size.y - corner - abs(y), 0.0);
|
||||||
|
let curved = half_size.x - corner + sqrt(max(0.0, corner * corner - delta * delta));
|
||||||
|
let integral = 0.5 + 0.5 * erf((x + vec2<f32>(-curved, curved)) * (sqrt(0.5) / sigma));
|
||||||
|
return integral.y - integral.x;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pick_corner_radius(point: vec2<f32>, radii: Corners) -> f32 {
|
||||||
|
if (point.x < 0.0) {
|
||||||
|
if (point.y < 0.0) {
|
||||||
|
return radii.top_left;
|
||||||
|
} else {
|
||||||
|
return radii.bottom_left;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (point.y < 0.0) {
|
||||||
|
return radii.top_right;
|
||||||
|
} else {
|
||||||
|
return radii.bottom_right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn quad_sdf(point: vec2<f32>, bounds: Bounds, corner_radii: Corners) -> f32 {
|
||||||
|
let half_size = bounds.size / 2.0;
|
||||||
|
let center = bounds.origin + half_size;
|
||||||
|
let center_to_point = point - center;
|
||||||
|
let corner_radius = pick_corner_radius(center_to_point, corner_radii);
|
||||||
|
let rounded_edge_to_point = abs(center_to_point) - half_size + corner_radius;
|
||||||
|
return length(max(vec2<f32>(0.0), rounded_edge_to_point)) +
|
||||||
|
min(0.0, max(rounded_edge_to_point.x, rounded_edge_to_point.y)) -
|
||||||
|
corner_radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Abstract away the final color transformation based on the
|
||||||
|
// target alpha compositing mode.
|
||||||
|
fn blend_color(color: vec4<f32>, alpha_factor: f32) -> vec4<f32> {
|
||||||
|
let alpha = color.a * alpha_factor;
|
||||||
|
let multiplier = select(1.0, alpha, globals.premultiplied_alpha != 0u);
|
||||||
|
return vec4<f32>(color.rgb * multiplier, alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- quads --- //
|
||||||
|
|
||||||
|
struct Quad {
|
||||||
|
order: u32,
|
||||||
|
pad: u32,
|
||||||
|
bounds: Bounds,
|
||||||
|
content_mask: Bounds,
|
||||||
|
background: Hsla,
|
||||||
|
border_color: Hsla,
|
||||||
|
corner_radii: Corners,
|
||||||
|
border_widths: Edges,
|
||||||
|
}
|
||||||
|
var<storage, read> b_quads: array<Quad>;
|
||||||
|
|
||||||
|
struct QuadVarying {
|
||||||
|
@builtin(position) position: vec4<f32>,
|
||||||
|
@location(0) @interpolate(flat) background_color: vec4<f32>,
|
||||||
|
@location(1) @interpolate(flat) border_color: vec4<f32>,
|
||||||
|
@location(2) @interpolate(flat) quad_id: u32,
|
||||||
|
//TODO: use `clip_distance` once Naga supports it
|
||||||
|
@location(3) clip_distances: vec4<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
fn vs_quad(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> QuadVarying {
|
||||||
|
let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
|
||||||
|
let quad = b_quads[instance_id];
|
||||||
|
|
||||||
|
var out = QuadVarying();
|
||||||
|
out.position = to_device_position(unit_vertex, quad.bounds);
|
||||||
|
out.background_color = hsla_to_rgba(quad.background);
|
||||||
|
out.border_color = hsla_to_rgba(quad.border_color);
|
||||||
|
out.quad_id = instance_id;
|
||||||
|
out.clip_distances = distance_from_clip_rect(unit_vertex, quad.bounds, quad.content_mask);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fs_quad(input: QuadVarying) -> @location(0) vec4<f32> {
|
||||||
|
// Alpha clip first, since we don't have `clip_distance`.
|
||||||
|
if (any(input.clip_distances < vec4<f32>(0.0))) {
|
||||||
|
return vec4<f32>(0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let quad = b_quads[input.quad_id];
|
||||||
|
// Fast path when the quad is not rounded and doesn't have any border.
|
||||||
|
if (quad.corner_radii.top_left == 0.0 && quad.corner_radii.bottom_left == 0.0 &&
|
||||||
|
quad.corner_radii.top_right == 0.0 &&
|
||||||
|
quad.corner_radii.bottom_right == 0.0 && quad.border_widths.top == 0.0 &&
|
||||||
|
quad.border_widths.left == 0.0 && quad.border_widths.right == 0.0 &&
|
||||||
|
quad.border_widths.bottom == 0.0) {
|
||||||
|
return blend_color(input.background_color, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let half_size = quad.bounds.size / 2.0;
|
||||||
|
let center = quad.bounds.origin + half_size;
|
||||||
|
let center_to_point = input.position.xy - center;
|
||||||
|
|
||||||
|
let corner_radius = pick_corner_radius(center_to_point, quad.corner_radii);
|
||||||
|
|
||||||
|
let rounded_edge_to_point = abs(center_to_point) - half_size + corner_radius;
|
||||||
|
let distance =
|
||||||
|
length(max(vec2<f32>(0.0), rounded_edge_to_point)) +
|
||||||
|
min(0.0, max(rounded_edge_to_point.x, rounded_edge_to_point.y)) -
|
||||||
|
corner_radius;
|
||||||
|
|
||||||
|
let vertical_border = select(quad.border_widths.left, quad.border_widths.right, center_to_point.x > 0.0);
|
||||||
|
let horizontal_border = select(quad.border_widths.top, quad.border_widths.bottom, center_to_point.y > 0.0);
|
||||||
|
let inset_size = half_size - corner_radius - vec2<f32>(vertical_border, horizontal_border);
|
||||||
|
let point_to_inset_corner = abs(center_to_point) - inset_size;
|
||||||
|
|
||||||
|
var border_width = 0.0;
|
||||||
|
if (point_to_inset_corner.x < 0.0 && point_to_inset_corner.y < 0.0) {
|
||||||
|
border_width = 0.0;
|
||||||
|
} else if (point_to_inset_corner.y > point_to_inset_corner.x) {
|
||||||
|
border_width = horizontal_border;
|
||||||
|
} else {
|
||||||
|
border_width = vertical_border;
|
||||||
|
}
|
||||||
|
|
||||||
|
var color = input.background_color;
|
||||||
|
if (border_width > 0.0) {
|
||||||
|
let inset_distance = distance + border_width;
|
||||||
|
// Blend the border on top of the background and then linearly interpolate
|
||||||
|
// between the two as we slide inside the background.
|
||||||
|
let blended_border = over(input.background_color, input.border_color);
|
||||||
|
color = mix(blended_border, input.background_color,
|
||||||
|
saturate(0.5 - inset_distance));
|
||||||
|
}
|
||||||
|
|
||||||
|
return blend_color(color, saturate(0.5 - distance));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- shadows --- //
|
||||||
|
|
||||||
|
struct Shadow {
|
||||||
|
order: u32,
|
||||||
|
blur_radius: f32,
|
||||||
|
bounds: Bounds,
|
||||||
|
corner_radii: Corners,
|
||||||
|
content_mask: Bounds,
|
||||||
|
color: Hsla,
|
||||||
|
}
|
||||||
|
var<storage, read> b_shadows: array<Shadow>;
|
||||||
|
|
||||||
|
struct ShadowVarying {
|
||||||
|
@builtin(position) position: vec4<f32>,
|
||||||
|
@location(0) @interpolate(flat) color: vec4<f32>,
|
||||||
|
@location(1) @interpolate(flat) shadow_id: u32,
|
||||||
|
//TODO: use `clip_distance` once Naga supports it
|
||||||
|
@location(3) clip_distances: vec4<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
fn vs_shadow(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> ShadowVarying {
|
||||||
|
let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
|
||||||
|
var shadow = b_shadows[instance_id];
|
||||||
|
|
||||||
|
let margin = 3.0 * shadow.blur_radius;
|
||||||
|
// Set the bounds of the shadow and adjust its size based on the shadow's
|
||||||
|
// spread radius to achieve the spreading effect
|
||||||
|
shadow.bounds.origin -= vec2<f32>(margin);
|
||||||
|
shadow.bounds.size += 2.0 * vec2<f32>(margin);
|
||||||
|
|
||||||
|
var out = ShadowVarying();
|
||||||
|
out.position = to_device_position(unit_vertex, shadow.bounds);
|
||||||
|
out.color = hsla_to_rgba(shadow.color);
|
||||||
|
out.shadow_id = instance_id;
|
||||||
|
out.clip_distances = distance_from_clip_rect(unit_vertex, shadow.bounds, shadow.content_mask);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fs_shadow(input: ShadowVarying) -> @location(0) vec4<f32> {
|
||||||
|
// Alpha clip first, since we don't have `clip_distance`.
|
||||||
|
if (any(input.clip_distances < vec4<f32>(0.0))) {
|
||||||
|
return vec4<f32>(0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let shadow = b_shadows[input.shadow_id];
|
||||||
|
let half_size = shadow.bounds.size / 2.0;
|
||||||
|
let center = shadow.bounds.origin + half_size;
|
||||||
|
let center_to_point = input.position.xy - center;
|
||||||
|
|
||||||
|
let corner_radius = pick_corner_radius(center_to_point, shadow.corner_radii);
|
||||||
|
|
||||||
|
// The signal is only non-zero in a limited range, so don't waste samples
|
||||||
|
let low = center_to_point.y - half_size.y;
|
||||||
|
let high = center_to_point.y + half_size.y;
|
||||||
|
let start = clamp(-3.0 * shadow.blur_radius, low, high);
|
||||||
|
let end = clamp(3.0 * shadow.blur_radius, low, high);
|
||||||
|
|
||||||
|
// Accumulate samples (we can get away with surprisingly few samples)
|
||||||
|
let step = (end - start) / 4.0;
|
||||||
|
var y = start + step * 0.5;
|
||||||
|
var alpha = 0.0;
|
||||||
|
for (var i = 0; i < 4; i += 1) {
|
||||||
|
let blur = blur_along_x(center_to_point.x, center_to_point.y - y,
|
||||||
|
shadow.blur_radius, corner_radius, half_size);
|
||||||
|
alpha += blur * gaussian(y, shadow.blur_radius) * step;
|
||||||
|
y += step;
|
||||||
|
}
|
||||||
|
|
||||||
|
return blend_color(input.color, alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- path rasterization --- //
|
||||||
|
|
||||||
|
struct PathVertex {
|
||||||
|
xy_position: vec2<f32>,
|
||||||
|
st_position: vec2<f32>,
|
||||||
|
content_mask: Bounds,
|
||||||
|
}
|
||||||
|
var<storage, read> b_path_vertices: array<PathVertex>;
|
||||||
|
|
||||||
|
struct PathRasterizationVarying {
|
||||||
|
@builtin(position) position: vec4<f32>,
|
||||||
|
@location(0) st_position: vec2<f32>,
|
||||||
|
//TODO: use `clip_distance` once Naga supports it
|
||||||
|
@location(3) clip_distances: vec4<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
fn vs_path_rasterization(@builtin(vertex_index) vertex_id: u32) -> PathRasterizationVarying {
|
||||||
|
let v = b_path_vertices[vertex_id];
|
||||||
|
|
||||||
|
var out = PathRasterizationVarying();
|
||||||
|
out.position = to_device_position_impl(v.xy_position);
|
||||||
|
out.st_position = v.st_position;
|
||||||
|
out.clip_distances = distance_from_clip_rect_impl(v.xy_position, v.content_mask);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fs_path_rasterization(input: PathRasterizationVarying) -> @location(0) f32 {
|
||||||
|
let dx = dpdx(input.st_position);
|
||||||
|
let dy = dpdy(input.st_position);
|
||||||
|
if (any(input.clip_distances < vec4<f32>(0.0))) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let gradient = 2.0 * input.st_position.xx * vec2<f32>(dx.x, dy.x) - vec2<f32>(dx.y, dy.y);
|
||||||
|
let f = input.st_position.x * input.st_position.x - input.st_position.y;
|
||||||
|
let distance = f / length(gradient);
|
||||||
|
return saturate(0.5 - distance);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- paths --- //
|
||||||
|
|
||||||
|
struct PathSprite {
|
||||||
|
bounds: Bounds,
|
||||||
|
color: Hsla,
|
||||||
|
tile: AtlasTile,
|
||||||
|
}
|
||||||
|
var<storage, read> b_path_sprites: array<PathSprite>;
|
||||||
|
|
||||||
|
struct PathVarying {
|
||||||
|
@builtin(position) position: vec4<f32>,
|
||||||
|
@location(0) tile_position: vec2<f32>,
|
||||||
|
@location(1) color: vec4<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
fn vs_path(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> PathVarying {
|
||||||
|
let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
|
||||||
|
let sprite = b_path_sprites[instance_id];
|
||||||
|
// Don't apply content mask because it was already accounted for when rasterizing the path.
|
||||||
|
|
||||||
|
var out = PathVarying();
|
||||||
|
out.position = to_device_position(unit_vertex, sprite.bounds);
|
||||||
|
out.tile_position = to_tile_position(unit_vertex, sprite.tile);
|
||||||
|
out.color = hsla_to_rgba(sprite.color);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fs_path(input: PathVarying) -> @location(0) vec4<f32> {
|
||||||
|
let sample = textureSample(t_sprite, s_sprite, input.tile_position).r;
|
||||||
|
let mask = 1.0 - abs(1.0 - sample % 2.0);
|
||||||
|
return blend_color(input.color, mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- underlines --- //
|
||||||
|
|
||||||
|
struct Underline {
|
||||||
|
order: u32,
|
||||||
|
pad: u32,
|
||||||
|
bounds: Bounds,
|
||||||
|
content_mask: Bounds,
|
||||||
|
color: Hsla,
|
||||||
|
thickness: f32,
|
||||||
|
wavy: u32,
|
||||||
|
}
|
||||||
|
var<storage, read> b_underlines: array<Underline>;
|
||||||
|
|
||||||
|
struct UnderlineVarying {
|
||||||
|
@builtin(position) position: vec4<f32>,
|
||||||
|
@location(0) @interpolate(flat) color: vec4<f32>,
|
||||||
|
@location(1) @interpolate(flat) underline_id: u32,
|
||||||
|
//TODO: use `clip_distance` once Naga supports it
|
||||||
|
@location(3) clip_distances: vec4<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
fn vs_underline(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> UnderlineVarying {
|
||||||
|
let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
|
||||||
|
let underline = b_underlines[instance_id];
|
||||||
|
|
||||||
|
var out = UnderlineVarying();
|
||||||
|
out.position = to_device_position(unit_vertex, underline.bounds);
|
||||||
|
out.color = hsla_to_rgba(underline.color);
|
||||||
|
out.underline_id = instance_id;
|
||||||
|
out.clip_distances = distance_from_clip_rect(unit_vertex, underline.bounds, underline.content_mask);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fs_underline(input: UnderlineVarying) -> @location(0) vec4<f32> {
|
||||||
|
// Alpha clip first, since we don't have `clip_distance`.
|
||||||
|
if (any(input.clip_distances < vec4<f32>(0.0))) {
|
||||||
|
return vec4<f32>(0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let underline = b_underlines[input.underline_id];
|
||||||
|
if ((underline.wavy & 0xFFu) == 0u)
|
||||||
|
{
|
||||||
|
return vec4<f32>(0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let half_thickness = underline.thickness * 0.5;
|
||||||
|
let st = (input.position.xy - underline.bounds.origin) / underline.bounds.size.y - vec2<f32>(0.0, 0.5);
|
||||||
|
let frequency = M_PI_F * 3.0 * underline.thickness / 8.0;
|
||||||
|
let amplitude = 1.0 / (2.0 * underline.thickness);
|
||||||
|
let sine = sin(st.x * frequency) * amplitude;
|
||||||
|
let dSine = cos(st.x * frequency) * amplitude * frequency;
|
||||||
|
let distance = (st.y - sine) / sqrt(1.0 + dSine * dSine);
|
||||||
|
let distance_in_pixels = distance * underline.bounds.size.y;
|
||||||
|
let distance_from_top_border = distance_in_pixels - half_thickness;
|
||||||
|
let distance_from_bottom_border = distance_in_pixels + half_thickness;
|
||||||
|
let alpha = saturate(0.5 - max(-distance_from_bottom_border, distance_from_top_border));
|
||||||
|
return blend_color(input.color, alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- monochrome sprites --- //
|
||||||
|
|
||||||
|
struct MonochromeSprite {
|
||||||
|
order: u32,
|
||||||
|
pad: u32,
|
||||||
|
bounds: Bounds,
|
||||||
|
content_mask: Bounds,
|
||||||
|
color: Hsla,
|
||||||
|
tile: AtlasTile,
|
||||||
|
transformation: TransformationMatrix,
|
||||||
|
}
|
||||||
|
var<storage, read> b_mono_sprites: array<MonochromeSprite>;
|
||||||
|
|
||||||
|
struct MonoSpriteVarying {
|
||||||
|
@builtin(position) position: vec4<f32>,
|
||||||
|
@location(0) tile_position: vec2<f32>,
|
||||||
|
@location(1) @interpolate(flat) color: vec4<f32>,
|
||||||
|
@location(3) clip_distances: vec4<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
fn vs_mono_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> MonoSpriteVarying {
|
||||||
|
let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
|
||||||
|
let sprite = b_mono_sprites[instance_id];
|
||||||
|
|
||||||
|
var out = MonoSpriteVarying();
|
||||||
|
out.position = to_device_position_transformed(unit_vertex, sprite.bounds, sprite.transformation);
|
||||||
|
|
||||||
|
out.tile_position = to_tile_position(unit_vertex, sprite.tile);
|
||||||
|
out.color = hsla_to_rgba(sprite.color);
|
||||||
|
out.clip_distances = distance_from_clip_rect(unit_vertex, sprite.bounds, sprite.content_mask);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fs_mono_sprite(input: MonoSpriteVarying) -> @location(0) vec4<f32> {
|
||||||
|
let sample = textureSample(t_sprite, s_sprite, input.tile_position).r;
|
||||||
|
// Alpha clip after using the derivatives.
|
||||||
|
if (any(input.clip_distances < vec4<f32>(0.0))) {
|
||||||
|
return vec4<f32>(0.0);
|
||||||
|
}
|
||||||
|
return blend_color(input.color, sample);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- polychrome sprites --- //
|
||||||
|
|
||||||
|
struct PolychromeSprite {
|
||||||
|
order: u32,
|
||||||
|
grayscale: u32,
|
||||||
|
bounds: Bounds,
|
||||||
|
content_mask: Bounds,
|
||||||
|
corner_radii: Corners,
|
||||||
|
tile: AtlasTile,
|
||||||
|
}
|
||||||
|
var<storage, read> b_poly_sprites: array<PolychromeSprite>;
|
||||||
|
|
||||||
|
struct PolySpriteVarying {
|
||||||
|
@builtin(position) position: vec4<f32>,
|
||||||
|
@location(0) tile_position: vec2<f32>,
|
||||||
|
@location(1) @interpolate(flat) sprite_id: u32,
|
||||||
|
@location(3) clip_distances: vec4<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
fn vs_poly_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> PolySpriteVarying {
|
||||||
|
let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
|
||||||
|
let sprite = b_poly_sprites[instance_id];
|
||||||
|
|
||||||
|
var out = PolySpriteVarying();
|
||||||
|
out.position = to_device_position(unit_vertex, sprite.bounds);
|
||||||
|
out.tile_position = to_tile_position(unit_vertex, sprite.tile);
|
||||||
|
out.sprite_id = instance_id;
|
||||||
|
out.clip_distances = distance_from_clip_rect(unit_vertex, sprite.bounds, sprite.content_mask);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fs_poly_sprite(input: PolySpriteVarying) -> @location(0) vec4<f32> {
|
||||||
|
let sample = textureSample(t_sprite, s_sprite, input.tile_position);
|
||||||
|
// Alpha clip after using the derivatives.
|
||||||
|
if (any(input.clip_distances < vec4<f32>(0.0))) {
|
||||||
|
return vec4<f32>(0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let sprite = b_poly_sprites[input.sprite_id];
|
||||||
|
let distance = quad_sdf(input.position.xy, sprite.bounds, sprite.corner_radii);
|
||||||
|
|
||||||
|
var color = sample;
|
||||||
|
if ((sprite.grayscale & 0xFFu) != 0u) {
|
||||||
|
let grayscale = dot(color.rgb, GRAYSCALE_FACTORS);
|
||||||
|
color = vec4<f32>(vec3<f32>(grayscale), sample.a);
|
||||||
|
}
|
||||||
|
return blend_color(color, saturate(0.5 - distance));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- surfaces --- //
|
||||||
|
|
||||||
|
struct SurfaceParams {
|
||||||
|
bounds: Bounds,
|
||||||
|
content_mask: Bounds,
|
||||||
|
}
|
||||||
|
|
||||||
|
var<uniform> surface_locals: SurfaceParams;
|
||||||
|
var t_y: texture_2d<f32>;
|
||||||
|
var t_cb_cr: texture_2d<f32>;
|
||||||
|
var s_surface: sampler;
|
||||||
|
|
||||||
|
const ycbcr_to_RGB = mat4x4<f32>(
|
||||||
|
vec4<f32>( 1.0000f, 1.0000f, 1.0000f, 0.0),
|
||||||
|
vec4<f32>( 0.0000f, -0.3441f, 1.7720f, 0.0),
|
||||||
|
vec4<f32>( 1.4020f, -0.7141f, 0.0000f, 0.0),
|
||||||
|
vec4<f32>(-0.7010f, 0.5291f, -0.8860f, 1.0),
|
||||||
|
);
|
||||||
|
|
||||||
|
struct SurfaceVarying {
|
||||||
|
@builtin(position) position: vec4<f32>,
|
||||||
|
@location(0) texture_position: vec2<f32>,
|
||||||
|
@location(3) clip_distances: vec4<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
fn vs_surface(@builtin(vertex_index) vertex_id: u32) -> SurfaceVarying {
|
||||||
|
let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
|
||||||
|
|
||||||
|
var out = SurfaceVarying();
|
||||||
|
out.position = to_device_position(unit_vertex, surface_locals.bounds);
|
||||||
|
out.texture_position = unit_vertex;
|
||||||
|
out.clip_distances = distance_from_clip_rect(unit_vertex, surface_locals.bounds, surface_locals.content_mask);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fs_surface(input: SurfaceVarying) -> @location(0) vec4<f32> {
|
||||||
|
// Alpha clip after using the derivatives.
|
||||||
|
if (any(input.clip_distances < vec4<f32>(0.0))) {
|
||||||
|
return vec4<f32>(0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let y_cb_cr = vec4<f32>(
|
||||||
|
textureSampleLevel(t_y, s_surface, input.texture_position, 0.0).r,
|
||||||
|
textureSampleLevel(t_cb_cr, s_surface, input.texture_position, 0.0).rg,
|
||||||
|
1.0);
|
||||||
|
|
||||||
|
return ycbcr_to_RGB * y_cb_cr;
|
||||||
|
}
|
3
crates/ming/src/platform/cosmic_text.rs
Normal file
3
crates/ming/src/platform/cosmic_text.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
mod text_system;
|
||||||
|
|
||||||
|
pub(crate) use text_system::*;
|
516
crates/ming/src/platform/cosmic_text/text_system.rs
Normal file
516
crates/ming/src/platform/cosmic_text/text_system.rs
Normal file
|
@ -0,0 +1,516 @@
|
||||||
|
use crate::{
|
||||||
|
point, size, Bounds, DevicePixels, Font, FontFeatures, FontId, FontMetrics, FontRun, FontStyle,
|
||||||
|
FontWeight, GlyphId, LineLayout, Pixels, PlatformTextSystem, Point, RenderGlyphParams,
|
||||||
|
ShapedGlyph, SharedString, Size,
|
||||||
|
};
|
||||||
|
use anyhow::{anyhow, Context, Ok, Result};
|
||||||
|
use collections::HashMap;
|
||||||
|
use cosmic_text::{
|
||||||
|
Attrs, AttrsList, BufferLine, CacheKey, Family, Font as CosmicTextFont, FontSystem, SwashCache,
|
||||||
|
};
|
||||||
|
|
||||||
|
use itertools::Itertools;
|
||||||
|
use parking_lot::RwLock;
|
||||||
|
use pathfinder_geometry::{
|
||||||
|
rect::{RectF, RectI},
|
||||||
|
vector::{Vector2F, Vector2I},
|
||||||
|
};
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
use std::{borrow::Cow, sync::Arc};
|
||||||
|
|
||||||
|
pub(crate) struct CosmicTextSystem(RwLock<CosmicTextSystemState>);
|
||||||
|
|
||||||
|
struct CosmicTextSystemState {
|
||||||
|
swash_cache: SwashCache,
|
||||||
|
font_system: FontSystem,
|
||||||
|
/// Contains all already loaded fonts, including all faces. Indexed by `FontId`.
|
||||||
|
loaded_fonts_store: Vec<Arc<CosmicTextFont>>,
|
||||||
|
/// Caches the `FontId`s associated with a specific family to avoid iterating the font database
|
||||||
|
/// for every font face in a family.
|
||||||
|
font_ids_by_family_cache: HashMap<SharedString, SmallVec<[FontId; 4]>>,
|
||||||
|
/// The name of each font associated with the given font id
|
||||||
|
postscript_names: HashMap<FontId, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CosmicTextSystem {
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
let mut font_system = FontSystem::new();
|
||||||
|
|
||||||
|
// todo(linux) make font loading non-blocking
|
||||||
|
font_system.db_mut().load_system_fonts();
|
||||||
|
|
||||||
|
Self(RwLock::new(CosmicTextSystemState {
|
||||||
|
font_system,
|
||||||
|
swash_cache: SwashCache::new(),
|
||||||
|
loaded_fonts_store: Vec::new(),
|
||||||
|
font_ids_by_family_cache: HashMap::default(),
|
||||||
|
postscript_names: HashMap::default(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CosmicTextSystem {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlatformTextSystem for CosmicTextSystem {
|
||||||
|
fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
|
||||||
|
self.0.write().add_fonts(fonts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo(linux) ensure that this integrates with platform font loading
|
||||||
|
// do we need to do more than call load_system_fonts()?
|
||||||
|
fn all_font_names(&self) -> Vec<String> {
|
||||||
|
self.0
|
||||||
|
.read()
|
||||||
|
.font_system
|
||||||
|
.db()
|
||||||
|
.faces()
|
||||||
|
.map(|face| face.post_script_name.clone())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn all_font_families(&self) -> Vec<String> {
|
||||||
|
self.0
|
||||||
|
.read()
|
||||||
|
.font_system
|
||||||
|
.db()
|
||||||
|
.faces()
|
||||||
|
// todo(linux) this will list the same font family multiple times
|
||||||
|
.filter_map(|face| face.families.first().map(|family| family.0.clone()))
|
||||||
|
.collect_vec()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn font_id(&self, font: &Font) -> Result<FontId> {
|
||||||
|
// todo(linux): Do we need to use CosmicText's Font APIs? Can we consolidate this to use font_kit?
|
||||||
|
let mut state = self.0.write();
|
||||||
|
|
||||||
|
let candidates = if let Some(font_ids) = state.font_ids_by_family_cache.get(&font.family) {
|
||||||
|
font_ids.as_slice()
|
||||||
|
} else {
|
||||||
|
let font_ids = state.load_family(&font.family, &font.features)?;
|
||||||
|
state
|
||||||
|
.font_ids_by_family_cache
|
||||||
|
.insert(font.family.clone(), font_ids);
|
||||||
|
state.font_ids_by_family_cache[&font.family].as_ref()
|
||||||
|
};
|
||||||
|
|
||||||
|
// todo(linux) ideally we would make fontdb's `find_best_match` pub instead of using font-kit here
|
||||||
|
let candidate_properties = candidates
|
||||||
|
.iter()
|
||||||
|
.map(|font_id| {
|
||||||
|
let database_id = state.loaded_fonts_store[font_id.0].id();
|
||||||
|
let face_info = state.font_system.db().face(database_id).expect("");
|
||||||
|
face_info_into_properties(face_info)
|
||||||
|
})
|
||||||
|
.collect::<SmallVec<[_; 4]>>();
|
||||||
|
|
||||||
|
let ix =
|
||||||
|
font_kit::matching::find_best_match(&candidate_properties, &font_into_properties(font))
|
||||||
|
.context("requested font family contains no font matching the other parameters")?;
|
||||||
|
|
||||||
|
Ok(candidates[ix])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn font_metrics(&self, font_id: FontId) -> FontMetrics {
|
||||||
|
let metrics = self.0.read().loaded_fonts_store[font_id.0]
|
||||||
|
.as_swash()
|
||||||
|
.metrics(&[]);
|
||||||
|
|
||||||
|
FontMetrics {
|
||||||
|
units_per_em: metrics.units_per_em as u32,
|
||||||
|
ascent: metrics.ascent,
|
||||||
|
descent: -metrics.descent, // todo(linux) confirm this is correct
|
||||||
|
line_gap: metrics.leading,
|
||||||
|
underline_position: metrics.underline_offset,
|
||||||
|
underline_thickness: metrics.stroke_size,
|
||||||
|
cap_height: metrics.cap_height,
|
||||||
|
x_height: metrics.x_height,
|
||||||
|
// todo(linux): Compute this correctly
|
||||||
|
bounding_box: Bounds {
|
||||||
|
origin: point(0.0, 0.0),
|
||||||
|
size: size(metrics.max_width, metrics.ascent + metrics.descent),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>> {
|
||||||
|
let lock = self.0.read();
|
||||||
|
let glyph_metrics = lock.loaded_fonts_store[font_id.0]
|
||||||
|
.as_swash()
|
||||||
|
.glyph_metrics(&[]);
|
||||||
|
let glyph_id = glyph_id.0 as u16;
|
||||||
|
// todo(linux): Compute this correctly
|
||||||
|
// see https://github.com/servo/font-kit/blob/master/src/loaders/freetype.rs#L614-L620
|
||||||
|
Ok(Bounds {
|
||||||
|
origin: point(0.0, 0.0),
|
||||||
|
size: size(
|
||||||
|
glyph_metrics.advance_width(glyph_id),
|
||||||
|
glyph_metrics.advance_height(glyph_id),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
|
||||||
|
self.0.read().advance(font_id, glyph_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
|
||||||
|
self.0.read().glyph_for_char(font_id, ch)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
|
||||||
|
self.0.write().raster_bounds(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rasterize_glyph(
|
||||||
|
&self,
|
||||||
|
params: &RenderGlyphParams,
|
||||||
|
raster_bounds: Bounds<DevicePixels>,
|
||||||
|
) -> Result<(Size<DevicePixels>, Vec<u8>)> {
|
||||||
|
self.0.write().rasterize_glyph(params, raster_bounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout {
|
||||||
|
self.0.write().layout_line(text, font_size, runs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CosmicTextSystemState {
|
||||||
|
#[profiling::function]
|
||||||
|
fn add_fonts(&mut self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
|
||||||
|
let db = self.font_system.db_mut();
|
||||||
|
for bytes in fonts {
|
||||||
|
match bytes {
|
||||||
|
Cow::Borrowed(embedded_font) => {
|
||||||
|
db.load_font_data(embedded_font.to_vec());
|
||||||
|
}
|
||||||
|
Cow::Owned(bytes) => {
|
||||||
|
db.load_font_data(bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo(linux) handle `FontFeatures`
|
||||||
|
#[profiling::function]
|
||||||
|
fn load_family(
|
||||||
|
&mut self,
|
||||||
|
name: &str,
|
||||||
|
_features: &FontFeatures,
|
||||||
|
) -> Result<SmallVec<[FontId; 4]>> {
|
||||||
|
// TODO: Determine the proper system UI font.
|
||||||
|
let name = if name == ".SystemUIFont" {
|
||||||
|
"Zed Sans"
|
||||||
|
} else {
|
||||||
|
name
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut font_ids = SmallVec::new();
|
||||||
|
let families = self
|
||||||
|
.font_system
|
||||||
|
.db()
|
||||||
|
.faces()
|
||||||
|
.filter(|face| face.families.iter().any(|family| *name == family.0))
|
||||||
|
.map(|face| (face.id, face.post_script_name.clone()))
|
||||||
|
.collect::<SmallVec<[_; 4]>>();
|
||||||
|
|
||||||
|
for (font_id, postscript_name) in families {
|
||||||
|
let font = self
|
||||||
|
.font_system
|
||||||
|
.get_font(font_id)
|
||||||
|
.ok_or_else(|| anyhow!("Could not load font"))?;
|
||||||
|
|
||||||
|
// HACK: To let the storybook run and render Windows caption icons. We should actually do better font fallback.
|
||||||
|
let allowed_bad_font_names = [
|
||||||
|
"SegoeFluentIcons", // NOTE: Segoe fluent icons postscript name is inconsistent
|
||||||
|
"Segoe Fluent Icons",
|
||||||
|
];
|
||||||
|
|
||||||
|
if font.as_swash().charmap().map('m') == 0
|
||||||
|
&& !allowed_bad_font_names.contains(&postscript_name.as_str())
|
||||||
|
{
|
||||||
|
self.font_system.db_mut().remove_face(font.id());
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let font_id = FontId(self.loaded_fonts_store.len());
|
||||||
|
font_ids.push(font_id);
|
||||||
|
self.loaded_fonts_store.push(font);
|
||||||
|
self.postscript_names.insert(font_id, postscript_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(font_ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
|
||||||
|
let width = self.loaded_fonts_store[font_id.0]
|
||||||
|
.as_swash()
|
||||||
|
.glyph_metrics(&[])
|
||||||
|
.advance_width(glyph_id.0 as u16);
|
||||||
|
let height = self.loaded_fonts_store[font_id.0]
|
||||||
|
.as_swash()
|
||||||
|
.glyph_metrics(&[])
|
||||||
|
.advance_height(glyph_id.0 as u16);
|
||||||
|
Ok(Size { width, height })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
|
||||||
|
let glyph_id = self.loaded_fonts_store[font_id.0]
|
||||||
|
.as_swash()
|
||||||
|
.charmap()
|
||||||
|
.map(ch);
|
||||||
|
if glyph_id == 0 {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(GlyphId(glyph_id.into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_emoji(&self, font_id: FontId) -> bool {
|
||||||
|
// TODO: Include other common emoji fonts
|
||||||
|
self.postscript_names
|
||||||
|
.get(&font_id)
|
||||||
|
.map_or(false, |postscript_name| postscript_name == "NotoColorEmoji")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raster_bounds(&mut self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
|
||||||
|
let font = &self.loaded_fonts_store[params.font_id.0];
|
||||||
|
let font_system = &mut self.font_system;
|
||||||
|
let image = self
|
||||||
|
.swash_cache
|
||||||
|
.get_image(
|
||||||
|
font_system,
|
||||||
|
CacheKey::new(
|
||||||
|
font.id(),
|
||||||
|
params.glyph_id.0 as u16,
|
||||||
|
(params.font_size * params.scale_factor).into(),
|
||||||
|
(0.0, 0.0),
|
||||||
|
cosmic_text::CacheKeyFlags::empty(),
|
||||||
|
)
|
||||||
|
.0,
|
||||||
|
)
|
||||||
|
.clone()
|
||||||
|
.unwrap();
|
||||||
|
Ok(Bounds {
|
||||||
|
origin: point(image.placement.left.into(), (-image.placement.top).into()),
|
||||||
|
size: size(image.placement.width.into(), image.placement.height.into()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[profiling::function]
|
||||||
|
fn rasterize_glyph(
|
||||||
|
&mut self,
|
||||||
|
params: &RenderGlyphParams,
|
||||||
|
glyph_bounds: Bounds<DevicePixels>,
|
||||||
|
) -> Result<(Size<DevicePixels>, Vec<u8>)> {
|
||||||
|
if glyph_bounds.size.width.0 == 0 || glyph_bounds.size.height.0 == 0 {
|
||||||
|
Err(anyhow!("glyph bounds are empty"))
|
||||||
|
} else {
|
||||||
|
// todo(linux) handle subpixel variants
|
||||||
|
let bitmap_size = glyph_bounds.size;
|
||||||
|
let font = &self.loaded_fonts_store[params.font_id.0];
|
||||||
|
let font_system = &mut self.font_system;
|
||||||
|
let image = self
|
||||||
|
.swash_cache
|
||||||
|
.get_image(
|
||||||
|
font_system,
|
||||||
|
CacheKey::new(
|
||||||
|
font.id(),
|
||||||
|
params.glyph_id.0 as u16,
|
||||||
|
(params.font_size * params.scale_factor).into(),
|
||||||
|
(0.0, 0.0),
|
||||||
|
cosmic_text::CacheKeyFlags::empty(),
|
||||||
|
)
|
||||||
|
.0,
|
||||||
|
)
|
||||||
|
.clone()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Ok((bitmap_size, image.data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn font_id_for_cosmic_id(&mut self, id: cosmic_text::fontdb::ID) -> FontId {
|
||||||
|
if let Some(ix) = self
|
||||||
|
.loaded_fonts_store
|
||||||
|
.iter()
|
||||||
|
.position(|font| font.id() == id)
|
||||||
|
{
|
||||||
|
FontId(ix)
|
||||||
|
} else {
|
||||||
|
// This matches the behavior of the mac text system
|
||||||
|
let font = self.font_system.get_font(id).unwrap();
|
||||||
|
let face = self
|
||||||
|
.font_system
|
||||||
|
.db()
|
||||||
|
.faces()
|
||||||
|
.find(|info| info.id == id)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let font_id = FontId(self.loaded_fonts_store.len());
|
||||||
|
self.loaded_fonts_store.push(font);
|
||||||
|
self.postscript_names
|
||||||
|
.insert(font_id, face.post_script_name.clone());
|
||||||
|
|
||||||
|
font_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo(linux) This is all a quick first pass, maybe we should be using cosmic_text::Buffer
|
||||||
|
#[profiling::function]
|
||||||
|
fn layout_line(&mut self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout {
|
||||||
|
let mut attrs_list = AttrsList::new(Attrs::new());
|
||||||
|
let mut offs = 0;
|
||||||
|
for run in font_runs {
|
||||||
|
// todo(linux) We need to check we are doing utf properly
|
||||||
|
let font = &self.loaded_fonts_store[run.font_id.0];
|
||||||
|
let font = self.font_system.db().face(font.id()).unwrap();
|
||||||
|
attrs_list.add_span(
|
||||||
|
offs..(offs + run.len),
|
||||||
|
Attrs::new()
|
||||||
|
.family(Family::Name(&font.families.first().unwrap().0))
|
||||||
|
.stretch(font.stretch)
|
||||||
|
.style(font.style)
|
||||||
|
.weight(font.weight),
|
||||||
|
);
|
||||||
|
offs += run.len;
|
||||||
|
}
|
||||||
|
let mut line = BufferLine::new(text, attrs_list, cosmic_text::Shaping::Advanced);
|
||||||
|
|
||||||
|
let layout = line.layout(
|
||||||
|
&mut self.font_system,
|
||||||
|
font_size.0,
|
||||||
|
f32::MAX, // We do our own wrapping
|
||||||
|
cosmic_text::Wrap::None,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
let mut runs = Vec::new();
|
||||||
|
|
||||||
|
let layout = layout.first().unwrap();
|
||||||
|
for glyph in &layout.glyphs {
|
||||||
|
let font_id = glyph.font_id;
|
||||||
|
let font_id = self.font_id_for_cosmic_id(font_id);
|
||||||
|
let mut glyphs = SmallVec::new();
|
||||||
|
// todo(linux) this is definitely wrong, each glyph in glyphs from cosmic-text is a cluster with one glyph, ShapedRun takes a run of glyphs with the same font and direction
|
||||||
|
glyphs.push(ShapedGlyph {
|
||||||
|
id: GlyphId(glyph.glyph_id as u32),
|
||||||
|
position: point((glyph.x).into(), glyph.y.into()),
|
||||||
|
index: glyph.start,
|
||||||
|
is_emoji: self.is_emoji(font_id),
|
||||||
|
});
|
||||||
|
|
||||||
|
runs.push(crate::ShapedRun { font_id, glyphs });
|
||||||
|
}
|
||||||
|
|
||||||
|
LineLayout {
|
||||||
|
font_size,
|
||||||
|
width: layout.w.into(),
|
||||||
|
ascent: layout.max_ascent.into(),
|
||||||
|
descent: layout.max_descent.into(),
|
||||||
|
runs,
|
||||||
|
len: text.len(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RectF> for Bounds<f32> {
|
||||||
|
fn from(rect: RectF) -> Self {
|
||||||
|
Bounds {
|
||||||
|
origin: point(rect.origin_x(), rect.origin_y()),
|
||||||
|
size: size(rect.width(), rect.height()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RectI> for Bounds<DevicePixels> {
|
||||||
|
fn from(rect: RectI) -> Self {
|
||||||
|
Bounds {
|
||||||
|
origin: point(DevicePixels(rect.origin_x()), DevicePixels(rect.origin_y())),
|
||||||
|
size: size(DevicePixels(rect.width()), DevicePixels(rect.height())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Vector2I> for Size<DevicePixels> {
|
||||||
|
fn from(value: Vector2I) -> Self {
|
||||||
|
size(value.x().into(), value.y().into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RectI> for Bounds<i32> {
|
||||||
|
fn from(rect: RectI) -> Self {
|
||||||
|
Bounds {
|
||||||
|
origin: point(rect.origin_x(), rect.origin_y()),
|
||||||
|
size: size(rect.width(), rect.height()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Point<u32>> for Vector2I {
|
||||||
|
fn from(size: Point<u32>) -> Self {
|
||||||
|
Vector2I::new(size.x as i32, size.y as i32)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Vector2F> for Size<f32> {
|
||||||
|
fn from(vec: Vector2F) -> Self {
|
||||||
|
size(vec.x(), vec.y())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<FontWeight> for cosmic_text::Weight {
|
||||||
|
fn from(value: FontWeight) -> Self {
|
||||||
|
cosmic_text::Weight(value.0 as u16)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<FontStyle> for cosmic_text::Style {
|
||||||
|
fn from(style: FontStyle) -> Self {
|
||||||
|
match style {
|
||||||
|
FontStyle::Normal => cosmic_text::Style::Normal,
|
||||||
|
FontStyle::Italic => cosmic_text::Style::Italic,
|
||||||
|
FontStyle::Oblique => cosmic_text::Style::Oblique,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn font_into_properties(font: &crate::Font) -> font_kit::properties::Properties {
|
||||||
|
font_kit::properties::Properties {
|
||||||
|
style: match font.style {
|
||||||
|
crate::FontStyle::Normal => font_kit::properties::Style::Normal,
|
||||||
|
crate::FontStyle::Italic => font_kit::properties::Style::Italic,
|
||||||
|
crate::FontStyle::Oblique => font_kit::properties::Style::Oblique,
|
||||||
|
},
|
||||||
|
weight: font_kit::properties::Weight(font.weight.0),
|
||||||
|
stretch: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn face_info_into_properties(
|
||||||
|
face_info: &cosmic_text::fontdb::FaceInfo,
|
||||||
|
) -> font_kit::properties::Properties {
|
||||||
|
font_kit::properties::Properties {
|
||||||
|
style: match face_info.style {
|
||||||
|
cosmic_text::Style::Normal => font_kit::properties::Style::Normal,
|
||||||
|
cosmic_text::Style::Italic => font_kit::properties::Style::Italic,
|
||||||
|
cosmic_text::Style::Oblique => font_kit::properties::Style::Oblique,
|
||||||
|
},
|
||||||
|
// both libs use the same values for weight
|
||||||
|
weight: font_kit::properties::Weight(face_info.weight.0.into()),
|
||||||
|
stretch: match face_info.stretch {
|
||||||
|
cosmic_text::Stretch::Condensed => font_kit::properties::Stretch::CONDENSED,
|
||||||
|
cosmic_text::Stretch::Expanded => font_kit::properties::Stretch::EXPANDED,
|
||||||
|
cosmic_text::Stretch::ExtraCondensed => font_kit::properties::Stretch::EXTRA_CONDENSED,
|
||||||
|
cosmic_text::Stretch::ExtraExpanded => font_kit::properties::Stretch::EXTRA_EXPANDED,
|
||||||
|
cosmic_text::Stretch::Normal => font_kit::properties::Stretch::NORMAL,
|
||||||
|
cosmic_text::Stretch::SemiCondensed => font_kit::properties::Stretch::SEMI_CONDENSED,
|
||||||
|
cosmic_text::Stretch::SemiExpanded => font_kit::properties::Stretch::SEMI_EXPANDED,
|
||||||
|
cosmic_text::Stretch::UltraCondensed => font_kit::properties::Stretch::ULTRA_CONDENSED,
|
||||||
|
cosmic_text::Stretch::UltraExpanded => font_kit::properties::Stretch::ULTRA_EXPANDED,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
307
crates/ming/src/platform/keystroke.rs
Normal file
307
crates/ming/src/platform/keystroke.rs
Normal file
|
@ -0,0 +1,307 @@
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
use std::fmt::Write;
|
||||||
|
|
||||||
|
/// A keystroke and associated metadata generated by the platform
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
|
||||||
|
pub struct Keystroke {
|
||||||
|
/// the state of the modifier keys at the time the keystroke was generated
|
||||||
|
pub modifiers: Modifiers,
|
||||||
|
|
||||||
|
/// key is the character printed on the key that was pressed
|
||||||
|
/// e.g. for option-s, key is "s"
|
||||||
|
pub key: String,
|
||||||
|
|
||||||
|
/// ime_key is the character inserted by the IME engine when that key was pressed.
|
||||||
|
/// e.g. for option-s, ime_key is "ß"
|
||||||
|
pub ime_key: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Keystroke {
|
||||||
|
/// When matching a key we cannot know whether the user intended to type
|
||||||
|
/// the ime_key or the key itself. On some non-US keyboards keys we use in our
|
||||||
|
/// bindings are behind option (for example `$` is typed `alt-ç` on a Czech keyboard),
|
||||||
|
/// and on some keyboards the IME handler converts a sequence of keys into a
|
||||||
|
/// specific character (for example `"` is typed as `" space` on a brazilian keyboard).
|
||||||
|
///
|
||||||
|
/// This method generates a list of potential keystroke candidates that could be matched
|
||||||
|
/// against when resolving a keybinding.
|
||||||
|
pub(crate) fn match_candidates(&self) -> SmallVec<[Keystroke; 2]> {
|
||||||
|
let mut possibilities = SmallVec::new();
|
||||||
|
match self.ime_key.as_ref() {
|
||||||
|
Some(ime_key) => {
|
||||||
|
if ime_key != &self.key {
|
||||||
|
possibilities.push(Keystroke {
|
||||||
|
modifiers: Modifiers {
|
||||||
|
control: self.modifiers.control,
|
||||||
|
alt: false,
|
||||||
|
shift: false,
|
||||||
|
platform: false,
|
||||||
|
function: false,
|
||||||
|
},
|
||||||
|
key: ime_key.to_string(),
|
||||||
|
ime_key: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
possibilities.push(Keystroke {
|
||||||
|
ime_key: None,
|
||||||
|
..self.clone()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
None => possibilities.push(self.clone()),
|
||||||
|
}
|
||||||
|
possibilities
|
||||||
|
}
|
||||||
|
|
||||||
|
/// key syntax is:
|
||||||
|
/// [ctrl-][alt-][shift-][cmd-][fn-]key[->ime_key]
|
||||||
|
/// ime_key syntax is only used for generating test events,
|
||||||
|
/// when matching a key with an ime_key set will be matched without it.
|
||||||
|
pub fn parse(source: &str) -> anyhow::Result<Self> {
|
||||||
|
let mut control = false;
|
||||||
|
let mut alt = false;
|
||||||
|
let mut shift = false;
|
||||||
|
let mut platform = false;
|
||||||
|
let mut function = false;
|
||||||
|
let mut key = None;
|
||||||
|
let mut ime_key = None;
|
||||||
|
|
||||||
|
let mut components = source.split('-').peekable();
|
||||||
|
while let Some(component) = components.next() {
|
||||||
|
match component {
|
||||||
|
"ctrl" => control = true,
|
||||||
|
"alt" => alt = true,
|
||||||
|
"shift" => shift = true,
|
||||||
|
"fn" => function = true,
|
||||||
|
"cmd" | "super" | "win" => platform = true,
|
||||||
|
_ => {
|
||||||
|
if let Some(next) = components.peek() {
|
||||||
|
if next.is_empty() && source.ends_with('-') {
|
||||||
|
key = Some(String::from("-"));
|
||||||
|
break;
|
||||||
|
} else if next.len() > 1 && next.starts_with('>') {
|
||||||
|
key = Some(String::from(component));
|
||||||
|
ime_key = Some(String::from(&next[1..]));
|
||||||
|
components.next();
|
||||||
|
} else {
|
||||||
|
return Err(anyhow!("Invalid keystroke `{}`", source));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
key = Some(String::from(component));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = key.ok_or_else(|| anyhow!("Invalid keystroke `{}`", source))?;
|
||||||
|
|
||||||
|
Ok(Keystroke {
|
||||||
|
modifiers: Modifiers {
|
||||||
|
control,
|
||||||
|
alt,
|
||||||
|
shift,
|
||||||
|
platform,
|
||||||
|
function,
|
||||||
|
},
|
||||||
|
key,
|
||||||
|
ime_key,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new keystroke with the ime_key filled.
|
||||||
|
/// This is used for dispatch_keystroke where we want users to
|
||||||
|
/// be able to simulate typing "space", etc.
|
||||||
|
pub fn with_simulated_ime(mut self) -> Self {
|
||||||
|
if self.ime_key.is_none()
|
||||||
|
&& !self.modifiers.platform
|
||||||
|
&& !self.modifiers.control
|
||||||
|
&& !self.modifiers.function
|
||||||
|
&& !self.modifiers.alt
|
||||||
|
{
|
||||||
|
self.ime_key = match self.key.as_str() {
|
||||||
|
"space" => Some(" ".into()),
|
||||||
|
"tab" => Some("\t".into()),
|
||||||
|
"enter" => Some("\n".into()),
|
||||||
|
"up" | "down" | "left" | "right" | "pageup" | "pagedown" | "home" | "end"
|
||||||
|
| "delete" | "escape" | "backspace" | "f1" | "f2" | "f3" | "f4" | "f5" | "f6"
|
||||||
|
| "f7" | "f8" | "f9" | "f10" | "f11" | "f12" => None,
|
||||||
|
key => {
|
||||||
|
if self.modifiers.shift {
|
||||||
|
Some(key.to_uppercase())
|
||||||
|
} else {
|
||||||
|
Some(key.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Keystroke {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
if self.modifiers.control {
|
||||||
|
f.write_char('^')?;
|
||||||
|
}
|
||||||
|
if self.modifiers.alt {
|
||||||
|
f.write_char('⌥')?;
|
||||||
|
}
|
||||||
|
if self.modifiers.platform {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
f.write_char('⌘')?;
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
f.write_char('❖')?;
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
f.write_char('⊞')?;
|
||||||
|
}
|
||||||
|
if self.modifiers.shift {
|
||||||
|
f.write_char('⇧')?;
|
||||||
|
}
|
||||||
|
let key = match self.key.as_str() {
|
||||||
|
"backspace" => '⌫',
|
||||||
|
"up" => '↑',
|
||||||
|
"down" => '↓',
|
||||||
|
"left" => '←',
|
||||||
|
"right" => '→',
|
||||||
|
"tab" => '⇥',
|
||||||
|
"escape" => '⎋',
|
||||||
|
key => {
|
||||||
|
if key.len() == 1 {
|
||||||
|
key.chars().next().unwrap().to_ascii_uppercase()
|
||||||
|
} else {
|
||||||
|
return f.write_str(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
f.write_char(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The state of the modifier keys at some point in time
|
||||||
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
|
||||||
|
pub struct Modifiers {
|
||||||
|
/// The control key
|
||||||
|
pub control: bool,
|
||||||
|
|
||||||
|
/// The alt key
|
||||||
|
/// Sometimes also known as the 'meta' key
|
||||||
|
pub alt: bool,
|
||||||
|
|
||||||
|
/// The shift key
|
||||||
|
pub shift: bool,
|
||||||
|
|
||||||
|
/// The command key, on macos
|
||||||
|
/// the windows key, on windows
|
||||||
|
/// the super key, on linux
|
||||||
|
pub platform: bool,
|
||||||
|
|
||||||
|
/// The function key
|
||||||
|
pub function: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Modifiers {
|
||||||
|
/// Returns true if any modifier key is pressed
|
||||||
|
pub fn modified(&self) -> bool {
|
||||||
|
self.control || self.alt || self.shift || self.platform || self.function
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the semantically 'secondary' modifier key is pressed
|
||||||
|
/// On macos, this is the command key
|
||||||
|
/// On windows and linux, this is the control key
|
||||||
|
pub fn secondary(&self) -> bool {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
return self.platform;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
{
|
||||||
|
return self.control;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// helper method for Modifiers with no modifiers
|
||||||
|
pub fn none() -> Modifiers {
|
||||||
|
Default::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// helper method for Modifiers with just the command key
|
||||||
|
pub fn command() -> Modifiers {
|
||||||
|
Modifiers {
|
||||||
|
platform: true,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A helper method for Modifiers with just the secondary key pressed
|
||||||
|
pub fn secondary_key() -> Modifiers {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
Modifiers {
|
||||||
|
platform: true,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
{
|
||||||
|
Modifiers {
|
||||||
|
control: true,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// helper method for Modifiers with just the windows key
|
||||||
|
pub fn windows() -> Modifiers {
|
||||||
|
Modifiers {
|
||||||
|
platform: true,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// helper method for Modifiers with just the super key
|
||||||
|
pub fn super_key() -> Modifiers {
|
||||||
|
Modifiers {
|
||||||
|
platform: true,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// helper method for Modifiers with just control
|
||||||
|
pub fn control() -> Modifiers {
|
||||||
|
Modifiers {
|
||||||
|
control: true,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// helper method for Modifiers with just shift
|
||||||
|
pub fn shift() -> Modifiers {
|
||||||
|
Modifiers {
|
||||||
|
shift: true,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// helper method for Modifiers with command + shift
|
||||||
|
pub fn command_shift() -> Modifiers {
|
||||||
|
Modifiers {
|
||||||
|
shift: true,
|
||||||
|
platform: true,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if this Modifiers is a subset of another Modifiers
|
||||||
|
pub fn is_subset_of(&self, other: &Modifiers) -> bool {
|
||||||
|
(other.control || !self.control)
|
||||||
|
&& (other.alt || !self.alt)
|
||||||
|
&& (other.shift || !self.shift)
|
||||||
|
&& (other.platform || !self.platform)
|
||||||
|
&& (other.function || !self.function)
|
||||||
|
}
|
||||||
|
}
|
14
crates/ming/src/platform/linux.rs
Normal file
14
crates/ming/src/platform/linux.rs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
// todo(linux): remove
|
||||||
|
#![allow(unused)]
|
||||||
|
|
||||||
|
mod dispatcher;
|
||||||
|
mod headless;
|
||||||
|
mod platform;
|
||||||
|
mod wayland;
|
||||||
|
mod x11;
|
||||||
|
|
||||||
|
pub(crate) use dispatcher::*;
|
||||||
|
pub(crate) use headless::*;
|
||||||
|
pub(crate) use platform::*;
|
||||||
|
pub(crate) use wayland::*;
|
||||||
|
pub(crate) use x11::*;
|
125
crates/ming/src/platform/linux/dispatcher.rs
Normal file
125
crates/ming/src/platform/linux/dispatcher.rs
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
#![allow(non_upper_case_globals)]
|
||||||
|
#![allow(non_camel_case_types)]
|
||||||
|
#![allow(non_snake_case)]
|
||||||
|
// todo(linux): remove
|
||||||
|
#![allow(unused_variables)]
|
||||||
|
|
||||||
|
use crate::{PlatformDispatcher, TaskLabel};
|
||||||
|
use async_task::Runnable;
|
||||||
|
use calloop::{
|
||||||
|
channel::{self, Sender},
|
||||||
|
timer::TimeoutAction,
|
||||||
|
EventLoop,
|
||||||
|
};
|
||||||
|
use parking::{Parker, Unparker};
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use std::{thread, time::Duration};
|
||||||
|
use util::ResultExt;
|
||||||
|
|
||||||
|
struct TimerAfter {
|
||||||
|
duration: Duration,
|
||||||
|
runnable: Runnable,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct LinuxDispatcher {
|
||||||
|
parker: Mutex<Parker>,
|
||||||
|
main_sender: Sender<Runnable>,
|
||||||
|
timer_sender: Sender<TimerAfter>,
|
||||||
|
background_sender: flume::Sender<Runnable>,
|
||||||
|
_background_threads: Vec<thread::JoinHandle<()>>,
|
||||||
|
main_thread_id: thread::ThreadId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LinuxDispatcher {
|
||||||
|
pub fn new(main_sender: Sender<Runnable>) -> Self {
|
||||||
|
let (background_sender, background_receiver) = flume::unbounded::<Runnable>();
|
||||||
|
let thread_count = std::thread::available_parallelism()
|
||||||
|
.map(|i| i.get())
|
||||||
|
.unwrap_or(1);
|
||||||
|
|
||||||
|
let mut background_threads = (0..thread_count)
|
||||||
|
.map(|_| {
|
||||||
|
let receiver = background_receiver.clone();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
for runnable in receiver {
|
||||||
|
runnable.run();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let (timer_sender, timer_channel) = calloop::channel::channel::<TimerAfter>();
|
||||||
|
let timer_thread = std::thread::spawn(|| {
|
||||||
|
let mut event_loop: EventLoop<()> =
|
||||||
|
EventLoop::try_new().expect("Failed to initialize timer loop!");
|
||||||
|
|
||||||
|
let handle = event_loop.handle();
|
||||||
|
let timer_handle = event_loop.handle();
|
||||||
|
handle
|
||||||
|
.insert_source(timer_channel, move |e, _, _| {
|
||||||
|
if let channel::Event::Msg(timer) = e {
|
||||||
|
// This has to be in an option to satisfy the borrow checker. The callback below should only be scheduled once.
|
||||||
|
let mut runnable = Some(timer.runnable);
|
||||||
|
timer_handle
|
||||||
|
.insert_source(
|
||||||
|
calloop::timer::Timer::from_duration(timer.duration),
|
||||||
|
move |e, _, _| {
|
||||||
|
if let Some(runnable) = runnable.take() {
|
||||||
|
runnable.run();
|
||||||
|
}
|
||||||
|
TimeoutAction::Drop
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("Failed to start timer");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.expect("Failed to start timer thread");
|
||||||
|
|
||||||
|
event_loop.run(None, &mut (), |_| {}).log_err();
|
||||||
|
});
|
||||||
|
|
||||||
|
background_threads.push(timer_thread);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
parker: Mutex::new(Parker::new()),
|
||||||
|
main_sender,
|
||||||
|
timer_sender,
|
||||||
|
background_sender,
|
||||||
|
_background_threads: background_threads,
|
||||||
|
main_thread_id: thread::current().id(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlatformDispatcher for LinuxDispatcher {
|
||||||
|
fn is_main_thread(&self) -> bool {
|
||||||
|
thread::current().id() == self.main_thread_id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dispatch(&self, runnable: Runnable, _: Option<TaskLabel>) {
|
||||||
|
self.background_sender.send(runnable).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dispatch_on_main_thread(&self, runnable: Runnable) {
|
||||||
|
self.main_sender.send(runnable).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dispatch_after(&self, duration: Duration, runnable: Runnable) {
|
||||||
|
self.timer_sender
|
||||||
|
.send(TimerAfter { duration, runnable })
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn park(&self, timeout: Option<Duration>) -> bool {
|
||||||
|
if let Some(timeout) = timeout {
|
||||||
|
self.parker.lock().park_timeout(timeout)
|
||||||
|
} else {
|
||||||
|
self.parker.lock().park();
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unparker(&self) -> Unparker {
|
||||||
|
self.parker.lock().unparker()
|
||||||
|
}
|
||||||
|
}
|
3
crates/ming/src/platform/linux/headless.rs
Normal file
3
crates/ming/src/platform/linux/headless.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
mod client;
|
||||||
|
|
||||||
|
pub(crate) use client::*;
|
105
crates/ming/src/platform/linux/headless/client.rs
Normal file
105
crates/ming/src/platform/linux/headless/client.rs
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::ops::Deref;
|
||||||
|
use std::rc::Rc;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use calloop::{EventLoop, LoopHandle};
|
||||||
|
use collections::HashMap;
|
||||||
|
|
||||||
|
use util::ResultExt;
|
||||||
|
|
||||||
|
use crate::platform::linux::LinuxClient;
|
||||||
|
use crate::platform::{LinuxCommon, PlatformWindow};
|
||||||
|
use crate::{
|
||||||
|
px, AnyWindowHandle, Bounds, CursorStyle, DisplayId, Modifiers, ModifiersChangedEvent, Pixels,
|
||||||
|
PlatformDisplay, PlatformInput, Point, ScrollDelta, Size, TouchPhase, WindowParams,
|
||||||
|
};
|
||||||
|
|
||||||
|
use calloop::{
|
||||||
|
generic::{FdWrapper, Generic},
|
||||||
|
RegistrationToken,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct HeadlessClientState {
|
||||||
|
pub(crate) loop_handle: LoopHandle<'static, HeadlessClient>,
|
||||||
|
pub(crate) event_loop: Option<calloop::EventLoop<'static, HeadlessClient>>,
|
||||||
|
pub(crate) common: LinuxCommon,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct HeadlessClient(Rc<RefCell<HeadlessClientState>>);
|
||||||
|
|
||||||
|
impl HeadlessClient {
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
let event_loop = EventLoop::try_new().unwrap();
|
||||||
|
|
||||||
|
let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal());
|
||||||
|
|
||||||
|
let handle = event_loop.handle();
|
||||||
|
|
||||||
|
handle.insert_source(main_receiver, |event, _, _: &mut HeadlessClient| {
|
||||||
|
if let calloop::channel::Event::Msg(runnable) = event {
|
||||||
|
runnable.run();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
HeadlessClient(Rc::new(RefCell::new(HeadlessClientState {
|
||||||
|
event_loop: Some(event_loop),
|
||||||
|
loop_handle: handle,
|
||||||
|
common,
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LinuxClient for HeadlessClient {
|
||||||
|
fn with_common<R>(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R {
|
||||||
|
f(&mut self.0.borrow_mut().common)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_window(
|
||||||
|
&self,
|
||||||
|
_handle: AnyWindowHandle,
|
||||||
|
params: WindowParams,
|
||||||
|
) -> Box<dyn PlatformWindow> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_cursor_style(&self, _style: CursorStyle) {}
|
||||||
|
|
||||||
|
fn open_uri(&self, _uri: &str) {}
|
||||||
|
|
||||||
|
fn write_to_primary(&self, item: crate::ClipboardItem) {}
|
||||||
|
|
||||||
|
fn write_to_clipboard(&self, item: crate::ClipboardItem) {}
|
||||||
|
|
||||||
|
fn read_from_primary(&self) -> Option<crate::ClipboardItem> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_from_clipboard(&self) -> Option<crate::ClipboardItem> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self) {
|
||||||
|
let mut event_loop = self
|
||||||
|
.0
|
||||||
|
.borrow_mut()
|
||||||
|
.event_loop
|
||||||
|
.take()
|
||||||
|
.expect("App is already running");
|
||||||
|
|
||||||
|
event_loop.run(None, &mut self.clone(), |_| {}).log_err();
|
||||||
|
}
|
||||||
|
}
|
703
crates/ming/src/platform/linux/platform.rs
Normal file
703
crates/ming/src/platform/linux/platform.rs
Normal file
|
@ -0,0 +1,703 @@
|
||||||
|
#![allow(unused)]
|
||||||
|
|
||||||
|
use std::any::{type_name, Any};
|
||||||
|
use std::cell::{self, RefCell};
|
||||||
|
use std::env;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Read;
|
||||||
|
use std::ops::{Deref, DerefMut};
|
||||||
|
use std::os::fd::{AsRawFd, FromRawFd};
|
||||||
|
use std::panic::Location;
|
||||||
|
use std::{
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
process::Command,
|
||||||
|
rc::Rc,
|
||||||
|
sync::Arc,
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use ashpd::desktop::file_chooser::{OpenFileRequest, SaveFileRequest};
|
||||||
|
use async_task::Runnable;
|
||||||
|
use calloop::channel::Channel;
|
||||||
|
use calloop::{EventLoop, LoopHandle, LoopSignal};
|
||||||
|
use copypasta::ClipboardProvider;
|
||||||
|
use filedescriptor::FileDescriptor;
|
||||||
|
use flume::{Receiver, Sender};
|
||||||
|
use futures::channel::oneshot;
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use time::UtcOffset;
|
||||||
|
use wayland_client::Connection;
|
||||||
|
use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::Shape;
|
||||||
|
use xkbcommon::xkb::{self, Keycode, Keysym, State};
|
||||||
|
|
||||||
|
use crate::platform::linux::wayland::WaylandClient;
|
||||||
|
use crate::{
|
||||||
|
px, Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CosmicTextSystem, CursorStyle,
|
||||||
|
DisplayId, ForegroundExecutor, Keymap, Keystroke, LinuxDispatcher, Menu, Modifiers,
|
||||||
|
PathPromptOptions, Pixels, Platform, PlatformDisplay, PlatformInputHandler, PlatformTextSystem,
|
||||||
|
PlatformWindow, Point, PromptLevel, Result, SemanticVersion, Size, Task, WindowAppearance,
|
||||||
|
WindowOptions, WindowParams,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::x11::X11Client;
|
||||||
|
|
||||||
|
pub(crate) const SCROLL_LINES: f64 = 3.0;
|
||||||
|
|
||||||
|
// Values match the defaults on GTK.
|
||||||
|
// Taken from https://github.com/GNOME/gtk/blob/main/gtk/gtksettings.c#L320
|
||||||
|
pub(crate) const DOUBLE_CLICK_INTERVAL: Duration = Duration::from_millis(400);
|
||||||
|
pub(crate) const DOUBLE_CLICK_DISTANCE: Pixels = px(5.0);
|
||||||
|
pub(crate) const KEYRING_LABEL: &str = "zed-github-account";
|
||||||
|
|
||||||
|
pub trait LinuxClient {
|
||||||
|
fn with_common<R>(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R;
|
||||||
|
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
|
||||||
|
fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>>;
|
||||||
|
fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>>;
|
||||||
|
fn open_window(
|
||||||
|
&self,
|
||||||
|
handle: AnyWindowHandle,
|
||||||
|
options: WindowParams,
|
||||||
|
) -> Box<dyn PlatformWindow>;
|
||||||
|
fn set_cursor_style(&self, style: CursorStyle);
|
||||||
|
fn open_uri(&self, uri: &str);
|
||||||
|
fn write_to_primary(&self, item: ClipboardItem);
|
||||||
|
fn write_to_clipboard(&self, item: ClipboardItem);
|
||||||
|
fn read_from_primary(&self) -> Option<ClipboardItem>;
|
||||||
|
fn read_from_clipboard(&self) -> Option<ClipboardItem>;
|
||||||
|
fn run(&self);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub(crate) struct PlatformHandlers {
|
||||||
|
pub(crate) open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
|
||||||
|
pub(crate) quit: Option<Box<dyn FnMut()>>,
|
||||||
|
pub(crate) reopen: Option<Box<dyn FnMut()>>,
|
||||||
|
pub(crate) app_menu_action: Option<Box<dyn FnMut(&dyn Action)>>,
|
||||||
|
pub(crate) will_open_app_menu: Option<Box<dyn FnMut()>>,
|
||||||
|
pub(crate) validate_app_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct LinuxCommon {
|
||||||
|
pub(crate) background_executor: BackgroundExecutor,
|
||||||
|
pub(crate) foreground_executor: ForegroundExecutor,
|
||||||
|
pub(crate) text_system: Arc<CosmicTextSystem>,
|
||||||
|
pub(crate) callbacks: PlatformHandlers,
|
||||||
|
pub(crate) signal: LoopSignal,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LinuxCommon {
|
||||||
|
pub fn new(signal: LoopSignal) -> (Self, Channel<Runnable>) {
|
||||||
|
let (main_sender, main_receiver) = calloop::channel::channel::<Runnable>();
|
||||||
|
let text_system = Arc::new(CosmicTextSystem::new());
|
||||||
|
let callbacks = PlatformHandlers::default();
|
||||||
|
|
||||||
|
let dispatcher = Arc::new(LinuxDispatcher::new(main_sender));
|
||||||
|
|
||||||
|
let common = LinuxCommon {
|
||||||
|
background_executor: BackgroundExecutor::new(dispatcher.clone()),
|
||||||
|
foreground_executor: ForegroundExecutor::new(dispatcher.clone()),
|
||||||
|
text_system,
|
||||||
|
callbacks,
|
||||||
|
signal,
|
||||||
|
};
|
||||||
|
|
||||||
|
(common, main_receiver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P: LinuxClient + 'static> Platform for P {
|
||||||
|
fn background_executor(&self) -> BackgroundExecutor {
|
||||||
|
self.with_common(|common| common.background_executor.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn foreground_executor(&self) -> ForegroundExecutor {
|
||||||
|
self.with_common(|common| common.foreground_executor.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn text_system(&self) -> Arc<dyn PlatformTextSystem> {
|
||||||
|
self.with_common(|common| common.text_system.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, on_finish_launching: Box<dyn FnOnce()>) {
|
||||||
|
on_finish_launching();
|
||||||
|
|
||||||
|
LinuxClient::run(self);
|
||||||
|
|
||||||
|
self.with_common(|common| {
|
||||||
|
if let Some(mut fun) = common.callbacks.quit.take() {
|
||||||
|
fun();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn quit(&self) {
|
||||||
|
self.with_common(|common| common.signal.stop());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn restart(&self, binary_path: Option<PathBuf>) {
|
||||||
|
use std::os::unix::process::CommandExt as _;
|
||||||
|
|
||||||
|
// get the process id of the current process
|
||||||
|
let app_pid = std::process::id().to_string();
|
||||||
|
// get the path to the executable
|
||||||
|
let app_path = if let Some(path) = binary_path {
|
||||||
|
path
|
||||||
|
} else {
|
||||||
|
match self.app_path() {
|
||||||
|
Ok(path) => path,
|
||||||
|
Err(err) => {
|
||||||
|
log::error!("Failed to get app path: {:?}", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
log::info!("Restarting process, using app path: {:?}", app_path);
|
||||||
|
|
||||||
|
// Script to wait for the current process to exit and then restart the app.
|
||||||
|
// We also wait for possibly open TCP sockets by the process to be closed,
|
||||||
|
// since on Linux it's not guaranteed that a process' resources have been
|
||||||
|
// cleaned up when `kill -0` returns.
|
||||||
|
let script = format!(
|
||||||
|
r#"
|
||||||
|
while kill -O {pid} 2>/dev/null; do
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
|
||||||
|
while lsof -nP -iTCP -a -p {pid} 2>/dev/null; do
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
|
||||||
|
{app_path}
|
||||||
|
"#,
|
||||||
|
pid = app_pid,
|
||||||
|
app_path = app_path.display()
|
||||||
|
);
|
||||||
|
|
||||||
|
// execute the script using /bin/bash
|
||||||
|
let restart_process = Command::new("/bin/bash")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(script)
|
||||||
|
.process_group(0)
|
||||||
|
.spawn();
|
||||||
|
|
||||||
|
match restart_process {
|
||||||
|
Ok(_) => self.quit(),
|
||||||
|
Err(e) => log::error!("failed to spawn restart script: {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo(linux)
|
||||||
|
fn activate(&self, ignoring_other_apps: bool) {}
|
||||||
|
|
||||||
|
// todo(linux)
|
||||||
|
fn hide(&self) {}
|
||||||
|
|
||||||
|
fn hide_other_apps(&self) {
|
||||||
|
log::warn!("hide_other_apps is not implemented on Linux, ignoring the call")
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo(linux)
|
||||||
|
fn unhide_other_apps(&self) {}
|
||||||
|
|
||||||
|
fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
|
||||||
|
self.primary_display()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
|
||||||
|
self.displays()
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo(linux)
|
||||||
|
fn active_window(&self) -> Option<AnyWindowHandle> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_window(
|
||||||
|
&self,
|
||||||
|
handle: AnyWindowHandle,
|
||||||
|
options: WindowParams,
|
||||||
|
) -> Box<dyn PlatformWindow> {
|
||||||
|
self.open_window(handle, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_url(&self, url: &str) {
|
||||||
|
self.open_uri(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>) {
|
||||||
|
self.with_common(|common| common.callbacks.open_urls = Some(callback));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt_for_paths(
|
||||||
|
&self,
|
||||||
|
options: PathPromptOptions,
|
||||||
|
) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
|
||||||
|
let (done_tx, done_rx) = oneshot::channel();
|
||||||
|
self.foreground_executor()
|
||||||
|
.spawn(async move {
|
||||||
|
let title = if options.multiple {
|
||||||
|
if !options.files {
|
||||||
|
"Open folders"
|
||||||
|
} else {
|
||||||
|
"Open files"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !options.files {
|
||||||
|
"Open folder"
|
||||||
|
} else {
|
||||||
|
"Open file"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = OpenFileRequest::default()
|
||||||
|
.modal(true)
|
||||||
|
.title(title)
|
||||||
|
.accept_label("Select")
|
||||||
|
.multiple(options.multiple)
|
||||||
|
.directory(options.directories)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.and_then(|request| request.response().ok())
|
||||||
|
.and_then(|response| {
|
||||||
|
response
|
||||||
|
.uris()
|
||||||
|
.iter()
|
||||||
|
.map(|uri| uri.to_file_path().ok())
|
||||||
|
.collect()
|
||||||
|
});
|
||||||
|
|
||||||
|
done_tx.send(result);
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
done_rx
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>> {
|
||||||
|
let (done_tx, done_rx) = oneshot::channel();
|
||||||
|
let directory = directory.to_owned();
|
||||||
|
self.foreground_executor()
|
||||||
|
.spawn(async move {
|
||||||
|
let result = SaveFileRequest::default()
|
||||||
|
.modal(true)
|
||||||
|
.title("Select new path")
|
||||||
|
.accept_label("Accept")
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.and_then(|request| request.response().ok())
|
||||||
|
.and_then(|response| {
|
||||||
|
response
|
||||||
|
.uris()
|
||||||
|
.first()
|
||||||
|
.and_then(|uri| uri.to_file_path().ok())
|
||||||
|
});
|
||||||
|
|
||||||
|
done_tx.send(result);
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
done_rx
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reveal_path(&self, path: &Path) {
|
||||||
|
if path.is_dir() {
|
||||||
|
open::that(path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// If `path` is a file, the system may try to open it in a text editor
|
||||||
|
let dir = path.parent().unwrap_or(Path::new(""));
|
||||||
|
open::that(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_quit(&self, callback: Box<dyn FnMut()>) {
|
||||||
|
self.with_common(|common| {
|
||||||
|
common.callbacks.quit = Some(callback);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_reopen(&self, callback: Box<dyn FnMut()>) {
|
||||||
|
self.with_common(|common| {
|
||||||
|
common.callbacks.reopen = Some(callback);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>) {
|
||||||
|
self.with_common(|common| {
|
||||||
|
common.callbacks.app_menu_action = Some(callback);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>) {
|
||||||
|
self.with_common(|common| {
|
||||||
|
common.callbacks.will_open_app_menu = Some(callback);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>) {
|
||||||
|
self.with_common(|common| {
|
||||||
|
common.callbacks.validate_app_menu_command = Some(callback);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn os_name(&self) -> &'static str {
|
||||||
|
"Linux"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn os_version(&self) -> Result<SemanticVersion> {
|
||||||
|
Ok(SemanticVersion::new(1, 0, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app_version(&self) -> Result<SemanticVersion> {
|
||||||
|
const VERSION: Option<&str> = option_env!("RELEASE_VERSION");
|
||||||
|
if let Some(version) = VERSION {
|
||||||
|
version.parse()
|
||||||
|
} else {
|
||||||
|
Ok(SemanticVersion::new(1, 0, 0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app_path(&self) -> Result<PathBuf> {
|
||||||
|
// get the path of the executable of the current process
|
||||||
|
let exe_path = std::env::current_exe()?;
|
||||||
|
Ok(exe_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo(linux)
|
||||||
|
fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap) {}
|
||||||
|
|
||||||
|
fn local_timezone(&self) -> UtcOffset {
|
||||||
|
UtcOffset::UTC
|
||||||
|
}
|
||||||
|
|
||||||
|
//todo(linux)
|
||||||
|
fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
|
||||||
|
Err(anyhow::Error::msg(
|
||||||
|
"Platform<LinuxPlatform>::path_for_auxiliary_executable is not implemented yet",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_cursor_style(&self, style: CursorStyle) {
|
||||||
|
self.set_cursor_style(style)
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo(linux)
|
||||||
|
fn should_auto_hide_scrollbars(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>> {
|
||||||
|
let url = url.to_string();
|
||||||
|
let username = username.to_string();
|
||||||
|
let password = password.to_vec();
|
||||||
|
self.background_executor().spawn(async move {
|
||||||
|
let keyring = oo7::Keyring::new().await?;
|
||||||
|
keyring.unlock().await?;
|
||||||
|
keyring
|
||||||
|
.create_item(
|
||||||
|
KEYRING_LABEL,
|
||||||
|
&vec![("url", &url), ("username", &username)],
|
||||||
|
password,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
|
||||||
|
let url = url.to_string();
|
||||||
|
self.background_executor().spawn(async move {
|
||||||
|
let keyring = oo7::Keyring::new().await?;
|
||||||
|
keyring.unlock().await?;
|
||||||
|
|
||||||
|
let items = keyring.search_items(&vec![("url", &url)]).await?;
|
||||||
|
|
||||||
|
for item in items.into_iter() {
|
||||||
|
if item.label().await.is_ok_and(|label| label == KEYRING_LABEL) {
|
||||||
|
let attributes = item.attributes().await?;
|
||||||
|
let username = attributes
|
||||||
|
.get("username")
|
||||||
|
.ok_or_else(|| anyhow!("Cannot find username in stored credentials"))?;
|
||||||
|
let secret = item.secret().await?;
|
||||||
|
|
||||||
|
// we lose the zeroizing capabilities at this boundary,
|
||||||
|
// a current limitation GPUI's credentials api
|
||||||
|
return Ok(Some((username.to_string(), secret.to_vec())));
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_credentials(&self, url: &str) -> Task<Result<()>> {
|
||||||
|
let url = url.to_string();
|
||||||
|
self.background_executor().spawn(async move {
|
||||||
|
let keyring = oo7::Keyring::new().await?;
|
||||||
|
keyring.unlock().await?;
|
||||||
|
|
||||||
|
let items = keyring.search_items(&vec![("url", &url)]).await?;
|
||||||
|
|
||||||
|
for item in items.into_iter() {
|
||||||
|
if item.label().await.is_ok_and(|label| label == KEYRING_LABEL) {
|
||||||
|
item.delete().await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn window_appearance(&self) -> crate::WindowAppearance {
|
||||||
|
crate::WindowAppearance::Light
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_url_scheme(&self, _: &str) -> Task<anyhow::Result<()>> {
|
||||||
|
Task::ready(Err(anyhow!("register_url_scheme unimplemented")))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_to_primary(&self, item: ClipboardItem) {
|
||||||
|
self.write_to_primary(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_to_clipboard(&self, item: ClipboardItem) {
|
||||||
|
self.write_to_clipboard(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_from_primary(&self) -> Option<ClipboardItem> {
|
||||||
|
self.read_from_primary()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||||
|
self.read_from_clipboard()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn open_uri_internal(uri: &str, activation_token: Option<&str>) {
|
||||||
|
let mut last_err = None;
|
||||||
|
for mut command in open::commands(uri) {
|
||||||
|
if let Some(token) = activation_token {
|
||||||
|
command.env("XDG_ACTIVATION_TOKEN", token);
|
||||||
|
}
|
||||||
|
match command.status() {
|
||||||
|
Ok(_) => return,
|
||||||
|
Err(err) => last_err = Some(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log::error!("failed to open uri: {uri:?}, last error: {last_err:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn is_within_click_distance(a: Point<Pixels>, b: Point<Pixels>) -> bool {
|
||||||
|
let diff = a - b;
|
||||||
|
diff.x.abs() <= DOUBLE_CLICK_DISTANCE && diff.y.abs() <= DOUBLE_CLICK_DISTANCE
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) unsafe fn read_fd(mut fd: FileDescriptor) -> Result<String> {
|
||||||
|
let mut file = File::from_raw_fd(fd.as_raw_fd());
|
||||||
|
|
||||||
|
let mut buffer = String::new();
|
||||||
|
file.read_to_string(&mut buffer)?;
|
||||||
|
|
||||||
|
// Normalize the text to unix line endings, otherwise
|
||||||
|
// copying from eg: firefox inserts a lot of blank
|
||||||
|
// lines, and that is super annoying.
|
||||||
|
let result = buffer.replace("\r\n", "\n");
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CursorStyle {
|
||||||
|
pub(super) fn to_shape(&self) -> Shape {
|
||||||
|
match self {
|
||||||
|
CursorStyle::Arrow => Shape::Default,
|
||||||
|
CursorStyle::IBeam => Shape::Text,
|
||||||
|
CursorStyle::Crosshair => Shape::Crosshair,
|
||||||
|
CursorStyle::ClosedHand => Shape::Grabbing,
|
||||||
|
CursorStyle::OpenHand => Shape::Grab,
|
||||||
|
CursorStyle::PointingHand => Shape::Pointer,
|
||||||
|
CursorStyle::ResizeLeft => Shape::WResize,
|
||||||
|
CursorStyle::ResizeRight => Shape::EResize,
|
||||||
|
CursorStyle::ResizeLeftRight => Shape::EwResize,
|
||||||
|
CursorStyle::ResizeUp => Shape::NResize,
|
||||||
|
CursorStyle::ResizeDown => Shape::SResize,
|
||||||
|
CursorStyle::ResizeUpDown => Shape::NsResize,
|
||||||
|
CursorStyle::ResizeColumn => Shape::ColResize,
|
||||||
|
CursorStyle::ResizeRow => Shape::RowResize,
|
||||||
|
CursorStyle::DisappearingItem => Shape::Grabbing, // todo(linux) - couldn't find equivalent icon in linux
|
||||||
|
CursorStyle::IBeamCursorForVerticalLayout => Shape::VerticalText,
|
||||||
|
CursorStyle::OperationNotAllowed => Shape::NotAllowed,
|
||||||
|
CursorStyle::DragLink => Shape::Alias,
|
||||||
|
CursorStyle::DragCopy => Shape::Copy,
|
||||||
|
CursorStyle::ContextualMenu => Shape::ContextMenu,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn to_icon_name(&self) -> String {
|
||||||
|
// Based on cursor names from https://gitlab.gnome.org/GNOME/adwaita-icon-theme (GNOME)
|
||||||
|
// and https://github.com/KDE/breeze (KDE). Both of them seem to be also derived from
|
||||||
|
// Web CSS cursor names: https://developer.mozilla.org/en-US/docs/Web/CSS/cursor#values
|
||||||
|
match self {
|
||||||
|
CursorStyle::Arrow => "arrow",
|
||||||
|
CursorStyle::IBeam => "text",
|
||||||
|
CursorStyle::Crosshair => "crosshair",
|
||||||
|
CursorStyle::ClosedHand => "grabbing",
|
||||||
|
CursorStyle::OpenHand => "grab",
|
||||||
|
CursorStyle::PointingHand => "pointer",
|
||||||
|
CursorStyle::ResizeLeft => "w-resize",
|
||||||
|
CursorStyle::ResizeRight => "e-resize",
|
||||||
|
CursorStyle::ResizeLeftRight => "ew-resize",
|
||||||
|
CursorStyle::ResizeUp => "n-resize",
|
||||||
|
CursorStyle::ResizeDown => "s-resize",
|
||||||
|
CursorStyle::ResizeUpDown => "ns-resize",
|
||||||
|
CursorStyle::ResizeColumn => "col-resize",
|
||||||
|
CursorStyle::ResizeRow => "row-resize",
|
||||||
|
CursorStyle::DisappearingItem => "grabbing", // todo(linux) - couldn't find equivalent icon in linux
|
||||||
|
CursorStyle::IBeamCursorForVerticalLayout => "vertical-text",
|
||||||
|
CursorStyle::OperationNotAllowed => "not-allowed",
|
||||||
|
CursorStyle::DragLink => "alias",
|
||||||
|
CursorStyle::DragCopy => "copy",
|
||||||
|
CursorStyle::ContextualMenu => "context-menu",
|
||||||
|
}
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Keystroke {
|
||||||
|
pub(super) fn from_xkb(state: &State, modifiers: Modifiers, keycode: Keycode) -> Self {
|
||||||
|
let mut modifiers = modifiers;
|
||||||
|
|
||||||
|
let key_utf32 = state.key_get_utf32(keycode);
|
||||||
|
let key_utf8 = state.key_get_utf8(keycode);
|
||||||
|
let key_sym = state.key_get_one_sym(keycode);
|
||||||
|
|
||||||
|
// The logic here tries to replicate the logic in `../mac/events.rs`
|
||||||
|
// "Consumed" modifiers are modifiers that have been used to translate a key, for example
|
||||||
|
// pressing "shift" and "1" on US layout produces the key `!` but "consumes" the shift.
|
||||||
|
// Notes:
|
||||||
|
// - macOS gets the key character directly ("."), xkb gives us the key name ("period")
|
||||||
|
// - macOS logic removes consumed shift modifier for symbols: "{", not "shift-{"
|
||||||
|
// - macOS logic keeps consumed shift modifiers for letters: "shift-a", not "a" or "A"
|
||||||
|
|
||||||
|
let mut handle_consumed_modifiers = true;
|
||||||
|
let key = match key_sym {
|
||||||
|
Keysym::Return => "enter".to_owned(),
|
||||||
|
Keysym::Prior => "pageup".to_owned(),
|
||||||
|
Keysym::Next => "pagedown".to_owned(),
|
||||||
|
|
||||||
|
Keysym::comma => ",".to_owned(),
|
||||||
|
Keysym::period => ".".to_owned(),
|
||||||
|
Keysym::less => "<".to_owned(),
|
||||||
|
Keysym::greater => ">".to_owned(),
|
||||||
|
Keysym::slash => "/".to_owned(),
|
||||||
|
Keysym::question => "?".to_owned(),
|
||||||
|
|
||||||
|
Keysym::semicolon => ";".to_owned(),
|
||||||
|
Keysym::colon => ":".to_owned(),
|
||||||
|
Keysym::apostrophe => "'".to_owned(),
|
||||||
|
Keysym::quotedbl => "\"".to_owned(),
|
||||||
|
|
||||||
|
Keysym::bracketleft => "[".to_owned(),
|
||||||
|
Keysym::braceleft => "{".to_owned(),
|
||||||
|
Keysym::bracketright => "]".to_owned(),
|
||||||
|
Keysym::braceright => "}".to_owned(),
|
||||||
|
Keysym::backslash => "\\".to_owned(),
|
||||||
|
Keysym::bar => "|".to_owned(),
|
||||||
|
|
||||||
|
Keysym::grave => "`".to_owned(),
|
||||||
|
Keysym::asciitilde => "~".to_owned(),
|
||||||
|
Keysym::exclam => "!".to_owned(),
|
||||||
|
Keysym::at => "@".to_owned(),
|
||||||
|
Keysym::numbersign => "#".to_owned(),
|
||||||
|
Keysym::dollar => "$".to_owned(),
|
||||||
|
Keysym::percent => "%".to_owned(),
|
||||||
|
Keysym::asciicircum => "^".to_owned(),
|
||||||
|
Keysym::ampersand => "&".to_owned(),
|
||||||
|
Keysym::asterisk => "*".to_owned(),
|
||||||
|
Keysym::parenleft => "(".to_owned(),
|
||||||
|
Keysym::parenright => ")".to_owned(),
|
||||||
|
Keysym::minus => "-".to_owned(),
|
||||||
|
Keysym::underscore => "_".to_owned(),
|
||||||
|
Keysym::equal => "=".to_owned(),
|
||||||
|
Keysym::plus => "+".to_owned(),
|
||||||
|
|
||||||
|
Keysym::ISO_Left_Tab => {
|
||||||
|
handle_consumed_modifiers = false;
|
||||||
|
"tab".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
handle_consumed_modifiers = false;
|
||||||
|
xkb::keysym_get_name(key_sym).to_lowercase()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ignore control characters (and DEL) for the purposes of ime_key
|
||||||
|
let ime_key =
|
||||||
|
(key_utf32 >= 32 && key_utf32 != 127 && !key_utf8.is_empty()).then_some(key_utf8);
|
||||||
|
|
||||||
|
if handle_consumed_modifiers {
|
||||||
|
let mod_shift_index = state.get_keymap().mod_get_index(xkb::MOD_NAME_SHIFT);
|
||||||
|
let is_shift_consumed = state.mod_index_is_consumed(keycode, mod_shift_index);
|
||||||
|
|
||||||
|
if modifiers.shift && is_shift_consumed {
|
||||||
|
modifiers.shift = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Keystroke {
|
||||||
|
modifiers,
|
||||||
|
key,
|
||||||
|
ime_key,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Modifiers {
|
||||||
|
pub(super) fn from_xkb(keymap_state: &State) -> Self {
|
||||||
|
let shift = keymap_state.mod_name_is_active(xkb::MOD_NAME_SHIFT, xkb::STATE_MODS_EFFECTIVE);
|
||||||
|
let alt = keymap_state.mod_name_is_active(xkb::MOD_NAME_ALT, xkb::STATE_MODS_EFFECTIVE);
|
||||||
|
let control =
|
||||||
|
keymap_state.mod_name_is_active(xkb::MOD_NAME_CTRL, xkb::STATE_MODS_EFFECTIVE);
|
||||||
|
let platform =
|
||||||
|
keymap_state.mod_name_is_active(xkb::MOD_NAME_LOGO, xkb::STATE_MODS_EFFECTIVE);
|
||||||
|
Modifiers {
|
||||||
|
shift,
|
||||||
|
alt,
|
||||||
|
control,
|
||||||
|
platform,
|
||||||
|
function: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::{px, Point};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_is_within_click_distance() {
|
||||||
|
let zero = Point::new(px(0.0), px(0.0));
|
||||||
|
assert_eq!(
|
||||||
|
is_within_click_distance(zero, Point::new(px(5.0), px(5.0))),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
is_within_click_distance(zero, Point::new(px(-4.9), px(5.0))),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
is_within_click_distance(Point::new(px(3.0), px(2.0)), Point::new(px(-2.0), px(-2.0))),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
is_within_click_distance(zero, Point::new(px(5.0), px(5.1))),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
7
crates/ming/src/platform/linux/wayland.rs
Normal file
7
crates/ming/src/platform/linux/wayland.rs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
mod client;
|
||||||
|
mod cursor;
|
||||||
|
mod display;
|
||||||
|
mod serial;
|
||||||
|
mod window;
|
||||||
|
|
||||||
|
pub(crate) use client::*;
|
1427
crates/ming/src/platform/linux/wayland/client.rs
Normal file
1427
crates/ming/src/platform/linux/wayland/client.rs
Normal file
File diff suppressed because it is too large
Load diff
60
crates/ming/src/platform/linux/wayland/cursor.rs
Normal file
60
crates/ming/src/platform/linux/wayland/cursor.rs
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
use crate::Globals;
|
||||||
|
use util::ResultExt;
|
||||||
|
|
||||||
|
use wayland_client::protocol::wl_pointer::WlPointer;
|
||||||
|
use wayland_client::protocol::wl_surface::WlSurface;
|
||||||
|
use wayland_client::Connection;
|
||||||
|
use wayland_cursor::{CursorImageBuffer, CursorTheme};
|
||||||
|
|
||||||
|
pub(crate) struct Cursor {
|
||||||
|
theme: Option<CursorTheme>,
|
||||||
|
surface: WlSurface,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Cursor {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.theme.take();
|
||||||
|
self.surface.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cursor {
|
||||||
|
pub fn new(connection: &Connection, globals: &Globals, size: u32) -> Self {
|
||||||
|
Self {
|
||||||
|
theme: CursorTheme::load(&connection, globals.shm.clone(), size).log_err(),
|
||||||
|
surface: globals.compositor.create_surface(&globals.qh, ()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_icon(&mut self, wl_pointer: &WlPointer, serial_id: u32, mut cursor_icon_name: &str) {
|
||||||
|
if let Some(theme) = &mut self.theme {
|
||||||
|
let mut buffer: Option<&CursorImageBuffer>;
|
||||||
|
|
||||||
|
if let Some(cursor) = theme.get_cursor(&cursor_icon_name) {
|
||||||
|
buffer = Some(&cursor[0]);
|
||||||
|
} else if let Some(cursor) = theme.get_cursor("default") {
|
||||||
|
buffer = Some(&cursor[0]);
|
||||||
|
cursor_icon_name = "default";
|
||||||
|
log::warn!(
|
||||||
|
"Linux: Wayland: Unable to get cursor icon: {}. Using default cursor icon",
|
||||||
|
cursor_icon_name
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
buffer = None;
|
||||||
|
log::warn!("Linux: Wayland: Unable to get default cursor too!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(buffer) = &mut buffer {
|
||||||
|
let (width, height) = buffer.dimensions();
|
||||||
|
let (hot_x, hot_y) = buffer.hotspot();
|
||||||
|
|
||||||
|
wl_pointer.set_cursor(serial_id, Some(&self.surface), hot_x as i32, hot_y as i32);
|
||||||
|
self.surface.attach(Some(&buffer), 0, 0);
|
||||||
|
self.surface.damage(0, 0, width as i32, height as i32);
|
||||||
|
self.surface.commit();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log::warn!("Linux: Wayland: Unable to load cursor themes");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
31
crates/ming/src/platform/linux/wayland/display.rs
Normal file
31
crates/ming/src/platform/linux/wayland/display.rs
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{Bounds, DevicePixels, DisplayId, PlatformDisplay, Size};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct WaylandDisplay {}
|
||||||
|
|
||||||
|
impl PlatformDisplay for WaylandDisplay {
|
||||||
|
// todo(linux)
|
||||||
|
fn id(&self) -> DisplayId {
|
||||||
|
DisplayId(123) // return some fake data so it doesn't panic
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo(linux)
|
||||||
|
fn uuid(&self) -> anyhow::Result<Uuid> {
|
||||||
|
Ok(Uuid::from_bytes([0; 16])) // return some fake data so it doesn't panic
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo(linux)
|
||||||
|
fn bounds(&self) -> Bounds<DevicePixels> {
|
||||||
|
Bounds {
|
||||||
|
origin: Default::default(),
|
||||||
|
size: Size {
|
||||||
|
width: DevicePixels(1000),
|
||||||
|
height: DevicePixels(500),
|
||||||
|
},
|
||||||
|
} // return some fake data so it doesn't panic
|
||||||
|
}
|
||||||
|
}
|
91
crates/ming/src/platform/linux/wayland/serial.rs
Normal file
91
crates/ming/src/platform/linux/wayland/serial.rs
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
use collections::HashMap;
|
||||||
|
|
||||||
|
#[derive(Debug, Hash, PartialEq, Eq)]
|
||||||
|
pub(crate) enum SerialKind {
|
||||||
|
DataDevice,
|
||||||
|
MouseEnter,
|
||||||
|
MousePress,
|
||||||
|
KeyPress,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct SerialData {
|
||||||
|
serial: u32,
|
||||||
|
time: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SerialData {
|
||||||
|
fn new(value: u32) -> Self {
|
||||||
|
Self {
|
||||||
|
serial: value,
|
||||||
|
time: Instant::now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
/// Helper for tracking of different serial kinds.
|
||||||
|
pub(crate) struct SerialTracker {
|
||||||
|
serials: HashMap<SerialKind, SerialData>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SerialTracker {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
serials: HashMap::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&mut self, kind: SerialKind, value: u32) {
|
||||||
|
self.serials.insert(kind, SerialData::new(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the latest tracked serial of the provided [`SerialKind`]
|
||||||
|
///
|
||||||
|
/// Will return 0 if not tracked.
|
||||||
|
pub fn get(&self, kind: SerialKind) -> u32 {
|
||||||
|
self.serials
|
||||||
|
.get(&kind)
|
||||||
|
.map(|serial_data| serial_data.serial)
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the newest serial of any of the provided [`SerialKind`]
|
||||||
|
pub fn get_newest_of(&self, kinds: &[SerialKind]) -> u32 {
|
||||||
|
kinds
|
||||||
|
.iter()
|
||||||
|
.filter_map(|kind| self.serials.get(&kind))
|
||||||
|
.max_by_key(|serial_data| serial_data.time)
|
||||||
|
.map(|serial_data| serial_data.serial)
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serial_tracker() {
|
||||||
|
let mut tracker = SerialTracker::new();
|
||||||
|
|
||||||
|
tracker.update(SerialKind::KeyPress, 100);
|
||||||
|
tracker.update(SerialKind::MousePress, 50);
|
||||||
|
tracker.update(SerialKind::MouseEnter, 300);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
tracker.get_newest_of(&[SerialKind::KeyPress, SerialKind::MousePress]),
|
||||||
|
50
|
||||||
|
);
|
||||||
|
assert_eq!(tracker.get(SerialKind::DataDevice), 0);
|
||||||
|
|
||||||
|
tracker.update(SerialKind::KeyPress, 2000);
|
||||||
|
assert_eq!(tracker.get(SerialKind::KeyPress), 2000);
|
||||||
|
assert_eq!(
|
||||||
|
tracker.get_newest_of(&[SerialKind::KeyPress, SerialKind::MousePress]),
|
||||||
|
2000
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
765
crates/ming/src/platform/linux/wayland/window.rs
Normal file
765
crates/ming/src/platform/linux/wayland/window.rs
Normal file
|
@ -0,0 +1,765 @@
|
||||||
|
use std::any::Any;
|
||||||
|
use std::cell::{Ref, RefCell, RefMut};
|
||||||
|
use std::ffi::c_void;
|
||||||
|
use std::num::NonZeroU32;
|
||||||
|
use std::ptr::NonNull;
|
||||||
|
use std::rc::{Rc, Weak};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use blade_graphics as gpu;
|
||||||
|
use collections::{HashMap, HashSet};
|
||||||
|
use futures::channel::oneshot::Receiver;
|
||||||
|
use raw_window_handle as rwh;
|
||||||
|
use wayland_backend::client::ObjectId;
|
||||||
|
use wayland_client::protocol::wl_region::WlRegion;
|
||||||
|
use wayland_client::WEnum;
|
||||||
|
use wayland_client::{protocol::wl_surface, Proxy};
|
||||||
|
use wayland_protocols::wp::fractional_scale::v1::client::wp_fractional_scale_v1;
|
||||||
|
use wayland_protocols::wp::viewporter::client::wp_viewport;
|
||||||
|
use wayland_protocols::xdg::decoration::zv1::client::zxdg_toplevel_decoration_v1;
|
||||||
|
use wayland_protocols::xdg::shell::client::xdg_surface;
|
||||||
|
use wayland_protocols::xdg::shell::client::xdg_toplevel::{self, WmCapabilities};
|
||||||
|
use wayland_protocols_plasma::blur::client::{org_kde_kwin_blur, org_kde_kwin_blur_manager};
|
||||||
|
|
||||||
|
use crate::platform::blade::{BladeRenderer, BladeSurfaceConfig};
|
||||||
|
use crate::platform::linux::wayland::display::WaylandDisplay;
|
||||||
|
use crate::platform::{PlatformAtlas, PlatformInputHandler, PlatformWindow};
|
||||||
|
use crate::scene::Scene;
|
||||||
|
use crate::{
|
||||||
|
px, size, Bounds, DevicePixels, Globals, Modifiers, Pixels, PlatformDisplay, PlatformInput,
|
||||||
|
Point, PromptLevel, Size, WaylandClientState, WaylandClientStatePtr, WindowAppearance,
|
||||||
|
WindowBackgroundAppearance, WindowBounds, WindowParams,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub(crate) struct Callbacks {
|
||||||
|
request_frame: Option<Box<dyn FnMut()>>,
|
||||||
|
input: Option<Box<dyn FnMut(crate::PlatformInput) -> crate::DispatchEventResult>>,
|
||||||
|
active_status_change: Option<Box<dyn FnMut(bool)>>,
|
||||||
|
resize: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
|
||||||
|
moved: Option<Box<dyn FnMut()>>,
|
||||||
|
should_close: Option<Box<dyn FnMut() -> bool>>,
|
||||||
|
close: Option<Box<dyn FnOnce()>>,
|
||||||
|
appearance_changed: Option<Box<dyn FnMut()>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RawWindow {
|
||||||
|
window: *mut c_void,
|
||||||
|
display: *mut c_void,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl rwh::HasWindowHandle for RawWindow {
|
||||||
|
fn window_handle(&self) -> Result<rwh::WindowHandle<'_>, rwh::HandleError> {
|
||||||
|
let window = NonNull::new(self.window).unwrap();
|
||||||
|
let handle = rwh::WaylandWindowHandle::new(window);
|
||||||
|
Ok(unsafe { rwh::WindowHandle::borrow_raw(handle.into()) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl rwh::HasDisplayHandle for RawWindow {
|
||||||
|
fn display_handle(&self) -> Result<rwh::DisplayHandle<'_>, rwh::HandleError> {
|
||||||
|
let display = NonNull::new(self.display).unwrap();
|
||||||
|
let handle = rwh::WaylandDisplayHandle::new(display);
|
||||||
|
Ok(unsafe { rwh::DisplayHandle::borrow_raw(handle.into()) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct WaylandWindowState {
|
||||||
|
xdg_surface: xdg_surface::XdgSurface,
|
||||||
|
acknowledged_first_configure: bool,
|
||||||
|
pub surface: wl_surface::WlSurface,
|
||||||
|
decoration: Option<zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1>,
|
||||||
|
blur: Option<org_kde_kwin_blur::OrgKdeKwinBlur>,
|
||||||
|
toplevel: xdg_toplevel::XdgToplevel,
|
||||||
|
viewport: Option<wp_viewport::WpViewport>,
|
||||||
|
outputs: HashSet<ObjectId>,
|
||||||
|
globals: Globals,
|
||||||
|
renderer: BladeRenderer,
|
||||||
|
bounds: Bounds<u32>,
|
||||||
|
scale: f32,
|
||||||
|
input_handler: Option<PlatformInputHandler>,
|
||||||
|
decoration_state: WaylandDecorationState,
|
||||||
|
fullscreen: bool,
|
||||||
|
restore_bounds: Bounds<DevicePixels>,
|
||||||
|
maximized: bool,
|
||||||
|
client: WaylandClientStatePtr,
|
||||||
|
callbacks: Callbacks,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct WaylandWindowStatePtr {
|
||||||
|
state: Rc<RefCell<WaylandWindowState>>,
|
||||||
|
callbacks: Rc<RefCell<Callbacks>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WaylandWindowState {
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub(crate) fn new(
|
||||||
|
surface: wl_surface::WlSurface,
|
||||||
|
xdg_surface: xdg_surface::XdgSurface,
|
||||||
|
toplevel: xdg_toplevel::XdgToplevel,
|
||||||
|
decoration: Option<zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1>,
|
||||||
|
viewport: Option<wp_viewport::WpViewport>,
|
||||||
|
client: WaylandClientStatePtr,
|
||||||
|
globals: Globals,
|
||||||
|
options: WindowParams,
|
||||||
|
) -> Self {
|
||||||
|
let bounds = options.bounds.map(|p| p.0 as u32);
|
||||||
|
|
||||||
|
let raw = RawWindow {
|
||||||
|
window: surface.id().as_ptr().cast::<c_void>(),
|
||||||
|
display: surface
|
||||||
|
.backend()
|
||||||
|
.upgrade()
|
||||||
|
.unwrap()
|
||||||
|
.display_ptr()
|
||||||
|
.cast::<c_void>(),
|
||||||
|
};
|
||||||
|
let gpu = Arc::new(
|
||||||
|
unsafe {
|
||||||
|
gpu::Context::init_windowed(
|
||||||
|
&raw,
|
||||||
|
gpu::ContextDesc {
|
||||||
|
validation: false,
|
||||||
|
capture: false,
|
||||||
|
overlay: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
let config = BladeSurfaceConfig {
|
||||||
|
size: gpu::Extent {
|
||||||
|
width: bounds.size.width,
|
||||||
|
height: bounds.size.height,
|
||||||
|
depth: 1,
|
||||||
|
},
|
||||||
|
transparent: options.window_background != WindowBackgroundAppearance::Opaque,
|
||||||
|
};
|
||||||
|
|
||||||
|
Self {
|
||||||
|
xdg_surface,
|
||||||
|
acknowledged_first_configure: false,
|
||||||
|
surface,
|
||||||
|
decoration,
|
||||||
|
blur: None,
|
||||||
|
toplevel,
|
||||||
|
viewport,
|
||||||
|
globals,
|
||||||
|
outputs: HashSet::default(),
|
||||||
|
renderer: BladeRenderer::new(gpu, config),
|
||||||
|
bounds,
|
||||||
|
scale: 1.0,
|
||||||
|
input_handler: None,
|
||||||
|
decoration_state: WaylandDecorationState::Client,
|
||||||
|
fullscreen: false,
|
||||||
|
restore_bounds: Bounds::default(),
|
||||||
|
maximized: false,
|
||||||
|
callbacks: Callbacks::default(),
|
||||||
|
client,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct WaylandWindow(pub WaylandWindowStatePtr);
|
||||||
|
|
||||||
|
impl Drop for WaylandWindow {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let mut state = self.0.state.borrow_mut();
|
||||||
|
let surface_id = state.surface.id();
|
||||||
|
let client = state.client.clone();
|
||||||
|
|
||||||
|
state.renderer.destroy();
|
||||||
|
if let Some(decoration) = &state.decoration {
|
||||||
|
decoration.destroy();
|
||||||
|
}
|
||||||
|
if let Some(blur) = &state.blur {
|
||||||
|
blur.release();
|
||||||
|
}
|
||||||
|
state.toplevel.destroy();
|
||||||
|
if let Some(viewport) = &state.viewport {
|
||||||
|
viewport.destroy();
|
||||||
|
}
|
||||||
|
state.xdg_surface.destroy();
|
||||||
|
state.surface.destroy();
|
||||||
|
|
||||||
|
let state_ptr = self.0.clone();
|
||||||
|
state
|
||||||
|
.globals
|
||||||
|
.executor
|
||||||
|
.spawn(async move {
|
||||||
|
state_ptr.close();
|
||||||
|
client.drop_window(&surface_id)
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
drop(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WaylandWindow {
|
||||||
|
fn borrow(&self) -> Ref<WaylandWindowState> {
|
||||||
|
self.0.state.borrow()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn borrow_mut(&self) -> RefMut<WaylandWindowState> {
|
||||||
|
self.0.state.borrow_mut()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(
|
||||||
|
globals: Globals,
|
||||||
|
client: WaylandClientStatePtr,
|
||||||
|
params: WindowParams,
|
||||||
|
) -> (Self, ObjectId) {
|
||||||
|
let surface = globals.compositor.create_surface(&globals.qh, ());
|
||||||
|
let xdg_surface = globals
|
||||||
|
.wm_base
|
||||||
|
.get_xdg_surface(&surface, &globals.qh, surface.id());
|
||||||
|
let toplevel = xdg_surface.get_toplevel(&globals.qh, surface.id());
|
||||||
|
|
||||||
|
if let Some(fractional_scale_manager) = globals.fractional_scale_manager.as_ref() {
|
||||||
|
fractional_scale_manager.get_fractional_scale(&surface, &globals.qh, surface.id());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to set up window decorations based on the requested configuration
|
||||||
|
let decoration = globals
|
||||||
|
.decoration_manager
|
||||||
|
.as_ref()
|
||||||
|
.map(|decoration_manager| {
|
||||||
|
let decoration = decoration_manager.get_toplevel_decoration(
|
||||||
|
&toplevel,
|
||||||
|
&globals.qh,
|
||||||
|
surface.id(),
|
||||||
|
);
|
||||||
|
decoration.set_mode(zxdg_toplevel_decoration_v1::Mode::ClientSide);
|
||||||
|
decoration
|
||||||
|
});
|
||||||
|
|
||||||
|
let viewport = globals
|
||||||
|
.viewporter
|
||||||
|
.as_ref()
|
||||||
|
.map(|viewporter| viewporter.get_viewport(&surface, &globals.qh, ()));
|
||||||
|
|
||||||
|
let this = Self(WaylandWindowStatePtr {
|
||||||
|
state: Rc::new(RefCell::new(WaylandWindowState::new(
|
||||||
|
surface.clone(),
|
||||||
|
xdg_surface,
|
||||||
|
toplevel,
|
||||||
|
decoration,
|
||||||
|
viewport,
|
||||||
|
client,
|
||||||
|
globals,
|
||||||
|
params,
|
||||||
|
))),
|
||||||
|
callbacks: Rc::new(RefCell::new(Callbacks::default())),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Kick things off
|
||||||
|
surface.commit();
|
||||||
|
|
||||||
|
(this, surface.id())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WaylandWindowStatePtr {
|
||||||
|
pub fn surface(&self) -> wl_surface::WlSurface {
|
||||||
|
self.state.borrow().surface.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ptr_eq(&self, other: &Self) -> bool {
|
||||||
|
Rc::ptr_eq(&self.state, &other.state)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn frame(&self, request_frame_callback: bool) {
|
||||||
|
if request_frame_callback {
|
||||||
|
let state = self.state.borrow_mut();
|
||||||
|
state.surface.frame(&state.globals.qh, state.surface.id());
|
||||||
|
drop(state);
|
||||||
|
}
|
||||||
|
let mut cb = self.callbacks.borrow_mut();
|
||||||
|
if let Some(fun) = cb.request_frame.as_mut() {
|
||||||
|
fun();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_xdg_surface_event(&self, event: xdg_surface::Event) {
|
||||||
|
match event {
|
||||||
|
xdg_surface::Event::Configure { serial } => {
|
||||||
|
let mut state = self.state.borrow_mut();
|
||||||
|
state.xdg_surface.ack_configure(serial);
|
||||||
|
let request_frame_callback = !state.acknowledged_first_configure;
|
||||||
|
state.acknowledged_first_configure = true;
|
||||||
|
drop(state);
|
||||||
|
self.frame(request_frame_callback);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_toplevel_decoration_event(&self, event: zxdg_toplevel_decoration_v1::Event) {
|
||||||
|
match event {
|
||||||
|
zxdg_toplevel_decoration_v1::Event::Configure { mode } => match mode {
|
||||||
|
WEnum::Value(zxdg_toplevel_decoration_v1::Mode::ServerSide) => {
|
||||||
|
self.set_decoration_state(WaylandDecorationState::Server)
|
||||||
|
}
|
||||||
|
WEnum::Value(zxdg_toplevel_decoration_v1::Mode::ClientSide) => {
|
||||||
|
self.set_decoration_state(WaylandDecorationState::Client)
|
||||||
|
}
|
||||||
|
WEnum::Value(_) => {
|
||||||
|
log::warn!("Unknown decoration mode");
|
||||||
|
}
|
||||||
|
WEnum::Unknown(v) => {
|
||||||
|
log::warn!("Unknown decoration mode: {}", v);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_fractional_scale_event(&self, event: wp_fractional_scale_v1::Event) {
|
||||||
|
match event {
|
||||||
|
wp_fractional_scale_v1::Event::PreferredScale { scale } => {
|
||||||
|
self.rescale(scale as f32 / 120.0);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_toplevel_event(&self, event: xdg_toplevel::Event) -> bool {
|
||||||
|
match event {
|
||||||
|
xdg_toplevel::Event::Configure {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
states,
|
||||||
|
} => {
|
||||||
|
let width = NonZeroU32::new(width as u32);
|
||||||
|
let height = NonZeroU32::new(height as u32);
|
||||||
|
let fullscreen = states.contains(&(xdg_toplevel::State::Fullscreen as u8));
|
||||||
|
let maximized = states.contains(&(xdg_toplevel::State::Maximized as u8));
|
||||||
|
let mut state = self.state.borrow_mut();
|
||||||
|
state.maximized = maximized;
|
||||||
|
state.fullscreen = fullscreen;
|
||||||
|
if fullscreen || maximized {
|
||||||
|
state.restore_bounds = state.bounds.map(|p| DevicePixels(p as i32));
|
||||||
|
}
|
||||||
|
drop(state);
|
||||||
|
self.resize(width, height);
|
||||||
|
self.set_fullscreen(fullscreen);
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
xdg_toplevel::Event::Close => {
|
||||||
|
let mut cb = self.callbacks.borrow_mut();
|
||||||
|
if let Some(mut should_close) = cb.should_close.take() {
|
||||||
|
let result = (should_close)();
|
||||||
|
cb.should_close = Some(should_close);
|
||||||
|
if result {
|
||||||
|
drop(cb);
|
||||||
|
self.close();
|
||||||
|
}
|
||||||
|
result
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_surface_event(
|
||||||
|
&self,
|
||||||
|
event: wl_surface::Event,
|
||||||
|
output_scales: HashMap<ObjectId, i32>,
|
||||||
|
) {
|
||||||
|
let mut state = self.state.borrow_mut();
|
||||||
|
|
||||||
|
// We use `WpFractionalScale` instead to set the scale if it's available
|
||||||
|
if state.globals.fractional_scale_manager.is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
match event {
|
||||||
|
wl_surface::Event::Enter { output } => {
|
||||||
|
// We use `PreferredBufferScale` instead to set the scale if it's available
|
||||||
|
if state.surface.version() >= wl_surface::EVT_PREFERRED_BUFFER_SCALE_SINCE {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.outputs.insert(output.id());
|
||||||
|
|
||||||
|
let mut scale = 1;
|
||||||
|
for output in state.outputs.iter() {
|
||||||
|
if let Some(s) = output_scales.get(output) {
|
||||||
|
scale = scale.max(*s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.surface.set_buffer_scale(scale);
|
||||||
|
drop(state);
|
||||||
|
self.rescale(scale as f32);
|
||||||
|
}
|
||||||
|
wl_surface::Event::Leave { output } => {
|
||||||
|
// We use `PreferredBufferScale` instead to set the scale if it's available
|
||||||
|
if state.surface.version() >= wl_surface::EVT_PREFERRED_BUFFER_SCALE_SINCE {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.outputs.remove(&output.id());
|
||||||
|
|
||||||
|
let mut scale = 1;
|
||||||
|
for output in state.outputs.iter() {
|
||||||
|
if let Some(s) = output_scales.get(output) {
|
||||||
|
scale = scale.max(*s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.surface.set_buffer_scale(scale);
|
||||||
|
drop(state);
|
||||||
|
self.rescale(scale as f32);
|
||||||
|
}
|
||||||
|
wl_surface::Event::PreferredBufferScale { factor } => {
|
||||||
|
state.surface.set_buffer_scale(factor);
|
||||||
|
drop(state);
|
||||||
|
self.rescale(factor as f32);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_size_and_scale(
|
||||||
|
&self,
|
||||||
|
width: Option<NonZeroU32>,
|
||||||
|
height: Option<NonZeroU32>,
|
||||||
|
scale: Option<f32>,
|
||||||
|
) {
|
||||||
|
let (width, height, scale) = {
|
||||||
|
let mut state = self.state.borrow_mut();
|
||||||
|
if width.map_or(true, |width| width.get() == state.bounds.size.width)
|
||||||
|
&& height.map_or(true, |height| height.get() == state.bounds.size.height)
|
||||||
|
&& scale.map_or(true, |scale| scale == state.scale)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(width) = width {
|
||||||
|
state.bounds.size.width = width.get();
|
||||||
|
}
|
||||||
|
if let Some(height) = height {
|
||||||
|
state.bounds.size.height = height.get();
|
||||||
|
}
|
||||||
|
if let Some(scale) = scale {
|
||||||
|
state.scale = scale;
|
||||||
|
}
|
||||||
|
let width = state.bounds.size.width;
|
||||||
|
let height = state.bounds.size.height;
|
||||||
|
let scale = state.scale;
|
||||||
|
state.renderer.update_drawable_size(size(
|
||||||
|
width as f64 * scale as f64,
|
||||||
|
height as f64 * scale as f64,
|
||||||
|
));
|
||||||
|
(width, height, scale)
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(ref mut fun) = self.callbacks.borrow_mut().resize {
|
||||||
|
fun(
|
||||||
|
Size {
|
||||||
|
width: px(width as f32),
|
||||||
|
height: px(height as f32),
|
||||||
|
},
|
||||||
|
scale,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let state = self.state.borrow();
|
||||||
|
if let Some(viewport) = &state.viewport {
|
||||||
|
viewport.set_destination(width as i32, height as i32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resize(&self, width: Option<NonZeroU32>, height: Option<NonZeroU32>) {
|
||||||
|
self.set_size_and_scale(width, height, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rescale(&self, scale: f32) {
|
||||||
|
self.set_size_and_scale(None, None, Some(scale));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_fullscreen(&self, fullscreen: bool) {
|
||||||
|
let mut state = self.state.borrow_mut();
|
||||||
|
state.fullscreen = fullscreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Notifies the window of the state of the decorations.
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// This API is indirectly called by the wayland compositor and
|
||||||
|
/// not meant to be called by a user who wishes to change the state
|
||||||
|
/// of the decorations. This is because the state of the decorations
|
||||||
|
/// is managed by the compositor and not the client.
|
||||||
|
pub fn set_decoration_state(&self, state: WaylandDecorationState) {
|
||||||
|
self.state.borrow_mut().decoration_state = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn close(&self) {
|
||||||
|
let mut callbacks = self.callbacks.borrow_mut();
|
||||||
|
if let Some(fun) = callbacks.close.take() {
|
||||||
|
fun()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_input(&self, input: PlatformInput) {
|
||||||
|
if let Some(ref mut fun) = self.callbacks.borrow_mut().input {
|
||||||
|
if !fun(input.clone()).propagate {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let PlatformInput::KeyDown(event) = input {
|
||||||
|
if let Some(ime_key) = &event.keystroke.ime_key {
|
||||||
|
let mut state = self.state.borrow_mut();
|
||||||
|
if let Some(mut input_handler) = state.input_handler.take() {
|
||||||
|
drop(state);
|
||||||
|
input_handler.replace_text_in_range(None, ime_key);
|
||||||
|
self.state.borrow_mut().input_handler = Some(input_handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_focused(&self, focus: bool) {
|
||||||
|
if let Some(ref mut fun) = self.callbacks.borrow_mut().active_status_change {
|
||||||
|
fun(focus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl rwh::HasWindowHandle for WaylandWindow {
|
||||||
|
fn window_handle(&self) -> Result<rwh::WindowHandle<'_>, rwh::HandleError> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl rwh::HasDisplayHandle for WaylandWindow {
|
||||||
|
fn display_handle(&self) -> Result<rwh::DisplayHandle<'_>, rwh::HandleError> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlatformWindow for WaylandWindow {
|
||||||
|
fn bounds(&self) -> Bounds<DevicePixels> {
|
||||||
|
self.borrow().bounds.map(|p| DevicePixels(p as i32))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_maximized(&self) -> bool {
|
||||||
|
self.borrow().maximized
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo(linux)
|
||||||
|
// check if it is right
|
||||||
|
fn window_bounds(&self) -> WindowBounds {
|
||||||
|
let state = self.borrow();
|
||||||
|
if state.fullscreen {
|
||||||
|
WindowBounds::Fullscreen(state.restore_bounds)
|
||||||
|
} else if state.maximized {
|
||||||
|
WindowBounds::Maximized(state.restore_bounds)
|
||||||
|
} else {
|
||||||
|
WindowBounds::Windowed(state.bounds.map(|p| DevicePixels(p as i32)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn content_size(&self) -> Size<Pixels> {
|
||||||
|
let state = self.borrow();
|
||||||
|
Size {
|
||||||
|
width: Pixels(state.bounds.size.width as f32),
|
||||||
|
height: Pixels(state.bounds.size.height as f32),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scale_factor(&self) -> f32 {
|
||||||
|
self.borrow().scale
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo(linux)
|
||||||
|
fn appearance(&self) -> WindowAppearance {
|
||||||
|
WindowAppearance::Light
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo(linux)
|
||||||
|
fn display(&self) -> Rc<dyn PlatformDisplay> {
|
||||||
|
Rc::new(WaylandDisplay {})
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo(linux)
|
||||||
|
fn mouse_position(&self) -> Point<Pixels> {
|
||||||
|
Point::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo(linux)
|
||||||
|
fn modifiers(&self) -> Modifiers {
|
||||||
|
crate::Modifiers::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_input_handler(&mut self, input_handler: PlatformInputHandler) {
|
||||||
|
self.borrow_mut().input_handler = Some(input_handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn take_input_handler(&mut self) -> Option<PlatformInputHandler> {
|
||||||
|
self.borrow_mut().input_handler.take()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt(
|
||||||
|
&self,
|
||||||
|
level: PromptLevel,
|
||||||
|
msg: &str,
|
||||||
|
detail: Option<&str>,
|
||||||
|
answers: &[&str],
|
||||||
|
) -> Option<Receiver<usize>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn activate(&self) {
|
||||||
|
// todo(linux)
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo(linux)
|
||||||
|
fn is_active(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_title(&mut self, title: &str) {
|
||||||
|
self.borrow().toplevel.set_title(title.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_app_id(&mut self, app_id: &str) {
|
||||||
|
self.borrow().toplevel.set_app_id(app_id.to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance) {
|
||||||
|
let opaque = background_appearance == WindowBackgroundAppearance::Opaque;
|
||||||
|
let mut state = self.borrow_mut();
|
||||||
|
state.renderer.update_transparency(!opaque);
|
||||||
|
|
||||||
|
let region = state
|
||||||
|
.globals
|
||||||
|
.compositor
|
||||||
|
.create_region(&state.globals.qh, ());
|
||||||
|
region.add(0, 0, i32::MAX, i32::MAX);
|
||||||
|
|
||||||
|
if opaque {
|
||||||
|
// Promise the compositor that this region of the window surface
|
||||||
|
// contains no transparent pixels. This allows the compositor to
|
||||||
|
// do skip whatever is behind the surface for better performance.
|
||||||
|
state.surface.set_opaque_region(Some(®ion));
|
||||||
|
} else {
|
||||||
|
state.surface.set_opaque_region(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref blur_manager) = state.globals.blur_manager {
|
||||||
|
if (background_appearance == WindowBackgroundAppearance::Blurred) {
|
||||||
|
if (state.blur.is_none()) {
|
||||||
|
let blur = blur_manager.create(&state.surface, &state.globals.qh, ());
|
||||||
|
blur.set_region(Some(®ion));
|
||||||
|
state.blur = Some(blur);
|
||||||
|
}
|
||||||
|
state.blur.as_ref().unwrap().commit();
|
||||||
|
} else {
|
||||||
|
// It probably doesn't hurt to clear the blur for opaque windows
|
||||||
|
blur_manager.unset(&state.surface);
|
||||||
|
if let Some(b) = state.blur.take() {
|
||||||
|
b.release()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
region.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_edited(&mut self, edited: bool) {
|
||||||
|
// todo(linux)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_character_palette(&self) {
|
||||||
|
// todo(linux)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn minimize(&self) {
|
||||||
|
self.borrow().toplevel.set_minimized();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn zoom(&self) {
|
||||||
|
let state = self.borrow();
|
||||||
|
if !state.maximized {
|
||||||
|
state.toplevel.set_maximized();
|
||||||
|
} else {
|
||||||
|
state.toplevel.unset_maximized();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggle_fullscreen(&self) {
|
||||||
|
let mut state = self.borrow_mut();
|
||||||
|
state.restore_bounds = state.bounds.map(|p| DevicePixels(p as i32));
|
||||||
|
if !state.fullscreen {
|
||||||
|
state.toplevel.set_fullscreen(None);
|
||||||
|
} else {
|
||||||
|
state.toplevel.unset_fullscreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_fullscreen(&self) -> bool {
|
||||||
|
self.borrow().fullscreen
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_request_frame(&self, callback: Box<dyn FnMut()>) {
|
||||||
|
self.0.callbacks.borrow_mut().request_frame = Some(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> crate::DispatchEventResult>) {
|
||||||
|
self.0.callbacks.borrow_mut().input = Some(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>) {
|
||||||
|
self.0.callbacks.borrow_mut().active_status_change = Some(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>) {
|
||||||
|
self.0.callbacks.borrow_mut().resize = Some(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_moved(&self, callback: Box<dyn FnMut()>) {
|
||||||
|
self.0.callbacks.borrow_mut().moved = Some(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>) {
|
||||||
|
self.0.callbacks.borrow_mut().should_close = Some(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_close(&self, callback: Box<dyn FnOnce()>) {
|
||||||
|
self.0.callbacks.borrow_mut().close = Some(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_appearance_changed(&self, callback: Box<dyn FnMut()>) {
|
||||||
|
// todo(linux)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(&self, scene: &Scene) {
|
||||||
|
let mut state = self.borrow_mut();
|
||||||
|
state.renderer.draw(scene);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn completed_frame(&self) {
|
||||||
|
let mut state = self.borrow_mut();
|
||||||
|
state.surface.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
|
||||||
|
let state = self.borrow();
|
||||||
|
state.renderer.sprite_atlas().clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||||
|
pub enum WaylandDecorationState {
|
||||||
|
/// Decorations are to be provided by the client
|
||||||
|
Client,
|
||||||
|
|
||||||
|
/// Decorations are provided by the server
|
||||||
|
Server,
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue