mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Merge pull request #591 from Textualize/table-caret
Add a cursor to table
This commit is contained in:
168
poetry.lock
generated
168
poetry.lock
generated
@@ -74,7 +74,7 @@ tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>
|
||||
|
||||
[[package]]
|
||||
name = "black"
|
||||
version = "22.3.0"
|
||||
version = "22.6.0"
|
||||
description = "The uncompromising code formatter."
|
||||
category = "dev"
|
||||
optional = false
|
||||
@@ -85,7 +85,7 @@ click = ">=8.0.0"
|
||||
mypy-extensions = ">=0.4.3"
|
||||
pathspec = ">=0.9.0"
|
||||
platformdirs = ">=2"
|
||||
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
|
||||
tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""}
|
||||
typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""}
|
||||
typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}
|
||||
|
||||
@@ -208,14 +208,14 @@ dev = ["twine", "markdown", "flake8", "wheel"]
|
||||
|
||||
[[package]]
|
||||
name = "griffe"
|
||||
version = "0.20.0"
|
||||
version = "0.21.0"
|
||||
description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
cached_property = {version = "*", markers = "python_version < \"3.8\""}
|
||||
cached-property = {version = "*", markers = "python_version < \"3.8\""}
|
||||
|
||||
[package.extras]
|
||||
async = ["aiofiles (>=0.7,<1.0)"]
|
||||
@@ -241,7 +241,7 @@ python-versions = ">=3.5"
|
||||
|
||||
[[package]]
|
||||
name = "importlib-metadata"
|
||||
version = "4.11.4"
|
||||
version = "4.12.0"
|
||||
description = "Read metadata from Python packages"
|
||||
category = "main"
|
||||
optional = false
|
||||
@@ -254,7 +254,7 @@ zipp = ">=0.5"
|
||||
[package.extras]
|
||||
docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"]
|
||||
perf = ["ipython"]
|
||||
testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"]
|
||||
testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
@@ -345,7 +345,7 @@ mkdocs = ">=1.1"
|
||||
|
||||
[[package]]
|
||||
name = "mkdocs-material"
|
||||
version = "8.3.6"
|
||||
version = "8.3.8"
|
||||
description = "Documentation that simply works"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@@ -446,11 +446,11 @@ python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "nodeenv"
|
||||
version = "1.6.0"
|
||||
version = "1.7.0"
|
||||
description = "Node.js virtual environment builder"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
@@ -678,7 +678,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
|
||||
[[package]]
|
||||
name = "time-machine"
|
||||
version = "2.7.0"
|
||||
version = "2.7.1"
|
||||
description = "Travel through time in your tests."
|
||||
category = "dev"
|
||||
optional = false
|
||||
@@ -721,7 +721,7 @@ python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "virtualenv"
|
||||
version = "20.14.1"
|
||||
version = "20.15.0"
|
||||
description = "Virtual Python Environment builder"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@@ -878,29 +878,29 @@ attrs = [
|
||||
{file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"},
|
||||
]
|
||||
black = [
|
||||
{file = "black-22.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09"},
|
||||
{file = "black-22.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb"},
|
||||
{file = "black-22.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a"},
|
||||
{file = "black-22.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968"},
|
||||
{file = "black-22.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d"},
|
||||
{file = "black-22.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce"},
|
||||
{file = "black-22.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82"},
|
||||
{file = "black-22.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b"},
|
||||
{file = "black-22.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015"},
|
||||
{file = "black-22.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b"},
|
||||
{file = "black-22.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a"},
|
||||
{file = "black-22.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163"},
|
||||
{file = "black-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464"},
|
||||
{file = "black-22.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0"},
|
||||
{file = "black-22.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176"},
|
||||
{file = "black-22.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0"},
|
||||
{file = "black-22.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20"},
|
||||
{file = "black-22.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a"},
|
||||
{file = "black-22.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad"},
|
||||
{file = "black-22.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21"},
|
||||
{file = "black-22.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265"},
|
||||
{file = "black-22.3.0-py3-none-any.whl", hash = "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72"},
|
||||
{file = "black-22.3.0.tar.gz", hash = "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79"},
|
||||
{file = "black-22.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f586c26118bc6e714ec58c09df0157fe2d9ee195c764f630eb0d8e7ccce72e69"},
|
||||
{file = "black-22.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b270a168d69edb8b7ed32c193ef10fd27844e5c60852039599f9184460ce0807"},
|
||||
{file = "black-22.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6797f58943fceb1c461fb572edbe828d811e719c24e03375fd25170ada53825e"},
|
||||
{file = "black-22.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c85928b9d5f83b23cee7d0efcb310172412fbf7cb9d9ce963bd67fd141781def"},
|
||||
{file = "black-22.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:f6fe02afde060bbeef044af7996f335fbe90b039ccf3f5eb8f16df8b20f77666"},
|
||||
{file = "black-22.6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cfaf3895a9634e882bf9d2363fed5af8888802d670f58b279b0bece00e9a872d"},
|
||||
{file = "black-22.6.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94783f636bca89f11eb5d50437e8e17fbc6a929a628d82304c80fa9cd945f256"},
|
||||
{file = "black-22.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:2ea29072e954a4d55a2ff58971b83365eba5d3d357352a07a7a4df0d95f51c78"},
|
||||
{file = "black-22.6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e439798f819d49ba1c0bd9664427a05aab79bfba777a6db94fd4e56fae0cb849"},
|
||||
{file = "black-22.6.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:187d96c5e713f441a5829e77120c269b6514418f4513a390b0499b0987f2ff1c"},
|
||||
{file = "black-22.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:074458dc2f6e0d3dab7928d4417bb6957bb834434516f21514138437accdbe90"},
|
||||
{file = "black-22.6.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a218d7e5856f91d20f04e931b6f16d15356db1c846ee55f01bac297a705ca24f"},
|
||||
{file = "black-22.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:568ac3c465b1c8b34b61cd7a4e349e93f91abf0f9371eda1cf87194663ab684e"},
|
||||
{file = "black-22.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6c1734ab264b8f7929cef8ae5f900b85d579e6cbfde09d7387da8f04771b51c6"},
|
||||
{file = "black-22.6.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9a3ac16efe9ec7d7381ddebcc022119794872abce99475345c5a61aa18c45ad"},
|
||||
{file = "black-22.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:b9fd45787ba8aa3f5e0a0a98920c1012c884622c6c920dbe98dbd05bc7c70fbf"},
|
||||
{file = "black-22.6.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7ba9be198ecca5031cd78745780d65a3f75a34b2ff9be5837045dce55db83d1c"},
|
||||
{file = "black-22.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a3db5b6409b96d9bd543323b23ef32a1a2b06416d525d27e0f67e74f1446c8f2"},
|
||||
{file = "black-22.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:560558527e52ce8afba936fcce93a7411ab40c7d5fe8c2463e279e843c0328ee"},
|
||||
{file = "black-22.6.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b154e6bbde1e79ea3260c4b40c0b7b3109ffcdf7bc4ebf8859169a6af72cd70b"},
|
||||
{file = "black-22.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:4af5bc0e1f96be5ae9bd7aaec219c901a94d6caa2484c21983d043371c733fc4"},
|
||||
{file = "black-22.6.0-py3-none-any.whl", hash = "sha256:ac609cf8ef5e7115ddd07d85d988d074ed00e10fbc3445aee393e70164a2219c"},
|
||||
{file = "black-22.6.0.tar.gz", hash = "sha256:6c6d39e28aed379aec40da1c65434c77d75e65bb59a1e1c283de545fb4e7c6c9"},
|
||||
]
|
||||
cached-property = [
|
||||
{file = "cached-property-1.5.2.tar.gz", hash = "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130"},
|
||||
@@ -1043,8 +1043,8 @@ ghp-import = [
|
||||
{file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"},
|
||||
]
|
||||
griffe = [
|
||||
{file = "griffe-0.20.0-py3-none-any.whl", hash = "sha256:899e0c9c09baf22b31de1c969a03edaf0ddf72d0a7183df8de746b6c26ed62f4"},
|
||||
{file = "griffe-0.20.0.tar.gz", hash = "sha256:bf181de6e661c0d2a229c1dc7e90db0def280ee3a89c6829fcc1695baee65f7f"},
|
||||
{file = "griffe-0.21.0-py3-none-any.whl", hash = "sha256:e9fb5eeb7c721e1d84804452bdc742bd57b120b13aba663157668ae2d217088a"},
|
||||
{file = "griffe-0.21.0.tar.gz", hash = "sha256:61ab3bc02b09afeb489f1aef44c646a09f1837d9cdf15943ac6021903a4d3984"},
|
||||
]
|
||||
identify = [
|
||||
{file = "identify-2.5.1-py2.py3-none-any.whl", hash = "sha256:0dca2ea3e4381c435ef9c33ba100a78a9b40c0bab11189c7cf121f75815efeaa"},
|
||||
@@ -1055,8 +1055,8 @@ idna = [
|
||||
{file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
|
||||
]
|
||||
importlib-metadata = [
|
||||
{file = "importlib_metadata-4.11.4-py3-none-any.whl", hash = "sha256:c58c8eb8a762858f49e18436ff552e83914778e50e9d2f1660535ffb364552ec"},
|
||||
{file = "importlib_metadata-4.11.4.tar.gz", hash = "sha256:5d26852efe48c0a32b0509ffbc583fda1a2266545a78d104a6f4aff3db17d700"},
|
||||
{file = "importlib_metadata-4.12.0-py3-none-any.whl", hash = "sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23"},
|
||||
{file = "importlib_metadata-4.12.0.tar.gz", hash = "sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670"},
|
||||
]
|
||||
iniconfig = [
|
||||
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
|
||||
@@ -1125,8 +1125,8 @@ mkdocs-autorefs = [
|
||||
{file = "mkdocs_autorefs-0.4.1-py3-none-any.whl", hash = "sha256:a2248a9501b29dc0cc8ba4c09f4f47ff121945f6ce33d760f145d6f89d313f5b"},
|
||||
]
|
||||
mkdocs-material = [
|
||||
{file = "mkdocs-material-8.3.6.tar.gz", hash = "sha256:be8f95c0dfb927339b55b2cc066423dc0b381be9828ff74a5b02df979a859b66"},
|
||||
{file = "mkdocs_material-8.3.6-py2.py3-none-any.whl", hash = "sha256:01f3fbab055751b3b75a64b538e86b9ce0c6a0f8d43620f6287dfa16534443e5"},
|
||||
{file = "mkdocs-material-8.3.8.tar.gz", hash = "sha256:b9cd305c3c29ef758931dae06e4aea0ca9f8bcc8ac6b2d45f10f932a015d6b83"},
|
||||
{file = "mkdocs_material-8.3.8-py2.py3-none-any.whl", hash = "sha256:949c75fa934d4b9ecc7b519964e58f0c9fc29f2ceb04736c85809cdbc403dfb5"},
|
||||
]
|
||||
mkdocs-material-extensions = [
|
||||
{file = "mkdocs-material-extensions-1.0.3.tar.gz", hash = "sha256:bfd24dfdef7b41c312ede42648f9eb83476ea168ec163b613f9abd12bbfddba2"},
|
||||
@@ -1285,8 +1285,8 @@ mypy-extensions = [
|
||||
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
|
||||
]
|
||||
nodeenv = [
|
||||
{file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"},
|
||||
{file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"},
|
||||
{file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"},
|
||||
{file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"},
|
||||
]
|
||||
packaging = [
|
||||
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
|
||||
@@ -1393,46 +1393,46 @@ six = [
|
||||
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
|
||||
]
|
||||
time-machine = [
|
||||
{file = "time-machine-2.7.0.tar.gz", hash = "sha256:0aa0ccd531d7d98e71f7945b65d26d92d31ab74a21a111b9afe61b981c1eb7b2"},
|
||||
{file = "time_machine-2.7.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:aac9e9fa665d0ed7d105ddaaebda9ef3a2de30aaaf56cfe894f15ba60de8ae09"},
|
||||
{file = "time_machine-2.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:93c6baea546a8edffa57dc691cbb61e5070d6ec791ff7b4a025d4f34b9808516"},
|
||||
{file = "time_machine-2.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f056cc4b9212424eb0796389cb019fb61ed0691674649f824585973f001ab66"},
|
||||
{file = "time_machine-2.7.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dacd42e9f4a81a7c9e308874802e801aab1fed119698958fc00920038a652145"},
|
||||
{file = "time_machine-2.7.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d607303fe6c046b246f1befa82218624db7e92695950c4f6e11f416b8f61129"},
|
||||
{file = "time_machine-2.7.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:adf738e2d21b265cab4dcb2dc3c0592087f44614a03657c87a6e79131e3be715"},
|
||||
{file = "time_machine-2.7.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e0a774c3d94b6b4dd58a939ae886d4533ca9f308f71ab6e174d41aa092fcd807"},
|
||||
{file = "time_machine-2.7.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b1115628d39ebe7af89bb187634f119b58f163ee38d9a8369d779a496ac6105"},
|
||||
{file = "time_machine-2.7.0-cp310-cp310-win32.whl", hash = "sha256:6ad4b6dfec23acc7b5549fcbe9d632347bd62781471d79e44001242821635393"},
|
||||
{file = "time_machine-2.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:ef7cf446a19536a6c4fcdc74c6023bf4c85f6ebe97c63b18b4bf97905bf7919d"},
|
||||
{file = "time_machine-2.7.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:252f53e72da83e876c004e312d15a2e7c920b84dc60a469ad3c5404c5aa0d2b1"},
|
||||
{file = "time_machine-2.7.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f39695eb780795f7109ddf6b6c04d97f5610d75b08eb968deed3f0e09c43bb0"},
|
||||
{file = "time_machine-2.7.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9c6236ea7f2744f8b9ceab85c9cd71f0aeb03530e2b47377678c39fcd77aafb6"},
|
||||
{file = "time_machine-2.7.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39aabce90c4c936abe78efe483a6b2fe99901ff431afbeab65c0815eeff66f0b"},
|
||||
{file = "time_machine-2.7.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:75cdf9aa1b0fba833ca4ebafd738a88983a6597caa4b6ac4a9bfa83c6ca7d8fc"},
|
||||
{file = "time_machine-2.7.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:038e642d88ba5b76bfa77ec95bcf0d35d7fc6c6ea264fc0aa36e02eff09d3ad2"},
|
||||
{file = "time_machine-2.7.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3cca3f11059ab6d39dc33495be94ecceaa3d6760b7a983a5c67e172d6b5f6781"},
|
||||
{file = "time_machine-2.7.0-cp37-cp37m-win32.whl", hash = "sha256:1d40c3be8b075868e73e09a9f600ecf383ad30c8921806c7dc0915820b8fa1c7"},
|
||||
{file = "time_machine-2.7.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e125d7a406f8a3b48ddd3f836cf41be98de446db76147a2da8d1f1e82254946e"},
|
||||
{file = "time_machine-2.7.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:7c35a6971ebfe10ed4141a5e5da98cbb7b0f029385d7d97f47b19772e34cdd8a"},
|
||||
{file = "time_machine-2.7.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c2c3a7f36e3c12f229c651a9e58e5a034d2360c27bea24bfebf95001ce1359b4"},
|
||||
{file = "time_machine-2.7.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16ba783c4836a7dc74221a6b937deb88805ccb208a246a160d716c426c074462"},
|
||||
{file = "time_machine-2.7.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b7ba3a382b96db3f47c9f93207aefdc2817563e3ec727dbda38399035bcd475"},
|
||||
{file = "time_machine-2.7.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:514413eb7f0b55bd36a5e2ab67b357430c8038382ddb5896ff67f73668b1a23f"},
|
||||
{file = "time_machine-2.7.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:62a8ae6dbb9ed623f7e42089661974a8fa256c4c64c5391d1f6c60da2b8d54c8"},
|
||||
{file = "time_machine-2.7.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:fcebeff41e50445100f3f881699d8deeda8f1c6dd80c0c0381b61977aae9dedc"},
|
||||
{file = "time_machine-2.7.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:985e773fa70a03b41ef33e1e77b0a655ca1503b77f60d52b613fe1707636b93d"},
|
||||
{file = "time_machine-2.7.0-cp38-cp38-win32.whl", hash = "sha256:0579fa83e608ef4f1b16bd63e0a01c2bec5c4b3f8c2349a158af2c886cc8e0b5"},
|
||||
{file = "time_machine-2.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:fc735fdfa2cbe00f63ed66c48cf32c13b91b9dd9817bec37bdb9c2df5ea09da8"},
|
||||
{file = "time_machine-2.7.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a1ae378d4bd4101edd605e82da4859e0a5c509ddf2df0f8094160d795fc22777"},
|
||||
{file = "time_machine-2.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:747825e968290c8ec98202a60411586edd9b8669cbbf31f61224240e5951d1fb"},
|
||||
{file = "time_machine-2.7.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd4797046126bfa1524a9ef8acac83282ce9365ad2cbcab0843ee2662e103502"},
|
||||
{file = "time_machine-2.7.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50585410381f9b9799fdd0ab7b1ac5009a341127cf9e72222bc2cb870eddf44d"},
|
||||
{file = "time_machine-2.7.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd0b8cf6ad69ed54d2e11c2d5cc3681c6defc7ce022121312cead610d54685c3"},
|
||||
{file = "time_machine-2.7.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:72d03615caf11eb47c7efeb48c839e307a18c272637d967b697a02f6561dad3c"},
|
||||
{file = "time_machine-2.7.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:fa5a0a01f9562be8a13338255a9497b3c1fc8bb018582fa4a881184a9f480c9a"},
|
||||
{file = "time_machine-2.7.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:06473db7145f82992d47f02d04a164abe499ec3f4ab24fc9877d49a7e46ae7a0"},
|
||||
{file = "time_machine-2.7.0-cp39-cp39-win32.whl", hash = "sha256:b377991a5ead8f4d4c887293fde66cb1e8abb5c709e311c724016d16e2ef4c7d"},
|
||||
{file = "time_machine-2.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:5ba2f608684b19be35b6bc0dca362eca2ded0b7c6f1ff88ed1e55b8dfdfb869d"},
|
||||
{file = "time-machine-2.7.1.tar.gz", hash = "sha256:be6c1f0421a77a046db8fae00886fb364f683a86612b71dd5c74b22891590042"},
|
||||
{file = "time_machine-2.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ae93d2f761435d192bc80c148438a0c4261979db0610cef08dfe2c8d21ca1c67"},
|
||||
{file = "time_machine-2.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:342b431154fbfb1889f8d7aa3d857373a837106bba395a5cc99123f11a7cea03"},
|
||||
{file = "time_machine-2.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f4011ea76f6ad2f932f00cf9e77a25b575a024d6bc15bcf891a3f9916ceeb6e"},
|
||||
{file = "time_machine-2.7.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43ae8192d370a90d2246fca565a55633f592b314264c65c5c9151c361b715fb9"},
|
||||
{file = "time_machine-2.7.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cea12d0592ebbe738db952ce6fd272ed90e7bbb095e802f4f2145f8f0e322fa3"},
|
||||
{file = "time_machine-2.7.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:732d5fd2d442fa87538b5a6ca623cb205b9b048d2c9aaf79e5cfc7ec7f637848"},
|
||||
{file = "time_machine-2.7.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c34e1f49cad2fd41d42c4aabd3d69a32c79d9a8e0779064554843823cd1fb1e4"},
|
||||
{file = "time_machine-2.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6af3e81cf663b6d5660953ae59da2bb2ae802452ecbc9907272979ed06253659"},
|
||||
{file = "time_machine-2.7.1-cp310-cp310-win32.whl", hash = "sha256:10c2937d3556f4358205dac5c7cd2d33832b8b911f3deff050f59e1fe2be3231"},
|
||||
{file = "time_machine-2.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:200974e9bb8a1cb227ce579caafeaeebb0f9de81758c444cbccc0ea464313caf"},
|
||||
{file = "time_machine-2.7.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d5e2376b7922c9d96921709c7e730498b9c69da889f359a465d0c43117b62da3"},
|
||||
{file = "time_machine-2.7.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ad9117abe223cdc7b4a4432e0a0cfebb1b351a091ee996c653e90f27a734fce"},
|
||||
{file = "time_machine-2.7.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:626ef686723147468e84da3edcd67ff757a463250fd35f8f6a8e5b899c43b43d"},
|
||||
{file = "time_machine-2.7.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9331946ed13acd50bc484f408e26b8eefa67e3dbca41927d2052f2148d3661d"},
|
||||
{file = "time_machine-2.7.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3d0612e0323047f29c23732963d9926f1a95e2ce334d86fecd37c803ac240fc6"},
|
||||
{file = "time_machine-2.7.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b474499ad0083252240bc5be13f8116cc2ca8a89d1ca4967ed74a7b5f0883f95"},
|
||||
{file = "time_machine-2.7.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7df0857709432585b62d2667c0e6e64029b652e2df776b9fb85223c60dce52c7"},
|
||||
{file = "time_machine-2.7.1-cp37-cp37m-win32.whl", hash = "sha256:77c8dfe8dc7f45bbfe73494c72f3728d99abec5a020460ad7ffee5247365eba4"},
|
||||
{file = "time_machine-2.7.1-cp37-cp37m-win_amd64.whl", hash = "sha256:c1fd1c231377ce076f99c8c16999a95510690f8dbd35db0e5fbbc74a17f84b39"},
|
||||
{file = "time_machine-2.7.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:462924fb87826882fc7830098e621116599f9259d181a7bbf5a4e49f74ec325b"},
|
||||
{file = "time_machine-2.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:46bf3b4a52d43289b23f0015a9d8592ddf621a5058e566c275cb060347d430c1"},
|
||||
{file = "time_machine-2.7.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30afd5b978d8121334c80fa23119d7bd7c9f954169854edf5103e5c8b38358bb"},
|
||||
{file = "time_machine-2.7.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:633fb8c47f3cd64690591ca6981e4fdbcaa54c18d8a57a3cdc24638ca98f8216"},
|
||||
{file = "time_machine-2.7.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80b6093c3b70d1d1a66b65f18a6e53b233c8dd5d8ffe7ac59e9d048fb1d5e15c"},
|
||||
{file = "time_machine-2.7.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e62ed7d78694b7e0a2ab30b3dd52ebf26b03e17d6eda0f231fd77e24307a55a9"},
|
||||
{file = "time_machine-2.7.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0eaf024d16482ec211a579fd389cbbd4fedd8a1f0a0c41642508815f880ca3a9"},
|
||||
{file = "time_machine-2.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2688091ce0c16151faa80625efb34e3096731fbdee6d5284c48c984bce95c311"},
|
||||
{file = "time_machine-2.7.1-cp38-cp38-win32.whl", hash = "sha256:2e54bf0521b6e397fcaa03060feb187bbe5aa63ac51dbb97d5bc59fb0c4725f8"},
|
||||
{file = "time_machine-2.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:cee72d9e14d36e4b8da6af1d2d784f14da53f76aeb5066540a38318aa907b551"},
|
||||
{file = "time_machine-2.7.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:06322d41d45d86e2dc2520794c95129ff25b8620b33851ed40700c859ebf8c30"},
|
||||
{file = "time_machine-2.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:193b14daa3b3cf67e6b55d6e2d63c2eb7c1d3f49017704d4b43963b198656888"},
|
||||
{file = "time_machine-2.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1367a89fb857f68cfa723e236cd47febaf201a3a625ad8423110fe0509d5fca8"},
|
||||
{file = "time_machine-2.7.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce350f7e8bd51a0bb064180486300283bec5cd1a21a318a8ffe5f7df11735f36"},
|
||||
{file = "time_machine-2.7.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68ff623d835760314e279aedc0d19a1dc4dec117c6bca388e1ff077c781256bd"},
|
||||
{file = "time_machine-2.7.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:05fecd818d41727d31109a0d039ce07c8311602b45ffc07bffd8ae8b6f266ee5"},
|
||||
{file = "time_machine-2.7.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:1fe4e604c5effc290c1bbecd3ea98687690d0a88fd98ba93e0246bf19ae2a520"},
|
||||
{file = "time_machine-2.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff07a5635d42957f2bd7eb5ca6579f64de368c842e754a4d3414520693b75db9"},
|
||||
{file = "time_machine-2.7.1-cp39-cp39-win32.whl", hash = "sha256:8c6314e7e0ffd7af82c8026786d5551aff973e0c86ec1368b0590be9a7620cad"},
|
||||
{file = "time_machine-2.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:d50a2620d726788cbde97c58e0f6f61d10337d16d088a1fad789f50a1b5ff4d1"},
|
||||
]
|
||||
toml = [
|
||||
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
|
||||
@@ -1473,8 +1473,8 @@ typing-extensions = [
|
||||
{file = "typing_extensions-4.2.0.tar.gz", hash = "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376"},
|
||||
]
|
||||
virtualenv = [
|
||||
{file = "virtualenv-20.14.1-py2.py3-none-any.whl", hash = "sha256:e617f16e25b42eb4f6e74096b9c9e37713cf10bf30168fb4a739f3fa8f898a3a"},
|
||||
{file = "virtualenv-20.14.1.tar.gz", hash = "sha256:ef589a79795589aada0c1c5b319486797c03b67ac3984c48c669c0e4f50df3a5"},
|
||||
{file = "virtualenv-20.15.0-py2.py3-none-any.whl", hash = "sha256:804cce4de5b8a322f099897e308eecc8f6e2951f1a8e7e2b3598dff865f01336"},
|
||||
{file = "virtualenv-20.15.0.tar.gz", hash = "sha256:4c44b1d77ca81f8368e2d7414f9b20c428ad16b343ac6d226206c5b84e2b4fcc"},
|
||||
]
|
||||
watchdog = [
|
||||
{file = "watchdog-2.1.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a735a990a1095f75ca4f36ea2ef2752c99e6ee997c46b0de507ba40a09bf7330"},
|
||||
|
||||
@@ -112,7 +112,6 @@ class BasicApp(App, css_path="basic.css"):
|
||||
Tweet(TweetBody()),
|
||||
Widget(
|
||||
Static(Syntax(CODE, "python"), classes="code"),
|
||||
self.scroll_to_target,
|
||||
classes="scrollable",
|
||||
),
|
||||
Error(),
|
||||
|
||||
241
sandbox/will/basic.css
Normal file
241
sandbox/will/basic.css
Normal file
@@ -0,0 +1,241 @@
|
||||
/* CSS file for basic.py */
|
||||
|
||||
|
||||
|
||||
* {
|
||||
transition: color 300ms linear, background 300ms linear;
|
||||
}
|
||||
|
||||
* {
|
||||
scrollbar-background: $panel-darken-2;
|
||||
scrollbar-background-hover: $panel-darken-3;
|
||||
scrollbar-color: $system;
|
||||
scrollbar-color-active: $accent-darken-1;
|
||||
scrollbar-size-horizontal: 1;
|
||||
scrollbar-size-vertical: 2;
|
||||
}
|
||||
|
||||
/* *:hover {
|
||||
tint: red 30%;
|
||||
} */
|
||||
|
||||
App > Screen {
|
||||
layout: dock;
|
||||
docks: side=left/1;
|
||||
background: $surface;
|
||||
color: $text-surface;
|
||||
}
|
||||
|
||||
DataTable {
|
||||
border: solid red;
|
||||
|
||||
margin: 1 1;
|
||||
height: 12;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
color: $text-primary;
|
||||
background: $primary-background;
|
||||
dock: side;
|
||||
width: 30;
|
||||
offset-x: -100%;
|
||||
layout: dock;
|
||||
transition: offset 500ms in_out_cubic;
|
||||
}
|
||||
|
||||
#sidebar.-active {
|
||||
offset-x: 0;
|
||||
}
|
||||
|
||||
#sidebar .title {
|
||||
height: 3;
|
||||
background: $primary-background-darken-2;
|
||||
color: $text-primary-darken-2 ;
|
||||
border-right: outer $primary-darken-3;
|
||||
content-align: center middle;
|
||||
}
|
||||
|
||||
#sidebar .user {
|
||||
height: 8;
|
||||
background: $primary-background-darken-1;
|
||||
color: $text-primary-darken-1;
|
||||
border-right: outer $primary-background-darken-3;
|
||||
content-align: center middle;
|
||||
}
|
||||
|
||||
#sidebar .content {
|
||||
background: $primary-background;
|
||||
color: $text-primary-background;
|
||||
border-right: outer $primary-background-darken-3;
|
||||
content-align: center middle;
|
||||
}
|
||||
|
||||
#header {
|
||||
color: $text-primary-darken-1;
|
||||
background: $primary-darken-1;
|
||||
height: 3;
|
||||
content-align: center middle;
|
||||
}
|
||||
|
||||
#content {
|
||||
color: $text-background;
|
||||
background: $background;
|
||||
layout: vertical;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
|
||||
Tweet {
|
||||
height:12;
|
||||
width: 100%;
|
||||
|
||||
margin: 1 3;
|
||||
background: $panel;
|
||||
color: $text-panel;
|
||||
layout: vertical;
|
||||
/* border: outer $primary; */
|
||||
padding: 1;
|
||||
border: wide $panel-darken-2;
|
||||
overflow: auto;
|
||||
/* scrollbar-gutter: stable; */
|
||||
align-horizontal: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
||||
.scrollable {
|
||||
|
||||
overflow-y: scroll;
|
||||
margin: 1 2;
|
||||
height: 20;
|
||||
align-horizontal: center;
|
||||
layout: vertical;
|
||||
}
|
||||
|
||||
.code {
|
||||
|
||||
height: auto;
|
||||
|
||||
}
|
||||
|
||||
|
||||
TweetHeader {
|
||||
height:1;
|
||||
background: $accent;
|
||||
color: $text-accent
|
||||
}
|
||||
|
||||
TweetBody {
|
||||
width: 100%;
|
||||
background: $panel;
|
||||
color: $text-panel;
|
||||
height: auto;
|
||||
padding: 0 1 0 0;
|
||||
}
|
||||
|
||||
Tweet.scroll-horizontal TweetBody {
|
||||
width: 350;
|
||||
}
|
||||
|
||||
.button {
|
||||
background: $accent;
|
||||
color: $text-accent;
|
||||
width:20;
|
||||
height: 3;
|
||||
/* border-top: hidden $accent-darken-3; */
|
||||
border: tall $accent-darken-2;
|
||||
/* border-left: tall $accent-darken-1; */
|
||||
|
||||
|
||||
/* padding: 1 0 0 0 ; */
|
||||
|
||||
transition: background 400ms in_out_cubic, color 400ms in_out_cubic;
|
||||
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background: $accent-lighten-1;
|
||||
color: $text-accent-lighten-1;
|
||||
width: 20;
|
||||
height: 3;
|
||||
border: tall $accent-darken-1;
|
||||
/* border-left: tall $accent-darken-3; */
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
#footer {
|
||||
color: $text-accent;
|
||||
background: $accent;
|
||||
height: 1;
|
||||
border-top: hkey $accent-darken-2;
|
||||
content-align: center middle;
|
||||
}
|
||||
|
||||
|
||||
#sidebar .content {
|
||||
layout: vertical
|
||||
}
|
||||
|
||||
OptionItem {
|
||||
height: 3;
|
||||
background: $primary-background;
|
||||
border-right: outer $primary-background-darken-2;
|
||||
border-left: blank;
|
||||
content-align: center middle;
|
||||
}
|
||||
|
||||
OptionItem:hover {
|
||||
height: 3;
|
||||
color: $accent;
|
||||
background: $primary-background-darken-1;
|
||||
/* border-top: hkey $accent2-darken-3;
|
||||
border-bottom: hkey $accent2-darken-3; */
|
||||
text-style: bold;
|
||||
border-left: outer $accent-darken-2;
|
||||
}
|
||||
|
||||
Error {
|
||||
width: 100%;
|
||||
height:3;
|
||||
background: $error;
|
||||
color: $text-error;
|
||||
border-top: hkey $error-darken-2;
|
||||
border-bottom: hkey $error-darken-2;
|
||||
margin: 1 3;
|
||||
|
||||
text-style: bold;
|
||||
align-horizontal: center;
|
||||
}
|
||||
|
||||
Warning {
|
||||
width: 100%;
|
||||
height:3;
|
||||
background: $warning;
|
||||
color: $text-warning-fade-1;
|
||||
border-top: hkey $warning-darken-2;
|
||||
border-bottom: hkey $warning-darken-2;
|
||||
margin: 1 2;
|
||||
text-style: bold;
|
||||
align-horizontal: center;
|
||||
}
|
||||
|
||||
Success {
|
||||
width: 100%;
|
||||
height:3;
|
||||
box-sizing: border-box;
|
||||
background: $success-lighten-3;
|
||||
color: $text-success-lighten-3-fade-1;
|
||||
border-top: hkey $success;
|
||||
border-bottom: hkey $success;
|
||||
margin: 1 2;
|
||||
text-style: bold;
|
||||
align-horizontal: center;
|
||||
}
|
||||
|
||||
|
||||
.horizontal {
|
||||
layout: horizontal
|
||||
}
|
||||
199
sandbox/will/basic.py
Normal file
199
sandbox/will/basic.py
Normal file
@@ -0,0 +1,199 @@
|
||||
from rich.console import RenderableType
|
||||
from rich.style import Style
|
||||
from rich.syntax import Syntax
|
||||
from rich.text import Text
|
||||
|
||||
from textual.app import App
|
||||
from textual.reactive import Reactive
|
||||
from textual.widget import Widget
|
||||
from textual.widgets import Static, DataTable
|
||||
|
||||
CODE = '''
|
||||
class Offset(NamedTuple):
|
||||
"""A point defined by x and y coordinates."""
|
||||
|
||||
x: int = 0
|
||||
y: int = 0
|
||||
|
||||
@property
|
||||
def is_origin(self) -> bool:
|
||||
"""Check if the point is at the origin (0, 0)"""
|
||||
return self == (0, 0)
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return self != (0, 0)
|
||||
|
||||
def __add__(self, other: object) -> Offset:
|
||||
if isinstance(other, tuple):
|
||||
_x, _y = self
|
||||
x, y = other
|
||||
return Offset(_x + x, _y + y)
|
||||
return NotImplemented
|
||||
|
||||
def __sub__(self, other: object) -> Offset:
|
||||
if isinstance(other, tuple):
|
||||
_x, _y = self
|
||||
x, y = other
|
||||
return Offset(_x - x, _y - y)
|
||||
return NotImplemented
|
||||
|
||||
def __mul__(self, other: object) -> Offset:
|
||||
if isinstance(other, (float, int)):
|
||||
x, y = self
|
||||
return Offset(int(x * other), int(y * other))
|
||||
return NotImplemented
|
||||
'''
|
||||
|
||||
|
||||
lorem_short = """Lorem ipsum dolor sit amet, consectetur adipiscing elit. In velit liber a a a, volutpat nec hendrerit at, faucibus in odio. Aliquam hendrerit nibh sed quam volutpat maximus. Nullam suscipit convallis lorem quis sodales. In tristique lobortis ante et dictum. Ut at finibus ipsum."""
|
||||
lorem = (
|
||||
lorem_short
|
||||
+ """ In urna dolor, placerat et mi facilisis, congue sollicitudin massa. Phasellus felis turpis, cursus eu lectus et, porttitor malesuada augue. Sed feugiat volutpat velit, sollicitudin fringilla velit bibendum faucibus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. In velit libero, volutpat nec hendrerit at, faucibus in odio. Aliquam hendrerit nibh sed quam volutpat maximus. Nullam suscipit convallis lorem quis sodales. In tristique lobortis ante et dictum. Ut at finibus ipsum. In urna dolor, placerat et mi facilisis, congue sollicitudin massa. Phasellus felis turpis, cursus eu lectus et, porttitor malesuada augue. Sed feugiat volutpat velit, sollicitudin fringilla velit bibendum faucibus. """
|
||||
)
|
||||
|
||||
lorem_short_text = Text.from_markup(lorem_short)
|
||||
lorem_long_text = Text.from_markup(lorem * 2)
|
||||
|
||||
|
||||
class TweetHeader(Widget):
|
||||
def render(self) -> RenderableType:
|
||||
return Text("Lorem Impsum", justify="center")
|
||||
|
||||
|
||||
class TweetBody(Widget):
|
||||
short_lorem = Reactive(False)
|
||||
|
||||
def render(self) -> Text:
|
||||
return lorem_short_text if self.short_lorem else lorem_long_text
|
||||
|
||||
|
||||
class Tweet(Widget):
|
||||
pass
|
||||
|
||||
|
||||
class OptionItem(Widget):
|
||||
def render(self) -> Text:
|
||||
return Text("Option")
|
||||
|
||||
|
||||
class Error(Widget):
|
||||
def render(self) -> Text:
|
||||
return Text("This is an error message", justify="center")
|
||||
|
||||
|
||||
class Warning(Widget):
|
||||
def render(self) -> Text:
|
||||
return Text("This is a warning message", justify="center")
|
||||
|
||||
|
||||
class Success(Widget):
|
||||
def render(self) -> Text:
|
||||
return Text("This is a success message", justify="center")
|
||||
|
||||
|
||||
class BasicApp(App, css_path="basic.css"):
|
||||
"""A basic app demonstrating CSS"""
|
||||
|
||||
def on_load(self):
|
||||
"""Bind keys here."""
|
||||
self.bind("s", "toggle_class('#sidebar', '-active')")
|
||||
|
||||
def on_mount(self):
|
||||
"""Build layout here."""
|
||||
|
||||
table = DataTable()
|
||||
self.scroll_to_target = Tweet(TweetBody())
|
||||
self.mount(
|
||||
header=Static(
|
||||
Text.from_markup(
|
||||
"[b]This is a [u]Textual[/u] app, running in the terminal"
|
||||
),
|
||||
),
|
||||
content=Widget(
|
||||
Tweet(TweetBody()),
|
||||
Widget(
|
||||
Static(Syntax(CODE, "python"), classes="code"),
|
||||
classes="scrollable",
|
||||
),
|
||||
table,
|
||||
Error(),
|
||||
Tweet(TweetBody(), classes="scrollbar-size-custom"),
|
||||
Warning(),
|
||||
Tweet(TweetBody(), classes="scroll-horizontal"),
|
||||
Success(),
|
||||
Tweet(TweetBody(), classes="scroll-horizontal"),
|
||||
Tweet(TweetBody(), classes="scroll-horizontal"),
|
||||
Tweet(TweetBody(), classes="scroll-horizontal"),
|
||||
Tweet(TweetBody(), classes="scroll-horizontal"),
|
||||
Tweet(TweetBody(), classes="scroll-horizontal"),
|
||||
),
|
||||
footer=Widget(),
|
||||
sidebar=Widget(
|
||||
Widget(classes="title"),
|
||||
Widget(classes="user"),
|
||||
OptionItem(),
|
||||
OptionItem(),
|
||||
OptionItem(),
|
||||
Widget(classes="content"),
|
||||
),
|
||||
)
|
||||
table.add_column("Foo", width=20)
|
||||
table.add_column("Bar", width=20)
|
||||
table.add_column("Baz", width=20)
|
||||
table.add_column("Foo", width=20)
|
||||
table.add_column("Bar", width=20)
|
||||
table.add_column("Baz", width=20)
|
||||
table.zebra_stripes = True
|
||||
for n in range(100):
|
||||
table.add_row(*[f"Cell ([b]{n}[/b], {col})" for col in range(6)])
|
||||
|
||||
async def on_key(self, event) -> None:
|
||||
await self.dispatch_key(event)
|
||||
|
||||
def key_d(self):
|
||||
self.dark = not self.dark
|
||||
|
||||
async def key_q(self):
|
||||
await self.shutdown()
|
||||
|
||||
def key_x(self):
|
||||
self.panic(self.tree)
|
||||
|
||||
def key_escape(self):
|
||||
self.app.bell()
|
||||
|
||||
def key_t(self):
|
||||
# Pressing "t" toggles the content of the TweetBody widget, from a long "Lorem ipsum..." to a shorter one.
|
||||
tweet_body = self.query("TweetBody").first()
|
||||
tweet_body.short_lorem = not tweet_body.short_lorem
|
||||
|
||||
def key_v(self):
|
||||
self.get_child(id="content").scroll_to_widget(self.scroll_to_target)
|
||||
|
||||
def key_space(self):
|
||||
self.bell()
|
||||
|
||||
|
||||
app = BasicApp()
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
||||
|
||||
from textual.geometry import Region
|
||||
from textual.color import Color
|
||||
|
||||
print(Region.intersection.cache_info())
|
||||
print(Region.overlaps.cache_info())
|
||||
print(Region.union.cache_info())
|
||||
print(Region.split_vertical.cache_info())
|
||||
print(Region.__contains__.cache_info())
|
||||
from textual.css.scalar import Scalar
|
||||
|
||||
print(Scalar.resolve_dimension.cache_info())
|
||||
|
||||
from rich.style import Style
|
||||
from rich.cells import cached_cell_len
|
||||
|
||||
print(Style._add.cache_info())
|
||||
|
||||
print(cached_cell_len.cache_info())
|
||||
@@ -36,6 +36,8 @@ test_table.add_row("Dec 16, 2016", "Rogue One: A Star Wars Story", "$1,332,439,8
|
||||
class TableApp(App):
|
||||
def compose(self) -> ComposeResult:
|
||||
table = self.table = DataTable(id="data")
|
||||
yield table
|
||||
|
||||
table.add_column("Foo", width=20)
|
||||
table.add_column("Bar", width=60)
|
||||
table.add_column("Baz", width=20)
|
||||
@@ -47,14 +49,21 @@ class TableApp(App):
|
||||
height = 1
|
||||
row = [f"row [b]{n}[/b] col [i]{c}[/i]" for c in range(6)]
|
||||
if n == 10:
|
||||
row[1] = Syntax(CODE, "python", line_numbers=True, indent_guides=True)
|
||||
row[1] = Syntax(
|
||||
CODE,
|
||||
"python",
|
||||
theme="ansi_dark",
|
||||
line_numbers=True,
|
||||
indent_guides=True,
|
||||
)
|
||||
height = 13
|
||||
|
||||
if n == 30:
|
||||
row[1] = test_table
|
||||
height = 13
|
||||
table.add_row(*row, height=height)
|
||||
yield table
|
||||
|
||||
table.focus()
|
||||
|
||||
def on_mount(self):
|
||||
self.bind("d", "toggle_dark")
|
||||
@@ -186,6 +186,8 @@ class Border:
|
||||
if has_bottom and lines:
|
||||
lines.pop(-1)
|
||||
|
||||
# TODO: Divide is probably quite inefficient here,
|
||||
# It could be much faster for the specific case of one off the start end end
|
||||
divide = Segment.divide
|
||||
if has_left and has_right:
|
||||
for line in lines:
|
||||
|
||||
6
src/textual/_cells.py
Normal file
6
src/textual/_cells.py
Normal file
@@ -0,0 +1,6 @@
|
||||
__all__ = ["cell_len"]
|
||||
|
||||
try:
|
||||
from rich.cells import cached_cell_len as cell_len
|
||||
except ImportError:
|
||||
from rich.cells import cell_len
|
||||
@@ -19,6 +19,7 @@ import sys
|
||||
from typing import Callable, cast, Iterator, Iterable, NamedTuple, TYPE_CHECKING
|
||||
|
||||
import rich.repr
|
||||
|
||||
from rich.console import Console, ConsoleOptions, RenderResult, RenderableType
|
||||
from rich.control import Control
|
||||
from rich.segment import Segment
|
||||
@@ -27,9 +28,9 @@ from rich.style import Style
|
||||
from . import errors
|
||||
from .geometry import Region, Offset, Size
|
||||
|
||||
from ._cells import cell_len
|
||||
from ._profile import timer
|
||||
from ._loop import loop_last
|
||||
from ._segment_tools import line_crop
|
||||
from ._types import Lines
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
@@ -98,16 +99,21 @@ class ChopsUpdate:
|
||||
"""A renderable that applies updated spans to the screen."""
|
||||
|
||||
def __init__(
|
||||
self, chops: list[dict[int, list[Segment] | None]], crop: Region
|
||||
self,
|
||||
chops: list[dict[int, list[Segment] | None]],
|
||||
spans: list[tuple[int, int, int]],
|
||||
chop_ends: list[list[int]],
|
||||
) -> None:
|
||||
"""A renderable which updates chops (fragments of lines).
|
||||
|
||||
Args:
|
||||
chops (list[dict[int, list[Segment] | None]]): A mapping of offsets to list of segments, per line.
|
||||
crop (Region): Region to restrict update to.
|
||||
chop_ends (list[list[int]]): A list of the end offsets for each line
|
||||
"""
|
||||
self.chops = chops
|
||||
self.crop = crop
|
||||
self.spans = spans
|
||||
self.chop_ends = chop_ends
|
||||
|
||||
def __rich_console__(
|
||||
self, console: Console, options: ConsoleOptions
|
||||
@@ -115,20 +121,53 @@ class ChopsUpdate:
|
||||
move_to = Control.move_to
|
||||
new_line = Segment.line()
|
||||
chops = self.chops
|
||||
crop = self.crop
|
||||
last_y = crop.y_max - 1
|
||||
x1, x2 = crop.x_extents
|
||||
for y in crop.y_range:
|
||||
chop_ends = self.chop_ends
|
||||
last_y = self.spans[-1][0]
|
||||
|
||||
_cell_len = cell_len
|
||||
|
||||
for y, x1, x2 in self.spans:
|
||||
line = chops[y]
|
||||
for x, segments in line.items():
|
||||
if segments is not None and x2 > x >= x1:
|
||||
ends = chop_ends[y]
|
||||
for end, (x, segments) in zip(ends, line.items()):
|
||||
# TODO: crop to x extents
|
||||
if segments is None:
|
||||
continue
|
||||
|
||||
if x > x2 or end <= x1:
|
||||
continue
|
||||
|
||||
if x2 > x >= x1 and end <= x2:
|
||||
yield move_to(x, y)
|
||||
yield from segments
|
||||
continue
|
||||
|
||||
iter_segments = iter(segments)
|
||||
if x < x1:
|
||||
for segment in iter_segments:
|
||||
next_x = x + _cell_len(segment.text)
|
||||
if next_x > x1:
|
||||
yield move_to(x, y)
|
||||
yield segment
|
||||
break
|
||||
x = next_x
|
||||
else:
|
||||
yield move_to(x, y)
|
||||
if end <= x2:
|
||||
yield from iter_segments
|
||||
else:
|
||||
for segment in iter_segments:
|
||||
if x >= x2:
|
||||
break
|
||||
yield segment
|
||||
x += _cell_len(segment.text)
|
||||
|
||||
if y != last_y:
|
||||
yield new_line
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
yield None, self.crop
|
||||
return
|
||||
yield
|
||||
|
||||
|
||||
@rich.repr.auto(angular=True)
|
||||
@@ -159,14 +198,6 @@ class Compositor:
|
||||
# Regions that require an update
|
||||
self._dirty_regions: set[Region] = set()
|
||||
|
||||
def add_dirty_regions(self, regions: Iterable[Region]) -> None:
|
||||
"""Add dirty regions to be repainted next call to render.
|
||||
|
||||
Args:
|
||||
regions (Iterable[Region]): Regions that are "dirty" (changed since last render).
|
||||
"""
|
||||
self._dirty_regions.update(regions)
|
||||
|
||||
@classmethod
|
||||
def _regions_to_spans(
|
||||
cls, regions: Iterable[Region]
|
||||
@@ -328,7 +359,7 @@ class Compositor:
|
||||
sub_clip = clip.intersection(child_region)
|
||||
|
||||
# The region covered by children relative to parent widget
|
||||
total_region = child_region.reset_origin
|
||||
total_region = child_region.reset_offset
|
||||
|
||||
if widget.is_container:
|
||||
# Arrange the layout
|
||||
@@ -338,7 +369,7 @@ class Compositor:
|
||||
|
||||
# An offset added to all placements
|
||||
placement_offset = (
|
||||
container_region.origin + layout_offset - widget.scroll_offset
|
||||
container_region.offset + layout_offset - widget.scroll_offset
|
||||
)
|
||||
|
||||
# Add all the widgets
|
||||
@@ -358,7 +389,7 @@ class Compositor:
|
||||
container_size
|
||||
):
|
||||
map[chrome_widget] = MapGeometry(
|
||||
chrome_region + container_region.origin + layout_offset,
|
||||
chrome_region + container_region.offset + layout_offset,
|
||||
order,
|
||||
clip,
|
||||
container_size,
|
||||
@@ -414,7 +445,7 @@ class Compositor:
|
||||
def get_offset(self, widget: Widget) -> Offset:
|
||||
"""Get the offset of a widget."""
|
||||
try:
|
||||
return self.map[widget].region.origin
|
||||
return self.map[widget].region.offset
|
||||
except KeyError:
|
||||
raise errors.NoWidget("Widget is not in layout")
|
||||
|
||||
@@ -506,6 +537,7 @@ class Compositor:
|
||||
|
||||
# Sort the cuts for each line
|
||||
self._cuts = [sorted(set(line_cuts)) for line_cuts in cuts]
|
||||
|
||||
return self._cuts
|
||||
|
||||
def _get_renders(
|
||||
@@ -546,9 +578,8 @@ class Compositor:
|
||||
if not region:
|
||||
continue
|
||||
if region in clip:
|
||||
yield region, clip, widget.render_lines(
|
||||
Region(0, 0, region.width, region.height)
|
||||
)
|
||||
lines = widget.render_lines(Region(0, 0, region.width, region.height))
|
||||
yield region, clip, lines
|
||||
elif overlaps(clip, region):
|
||||
clipped_region = intersection(region, clip)
|
||||
if not clipped_region:
|
||||
@@ -615,7 +646,7 @@ class Compositor:
|
||||
)
|
||||
# A mapping of cut index to a list of segments for each line
|
||||
chops: list[dict[int, list[Segment] | None]]
|
||||
chops = [fromkeys(cut_set) for cut_set in cuts]
|
||||
chops = [fromkeys(cut_set[:-1]) for cut_set in cuts]
|
||||
|
||||
cut_segments: Iterable[list[Segment]]
|
||||
|
||||
@@ -631,12 +662,12 @@ class Compositor:
|
||||
continue
|
||||
|
||||
chops_line = chops[y]
|
||||
if all(chops_line):
|
||||
continue
|
||||
|
||||
first_cut, last_cut = render_region.x_extents
|
||||
final_cuts = [cut for cut in cuts[y] if (last_cut >= cut >= first_cut)]
|
||||
|
||||
cuts_line = cuts[y]
|
||||
final_cuts = [
|
||||
cut for cut in cuts_line if (last_cut >= cut >= first_cut)
|
||||
]
|
||||
if len(final_cuts) <= 2:
|
||||
# Two cuts, which means the entire line
|
||||
cut_segments = [line]
|
||||
@@ -654,7 +685,8 @@ class Compositor:
|
||||
render_lines = self._assemble_chops(chops)
|
||||
return LayoutUpdate(render_lines, screen_region)
|
||||
else:
|
||||
return ChopsUpdate(chops, crop)
|
||||
chop_ends = [cut_set[1:] for cut_set in cuts]
|
||||
return ChopsUpdate(chops, spans, chop_ends)
|
||||
|
||||
def __rich_console__(
|
||||
self, console: Console, options: ConsoleOptions
|
||||
@@ -674,7 +706,11 @@ class Compositor:
|
||||
add_region = regions.append
|
||||
for widget in self.regions.keys() & widgets:
|
||||
region, clip = self.regions[widget]
|
||||
update_region = region.intersection(clip)
|
||||
offset = region.offset
|
||||
intersection = clip.intersection
|
||||
for dirty_region in widget.get_dirty_regions():
|
||||
update_region = intersection(dirty_region.translate(offset))
|
||||
if update_region:
|
||||
add_region(update_region)
|
||||
self.add_dirty_regions(regions)
|
||||
|
||||
self._dirty_regions.update(regions)
|
||||
|
||||
@@ -85,5 +85,5 @@ class Layout(ABC):
|
||||
height = container.height
|
||||
else:
|
||||
placements, widgets = widget._arrange(Size(width, container.height))
|
||||
height = max(placement.region.y_max for placement in placements)
|
||||
height = max(placement.region.bottom for placement in placements)
|
||||
return height
|
||||
|
||||
@@ -6,10 +6,10 @@ from __future__ import annotations
|
||||
|
||||
from rich.segment import Segment
|
||||
|
||||
from ._cells import cell_len
|
||||
|
||||
def line_crop(
|
||||
segments: list[Segment], start: int, end: int, total: int
|
||||
) -> list[Segment]:
|
||||
|
||||
def line_crop(segments: list[Segment], start: int, end: int, total: int):
|
||||
"""Crops a list of segments between two cell offsets.
|
||||
|
||||
Args:
|
||||
@@ -23,13 +23,15 @@ def line_crop(
|
||||
# This is essentially a specialized version of Segment.divide
|
||||
# The following line has equivalent functionality (but a little slower)
|
||||
# return list(Segment.divide(segments, [start, end]))[1]
|
||||
|
||||
_cell_len = cell_len
|
||||
pos = 0
|
||||
output_segments: list[Segment] = []
|
||||
add_segment = output_segments.append
|
||||
iter_segments = iter(segments)
|
||||
segment: Segment | None = None
|
||||
for segment in iter_segments:
|
||||
end_pos = pos + segment.cell_length
|
||||
end_pos = pos + _cell_len(segment.text)
|
||||
if end_pos > start:
|
||||
segment = segment.split_cells(start - pos)[-1]
|
||||
break
|
||||
@@ -46,7 +48,7 @@ def line_crop(
|
||||
|
||||
pos = start
|
||||
while segment is not None:
|
||||
end_pos = pos + segment.cell_length
|
||||
end_pos = pos + _cell_len(segment.text)
|
||||
if end_pos < end:
|
||||
add_segment(segment)
|
||||
else:
|
||||
|
||||
@@ -60,6 +60,7 @@ from .layouts.dock import Dock
|
||||
from .message_pump import MessagePump
|
||||
from .reactive import Reactive
|
||||
from .renderables.blank import Blank
|
||||
from ._profile import timer
|
||||
|
||||
from .screen import Screen
|
||||
from .widget import Widget
|
||||
@@ -588,8 +589,21 @@ class App(Generic[ReturnType], DOMNode):
|
||||
self.check_idle()
|
||||
|
||||
def mount(self, *anon_widgets: Widget, **widgets: Widget) -> None:
|
||||
"""Mount widgets. Widgets specified as positional args, or keywords args. If supplied
|
||||
as keyword args they will be assigned an id of the key.
|
||||
|
||||
"""
|
||||
self.register(self.screen, *anon_widgets, **widgets)
|
||||
|
||||
def mount_all(self, widgets: Iterable[Widget]) -> None:
|
||||
"""Mount widgets from an iterable.
|
||||
|
||||
Args:
|
||||
widgets (Iterable[Widget]): An iterable of widgets.
|
||||
"""
|
||||
for widget in widgets:
|
||||
self.register(self.screen, widget)
|
||||
|
||||
def push_screen(self, screen: Screen | None = None) -> Screen:
|
||||
"""Push a new screen on the screen stack.
|
||||
|
||||
@@ -811,9 +825,9 @@ class App(Generic[ReturnType], DOMNode):
|
||||
self.set_timer(screenshot_timer, on_screenshot)
|
||||
|
||||
def on_mount(self) -> None:
|
||||
widgets = list(self.compose())
|
||||
widgets = self.compose()
|
||||
if widgets:
|
||||
self.mount(*widgets)
|
||||
self.mount_all(widgets)
|
||||
|
||||
async def on_idle(self) -> None:
|
||||
"""Perform actions when there are no messages in the queue."""
|
||||
@@ -911,6 +925,7 @@ class App(Generic[ReturnType], DOMNode):
|
||||
stylesheet.update(self.app, animate=animate)
|
||||
self.screen._refresh_layout(self.size, full=True)
|
||||
|
||||
@timer("_display")
|
||||
def _display(self, renderable: RenderableType | None) -> None:
|
||||
"""Display a renderable within a sync.
|
||||
|
||||
|
||||
@@ -428,7 +428,7 @@ class DOMNode(MessagePump):
|
||||
for node in self.walk_children():
|
||||
node._css_styles.reset()
|
||||
if isinstance(node, Widget):
|
||||
node.set_dirty()
|
||||
node._set_dirty()
|
||||
node._layout_required = True
|
||||
|
||||
def add_child(self, node: DOMNode) -> None:
|
||||
|
||||
@@ -8,6 +8,7 @@ from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from functools import lru_cache
|
||||
from operator import itemgetter, attrgetter
|
||||
from typing import Any, Collection, NamedTuple, Tuple, TypeVar, Union, cast
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
@@ -77,6 +78,10 @@ class Offset(NamedTuple):
|
||||
return Offset(int(x * other), int(y * other))
|
||||
return NotImplemented
|
||||
|
||||
def __neg__(self) -> Offset:
|
||||
x, y = self
|
||||
return Offset(-x, -y)
|
||||
|
||||
def blend(self, destination: Offset, factor: float) -> Offset:
|
||||
"""Blend (interpolate) to a new point.
|
||||
|
||||
@@ -89,7 +94,10 @@ class Offset(NamedTuple):
|
||||
"""
|
||||
x1, y1 = self
|
||||
x2, y2 = destination
|
||||
return Offset(int(x1 + (x2 - x1) * factor), int((y1 + (y2 - y1) * factor)))
|
||||
return Offset(
|
||||
int(x1 + (x2 - x1) * factor),
|
||||
int(y1 + (y2 - y1) * factor),
|
||||
)
|
||||
|
||||
def get_distance_to(self, other: Offset) -> float:
|
||||
"""Get the distance to another offset.
|
||||
@@ -191,7 +199,14 @@ class Region(NamedTuple):
|
||||
height: int = 0
|
||||
|
||||
@classmethod
|
||||
def from_union(cls, regions: Collection[Region]) -> Region:
|
||||
def from_union(
|
||||
cls,
|
||||
regions: Collection[Region],
|
||||
_get_x=itemgetter(0),
|
||||
_get_y=itemgetter(1),
|
||||
_get_right=attrgetter("right"),
|
||||
_get_bottom=attrgetter("bottom"),
|
||||
) -> Region:
|
||||
"""Create a Region from the union of other regions.
|
||||
|
||||
Args:
|
||||
@@ -202,10 +217,10 @@ class Region(NamedTuple):
|
||||
"""
|
||||
if not regions:
|
||||
raise ValueError("At least one region expected")
|
||||
min_x = min([region.x for region in regions])
|
||||
max_x = max([x + width for x, _y, width, _height in regions])
|
||||
min_y = min([region.y for region in regions])
|
||||
max_y = max([y + height for _x, y, _width, height in regions])
|
||||
min_x = min(regions, key=_get_x).x
|
||||
max_x = max(regions, key=_get_right).right
|
||||
min_y = min(regions, key=_get_y).y
|
||||
max_y = max(regions, key=_get_bottom).bottom
|
||||
return cls(min_x, min_y, max_x - min_x, max_y - min_y)
|
||||
|
||||
@classmethod
|
||||
@@ -224,20 +239,68 @@ class Region(NamedTuple):
|
||||
return cls(x1, y1, x2 - x1, y2 - y1)
|
||||
|
||||
@classmethod
|
||||
def from_origin(cls, origin: tuple[int, int], size: tuple[int, int]) -> Region:
|
||||
"""Create a region from origin and size.
|
||||
def from_offset(cls, offset: tuple[int, int], size: tuple[int, int]) -> Region:
|
||||
"""Create a region from offset and size.
|
||||
|
||||
Args:
|
||||
origin (Point): Origin (top left point)
|
||||
offset (Point): Offset (top left point)
|
||||
size (tuple[int, int]): Dimensions of region.
|
||||
|
||||
Returns:
|
||||
Region: A region instance.
|
||||
"""
|
||||
x, y = origin
|
||||
x, y = offset
|
||||
width, height = size
|
||||
return cls(x, y, width, height)
|
||||
|
||||
@classmethod
|
||||
def get_scroll_to_visible(cls, window_region: Region, region: Region) -> Offset:
|
||||
"""Calculate the smallest offset required to translate a window so that it contains
|
||||
another region.
|
||||
|
||||
This method is used to calculate the required offset to scroll something in to view.
|
||||
|
||||
Args:
|
||||
window_region (Region): The window region.
|
||||
region (Region): The region to move inside the window.
|
||||
|
||||
Returns:
|
||||
Offset: An offset required to add to region to move it inside window_region.
|
||||
"""
|
||||
|
||||
if region in window_region:
|
||||
# Region is already inside the window, so no need to move it.
|
||||
return Offset(0, 0)
|
||||
|
||||
window_left, window_top, window_right, window_bottom = window_region.corners
|
||||
region = region.crop_size(window_region.size)
|
||||
left, top, right, bottom = region.corners
|
||||
delta_x = delta_y = 0
|
||||
|
||||
if not (
|
||||
(window_right > left >= window_left)
|
||||
and (window_right > right >= window_left)
|
||||
):
|
||||
# The region does not fit
|
||||
# The window needs to scroll on the X axis to bring region in to view
|
||||
delta_x = min(
|
||||
left - window_left,
|
||||
left - (window_right - region.width),
|
||||
key=abs,
|
||||
)
|
||||
|
||||
if not (
|
||||
(window_bottom > top >= window_top)
|
||||
and (window_bottom > bottom >= window_top)
|
||||
):
|
||||
# The window needs to scroll on the Y axis to bring region in to view
|
||||
delta_y = min(
|
||||
top - window_top,
|
||||
top - (window_bottom - region.height),
|
||||
key=abs,
|
||||
)
|
||||
return Offset(delta_x, delta_y)
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
"""A Region is considered False when it has no area."""
|
||||
return bool(self.width and self.height)
|
||||
@@ -265,12 +328,12 @@ class Region(NamedTuple):
|
||||
return (self.y, self.y + self.height)
|
||||
|
||||
@property
|
||||
def x_max(self) -> int:
|
||||
def right(self) -> int:
|
||||
"""Maximum X value (non inclusive)"""
|
||||
return self.x + self.width
|
||||
|
||||
@property
|
||||
def y_max(self) -> int:
|
||||
def bottom(self) -> int:
|
||||
"""Maximum Y value (non inclusive)"""
|
||||
return self.y + self.height
|
||||
|
||||
@@ -280,7 +343,7 @@ class Region(NamedTuple):
|
||||
return self.width * self.height
|
||||
|
||||
@property
|
||||
def origin(self) -> Offset:
|
||||
def offset(self) -> Offset:
|
||||
"""Get the start point of the region."""
|
||||
return Offset(self.x, self.y)
|
||||
|
||||
@@ -309,10 +372,10 @@ class Region(NamedTuple):
|
||||
|
||||
@property
|
||||
def corners(self) -> tuple[int, int, int, int]:
|
||||
"""Get the maxima and minima of region.
|
||||
"""Get the top left and bottom right coordinates as a tuple of integers.
|
||||
|
||||
Returns:
|
||||
tuple[int, int, int, int]: A tuple of `(<min x>, <max x>, <min y>, <max y>)`
|
||||
tuple[int, int, int, int]: A tuple of `(<left>, <top>, <right>, <bottom>)`
|
||||
"""
|
||||
x, y, width, height = self
|
||||
return x, y, x + width, y + height
|
||||
@@ -328,7 +391,7 @@ class Region(NamedTuple):
|
||||
return range(self.y, self.y + self.height)
|
||||
|
||||
@property
|
||||
def reset_origin(self) -> Region:
|
||||
def reset_offset(self) -> Region:
|
||||
"""An region of the same size at (0, 0)."""
|
||||
_, _, width, height = self
|
||||
return Region(0, 0, width, height)
|
||||
@@ -347,6 +410,32 @@ class Region(NamedTuple):
|
||||
return Region(x - ox, y - oy, width, height)
|
||||
return NotImplemented
|
||||
|
||||
def at_offset(self, offset: tuple[int, int]) -> Region:
|
||||
"""Get a new Region with the same size at a given offset.
|
||||
|
||||
Args:
|
||||
offset (tuple[int, int]): An offset.
|
||||
|
||||
Returns:
|
||||
Region: New Region with adjusted offset.
|
||||
"""
|
||||
x, y = offset
|
||||
_x, _y, width, height = self
|
||||
return Region(x, y, width, height)
|
||||
|
||||
def crop_size(self, size: tuple[int, int]) -> Region:
|
||||
"""Get a region with the same offset, with a size no larger than `size`.
|
||||
|
||||
Args:
|
||||
size (tuple[int, int]): Maximum width and height (WIDTH, HEIGHT).
|
||||
|
||||
Returns:
|
||||
Region: New region that could fit within `size`.
|
||||
"""
|
||||
x, y, width1, height1 = self
|
||||
width2, height2 = size
|
||||
return Region(x, y, min(width1, width2), min(height1, height2))
|
||||
|
||||
def expand(self, size: tuple[int, int]) -> Region:
|
||||
"""Increase the size of the region by adding a border.
|
||||
|
||||
@@ -429,19 +518,19 @@ class Region(NamedTuple):
|
||||
and (y2 >= oy2 >= y1)
|
||||
)
|
||||
|
||||
def translate(self, x: int = 0, y: int = 0) -> Region:
|
||||
"""Move the origin of the Region.
|
||||
def translate(self, offset: tuple[int, int]) -> Region:
|
||||
"""Move the offset of the Region.
|
||||
|
||||
Args:
|
||||
translate_x (int): Value to add to x coordinate.
|
||||
translate_y (int): Value to add to y coordinate.
|
||||
translate (tuple[int, int]): Offset to add to region.
|
||||
|
||||
Returns:
|
||||
Region: A new region shifted by x, y
|
||||
Region: A new region shifted by (x, y)
|
||||
"""
|
||||
|
||||
self_x, self_y, width, height = self
|
||||
return Region(self_x + x, self_y + y, width, height)
|
||||
offset_x, offset_y = offset
|
||||
return Region(self_x + offset_x, self_y + offset_y, width, height)
|
||||
|
||||
@lru_cache(maxsize=4096)
|
||||
def __contains__(self, other: Any) -> bool:
|
||||
|
||||
@@ -253,10 +253,12 @@ class GridLayout(Layout):
|
||||
offset = (container - size) // 2
|
||||
return offset
|
||||
|
||||
offset_x = align(grid_size.width, container.width, col_align)
|
||||
offset_y = align(grid_size.height, container.height, row_align)
|
||||
offset = Offset(
|
||||
align(grid_size.width, container.width, col_align),
|
||||
align(grid_size.height, container.height, row_align),
|
||||
)
|
||||
|
||||
region = region.translate(offset_x, offset_y)
|
||||
region = region.translate(offset)
|
||||
return region
|
||||
|
||||
def get_widgets(self) -> Iterable[Widget]:
|
||||
|
||||
@@ -117,15 +117,14 @@ class Screen(Widget):
|
||||
self._refresh_layout()
|
||||
self._layout_required = False
|
||||
self._dirty_widgets.clear()
|
||||
elif self._dirty_widgets or self._dirty_regions:
|
||||
elif self._dirty_widgets:
|
||||
self.update_timer.resume()
|
||||
|
||||
def _on_update(self) -> None:
|
||||
"""Called by the _update_timer."""
|
||||
# Render widgets together
|
||||
if self._dirty_widgets or self._dirty_regions:
|
||||
if self._dirty_widgets:
|
||||
self._compositor.update_widgets(self._dirty_widgets)
|
||||
self._compositor.add_dirty_regions(self._dirty_regions)
|
||||
self.app._display(self._compositor.render())
|
||||
self._dirty_widgets.clear()
|
||||
self.update_timer.pause()
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Collection
|
||||
|
||||
from rich.console import RenderableType
|
||||
|
||||
|
||||
from .geometry import Size
|
||||
from .geometry import Region, Size
|
||||
from .widget import Widget
|
||||
|
||||
|
||||
@@ -38,6 +40,16 @@ class ScrollView(Widget):
|
||||
"""Not transparent, i.e. renders something."""
|
||||
return False
|
||||
|
||||
def get_dirty_regions(self) -> Collection[Region]:
|
||||
"""Get regions which require a repaint.
|
||||
|
||||
Returns:
|
||||
Collection[Region]: Regions to repaint.
|
||||
"""
|
||||
regions = self._dirty_regions.copy()
|
||||
self._dirty_regions.clear()
|
||||
return regions
|
||||
|
||||
def on_mount(self):
|
||||
self._refresh_scrollbars()
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ from typing import (
|
||||
Any,
|
||||
Awaitable,
|
||||
ClassVar,
|
||||
Collection,
|
||||
TYPE_CHECKING,
|
||||
Callable,
|
||||
Iterable,
|
||||
@@ -17,8 +18,6 @@ from rich.console import Console, RenderableType
|
||||
from rich.measure import Measurement
|
||||
from rich.padding import Padding
|
||||
from rich.style import Style
|
||||
from rich.styled import Styled
|
||||
|
||||
|
||||
from . import errors
|
||||
from . import events
|
||||
@@ -29,7 +28,7 @@ from ._context import active_app
|
||||
from ._types import Lines
|
||||
from .dom import DOMNode
|
||||
from ._layout import ArrangeResult
|
||||
from .geometry import clamp, Offset, Region, Size
|
||||
from .geometry import clamp, Offset, Region, Size, Spacing
|
||||
from .layouts.vertical import VerticalLayout
|
||||
from .message import Message
|
||||
from . import messages
|
||||
@@ -109,7 +108,7 @@ class Widget(DOMNode):
|
||||
self._horizontal_scrollbar: ScrollBar | None = None
|
||||
|
||||
self._render_cache = RenderCache(Size(0, 0), [])
|
||||
self._dirty_regions: list[Region] = []
|
||||
self._dirty_regions: set[Region] = set()
|
||||
|
||||
# Cache the auto content dimensions
|
||||
# TODO: add mechanism to explicitly clear this
|
||||
@@ -428,10 +427,33 @@ class Widget(DOMNode):
|
||||
else 0
|
||||
)
|
||||
|
||||
def set_dirty(self) -> None:
|
||||
"""Set the Widget as 'dirty' (requiring re-render)."""
|
||||
def _set_dirty(self, *regions: Region) -> None:
|
||||
"""Set the Widget as 'dirty' (requiring re-paint).
|
||||
|
||||
Regions should be specified as positional args. If no regions are added, then
|
||||
the entire widget will be considered dirty.
|
||||
|
||||
Args:
|
||||
*regions (Region): Regions which require a repaint.
|
||||
|
||||
"""
|
||||
|
||||
if regions:
|
||||
self._dirty_regions.update(regions)
|
||||
else:
|
||||
self._dirty_regions.clear()
|
||||
self._dirty_regions.append(self.size.region)
|
||||
# TODO: Does this need to be content region?
|
||||
# self._dirty_regions.append(self.size.region)
|
||||
self._dirty_regions.add(self.size.region)
|
||||
|
||||
def get_dirty_regions(self) -> Collection[Region]:
|
||||
"""Get regions which require a repaint.
|
||||
|
||||
Returns:
|
||||
Collection[Region]: Regions to repaint.
|
||||
"""
|
||||
regions = self._dirty_regions.copy()
|
||||
return regions
|
||||
|
||||
def scroll_to(
|
||||
self,
|
||||
@@ -576,6 +598,7 @@ class Widget(DOMNode):
|
||||
bool: True if any scrolling has occurred in any descendant, otherwise False.
|
||||
"""
|
||||
|
||||
# TODO: Update this to use scroll_to_region
|
||||
scrolls = set()
|
||||
|
||||
node = widget.parent
|
||||
@@ -595,9 +618,9 @@ class Widget(DOMNode):
|
||||
|
||||
# We can either scroll so the widget is at the top of the container, or so that
|
||||
# it is at the bottom. We want to pick which has the shortest distance
|
||||
top_delta = widget_region.origin - container_region.origin
|
||||
top_delta = widget_region.offset - container_region.origin
|
||||
|
||||
bottom_delta = widget_region.origin - (
|
||||
bottom_delta = widget_region.offset - (
|
||||
container_region.origin
|
||||
+ Offset(0, container_region.height - widget_region.height)
|
||||
)
|
||||
@@ -624,6 +647,36 @@ class Widget(DOMNode):
|
||||
|
||||
return any(scrolls)
|
||||
|
||||
def scroll_to_region(
|
||||
self, region: Region, *, spacing: Spacing | None = None, animate: bool = True
|
||||
) -> Offset:
|
||||
"""Scrolls a given region in to view, if required.
|
||||
|
||||
This method will scroll the least distance required to move `region` fully within
|
||||
the scrollable area.
|
||||
|
||||
Args:
|
||||
region (Region): A region that should be visible.
|
||||
animate (bool, optional): Enable animation. Defaults to True.
|
||||
spacing (Spacing): Space to subtract from the window region.
|
||||
|
||||
Returns:
|
||||
bool: True if the window was scrolled.
|
||||
"""
|
||||
|
||||
window = self.region.at_offset(self.scroll_offset)
|
||||
if spacing is not None:
|
||||
window = window.shrink(spacing)
|
||||
delta = Region.get_scroll_to_visible(window, region)
|
||||
if delta:
|
||||
self.scroll_relative(
|
||||
delta.x or None,
|
||||
delta.y or None,
|
||||
animate=animate,
|
||||
duration=0.2,
|
||||
)
|
||||
return delta
|
||||
|
||||
def __init_subclass__(
|
||||
cls,
|
||||
can_focus: bool = True,
|
||||
@@ -655,23 +708,23 @@ class Widget(DOMNode):
|
||||
"""
|
||||
show_vertical_scrollbar, show_horizontal_scrollbar = self.scrollbars_enabled
|
||||
|
||||
horizontal_scrollbar_thickness = self.scrollbar_size_horizontal
|
||||
vertical_scrollbar_thickness = self.scrollbar_size_vertical
|
||||
scrollbar_size_horizontal = self.styles.scrollbar_size_horizontal
|
||||
scrollbar_size_vertical = self.styles.scrollbar_size_vertical
|
||||
|
||||
if self.styles.scrollbar_gutter == "stable":
|
||||
# Let's _always_ reserve some space, whether the scrollbar is actually displayed or not:
|
||||
show_vertical_scrollbar = True
|
||||
vertical_scrollbar_thickness = self.styles.scrollbar_size_vertical
|
||||
scrollbar_size_vertical = self.styles.scrollbar_size_vertical
|
||||
|
||||
if show_horizontal_scrollbar and show_vertical_scrollbar:
|
||||
(region, _, _, _) = region.split(
|
||||
-vertical_scrollbar_thickness,
|
||||
-horizontal_scrollbar_thickness,
|
||||
-scrollbar_size_vertical,
|
||||
-scrollbar_size_horizontal,
|
||||
)
|
||||
elif show_vertical_scrollbar:
|
||||
region, _ = region.split_vertical(-vertical_scrollbar_thickness)
|
||||
region, _ = region.split_vertical(-scrollbar_size_vertical)
|
||||
elif show_horizontal_scrollbar:
|
||||
region, _ = region.split_horizontal(-horizontal_scrollbar_thickness)
|
||||
region, _ = region.split_horizontal(-scrollbar_size_horizontal)
|
||||
return region
|
||||
|
||||
def _arrange_scrollbars(self, size: Size) -> Iterable[tuple[Widget, Region]]:
|
||||
@@ -689,6 +742,7 @@ class Widget(DOMNode):
|
||||
|
||||
scrollbar_size_horizontal = self.scrollbar_size_horizontal
|
||||
scrollbar_size_vertical = self.scrollbar_size_vertical
|
||||
|
||||
if show_horizontal_scrollbar and show_vertical_scrollbar:
|
||||
(
|
||||
_,
|
||||
@@ -724,18 +778,18 @@ class Widget(DOMNode):
|
||||
def watch(self, attribute_name, callback: Callable[[Any], Awaitable[None]]) -> None:
|
||||
watch(self, attribute_name, callback)
|
||||
|
||||
def render_styled(self) -> RenderableType:
|
||||
"""Applies style attributes to the default renderable.
|
||||
def _style_renderable(self, renderable: RenderableType) -> RenderableType:
|
||||
"""Applies CSS styles to a renderable by wrapping it in another renderable.
|
||||
|
||||
Args:
|
||||
renderable (RenderableType): Renderable to apply styles to.
|
||||
|
||||
Returns:
|
||||
RenderableType: A new renderable.
|
||||
RenderableType: An updated renderable.
|
||||
"""
|
||||
|
||||
(base_background, base_color), (background, color) = self.colors
|
||||
styles = self.styles
|
||||
|
||||
renderable = self.render()
|
||||
|
||||
content_align = (styles.content_align_horizontal, styles.content_align_vertical)
|
||||
if content_align != ("left", "top"):
|
||||
horizontal, vertical = content_align
|
||||
@@ -771,6 +825,17 @@ class Widget(DOMNode):
|
||||
|
||||
return renderable
|
||||
|
||||
def render_styled(self) -> RenderableType:
|
||||
"""Applies style attributes to the default renderable.
|
||||
|
||||
Returns:
|
||||
RenderableType: A new renderable.
|
||||
"""
|
||||
|
||||
renderable = self.render()
|
||||
renderable = self._style_renderable(renderable)
|
||||
return renderable
|
||||
|
||||
@property
|
||||
def size(self) -> Size:
|
||||
return self._size
|
||||
@@ -798,6 +863,16 @@ class Widget(DOMNode):
|
||||
except errors.NoWidget:
|
||||
return Region()
|
||||
|
||||
@property
|
||||
def window_region(self) -> Region:
|
||||
"""The region within the scrollable area that is currently visible.
|
||||
|
||||
Returns:
|
||||
Region: New region.
|
||||
"""
|
||||
window_region = self.region.at_offset(self.scroll_offset)
|
||||
return window_region
|
||||
|
||||
@property
|
||||
def scroll_offset(self) -> Offset:
|
||||
return Offset(int(self.scroll_x), int(self.scroll_y))
|
||||
@@ -892,7 +967,8 @@ class Widget(DOMNode):
|
||||
def _crop_lines(self, lines: Lines, x1, x2) -> Lines:
|
||||
width = self.size.width
|
||||
if (x1, x2) != (0, width):
|
||||
lines = [line_crop(line, x1, x2, width) for line in lines]
|
||||
_line_crop = line_crop
|
||||
lines = [_line_crop(line, x1, x2, width) for line in lines]
|
||||
return lines
|
||||
|
||||
def render_lines(self, crop: Region) -> Lines:
|
||||
@@ -923,7 +999,9 @@ class Widget(DOMNode):
|
||||
event.set_forwarded()
|
||||
await self.post_message(event)
|
||||
|
||||
def refresh(self, *, repaint: bool = True, layout: bool = False) -> None:
|
||||
def refresh(
|
||||
self, *regions: Region, repaint: bool = True, layout: bool = False
|
||||
) -> None:
|
||||
"""Initiate a refresh of the widget.
|
||||
|
||||
This method sets an internal flag to perform a refresh, which will be done on the
|
||||
@@ -933,13 +1011,14 @@ class Widget(DOMNode):
|
||||
repaint (bool, optional): Repaint the widget (will call render() again). Defaults to True.
|
||||
layout (bool, optional): Also layout widgets in the view. Defaults to False.
|
||||
"""
|
||||
|
||||
if layout:
|
||||
self._layout_required = True
|
||||
self._clear_arrangement_cache()
|
||||
if repaint:
|
||||
self._set_dirty(*regions)
|
||||
self._content_width_cache = (None, 0)
|
||||
self._content_height_cache = (None, 0)
|
||||
self.set_dirty()
|
||||
self._repaint_required = True
|
||||
if isinstance(self.parent, Widget) and self.styles.auto_dimensions:
|
||||
self.parent.refresh(layout=True)
|
||||
|
||||
@@ -2,7 +2,8 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from itertools import chain
|
||||
from typing import Callable, ClassVar, Generic, TypeVar, cast
|
||||
import sys
|
||||
from typing import ClassVar, Generic, NamedTuple, TypeVar, cast
|
||||
|
||||
from rich.console import RenderableType
|
||||
from rich.padding import Padding
|
||||
@@ -11,29 +12,48 @@ from rich.segment import Segment
|
||||
from rich.style import Style
|
||||
from rich.text import Text, TextType
|
||||
|
||||
from .. import events
|
||||
from .._cache import LRUCache
|
||||
from .._segment_tools import line_crop
|
||||
from .._types import Lines
|
||||
from ..geometry import Region, Size
|
||||
from ..geometry import clamp, Region, Size, Spacing
|
||||
from ..reactive import Reactive
|
||||
from .._profile import timer
|
||||
from ..scroll_view import ScrollView
|
||||
from ..widget import Widget
|
||||
from .. import messages
|
||||
|
||||
|
||||
if sys.version_info >= (3, 8):
|
||||
from typing import Literal
|
||||
else:
|
||||
from typing_extensions import Literal
|
||||
|
||||
CursorType = Literal["cell", "row", "column"]
|
||||
CELL: CursorType = "cell"
|
||||
CellType = TypeVar("CellType")
|
||||
|
||||
|
||||
def default_cell_formatter(obj: object) -> RenderableType | None:
|
||||
"""Format a cell in to a renderable.
|
||||
|
||||
Args:
|
||||
obj (object): Data for a cell.
|
||||
|
||||
Returns:
|
||||
RenderableType | None: A renderable or None if the object could not be rendered.
|
||||
"""
|
||||
if isinstance(obj, str):
|
||||
return Text.from_markup(obj)
|
||||
if not is_renderable(obj):
|
||||
raise TypeError(f"Table cell {obj!r} is not renderable")
|
||||
return None
|
||||
return cast(RenderableType, obj)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Column:
|
||||
"""Table column."""
|
||||
|
||||
label: Text
|
||||
width: int
|
||||
visible: bool = False
|
||||
@@ -42,18 +62,46 @@ class Column:
|
||||
|
||||
@dataclass
|
||||
class Row:
|
||||
"""Table row."""
|
||||
|
||||
index: int
|
||||
height: int
|
||||
y: int
|
||||
cell_renderables: list[RenderableType] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Cell:
|
||||
"""Table cell."""
|
||||
|
||||
value: object
|
||||
|
||||
|
||||
class Header(Widget):
|
||||
pass
|
||||
class Coord(NamedTuple):
|
||||
"""An object to represent the cordinate of a cell within the data table."""
|
||||
|
||||
row: int
|
||||
column: int
|
||||
|
||||
def left(self) -> Coord:
|
||||
"""Get coordinate to the left."""
|
||||
row, column = self
|
||||
return Coord(row, column - 1)
|
||||
|
||||
def right(self) -> Coord:
|
||||
"""Get coordinate to the right."""
|
||||
row, column = self
|
||||
return Coord(row, column + 1)
|
||||
|
||||
def up(self) -> Coord:
|
||||
"""Get coordinate above."""
|
||||
row, column = self
|
||||
return Coord(row - 1, column)
|
||||
|
||||
def down(self) -> Coord:
|
||||
"""Get coordinate below."""
|
||||
row, column = self
|
||||
return Coord(row + 1, column)
|
||||
|
||||
|
||||
class DataTable(ScrollView, Generic[CellType]):
|
||||
@@ -82,13 +130,17 @@ class DataTable(ScrollView, Generic[CellType]):
|
||||
background: $primary 10%;
|
||||
}
|
||||
|
||||
DataTable > .datatable--cursor {
|
||||
background: $secondary;
|
||||
color: $text-secondary;
|
||||
}
|
||||
|
||||
.-dark-mode DataTable > .datatable--even-row {
|
||||
background: $primary 15%;
|
||||
}
|
||||
|
||||
DataTable > .datatable--highlight {
|
||||
background: $secondary;
|
||||
color: $text-secondary;
|
||||
background: $secondary 20%;
|
||||
}
|
||||
"""
|
||||
|
||||
@@ -98,6 +150,7 @@ class DataTable(ScrollView, Generic[CellType]):
|
||||
"datatable--odd-row",
|
||||
"datatable--even-row",
|
||||
"datatable--highlight",
|
||||
"datatable--cursor",
|
||||
}
|
||||
|
||||
def __init__(
|
||||
@@ -114,20 +167,47 @@ class DataTable(ScrollView, Generic[CellType]):
|
||||
|
||||
self._y_offsets: list[tuple[int, int]] = []
|
||||
|
||||
self._row_render_cache: LRUCache[tuple[int, int, Style], tuple[Lines, Lines]]
|
||||
self._row_render_cache: LRUCache[
|
||||
tuple[int, int, Style, int, int], tuple[Lines, Lines]
|
||||
]
|
||||
self._row_render_cache = LRUCache(1000)
|
||||
|
||||
self._cell_render_cache: LRUCache[tuple[int, int, Style], Lines]
|
||||
self._cell_render_cache: LRUCache[tuple[int, int, Style, bool, bool], Lines]
|
||||
self._cell_render_cache = LRUCache(10000)
|
||||
|
||||
self._line_cache: LRUCache[tuple[int, int, int, int], list[Segment]]
|
||||
self._line_cache: LRUCache[
|
||||
tuple[int, int, int, int, int, int, Style], list[Segment]
|
||||
]
|
||||
self._line_cache = LRUCache(1000)
|
||||
|
||||
self._line_no = 0
|
||||
|
||||
show_header = Reactive(True)
|
||||
fixed_rows = Reactive(0)
|
||||
fixed_columns = Reactive(1)
|
||||
fixed_columns = Reactive(0)
|
||||
zebra_stripes = Reactive(False)
|
||||
header_height = Reactive(1)
|
||||
show_cursor = Reactive(True)
|
||||
cursor_type = Reactive(CELL)
|
||||
|
||||
cursor_cell: Reactive[Coord] = Reactive(Coord(0, 0), repaint=False)
|
||||
hover_cell: Reactive[Coord] = Reactive(Coord(0, 0), repaint=False)
|
||||
|
||||
@property
|
||||
def hover_row(self) -> int:
|
||||
return self.hover_cell.row
|
||||
|
||||
@property
|
||||
def hover_column(self) -> int:
|
||||
return self.hover_cell.column
|
||||
|
||||
@property
|
||||
def cursor_row(self) -> int:
|
||||
return self.cursor_cell.row
|
||||
|
||||
@property
|
||||
def cursor_column(self) -> int:
|
||||
return self.cursor_cell.column
|
||||
|
||||
def _clear_caches(self) -> None:
|
||||
self._row_render_cache.clear()
|
||||
@@ -141,6 +221,7 @@ class DataTable(ScrollView, Generic[CellType]):
|
||||
|
||||
async def handle_styles_updated(self, message: messages.StylesUpdated) -> None:
|
||||
self._clear_caches()
|
||||
self.refresh()
|
||||
|
||||
def watch_show_header(self, show_header: bool) -> None:
|
||||
self._clear_caches()
|
||||
@@ -151,6 +232,20 @@ class DataTable(ScrollView, Generic[CellType]):
|
||||
def watch_zebra_stripes(self, zebra_stripes: bool) -> None:
|
||||
self._clear_caches()
|
||||
|
||||
def watch_hover_cell(self, old: Coord, value: Coord) -> None:
|
||||
self.refresh_cell(*old)
|
||||
self.refresh_cell(*value)
|
||||
|
||||
def watch_cursor_cell(self, old: Coord, value: Coord) -> None:
|
||||
self.refresh_cell(*old)
|
||||
self.refresh_cell(*value)
|
||||
|
||||
def validate_cursor_cell(self, value: Coord) -> Coord:
|
||||
row, column = value
|
||||
row = clamp(row, 0, self.row_count - 1)
|
||||
column = clamp(column, self.fixed_columns, len(self.columns) - 1)
|
||||
return Coord(row, column)
|
||||
|
||||
def _update_dimensions(self) -> None:
|
||||
"""Called to recalculate the virtual (scrollable) size."""
|
||||
total_width = sum(column.width for column in self.columns)
|
||||
@@ -159,6 +254,18 @@ class DataTable(ScrollView, Generic[CellType]):
|
||||
len(self._y_offsets) + (self.header_height if self.show_header else 0),
|
||||
)
|
||||
|
||||
def _get_cell_region(self, row_index: int, column_index: int) -> Region:
|
||||
if row_index not in self.rows:
|
||||
return Region(0, 0, 0, 0)
|
||||
row = self.rows[row_index]
|
||||
x = sum(column.width for column in self.columns[:column_index])
|
||||
width = self.columns[column_index].width
|
||||
height = row.height
|
||||
y = row.y
|
||||
if self.show_header:
|
||||
y += self.header_height
|
||||
return Region(x, y, width, height)
|
||||
|
||||
def add_column(self, label: TextType, *, width: int = 10) -> None:
|
||||
"""Add a column to the table.
|
||||
|
||||
@@ -179,15 +286,25 @@ class DataTable(ScrollView, Generic[CellType]):
|
||||
"""
|
||||
row_index = self.row_count
|
||||
self.data[row_index] = list(cells)
|
||||
self.rows[row_index] = Row(row_index, height=height)
|
||||
self.rows[row_index] = Row(row_index, height, self._line_no)
|
||||
|
||||
for line_no in range(height):
|
||||
self._y_offsets.append((row_index, line_no))
|
||||
|
||||
self.row_count += 1
|
||||
self._line_no += height
|
||||
self._update_dimensions()
|
||||
self.refresh()
|
||||
|
||||
def refresh_cell(self, row_index: int, column_index: int) -> None:
|
||||
if row_index < 0 or column_index < 0:
|
||||
return
|
||||
region = self._get_cell_region(row_index, column_index)
|
||||
if not self.window_region.overlaps(region):
|
||||
return
|
||||
region = region.translate(-self.scroll_offset)
|
||||
self.refresh(region)
|
||||
|
||||
def _get_row_renderables(self, row_index: int) -> list[RenderableType]:
|
||||
"""Get renderables for the given row.
|
||||
|
||||
@@ -210,7 +327,13 @@ class DataTable(ScrollView, Generic[CellType]):
|
||||
return [default_cell_formatter(datum) or empty for datum in data]
|
||||
|
||||
def _render_cell(
|
||||
self, row_index: int, column_index: int, style: Style, width: int
|
||||
self,
|
||||
row_index: int,
|
||||
column_index: int,
|
||||
style: Style,
|
||||
width: int,
|
||||
cursor: bool = False,
|
||||
hover: bool = False,
|
||||
) -> Lines:
|
||||
"""Render the given cell.
|
||||
|
||||
@@ -223,7 +346,11 @@ class DataTable(ScrollView, Generic[CellType]):
|
||||
Returns:
|
||||
Lines: A list of segments per line.
|
||||
"""
|
||||
cell_key = (row_index, column_index, style)
|
||||
if hover:
|
||||
style += self.component_styles["datatable--highlight"].node.rich_style
|
||||
if cursor:
|
||||
style += self.component_styles["datatable--cursor"].node.rich_style
|
||||
cell_key = (row_index, column_index, style, cursor, hover)
|
||||
if cell_key not in self._cell_render_cache:
|
||||
style += Style.from_meta({"row": row_index, "column": column_index})
|
||||
height = (
|
||||
@@ -239,7 +366,12 @@ class DataTable(ScrollView, Generic[CellType]):
|
||||
return self._cell_render_cache[cell_key]
|
||||
|
||||
def _render_row(
|
||||
self, row_index: int, line_no: int, base_style: Style
|
||||
self,
|
||||
row_index: int,
|
||||
line_no: int,
|
||||
base_style: Style,
|
||||
cursor_column: int = -1,
|
||||
hover_column: int = -1,
|
||||
) -> tuple[Lines, Lines]:
|
||||
"""Render a row in to lines for each cell.
|
||||
|
||||
@@ -252,7 +384,7 @@ class DataTable(ScrollView, Generic[CellType]):
|
||||
tuple[Lines, Lines]: Lines for fixed cells, and Lines for scrollable cells.
|
||||
"""
|
||||
|
||||
cache_key = (row_index, line_no, base_style)
|
||||
cache_key = (row_index, line_no, base_style, cursor_column, hover_column)
|
||||
|
||||
if cache_key in self._row_render_cache:
|
||||
return self._row_render_cache[cache_key]
|
||||
@@ -281,7 +413,14 @@ class DataTable(ScrollView, Generic[CellType]):
|
||||
row_style = base_style
|
||||
|
||||
scrollable_row = [
|
||||
render_cell(row_index, column.index, row_style, column.width)[line_no]
|
||||
render_cell(
|
||||
row_index,
|
||||
column.index,
|
||||
row_style,
|
||||
column.width,
|
||||
cursor=cursor_column == column.index,
|
||||
hover=hover_column == column.index,
|
||||
)[line_no]
|
||||
for column in self.columns
|
||||
]
|
||||
|
||||
@@ -302,6 +441,8 @@ class DataTable(ScrollView, Generic[CellType]):
|
||||
if y < self.header_height:
|
||||
return (-1, y)
|
||||
y -= self.header_height
|
||||
if y > len(self._y_offsets):
|
||||
raise LookupError("Y coord {y!r} is greater than total height")
|
||||
return self._y_offsets[y]
|
||||
|
||||
def _render_line(
|
||||
@@ -319,34 +460,42 @@ class DataTable(ScrollView, Generic[CellType]):
|
||||
list[Segment]: List of segments for rendering.
|
||||
"""
|
||||
|
||||
width = self.content_region.width
|
||||
width = self.region.width
|
||||
|
||||
cache_key = (y, x1, x2, width)
|
||||
try:
|
||||
row_index, line_no = self._get_offsets(y)
|
||||
except LookupError:
|
||||
return [Segment(" " * width, base_style)]
|
||||
cursor_column = (
|
||||
self.cursor_column
|
||||
if (self.show_cursor and self.cursor_row == row_index)
|
||||
else -1
|
||||
)
|
||||
hover_column = self.hover_column if (self.hover_row == row_index) else -1
|
||||
|
||||
cache_key = (y, x1, x2, width, cursor_column, hover_column, base_style)
|
||||
if cache_key in self._line_cache:
|
||||
return self._line_cache[cache_key]
|
||||
|
||||
row_index, line_no = self._get_offsets(y)
|
||||
|
||||
fixed, scrollable = self._render_row(row_index, line_no, base_style)
|
||||
fixed, scrollable = self._render_row(
|
||||
row_index,
|
||||
line_no,
|
||||
base_style,
|
||||
cursor_column=cursor_column,
|
||||
hover_column=hover_column,
|
||||
)
|
||||
fixed_width = sum(column.width for column in self.columns[: self.fixed_columns])
|
||||
|
||||
fixed_line: list[Segment] = list(chain.from_iterable(fixed)) if fixed else []
|
||||
scrollable_line: list[Segment] = list(chain.from_iterable(scrollable))
|
||||
|
||||
segments = fixed_line + line_crop(scrollable_line, x1 + fixed_width, x2, width)
|
||||
|
||||
remaining_width = width - (fixed_width + min(width, (x2 - x1 + fixed_width)))
|
||||
if remaining_width > 0:
|
||||
segments.append(Segment(" " * remaining_width, base_style))
|
||||
elif remaining_width < 0:
|
||||
segments = Segment.adjust_line_length(segments, width, style=base_style)
|
||||
|
||||
simplified_segments = list(Segment.simplify(segments))
|
||||
|
||||
self._line_cache[cache_key] = simplified_segments
|
||||
return segments
|
||||
|
||||
@timer("render_lines")
|
||||
def render_lines(self, crop: Region) -> Lines:
|
||||
"""Render lines within a given region.
|
||||
|
||||
@@ -356,9 +505,8 @@ class DataTable(ScrollView, Generic[CellType]):
|
||||
Returns:
|
||||
Lines: A list of segments for every line within crop region.
|
||||
"""
|
||||
|
||||
scroll_x, scroll_y = self.scroll_offset
|
||||
x1, y1, x2, y2 = crop.translate(scroll_x, scroll_y).corners
|
||||
scroll_y = self.scroll_offset.y
|
||||
x1, y1, x2, y2 = crop.translate(self.scroll_offset).corners
|
||||
|
||||
base_style = self.rich_style
|
||||
|
||||
@@ -380,5 +528,58 @@ class DataTable(ScrollView, Generic[CellType]):
|
||||
|
||||
return lines
|
||||
|
||||
def on_mouse_move(self, event):
|
||||
print(self.get_style_at(event.x, event.y).meta)
|
||||
def on_mouse_move(self, event: events.MouseMove):
|
||||
meta = event.style.meta
|
||||
if meta:
|
||||
try:
|
||||
self.hover_cell = Coord(meta["row"], meta["column"])
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
async def on_key(self, event) -> None:
|
||||
await self.dispatch_key(event)
|
||||
|
||||
def _get_cell_border(self) -> Spacing:
|
||||
top = self.header_height if self.show_header else 0
|
||||
top += sum(
|
||||
self.rows[row_index].height
|
||||
for row_index in range(self.fixed_rows)
|
||||
if row_index in self.rows
|
||||
)
|
||||
left = sum(column.width for column in self.columns[: self.fixed_columns])
|
||||
return Spacing(top, 0, 0, left)
|
||||
|
||||
def _scroll_cursor_in_to_view(self, animate: bool = False) -> None:
|
||||
region = self._get_cell_region(self.cursor_row, self.cursor_column)
|
||||
spacing = self._get_cell_border()
|
||||
self.scroll_to_region(region, animate=animate, spacing=spacing)
|
||||
|
||||
def on_click(self, event: events.Click) -> None:
|
||||
meta = self.get_style_at(event.x, event.y).meta
|
||||
if meta:
|
||||
self.cursor_cell = Coord(meta["row"], meta["column"])
|
||||
self._scroll_cursor_in_to_view()
|
||||
|
||||
def key_down(self, event: events.Key):
|
||||
self.cursor_cell = self.cursor_cell.down()
|
||||
event.stop()
|
||||
event.prevent_default()
|
||||
self._scroll_cursor_in_to_view()
|
||||
|
||||
def key_up(self, event: events.Key):
|
||||
self.cursor_cell = self.cursor_cell.up()
|
||||
event.stop()
|
||||
event.prevent_default()
|
||||
self._scroll_cursor_in_to_view()
|
||||
|
||||
def key_right(self, event: events.Key):
|
||||
self.cursor_cell = self.cursor_cell.right()
|
||||
event.stop()
|
||||
event.prevent_default()
|
||||
self._scroll_cursor_in_to_view(animate=True)
|
||||
|
||||
def key_left(self, event: events.Key):
|
||||
self.cursor_cell = self.cursor_cell.left()
|
||||
event.stop()
|
||||
event.prevent_default()
|
||||
self._scroll_cursor_in_to_view(animate=True)
|
||||
|
||||
@@ -89,6 +89,11 @@ def test_offset_sub():
|
||||
Offset(1, 1) - "foo"
|
||||
|
||||
|
||||
def test_offset_neg():
|
||||
assert Offset(0, 0) == Offset(0, 0)
|
||||
assert -Offset(2, -3) == Offset(-2, 3)
|
||||
|
||||
|
||||
def test_offset_mul():
|
||||
assert Offset(2, 1) * 2 == Offset(4, 2)
|
||||
assert Offset(2, 1) * -2 == Offset(-4, -2)
|
||||
@@ -125,8 +130,21 @@ def test_region_from_union():
|
||||
assert Region.from_union(regions) == Region(10, 20, 40, 40)
|
||||
|
||||
|
||||
def test_region_from_origin():
|
||||
assert Region.from_origin(Offset(3, 4), (5, 6)) == Region(3, 4, 5, 6)
|
||||
def test_region_from_offset():
|
||||
assert Region.from_offset(Offset(3, 4), (5, 6)) == Region(3, 4, 5, 6)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"window,region,scroll",
|
||||
[
|
||||
(Region(0, 0, 200, 100), Region(0, 0, 200, 100), Offset(0, 0)),
|
||||
(Region(0, 0, 200, 100), Region(0, -100, 10, 10), Offset(0, -100)),
|
||||
(Region(10, 15, 20, 10), Region(0, 0, 50, 50), Offset(-10, -15)),
|
||||
],
|
||||
)
|
||||
def test_get_scroll_to_visible(window, region, scroll):
|
||||
assert Region.get_scroll_to_visible(window, region) == scroll
|
||||
assert region.overlaps(window + scroll)
|
||||
|
||||
|
||||
def test_region_area():
|
||||
@@ -140,7 +158,7 @@ def test_region_size():
|
||||
|
||||
|
||||
def test_region_origin():
|
||||
assert Region(1, 2, 3, 4).origin == Offset(1, 2)
|
||||
assert Region(1, 2, 3, 4).offset == Offset(1, 2)
|
||||
|
||||
|
||||
def test_region_bottom_left():
|
||||
@@ -167,6 +185,16 @@ def test_region_sub():
|
||||
Region(1, 2, 3, 4) - "foo"
|
||||
|
||||
|
||||
def test_region_at_offset():
|
||||
assert Region(10, 10, 30, 40).at_offset((0, 0)) == Region(0, 0, 30, 40)
|
||||
assert Region(10, 10, 30, 40).at_offset((-15, 30)) == Region(-15, 30, 30, 40)
|
||||
|
||||
|
||||
def test_crop_size():
|
||||
assert Region(10, 20, 100, 200).crop_size((50, 40)) == Region(10, 20, 50, 40)
|
||||
assert Region(10, 20, 100, 200).crop_size((500, 40)) == Region(10, 20, 100, 40)
|
||||
|
||||
|
||||
def test_region_overlaps():
|
||||
assert Region(10, 10, 30, 20).overlaps(Region(0, 0, 20, 20))
|
||||
assert not Region(10, 10, 5, 5).overlaps(Region(15, 15, 20, 20))
|
||||
@@ -201,8 +229,8 @@ def test_region_contains_region():
|
||||
|
||||
|
||||
def test_region_translate():
|
||||
assert Region(1, 2, 3, 4).translate(10, 20) == Region(11, 22, 3, 4)
|
||||
assert Region(1, 2, 3, 4).translate(y=20) == Region(1, 22, 3, 4)
|
||||
assert Region(1, 2, 3, 4).translate((10, 20)) == Region(11, 22, 3, 4)
|
||||
assert Region(1, 2, 3, 4).translate((0, 20)) == Region(1, 22, 3, 4)
|
||||
|
||||
|
||||
def test_region_contains_special():
|
||||
@@ -259,11 +287,11 @@ def test_region_y_extents():
|
||||
|
||||
|
||||
def test_region_x_max():
|
||||
assert Region(5, 10, 20, 30).x_max == 25
|
||||
assert Region(5, 10, 20, 30).right == 25
|
||||
|
||||
|
||||
def test_region_y_max():
|
||||
assert Region(5, 10, 20, 30).y_max == 40
|
||||
assert Region(5, 10, 20, 30).bottom == 40
|
||||
|
||||
|
||||
def test_region_x_range():
|
||||
@@ -274,8 +302,8 @@ def test_region_y_range():
|
||||
assert Region(5, 10, 20, 30).y_range == range(10, 40)
|
||||
|
||||
|
||||
def test_region_reset_origin():
|
||||
assert Region(5, 10, 20, 30).reset_origin == Region(0, 0, 20, 30)
|
||||
def test_region_reset_offset():
|
||||
assert Region(5, 10, 20, 30).reset_offset == Region(0, 0, 20, 30)
|
||||
|
||||
|
||||
def test_region_expand():
|
||||
|
||||
Reference in New Issue
Block a user