mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Merge pull request #254 from Textualize/inline-styles-view
Inline styles view
This commit is contained in:
@@ -3,15 +3,11 @@
|
||||
$primary: #20639b;
|
||||
|
||||
App > View {
|
||||
layout: dock;
|
||||
docks: side=left/1;
|
||||
text: on $primary;
|
||||
}
|
||||
|
||||
Widget:hover {
|
||||
outline: heavy;
|
||||
text: bold !important;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
text: #09312e on #3caea3;
|
||||
dock: side;
|
||||
@@ -31,10 +27,6 @@ Widget:hover {
|
||||
border: hkey;
|
||||
}
|
||||
|
||||
#header.-visible {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#content {
|
||||
text: white on $primary;
|
||||
border-bottom: hkey #0f2b41;
|
||||
|
||||
@@ -8,7 +8,6 @@ class BasicApp(App):
|
||||
def on_load(self):
|
||||
"""Bind keys here."""
|
||||
self.bind("tab", "toggle_class('#sidebar', '-active')")
|
||||
self.bind("a", "toggle_class('#header', '-visible')")
|
||||
|
||||
def on_mount(self):
|
||||
"""Build layout here."""
|
||||
|
||||
154
poetry.lock
generated
154
poetry.lock
generated
@@ -33,29 +33,25 @@ tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>
|
||||
|
||||
[[package]]
|
||||
name = "black"
|
||||
version = "21.12b0"
|
||||
version = "22.1.0"
|
||||
description = "The uncompromising code formatter."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6.2"
|
||||
|
||||
[package.dependencies]
|
||||
click = ">=7.1.2"
|
||||
click = ">=8.0.0"
|
||||
mypy-extensions = ">=0.4.3"
|
||||
pathspec = ">=0.9.0,<1"
|
||||
pathspec = ">=0.9.0"
|
||||
platformdirs = ">=2"
|
||||
tomli = ">=0.2.6,<2.0.0"
|
||||
tomli = ">=1.1.0"
|
||||
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\""},
|
||||
{version = "!=3.10.0.1", markers = "python_version >= \"3.10\""},
|
||||
]
|
||||
typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}
|
||||
|
||||
[package.extras]
|
||||
colorama = ["colorama (>=0.4.3)"]
|
||||
d = ["aiohttp (>=3.7.4)"]
|
||||
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
|
||||
python2 = ["typed-ast (>=1.4.3)"]
|
||||
uvloop = ["uvloop (>=0.15.2)"]
|
||||
|
||||
[[package]]
|
||||
@@ -107,7 +103,7 @@ test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"]
|
||||
|
||||
[[package]]
|
||||
name = "coverage"
|
||||
version = "6.3"
|
||||
version = "6.3.1"
|
||||
description = "Code coverage measurement for Python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@@ -152,7 +148,7 @@ dev = ["twine", "markdown", "flake8", "wheel"]
|
||||
|
||||
[[package]]
|
||||
name = "identify"
|
||||
version = "2.4.6"
|
||||
version = "2.4.8"
|
||||
description = "File identification library for Python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@@ -526,7 +522,7 @@ pyyaml = "*"
|
||||
|
||||
[[package]]
|
||||
name = "rich"
|
||||
version = "11.1.0"
|
||||
version = "11.2.0"
|
||||
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
|
||||
category = "main"
|
||||
optional = false
|
||||
@@ -559,11 +555,11 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "1.2.3"
|
||||
version = "2.0.1"
|
||||
description = "A lil' TOML parser"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "typed-ast"
|
||||
@@ -583,7 +579,7 @@ python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "virtualenv"
|
||||
version = "20.13.0"
|
||||
version = "20.13.1"
|
||||
description = "Virtual Python Environment builder"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@@ -626,7 +622,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.7"
|
||||
content-hash = "cdb4f091bb4090e971acb3f64c73ee65d099ea7cbf76455ac6e7a99cf6552190"
|
||||
content-hash = "24761b9c22233b2d5c6f31dd03c25d1ae9ba744dbc46e92d702c184d75072334"
|
||||
|
||||
[metadata.files]
|
||||
astunparse = [
|
||||
@@ -642,8 +638,29 @@ attrs = [
|
||||
{file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"},
|
||||
]
|
||||
black = [
|
||||
{file = "black-21.12b0-py3-none-any.whl", hash = "sha256:a615e69ae185e08fdd73e4715e260e2479c861b5740057fde6e8b4e3b7dd589f"},
|
||||
{file = "black-21.12b0.tar.gz", hash = "sha256:77b80f693a569e2e527958459634f18df9b0ba2625ba4e0c2d5da5be42e6f2b3"},
|
||||
{file = "black-22.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1297c63b9e1b96a3d0da2d85d11cd9bf8664251fd69ddac068b98dc4f34f73b6"},
|
||||
{file = "black-22.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2ff96450d3ad9ea499fc4c60e425a1439c2120cbbc1ab959ff20f7c76ec7e866"},
|
||||
{file = "black-22.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e21e1f1efa65a50e3960edd068b6ae6d64ad6235bd8bfea116a03b21836af71"},
|
||||
{file = "black-22.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2f69158a7d120fd641d1fa9a921d898e20d52e44a74a6fbbcc570a62a6bc8ab"},
|
||||
{file = "black-22.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:228b5ae2c8e3d6227e4bde5920d2fc66cc3400fde7bcc74f480cb07ef0b570d5"},
|
||||
{file = "black-22.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b1a5ed73ab4c482208d20434f700d514f66ffe2840f63a6252ecc43a9bc77e8a"},
|
||||
{file = "black-22.1.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35944b7100af4a985abfcaa860b06af15590deb1f392f06c8683b4381e8eeaf0"},
|
||||
{file = "black-22.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:7835fee5238fc0a0baf6c9268fb816b5f5cd9b8793423a75e8cd663c48d073ba"},
|
||||
{file = "black-22.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dae63f2dbf82882fa3b2a3c49c32bffe144970a573cd68d247af6560fc493ae1"},
|
||||
{file = "black-22.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fa1db02410b1924b6749c245ab38d30621564e658297484952f3d8a39fce7e8"},
|
||||
{file = "black-22.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c8226f50b8c34a14608b848dc23a46e5d08397d009446353dad45e04af0c8e28"},
|
||||
{file = "black-22.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2d6f331c02f0f40aa51a22e479c8209d37fcd520c77721c034517d44eecf5912"},
|
||||
{file = "black-22.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:742ce9af3086e5bd07e58c8feb09dbb2b047b7f566eb5f5bc63fd455814979f3"},
|
||||
{file = "black-22.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fdb8754b453fb15fad3f72cd9cad3e16776f0964d67cf30ebcbf10327a3777a3"},
|
||||
{file = "black-22.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5660feab44c2e3cb24b2419b998846cbb01c23c7fe645fee45087efa3da2d61"},
|
||||
{file = "black-22.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:6f2f01381f91c1efb1451998bd65a129b3ed6f64f79663a55fe0e9b74a5f81fd"},
|
||||
{file = "black-22.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:efbadd9b52c060a8fc3b9658744091cb33c31f830b3f074422ed27bad2b18e8f"},
|
||||
{file = "black-22.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8871fcb4b447206904932b54b567923e5be802b9b19b744fdff092bd2f3118d0"},
|
||||
{file = "black-22.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccad888050f5393f0d6029deea2a33e5ae371fd182a697313bdbd835d3edaf9c"},
|
||||
{file = "black-22.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07e5c049442d7ca1a2fc273c79d1aecbbf1bc858f62e8184abe1ad175c4f7cc2"},
|
||||
{file = "black-22.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:373922fc66676133ddc3e754e4509196a8c392fec3f5ca4486673e685a421321"},
|
||||
{file = "black-22.1.0-py3-none-any.whl", hash = "sha256:3524739d76b6b3ed1132422bf9d82123cd1705086723bc3e235ca39fd21c667d"},
|
||||
{file = "black-22.1.0.tar.gz", hash = "sha256:a7c0192d35635f6fc1174be575cb7915e92e5dd629ee79fdaf0dcfa41a80afb5"},
|
||||
]
|
||||
cached-property = [
|
||||
{file = "cached-property-1.5.2.tar.gz", hash = "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130"},
|
||||
@@ -666,50 +683,47 @@ commonmark = [
|
||||
{file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"},
|
||||
]
|
||||
coverage = [
|
||||
{file = "coverage-6.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e8071e7d9ba9f457fc674afc3de054450be2c9b195c470147fbbc082468d8ff7"},
|
||||
{file = "coverage-6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86c91c511853dfda81c2cf2360502cb72783f4b7cebabef27869f00cbe1db07d"},
|
||||
{file = "coverage-6.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c4ce3b647bd1792d4394f5690d9df6dc035b00bcdbc5595099c01282a59ae01"},
|
||||
{file = "coverage-6.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a491e159294d756e7fc8462f98175e2d2225e4dbe062cca7d3e0d5a75ba6260"},
|
||||
{file = "coverage-6.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d008e0f67ac800b0ca04d7914b8501312c8c6c00ad8c7ba17754609fae1231a"},
|
||||
{file = "coverage-6.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4578728c36de2801c1deb1c6b760d31883e62e33f33c7ba8f982e609dc95167d"},
|
||||
{file = "coverage-6.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7ee317486593193e066fc5e98ac0ce712178c21529a85c07b7cb978171f25d53"},
|
||||
{file = "coverage-6.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2bc85664b06ba42d14bb74d6ddf19d8bfc520cb660561d2d9ce5786ae72f71b5"},
|
||||
{file = "coverage-6.3-cp310-cp310-win32.whl", hash = "sha256:27a94db5dc098c25048b0aca155f5fac674f2cf1b1736c5272ba28ead2fc267e"},
|
||||
{file = "coverage-6.3-cp310-cp310-win_amd64.whl", hash = "sha256:bde4aeabc0d1b2e52c4036c54440b1ad05beeca8113f47aceb4998bb7471e2c2"},
|
||||
{file = "coverage-6.3-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:509c68c3e2015022aeda03b003dd68fa19987cdcf64e9d4edc98db41cfc45d30"},
|
||||
{file = "coverage-6.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e4ff163602c5c77e7bb4ea81ba5d3b793b4419f8acd296aae149370902cf4e92"},
|
||||
{file = "coverage-6.3-cp311-cp311-win_amd64.whl", hash = "sha256:d1675db48490e5fa0b300f6329ecb8a9a37c29b9ab64fa9c964d34111788ca2d"},
|
||||
{file = "coverage-6.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7eed8459a2b81848cafb3280b39d7d49950d5f98e403677941c752e7e7ee47cb"},
|
||||
{file = "coverage-6.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b4285fde5286b946835a1a53bba3ad41ef74285ba9e8013e14b5ea93deaeafc"},
|
||||
{file = "coverage-6.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4748349734110fd32d46ff8897b561e6300d8989a494ad5a0a2e4f0ca974fc7"},
|
||||
{file = "coverage-6.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:823f9325283dc9565ba0aa2d240471a93ca8999861779b2b6c7aded45b58ee0f"},
|
||||
{file = "coverage-6.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:fff16a30fdf57b214778eff86391301c4509e327a65b877862f7c929f10a4253"},
|
||||
{file = "coverage-6.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:da1a428bdbe71f9a8c270c7baab29e9552ac9d0e0cba5e7e9a4c9ee6465d258d"},
|
||||
{file = "coverage-6.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7d82c610a2e10372e128023c5baf9ce3d270f3029fe7274ff5bc2897c68f1318"},
|
||||
{file = "coverage-6.3-cp37-cp37m-win32.whl", hash = "sha256:11e61c5548ecf74ea1f8b059730b049871f0e32b74f88bd0d670c20c819ad749"},
|
||||
{file = "coverage-6.3-cp37-cp37m-win_amd64.whl", hash = "sha256:8e0c3525b1a182c8ffc9bca7e56b521e0c2b8b3e82f033c8e16d6d721f1b54d6"},
|
||||
{file = "coverage-6.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a189036c50dcd56100746139a459f0d27540fef95b09aba03e786540b8feaa5f"},
|
||||
{file = "coverage-6.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:32168001f33025fd756884d56d01adebb34e6c8c0b3395ca8584cdcee9c7c9d2"},
|
||||
{file = "coverage-6.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5d79c9af3f410a2b5acad91258b4ae179ee9c83897eb9de69151b179b0227f5"},
|
||||
{file = "coverage-6.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:85c5fc9029043cf8b07f73fbb0a7ab6d3b717510c3b5642b77058ea55d7cacde"},
|
||||
{file = "coverage-6.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7596aa2f2b8fa5604129cfc9a27ad9beec0a96f18078cb424d029fdd707468d"},
|
||||
{file = "coverage-6.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ce443a3e6df90d692c38762f108fc4c88314bf477689f04de76b3f252e7a351c"},
|
||||
{file = "coverage-6.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:012157499ec4f135fc36cd2177e3d1a1840af9b236cbe80e9a5ccfc83d912a69"},
|
||||
{file = "coverage-6.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0a34d313105cdd0d3644c56df2d743fe467270d6ab93b5d4a347eb9fec8924d6"},
|
||||
{file = "coverage-6.3-cp38-cp38-win32.whl", hash = "sha256:6e78b1e25e5c5695dea012be473e442f7094d066925604be20b30713dbd47f89"},
|
||||
{file = "coverage-6.3-cp38-cp38-win_amd64.whl", hash = "sha256:433b99f7b0613bdcdc0b00cc3d39ed6d756797e3b078d2c43f8a38288520aec6"},
|
||||
{file = "coverage-6.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9ed3244b415725f08ca3bdf02ed681089fd95e9465099a21c8e2d9c5d6ca2606"},
|
||||
{file = "coverage-6.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab4fc4b866b279740e0d917402f0e9a08683e002f43fa408e9655818ed392196"},
|
||||
{file = "coverage-6.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8582e9280f8d0f38114fe95a92ae8d0790b56b099d728cc4f8a2e14b1c4a18c"},
|
||||
{file = "coverage-6.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c72bb4679283c6737f452eeb9b2a0e570acaef2197ad255fb20162adc80bea76"},
|
||||
{file = "coverage-6.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca29c352389ea27a24c79acd117abdd8a865c6eb01576b6f0990cd9a4e9c9f48"},
|
||||
{file = "coverage-6.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:152cc2624381df4e4e604e21bd8e95eb8059535f7b768c1fb8b8ae0b26f47ab0"},
|
||||
{file = "coverage-6.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:51372e24b1f7143ee2df6b45cff6a721f3abe93b1e506196f3ffa4155c2497f7"},
|
||||
{file = "coverage-6.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:72d9d186508325a456475dd05b1756f9a204c7086b07fffb227ef8cee03b1dc2"},
|
||||
{file = "coverage-6.3-cp39-cp39-win32.whl", hash = "sha256:649df3641eb351cdfd0d5533c92fc9df507b6b2bf48a7ef8c71ab63cbc7b5c3c"},
|
||||
{file = "coverage-6.3-cp39-cp39-win_amd64.whl", hash = "sha256:e67ccd53da5958ea1ec833a160b96357f90859c220a00150de011b787c27b98d"},
|
||||
{file = "coverage-6.3-pp36.pp37.pp38-none-any.whl", hash = "sha256:27ac7cb84538e278e07569ceaaa6f807a029dc194b1c819a9820b9bb5dbf63ab"},
|
||||
{file = "coverage-6.3.tar.gz", hash = "sha256:987a84ff98a309994ca77ed3cc4b92424f824278e48e4bf7d1bb79a63cfe2099"},
|
||||
{file = "coverage-6.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeffd96882d8c06d31b65dddcf51db7c612547babc1c4c5db6a011abe9798525"},
|
||||
{file = "coverage-6.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:621f6ea7260ea2ffdaec64fe5cb521669984f567b66f62f81445221d4754df4c"},
|
||||
{file = "coverage-6.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84f2436d6742c01136dd940ee158bfc7cf5ced3da7e4c949662b8703b5cd8145"},
|
||||
{file = "coverage-6.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de73fca6fb403dd72d4da517cfc49fcf791f74eee697d3219f6be29adf5af6ce"},
|
||||
{file = "coverage-6.3.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78fbb2be068a13a5d99dce9e1e7d168db880870f7bc73f876152130575bd6167"},
|
||||
{file = "coverage-6.3.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f5a4551dfd09c3bd12fca8144d47fe7745275adf3229b7223c2f9e29a975ebda"},
|
||||
{file = "coverage-6.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7bff3a98f63b47464480de1b5bdd80c8fade0ba2832c9381253c9b74c4153c27"},
|
||||
{file = "coverage-6.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a06c358f4aed05fa1099c39decc8022261bb07dfadc127c08cfbd1391b09689e"},
|
||||
{file = "coverage-6.3.1-cp310-cp310-win32.whl", hash = "sha256:9fff3ff052922cb99f9e52f63f985d4f7a54f6b94287463bc66b7cdf3eb41217"},
|
||||
{file = "coverage-6.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:276b13cc085474e482566c477c25ed66a097b44c6e77132f3304ac0b039f83eb"},
|
||||
{file = "coverage-6.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:56c4a409381ddd7bbff134e9756077860d4e8a583d310a6f38a2315b9ce301d0"},
|
||||
{file = "coverage-6.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9eb494070aa060ceba6e4bbf44c1bc5fa97bfb883a0d9b0c9049415f9e944793"},
|
||||
{file = "coverage-6.3.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e15d424b8153756b7c903bde6d4610be0c3daca3986173c18dd5c1a1625e4cd"},
|
||||
{file = "coverage-6.3.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61d47a897c1e91f33f177c21de897267b38fbb45f2cd8e22a710bcef1df09ac1"},
|
||||
{file = "coverage-6.3.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:25e73d4c81efa8ea3785274a2f7f3bfbbeccb6fcba2a0bdd3be9223371c37554"},
|
||||
{file = "coverage-6.3.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:fac0bcc5b7e8169bffa87f0dcc24435446d329cbc2b5486d155c2e0f3b493ae1"},
|
||||
{file = "coverage-6.3.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:72128176fea72012063200b7b395ed8a57849282b207321124d7ff14e26988e8"},
|
||||
{file = "coverage-6.3.1-cp37-cp37m-win32.whl", hash = "sha256:1bc6d709939ff262fd1432f03f080c5042dc6508b6e0d3d20e61dd045456a1a0"},
|
||||
{file = "coverage-6.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:618eeba986cea7f621d8607ee378ecc8c2504b98b3fdc4952b30fe3578304687"},
|
||||
{file = "coverage-6.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d5ed164af5c9078596cfc40b078c3b337911190d3faeac830c3f1274f26b8320"},
|
||||
{file = "coverage-6.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:352c68e233409c31048a3725c446a9e48bbff36e39db92774d4f2380d630d8f8"},
|
||||
{file = "coverage-6.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:448d7bde7ceb6c69e08474c2ddbc5b4cd13c9e4aa4a717467f716b5fc938a734"},
|
||||
{file = "coverage-6.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9fde6b90889522c220dd56a670102ceef24955d994ff7af2cb786b4ba8fe11e4"},
|
||||
{file = "coverage-6.3.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e647a0be741edbb529a72644e999acb09f2ad60465f80757da183528941ff975"},
|
||||
{file = "coverage-6.3.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a5cdc3adb4f8bb8d8f5e64c2e9e282bc12980ef055ec6da59db562ee9bdfefa"},
|
||||
{file = "coverage-6.3.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2dd70a167843b4b4b2630c0c56f1b586fe965b4f8ac5da05b6690344fd065c6b"},
|
||||
{file = "coverage-6.3.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9ad0a117b8dc2061ce9461ea4c1b4799e55edceb236522c5b8f958ce9ed8fa9a"},
|
||||
{file = "coverage-6.3.1-cp38-cp38-win32.whl", hash = "sha256:e92c7a5f7d62edff50f60a045dc9542bf939758c95b2fcd686175dd10ce0ed10"},
|
||||
{file = "coverage-6.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:482fb42eea6164894ff82abbcf33d526362de5d1a7ed25af7ecbdddd28fc124f"},
|
||||
{file = "coverage-6.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c5b81fb37db76ebea79aa963b76d96ff854e7662921ce742293463635a87a78d"},
|
||||
{file = "coverage-6.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a4f923b9ab265136e57cc14794a15b9dcea07a9c578609cd5dbbfff28a0d15e6"},
|
||||
{file = "coverage-6.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56d296cbc8254a7dffdd7bcc2eb70be5a233aae7c01856d2d936f5ac4e8ac1f1"},
|
||||
{file = "coverage-6.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1245ab82e8554fa88c4b2ab1e098ae051faac5af829efdcf2ce6b34dccd5567c"},
|
||||
{file = "coverage-6.3.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f2b05757c92ad96b33dbf8e8ec8d4ccb9af6ae3c9e9bd141c7cc44d20c6bcba"},
|
||||
{file = "coverage-6.3.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9e3dd806f34de38d4c01416344e98eab2437ac450b3ae39c62a0ede2f8b5e4ed"},
|
||||
{file = "coverage-6.3.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d651fde74a4d3122e5562705824507e2f5b2d3d57557f1916c4b27635f8fbe3f"},
|
||||
{file = "coverage-6.3.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:704f89b87c4f4737da2860695a18c852b78ec7279b24eedacab10b29067d3a38"},
|
||||
{file = "coverage-6.3.1-cp39-cp39-win32.whl", hash = "sha256:2aed4761809640f02e44e16b8b32c1a5dee5e80ea30a0ff0912158bde9c501f2"},
|
||||
{file = "coverage-6.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:9976fb0a5709988778ac9bc44f3d50fccd989987876dfd7716dee28beed0a9fa"},
|
||||
{file = "coverage-6.3.1-pp36.pp37.pp38-none-any.whl", hash = "sha256:463e52616ea687fd323888e86bf25e864a3cc6335a043fad6bbb037dbf49bbe2"},
|
||||
{file = "coverage-6.3.1.tar.gz", hash = "sha256:6c3f6158b02ac403868eea390930ae64e9a9a2a5bbfafefbb920d29258d9f2f8"},
|
||||
]
|
||||
distlib = [
|
||||
{file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"},
|
||||
@@ -724,8 +738,8 @@ ghp-import = [
|
||||
{file = "ghp_import-2.0.2-py3-none-any.whl", hash = "sha256:5f8962b30b20652cdffa9c5a9812f7de6bcb56ec475acac579807719bf242c46"},
|
||||
]
|
||||
identify = [
|
||||
{file = "identify-2.4.6-py2.py3-none-any.whl", hash = "sha256:cf06b1639e0dca0c184b1504d8b73448c99a68e004a80524c7923b95f7b6837c"},
|
||||
{file = "identify-2.4.6.tar.gz", hash = "sha256:233679e3f61a02015d4293dbccf16aa0e4996f868bd114688b8c124f18826706"},
|
||||
{file = "identify-2.4.8-py2.py3-none-any.whl", hash = "sha256:a55bdd671b6063eb837af938c250ec00bba6e610454265133b0d2db7ae718d0f"},
|
||||
{file = "identify-2.4.8.tar.gz", hash = "sha256:97e839c1779f07011b84c92af183e1883d9745d532d83412cca1ca76d3808c1c"},
|
||||
]
|
||||
importlib-metadata = [
|
||||
{file = "importlib_metadata-4.10.1-py3-none-any.whl", hash = "sha256:899e2a40a8c4a1aec681feef45733de8a6c58f3f6a0dbed2eb6574b4387a77b6"},
|
||||
@@ -928,8 +942,8 @@ pyyaml-env-tag = [
|
||||
{file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"},
|
||||
]
|
||||
rich = [
|
||||
{file = "rich-11.1.0-py3-none-any.whl", hash = "sha256:365ebcdbfb3aa8d4b0ed2490e0fbf7b886a39d14eb7ea5fb7aece950835e1eed"},
|
||||
{file = "rich-11.1.0.tar.gz", hash = "sha256:43e03d8eec12e21beaecc22c828a41c4247356414a12d5879834863d4ad53816"},
|
||||
{file = "rich-11.2.0-py3-none-any.whl", hash = "sha256:d5f49ad91fb343efcae45a2b2df04a9755e863e50413623ab8c9e74f05aee52b"},
|
||||
{file = "rich-11.2.0.tar.gz", hash = "sha256:1a6266a5738115017bb64a66c59c717e7aa047b3ae49a011ede4abdeffc6536e"},
|
||||
]
|
||||
six = [
|
||||
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
|
||||
@@ -940,8 +954,8 @@ toml = [
|
||||
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
|
||||
]
|
||||
tomli = [
|
||||
{file = "tomli-1.2.3-py3-none-any.whl", hash = "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"},
|
||||
{file = "tomli-1.2.3.tar.gz", hash = "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f"},
|
||||
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
|
||||
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
|
||||
]
|
||||
typed-ast = [
|
||||
{file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"},
|
||||
@@ -981,8 +995,8 @@ typing-extensions = [
|
||||
{file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"},
|
||||
]
|
||||
virtualenv = [
|
||||
{file = "virtualenv-20.13.0-py2.py3-none-any.whl", hash = "sha256:339f16c4a86b44240ba7223d0f93a7887c3ca04b5f9c8129da7958447d079b09"},
|
||||
{file = "virtualenv-20.13.0.tar.gz", hash = "sha256:d8458cf8d59d0ea495ad9b34c2599487f8a7772d796f9910858376d1600dd2dd"},
|
||||
{file = "virtualenv-20.13.1-py2.py3-none-any.whl", hash = "sha256:45e1d053cad4cd453181ae877c4ffc053546ae99e7dd049b9ff1d9be7491abf7"},
|
||||
{file = "virtualenv-20.13.1.tar.gz", hash = "sha256:e0621bcbf4160e4e1030f05065c8834b4e93f4fcc223255db2a823440aca9c14"},
|
||||
]
|
||||
watchdog = [
|
||||
{file = "watchdog-2.1.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9693f35162dc6208d10b10ddf0458cc09ad70c30ba689d9206e02cd836ce28a3"},
|
||||
|
||||
@@ -19,14 +19,14 @@ classifiers = [
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.7"
|
||||
rich = "^11.0.0"
|
||||
rich = "^11.2.0"
|
||||
|
||||
#rich = {git = "git@github.com:willmcgugan/rich", rev = "link-id"}
|
||||
typing-extensions = { version = "^3.10.0", python = "<3.8" }
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
pytest = "^6.2.3"
|
||||
black = "^21.11b1"
|
||||
black = "^22.1.0"
|
||||
mypy = "^0.910"
|
||||
pytest-cov = "^2.12.1"
|
||||
mkdocs = "^1.2.1"
|
||||
|
||||
3
sandbox/README.md
Normal file
3
sandbox/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Dev Sandbox
|
||||
|
||||
This directory contains test code. None of the .py files here are guaranteed to run or do anything useful, but you are welcome to look around.
|
||||
7
sandbox/local_styles.css
Normal file
7
sandbox/local_styles.css
Normal file
@@ -0,0 +1,7 @@
|
||||
App > View {
|
||||
layout: dock;
|
||||
}
|
||||
|
||||
Widget {
|
||||
text: on blue;
|
||||
}
|
||||
29
sandbox/local_styles.py
Normal file
29
sandbox/local_styles.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from textual.app import App
|
||||
from textual import events
|
||||
from textual.widgets import Placeholder
|
||||
from textual.widget import Widget
|
||||
|
||||
|
||||
class BasicApp(App):
|
||||
"""Sandbox application used for testing/development by Textual developers"""
|
||||
|
||||
def on_mount(self):
|
||||
"""Build layout here."""
|
||||
self.mount(
|
||||
header=Widget(),
|
||||
content=Placeholder(),
|
||||
footer=Widget(),
|
||||
sidebar=Widget(),
|
||||
)
|
||||
|
||||
async def on_key(self, event: events.Key) -> None:
|
||||
await self.dispatch_key(event)
|
||||
|
||||
def key_a(self) -> None:
|
||||
self.query("#footer").set_styles(text="on magenta")
|
||||
|
||||
def key_b(self) -> None:
|
||||
self["#footer"].set_styles("text: on green")
|
||||
|
||||
|
||||
BasicApp.run(css_file="local_styles.css", log="textual.log")
|
||||
@@ -127,12 +127,19 @@ class Animator:
|
||||
)
|
||||
|
||||
async def start(self) -> None:
|
||||
"""Start the animator task."""
|
||||
|
||||
self._timer.start()
|
||||
|
||||
async def stop(self) -> None:
|
||||
await self._timer.stop()
|
||||
"""Stop the animator task."""
|
||||
try:
|
||||
await self._timer.stop()
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
def bind(self, obj: object) -> BoundAnimator:
|
||||
"""Bind the animator to a given objects."""
|
||||
return BoundAnimator(self, obj)
|
||||
|
||||
def animate(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from rich.console import Console, ConsoleOptions, RenderResult, RenderableType
|
||||
import rich.repr
|
||||
from rich.segment import Segment, SegmentLines
|
||||
from rich.style import Style, StyleType
|
||||
|
||||
@@ -22,6 +23,7 @@ BORDER_STYLES: dict[str, tuple[str, str, str]] = {
|
||||
}
|
||||
|
||||
|
||||
@rich.repr.auto
|
||||
class Border:
|
||||
def __init__(
|
||||
self,
|
||||
@@ -36,13 +38,20 @@ class Border:
|
||||
self.style = style
|
||||
|
||||
(
|
||||
(top, top_style),
|
||||
(right, right_style),
|
||||
(bottom, bottom_style),
|
||||
(left, left_style),
|
||||
(top, top_color),
|
||||
(right, right_color),
|
||||
(bottom, bottom_color),
|
||||
(left, left_color),
|
||||
) = edge_styles
|
||||
self._sides = (top or "none", right or "none", bottom or "none", left or "none")
|
||||
self._styles = (top_style, right_style, bottom_style, left_style)
|
||||
from_color = Style.from_color
|
||||
|
||||
self._styles = (
|
||||
from_color(top_color),
|
||||
from_color(right_color),
|
||||
from_color(bottom_color),
|
||||
from_color(left_color),
|
||||
)
|
||||
|
||||
def _crop_renderable(self, lines: list[list[Segment]], width: int) -> None:
|
||||
"""Crops a renderable in place.
|
||||
@@ -165,16 +174,17 @@ class Border:
|
||||
|
||||
if __name__ == "__main__":
|
||||
from rich import print
|
||||
from rich.color import Color
|
||||
from rich.text import Text
|
||||
|
||||
text = Text("Textual " * 40, style="dim")
|
||||
border = Border(
|
||||
text,
|
||||
(
|
||||
("outer", Style.parse("green")),
|
||||
("outer", Style.parse("green")),
|
||||
("outer", Style.parse("green")),
|
||||
("outer", Style.parse("green")),
|
||||
("outer", Color.parse("green")),
|
||||
("outer", Color.parse("green")),
|
||||
("outer", Color.parse("green")),
|
||||
("outer", Color.parse("green")),
|
||||
),
|
||||
)
|
||||
print(text)
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal
|
||||
import sys
|
||||
|
||||
if sys.version_info >= (3, 8):
|
||||
from typing import Literal
|
||||
else:
|
||||
from typing_extensions import Literal
|
||||
|
||||
from rich.color import Color
|
||||
from rich.console import Console, ConsoleOptions, RenderResult, RenderableType
|
||||
from rich.segment import Segment
|
||||
from rich.style import Style
|
||||
|
||||
BOX_STYLES: dict[str, tuple[str, str, str]] = {
|
||||
"": (" ", " ", " "),
|
||||
@@ -25,11 +32,22 @@ class Box:
|
||||
renderable: RenderableType,
|
||||
*,
|
||||
sides: tuple[str, str, str, str],
|
||||
styles: tuple[str, str, str, str],
|
||||
colors: tuple[Color, Color, Color, Color],
|
||||
):
|
||||
self.renderable = renderable
|
||||
self.sides = sides
|
||||
self.styles = styles
|
||||
self.colors = colors
|
||||
|
||||
@property
|
||||
def styles(self) -> tuple[Style, Style, Style, Style]:
|
||||
color1, color2, color3, color4 = self.colors
|
||||
from_color = Style.from_color
|
||||
return (
|
||||
from_color(color1),
|
||||
from_color(color2),
|
||||
from_color(color3),
|
||||
from_color(color4),
|
||||
)
|
||||
|
||||
def __rich_console__(
|
||||
self, console: "Console", options: "ConsoleOptions"
|
||||
|
||||
@@ -5,8 +5,9 @@ import os
|
||||
import platform
|
||||
import warnings
|
||||
from asyncio import AbstractEventLoop
|
||||
from typing import Any, Callable, Iterable, Type, TypeVar
|
||||
from typing import Any, Callable, Iterable, Type, TypeVar, TYPE_CHECKING
|
||||
|
||||
import rich
|
||||
import rich.repr
|
||||
from rich.console import Console, RenderableType
|
||||
from rich.control import Control
|
||||
@@ -35,6 +36,11 @@ from .reactive import Reactive
|
||||
from .view import View
|
||||
from .widget import Widget
|
||||
|
||||
from .css.query import EmptyQueryError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .css.query import DOMQuery
|
||||
|
||||
PLATFORM = platform.system()
|
||||
WINDOWS = PLATFORM == "Windows"
|
||||
|
||||
@@ -260,6 +266,27 @@ class App(DOMNode):
|
||||
self.stylesheet.update(self)
|
||||
self.view.refresh(layout=True)
|
||||
|
||||
def query(self, selector: str | None = None) -> DOMQuery:
|
||||
"""Get a DOM query in the current view.
|
||||
|
||||
Args:
|
||||
selector (str, optional): A CSS selector or `None` for all nodes. Defaults to None.
|
||||
|
||||
Returns:
|
||||
DOMQuery: A query object.
|
||||
"""
|
||||
from .css.query import DOMQuery
|
||||
|
||||
return DOMQuery(self.view, selector)
|
||||
|
||||
def __getitem__(self, selector: str) -> DOMNode:
|
||||
from .css.query import DOMQuery
|
||||
|
||||
try:
|
||||
return DOMQuery(self.view, selector).first()
|
||||
except EmptyQueryError:
|
||||
raise KeyError(selector)
|
||||
|
||||
def update_styles(self) -> None:
|
||||
"""Request update of styles.
|
||||
|
||||
@@ -340,8 +367,14 @@ class App(DOMNode):
|
||||
"""
|
||||
|
||||
if not renderables:
|
||||
|
||||
renderables = (
|
||||
Traceback(show_locals=True, width=None, locals_max_length=5),
|
||||
Traceback(
|
||||
show_locals=True,
|
||||
width=None,
|
||||
locals_max_length=5,
|
||||
suppress=[rich],
|
||||
),
|
||||
)
|
||||
self._exit_renderables.extend(renderables)
|
||||
self.close_messages_no_wait()
|
||||
@@ -469,7 +502,9 @@ class App(DOMNode):
|
||||
try:
|
||||
if sync_available:
|
||||
console.file.write("\x1bP=1s\x1b\\")
|
||||
console.print(Screen(Control.home(), self.view, Control.home()))
|
||||
console.print(
|
||||
Screen(Control.home(), self.view.render_styled(), Control.home())
|
||||
)
|
||||
if sync_available:
|
||||
console.file.write("\x1bP=2s\x1b\\")
|
||||
console.file.flush()
|
||||
@@ -512,6 +547,10 @@ class App(DOMNode):
|
||||
"""
|
||||
return self.view.get_widget_at(x, y)
|
||||
|
||||
def bell(self) -> None:
|
||||
"""Play the console 'bell'."""
|
||||
self.console.bell()
|
||||
|
||||
async def press(self, key: str) -> bool:
|
||||
"""Handle a key press.
|
||||
|
||||
@@ -639,7 +678,7 @@ class App(DOMNode):
|
||||
1 / 0
|
||||
|
||||
async def action_bell(self) -> None:
|
||||
self.console.bell()
|
||||
self.bell()
|
||||
|
||||
async def action_add_class_(self, selector: str, class_name: str) -> None:
|
||||
self.view.query(selector).add_class(class_name)
|
||||
|
||||
@@ -9,7 +9,8 @@ when setting and getting.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Iterable, NamedTuple, Sequence, TYPE_CHECKING
|
||||
|
||||
from typing import Iterable, NamedTuple, TYPE_CHECKING
|
||||
|
||||
import rich.repr
|
||||
from rich.color import Color
|
||||
@@ -33,8 +34,12 @@ if TYPE_CHECKING:
|
||||
from ..layout import Layout
|
||||
from .styles import Styles
|
||||
from .styles import DockGroup
|
||||
from .._box import BoxType
|
||||
from ..layouts.factory import LayoutName
|
||||
|
||||
from .._box import BoxType
|
||||
|
||||
BorderDefinition = (
|
||||
"Sequence[tuple[BoxType, str | Color] | None] | tuple[BoxType, str | Color]"
|
||||
)
|
||||
|
||||
|
||||
class ScalarProperty:
|
||||
@@ -49,7 +54,6 @@ class ScalarProperty:
|
||||
|
||||
def __set_name__(self, owner: Styles, name: str) -> None:
|
||||
self.name = name
|
||||
self.internal_name = f"_rule_{name}"
|
||||
|
||||
def __get__(
|
||||
self, obj: Styles, objtype: type[Styles] | None = None
|
||||
@@ -63,7 +67,7 @@ class ScalarProperty:
|
||||
Returns:
|
||||
The Scalar object or ``None`` if it's not set.
|
||||
"""
|
||||
value = getattr(obj, self.internal_name)
|
||||
value = obj.get_rule(self.name)
|
||||
return value
|
||||
|
||||
def __set__(self, obj: Styles, value: float | Scalar | str | None) -> None:
|
||||
@@ -83,8 +87,9 @@ class ScalarProperty:
|
||||
cannot be parsed for any other reason.
|
||||
"""
|
||||
if value is None:
|
||||
new_value = None
|
||||
elif isinstance(value, float):
|
||||
obj.clear_rule(self.name)
|
||||
return
|
||||
if isinstance(value, float):
|
||||
new_value = Scalar(float(value), Unit.CELLS, Unit.WIDTH)
|
||||
elif isinstance(value, Scalar):
|
||||
new_value = value
|
||||
@@ -101,7 +106,7 @@ class ScalarProperty:
|
||||
)
|
||||
if new_value is not None and new_value.is_percent:
|
||||
new_value = Scalar(float(new_value.value), self.percent_unit, Unit.WIDTH)
|
||||
setattr(obj, self.internal_name, new_value)
|
||||
obj.set_rule(self.name, new_value)
|
||||
obj.refresh()
|
||||
|
||||
|
||||
@@ -110,17 +115,17 @@ class BoxProperty:
|
||||
For example "border-right", "outline-bottom", etc.
|
||||
"""
|
||||
|
||||
DEFAULT = ("", Style())
|
||||
DEFAULT = ("", Color.default())
|
||||
|
||||
def __set_name__(self, owner: Styles, name: str) -> None:
|
||||
self.internal_name = f"_rule_{name}"
|
||||
self.name = name
|
||||
_type, edge = name.split("_")
|
||||
self._type = _type
|
||||
self.edge = edge
|
||||
|
||||
def __get__(
|
||||
self, obj: Styles, objtype: type[Styles] | None = None
|
||||
) -> tuple[BoxType, Style]:
|
||||
) -> tuple[BoxType, Color]:
|
||||
"""Get the box property
|
||||
|
||||
Args:
|
||||
@@ -131,10 +136,10 @@ class BoxProperty:
|
||||
A ``tuple[BoxType, Style]`` containing the string type of the box and
|
||||
it's style. Example types are "rounded", "solid", and "dashed".
|
||||
"""
|
||||
value = getattr(obj, self.internal_name)
|
||||
return value or self.DEFAULT
|
||||
box_type, color = obj.get_rule(self.name) or self.DEFAULT
|
||||
return (box_type, color)
|
||||
|
||||
def __set__(self, obj: Styles, border: tuple[BoxType, str | Color | Style] | None):
|
||||
def __set__(self, obj: Styles, border: tuple[BoxType, str | Color] | None):
|
||||
"""Set the box property
|
||||
|
||||
Args:
|
||||
@@ -147,16 +152,15 @@ class BoxProperty:
|
||||
StyleSyntaxError: If the string supplied for the color has invalid syntax.
|
||||
"""
|
||||
if border is None:
|
||||
new_value = None
|
||||
obj.clear_rule(self.name)
|
||||
else:
|
||||
_type, color = border
|
||||
new_value = border
|
||||
if isinstance(color, str):
|
||||
new_value = (_type, Style.parse(color))
|
||||
new_value = (_type, Color.parse(color))
|
||||
elif isinstance(color, Color):
|
||||
new_value = (_type, Style.from_color(color))
|
||||
else:
|
||||
new_value = (_type, Style.from_color(Color.parse(color)))
|
||||
setattr(obj, self.internal_name, new_value)
|
||||
new_value = (_type, color)
|
||||
obj.set_rule(self.name, new_value)
|
||||
obj.refresh()
|
||||
|
||||
|
||||
@@ -164,10 +168,14 @@ class BoxProperty:
|
||||
class Edges(NamedTuple):
|
||||
"""Stores edges for border / outline."""
|
||||
|
||||
top: tuple[BoxType, Style]
|
||||
right: tuple[BoxType, Style]
|
||||
bottom: tuple[BoxType, Style]
|
||||
left: tuple[BoxType, Style]
|
||||
top: tuple[BoxType, Color]
|
||||
right: tuple[BoxType, Color]
|
||||
bottom: tuple[BoxType, Color]
|
||||
left: tuple[BoxType, Color]
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
(top, _), (right, _), (bottom, _), (left, _) = self
|
||||
return bool(top or right or bottom or left)
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
top, right, bottom, left = self
|
||||
@@ -199,6 +207,7 @@ class BorderProperty:
|
||||
"""Descriptor for getting and setting full borders and outlines."""
|
||||
|
||||
def __set_name__(self, owner: Styles, name: str) -> None:
|
||||
self.name = name
|
||||
self._properties = (
|
||||
f"{name}_top",
|
||||
f"{name}_right",
|
||||
@@ -217,6 +226,7 @@ class BorderProperty:
|
||||
An ``Edges`` object describing the type and style of each edge.
|
||||
"""
|
||||
top, right, bottom, left = self._properties
|
||||
|
||||
border = Edges(
|
||||
getattr(obj, top),
|
||||
getattr(obj, right),
|
||||
@@ -228,9 +238,7 @@ class BorderProperty:
|
||||
def __set__(
|
||||
self,
|
||||
obj: Styles,
|
||||
border: Sequence[tuple[BoxType, str | Color | Style] | None]
|
||||
| tuple[BoxType, str | Color | Style]
|
||||
| None,
|
||||
border: BorderDefinition | None,
|
||||
) -> None:
|
||||
"""Set the border
|
||||
|
||||
@@ -250,10 +258,11 @@ class BorderProperty:
|
||||
top, right, bottom, left = self._properties
|
||||
obj.refresh()
|
||||
if border is None:
|
||||
setattr(obj, top, None)
|
||||
setattr(obj, right, None)
|
||||
setattr(obj, bottom, None)
|
||||
setattr(obj, left, None)
|
||||
clear_rule = obj.clear_rule
|
||||
clear_rule(top)
|
||||
clear_rule(right)
|
||||
clear_rule(bottom)
|
||||
clear_rule(left)
|
||||
return
|
||||
if isinstance(border, tuple):
|
||||
setattr(obj, top, border)
|
||||
@@ -285,15 +294,10 @@ class BorderProperty:
|
||||
|
||||
|
||||
class StyleProperty:
|
||||
"""Descriptor for getting and setting full borders and outlines."""
|
||||
"""Descriptor for getting and setting the text style."""
|
||||
|
||||
DEFAULT_STYLE = Style()
|
||||
|
||||
def __set_name__(self, owner: Styles, name: str) -> None:
|
||||
self._color_name = f"_rule_{name}_color"
|
||||
self._bgcolor_name = f"_rule_{name}_background"
|
||||
self._style_name = f"_rule_{name}_style"
|
||||
|
||||
def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> Style:
|
||||
"""Get the Style
|
||||
|
||||
@@ -304,12 +308,15 @@ class StyleProperty:
|
||||
Returns:
|
||||
A ``Style`` object.
|
||||
"""
|
||||
color = getattr(obj, self._color_name)
|
||||
bgcolor = getattr(obj, self._bgcolor_name)
|
||||
style = Style.from_color(color, bgcolor)
|
||||
style_flags = getattr(obj, self._style_name)
|
||||
if style_flags:
|
||||
style += style_flags
|
||||
has_rule = obj.has_rule
|
||||
|
||||
style = Style.from_color(
|
||||
obj.text_color if has_rule("text_color") else None,
|
||||
obj.text_background if has_rule("text_background") else None,
|
||||
)
|
||||
if has_rule("text_style"):
|
||||
style += obj.text_style
|
||||
|
||||
return style
|
||||
|
||||
def __set__(self, obj: Styles, style: Style | str | None):
|
||||
@@ -324,26 +331,29 @@ class StyleProperty:
|
||||
StyleSyntaxError: When the supplied style string has invalid syntax.
|
||||
"""
|
||||
obj.refresh()
|
||||
|
||||
if style is None:
|
||||
setattr(obj, self._color_name, None)
|
||||
setattr(obj, self._bgcolor_name, None)
|
||||
setattr(obj, self._style_name, None)
|
||||
elif isinstance(style, Style):
|
||||
setattr(obj, self._color_name, style.color)
|
||||
setattr(obj, self._bgcolor_name, style.bgcolor)
|
||||
setattr(obj, self._style_name, style.without_color)
|
||||
elif isinstance(style, str):
|
||||
new_style = Style.parse(style)
|
||||
setattr(obj, self._color_name, new_style.color)
|
||||
setattr(obj, self._bgcolor_name, new_style.bgcolor)
|
||||
setattr(obj, self._style_name, new_style.without_color)
|
||||
clear_rule = obj.clear_rule
|
||||
clear_rule("text_color")
|
||||
clear_rule("text_background")
|
||||
clear_rule("text_style")
|
||||
else:
|
||||
if isinstance(style, str):
|
||||
style = Style.parse(style)
|
||||
|
||||
if style.color is not None:
|
||||
obj.text_color = style.color
|
||||
if style.bgcolor is not None:
|
||||
obj.text_background = style.bgcolor
|
||||
if style.without_color:
|
||||
obj.text_style = str(style.without_color)
|
||||
|
||||
|
||||
class SpacingProperty:
|
||||
"""Descriptor for getting and setting spacing properties (e.g. padding and margin)."""
|
||||
|
||||
def __set_name__(self, owner: Styles, name: str) -> None:
|
||||
self._internal_name = f"_rule_{name}"
|
||||
self.name = name
|
||||
|
||||
def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> Spacing:
|
||||
"""Get the Spacing
|
||||
@@ -355,9 +365,9 @@ class SpacingProperty:
|
||||
Returns:
|
||||
Spacing: The Spacing. If unset, returns the null spacing ``(0, 0, 0, 0)``.
|
||||
"""
|
||||
return getattr(obj, self._internal_name) or NULL_SPACING
|
||||
return obj.get_rule(self.name, NULL_SPACING)
|
||||
|
||||
def __set__(self, obj: Styles, spacing: SpacingDimensions):
|
||||
def __set__(self, obj: Styles, spacing: SpacingDimensions | None):
|
||||
"""Set the Spacing
|
||||
|
||||
Args:
|
||||
@@ -370,8 +380,10 @@ class SpacingProperty:
|
||||
not 1, 2, or 4.
|
||||
"""
|
||||
obj.refresh(layout=True)
|
||||
spacing = Spacing.unpack(spacing)
|
||||
setattr(obj, self._internal_name, spacing)
|
||||
if spacing is None:
|
||||
obj.clear_rule(self.name)
|
||||
else:
|
||||
obj.set_rule(self.name, Spacing.unpack(spacing))
|
||||
|
||||
|
||||
class DocksProperty:
|
||||
@@ -391,7 +403,7 @@ class DocksProperty:
|
||||
Returns:
|
||||
tuple[DockGroup, ...]: A ``tuple`` containing the defined docks.
|
||||
"""
|
||||
return obj._rule_docks or ()
|
||||
return obj.get_rule("docks", ())
|
||||
|
||||
def __set__(self, obj: Styles, docks: Iterable[DockGroup] | None):
|
||||
"""Set the Docks property
|
||||
@@ -402,9 +414,10 @@ class DocksProperty:
|
||||
"""
|
||||
obj.refresh(layout=True)
|
||||
if docks is None:
|
||||
obj._rule_docks = None
|
||||
obj.clear_rule("docks")
|
||||
|
||||
else:
|
||||
obj._rule_docks = tuple(docks)
|
||||
obj.set_rule("docks", tuple(docks))
|
||||
|
||||
|
||||
class DockProperty:
|
||||
@@ -424,7 +437,7 @@ class DockProperty:
|
||||
Returns:
|
||||
str: The dock name as a string, or "" if the rule is not set.
|
||||
"""
|
||||
return obj._rule_dock or ""
|
||||
return obj.get_rule("dock", "_default")
|
||||
|
||||
def __set__(self, obj: Styles, spacing: str | None):
|
||||
"""Set the Dock property
|
||||
@@ -434,16 +447,18 @@ class DockProperty:
|
||||
spacing (str | None): The spacing to use.
|
||||
"""
|
||||
obj.refresh(layout=True)
|
||||
obj._rule_dock = spacing
|
||||
obj.set_rule("dock", spacing)
|
||||
|
||||
|
||||
class LayoutProperty:
|
||||
"""Descriptor for getting and setting layout."""
|
||||
|
||||
def __set_name__(self, owner: Styles, name: str) -> None:
|
||||
self._internal_name = f"_rule_{name}"
|
||||
self.name = name
|
||||
|
||||
def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> Layout:
|
||||
def __get__(
|
||||
self, obj: Styles, objtype: type[Styles] | None = None
|
||||
) -> Layout | None:
|
||||
"""
|
||||
Args:
|
||||
obj (Styles): The Styles object
|
||||
@@ -451,23 +466,26 @@ class LayoutProperty:
|
||||
Returns:
|
||||
The ``Layout`` object.
|
||||
"""
|
||||
return getattr(obj, self._internal_name)
|
||||
return obj.get_rule(self.name)
|
||||
|
||||
def __set__(self, obj: Styles, layout: LayoutName | Layout):
|
||||
def __set__(self, obj: Styles, layout: str | Layout | None):
|
||||
"""
|
||||
Args:
|
||||
obj (Styles): The Styles object.
|
||||
layout (LayoutName | Layout): The layout to use. You can supply a ``LayoutName``
|
||||
(a string literal such as ``"dock"``) or a ``Layout`` object.
|
||||
layout (str | Layout): The layout to use. You can supply a the name of the layout
|
||||
or a ``Layout`` object.
|
||||
"""
|
||||
|
||||
from ..layouts.factory import get_layout, Layout # Prevents circular import
|
||||
|
||||
obj.refresh(layout=True)
|
||||
if isinstance(layout, Layout):
|
||||
new_layout = layout
|
||||
|
||||
if layout is None:
|
||||
obj.clear_rule("layout")
|
||||
elif isinstance(layout, Layout):
|
||||
obj.set_rule("layout", layout)
|
||||
else:
|
||||
new_layout = get_layout(layout)
|
||||
setattr(obj, self._internal_name, new_layout)
|
||||
obj.set_rule("layout", get_layout(layout))
|
||||
|
||||
|
||||
class OffsetProperty:
|
||||
@@ -477,7 +495,7 @@ class OffsetProperty:
|
||||
"""
|
||||
|
||||
def __set_name__(self, owner: Styles, name: str) -> None:
|
||||
self._internal_name = f"_rule_{name}"
|
||||
self.name = name
|
||||
|
||||
def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> ScalarOffset:
|
||||
"""Get the offset
|
||||
@@ -490,11 +508,11 @@ class OffsetProperty:
|
||||
ScalarOffset: The ``ScalarOffset`` indicating the adjustment that
|
||||
will be made to widget position prior to it being rendered.
|
||||
"""
|
||||
return getattr(obj, self._internal_name) or ScalarOffset(
|
||||
Scalar.from_number(0), Scalar.from_number(0)
|
||||
)
|
||||
return obj.get_rule(self.name, ScalarOffset.null())
|
||||
|
||||
def __set__(self, obj: Styles, offset: tuple[int | str, int | str] | ScalarOffset):
|
||||
def __set__(
|
||||
self, obj: Styles, offset: tuple[int | str, int | str] | ScalarOffset | None
|
||||
):
|
||||
"""Set the offset
|
||||
|
||||
Args:
|
||||
@@ -509,57 +527,24 @@ class OffsetProperty:
|
||||
be parsed into a Scalar. For example, if you specify an non-existent unit.
|
||||
"""
|
||||
obj.refresh(layout=True)
|
||||
if isinstance(offset, ScalarOffset):
|
||||
setattr(obj, self._internal_name, offset)
|
||||
return offset
|
||||
x, y = offset
|
||||
scalar_x = (
|
||||
Scalar.parse(x, Unit.WIDTH)
|
||||
if isinstance(x, str)
|
||||
else Scalar(float(x), Unit.CELLS, Unit.WIDTH)
|
||||
)
|
||||
scalar_y = (
|
||||
Scalar.parse(y, Unit.HEIGHT)
|
||||
if isinstance(y, str)
|
||||
else Scalar(float(y), Unit.CELLS, Unit.HEIGHT)
|
||||
)
|
||||
_offset = ScalarOffset(scalar_x, scalar_y)
|
||||
setattr(obj, self._internal_name, _offset)
|
||||
|
||||
|
||||
class IntegerProperty:
|
||||
"""Descriptor for getting and setting integer properties"""
|
||||
|
||||
def __set_name__(self, owner: Styles, name: str) -> None:
|
||||
self._name = name
|
||||
self._internal_name = f"_{name}"
|
||||
|
||||
def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> int:
|
||||
"""Get the integer property, or the default ``0`` if not set.
|
||||
|
||||
Args:
|
||||
obj (Styles): The ``Styles`` object.
|
||||
objtype (type[Styles]): The ``Styles`` class.
|
||||
|
||||
Returns:
|
||||
int: The integer property value
|
||||
"""
|
||||
return getattr(obj, self._internal_name, 0)
|
||||
|
||||
def __set__(self, obj: Styles, value: int):
|
||||
"""Set the integer property
|
||||
|
||||
Args:
|
||||
obj: The ``Styles`` object
|
||||
value: The value to set the integer to
|
||||
|
||||
Raises:
|
||||
StyleTypeError: If the supplied value is not an integer.
|
||||
"""
|
||||
obj.refresh()
|
||||
if not isinstance(value, int):
|
||||
raise StyleTypeError(f"{self._name} must be an integer")
|
||||
setattr(obj, self._internal_name, value)
|
||||
if offset is None:
|
||||
obj.clear_rule(self.name)
|
||||
elif isinstance(offset, ScalarOffset):
|
||||
obj.set_rule(self.name, offset)
|
||||
else:
|
||||
x, y = offset
|
||||
scalar_x = (
|
||||
Scalar.parse(x, Unit.WIDTH)
|
||||
if isinstance(x, str)
|
||||
else Scalar(float(x), Unit.CELLS, Unit.WIDTH)
|
||||
)
|
||||
scalar_y = (
|
||||
Scalar.parse(y, Unit.HEIGHT)
|
||||
if isinstance(y, str)
|
||||
else Scalar(float(y), Unit.CELLS, Unit.HEIGHT)
|
||||
)
|
||||
_offset = ScalarOffset(scalar_x, scalar_y)
|
||||
obj.set_rule(self.name, _offset)
|
||||
|
||||
|
||||
class StringEnumProperty:
|
||||
@@ -572,8 +557,7 @@ class StringEnumProperty:
|
||||
self._default = default
|
||||
|
||||
def __set_name__(self, owner: Styles, name: str) -> None:
|
||||
self._name = name
|
||||
self._internal_name = f"_rule_{name}"
|
||||
self.name = name
|
||||
|
||||
def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> str:
|
||||
"""Get the string property, or the default value if it's not set
|
||||
@@ -585,7 +569,7 @@ class StringEnumProperty:
|
||||
Returns:
|
||||
str: The string property value
|
||||
"""
|
||||
return getattr(obj, self._internal_name, None) or self._default
|
||||
return obj.get_rule(self.name, self._default)
|
||||
|
||||
def __set__(self, obj: Styles, value: str | None = None):
|
||||
"""Set the string property and ensure it is in the set of allowed values.
|
||||
@@ -598,20 +582,21 @@ class StringEnumProperty:
|
||||
StyleValueError: If the value is not in the set of valid values.
|
||||
"""
|
||||
obj.refresh()
|
||||
if value is not None:
|
||||
if value is None:
|
||||
obj.clear_rule(self.name)
|
||||
else:
|
||||
if value not in self._valid_values:
|
||||
raise StyleValueError(
|
||||
f"{self._name} must be one of {friendly_list(self._valid_values)}"
|
||||
f"{self.name} must be one of {friendly_list(self._valid_values)}"
|
||||
)
|
||||
setattr(obj, self._internal_name, value)
|
||||
obj.set_rule(self.name, value)
|
||||
|
||||
|
||||
class NameProperty:
|
||||
"""Descriptor for getting and setting name properties."""
|
||||
|
||||
def __set_name__(self, owner: Styles, name: str) -> None:
|
||||
self._name = name
|
||||
self._internal_name = f"_rule_{name}"
|
||||
self.name = name
|
||||
|
||||
def __get__(self, obj: Styles, objtype: type[Styles] | None) -> str:
|
||||
"""Get the name property
|
||||
@@ -623,7 +608,7 @@ class NameProperty:
|
||||
Returns:
|
||||
str: The name
|
||||
"""
|
||||
return getattr(obj, self._internal_name) or ""
|
||||
return obj.get_rule(self.name, "")
|
||||
|
||||
def __set__(self, obj: Styles, name: str | None):
|
||||
"""Set the name property
|
||||
@@ -636,42 +621,42 @@ class NameProperty:
|
||||
StyleTypeError: If the value is not a ``str``.
|
||||
"""
|
||||
obj.refresh(layout=True)
|
||||
if not isinstance(name, str):
|
||||
raise StyleTypeError(f"{self._name} must be a str")
|
||||
setattr(obj, self._internal_name, name)
|
||||
if name is None:
|
||||
obj.clear_rule(self.name)
|
||||
else:
|
||||
if not isinstance(name, str):
|
||||
raise StyleTypeError(f"{self.name} must be a str")
|
||||
obj.set_rule(self.name, name)
|
||||
|
||||
|
||||
class NameListProperty:
|
||||
def __set_name__(self, owner: Styles, name: str) -> None:
|
||||
self._name = name
|
||||
self._internal_name = f"_rule_{name}"
|
||||
self.name = name
|
||||
|
||||
def __get__(
|
||||
self, obj: Styles, objtype: type[Styles] | None = None
|
||||
) -> tuple[str, ...]:
|
||||
return getattr(obj, self._internal_name, None) or ()
|
||||
return obj.get_rule(self.name, ())
|
||||
|
||||
def __set__(
|
||||
self, obj: Styles, names: str | tuple[str] | None = None
|
||||
) -> str | tuple[str] | None:
|
||||
obj.refresh(layout=True)
|
||||
names_value: tuple[str, ...] | None = None
|
||||
if isinstance(names, str):
|
||||
names_value = tuple(name.strip().lower() for name in names.split(" "))
|
||||
if names is None:
|
||||
obj.clear_rule(self.name)
|
||||
elif isinstance(names, str):
|
||||
obj.set_rule(
|
||||
self.name, tuple(name.strip().lower() for name in names.split(" "))
|
||||
)
|
||||
elif isinstance(names, tuple):
|
||||
names_value = names
|
||||
elif names is None:
|
||||
names_value = None
|
||||
setattr(obj, self._internal_name, names_value)
|
||||
return names
|
||||
obj.set_rule(self.name, names)
|
||||
|
||||
|
||||
class ColorProperty:
|
||||
"""Descriptor for getting and setting color properties."""
|
||||
|
||||
def __set_name__(self, owner: Styles, name: str) -> None:
|
||||
self._name = name
|
||||
self._internal_name = f"_rule_{name}"
|
||||
self.name = name
|
||||
|
||||
def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> Color:
|
||||
"""Get the ``Color``, or ``Color.default()`` if no color is set.
|
||||
@@ -683,7 +668,7 @@ class ColorProperty:
|
||||
Returns:
|
||||
Color: The Color
|
||||
"""
|
||||
return getattr(obj, self._internal_name, None) or Color.default()
|
||||
return obj.get_rule(self.name) or Color.default()
|
||||
|
||||
def __set__(self, obj: Styles, color: Color | str | None):
|
||||
"""Set the Color
|
||||
@@ -699,19 +684,18 @@ class ColorProperty:
|
||||
"""
|
||||
obj.refresh()
|
||||
if color is None:
|
||||
setattr(self, self._internal_name, None)
|
||||
else:
|
||||
if isinstance(color, Color):
|
||||
setattr(self, self._internal_name, color)
|
||||
elif isinstance(color, str):
|
||||
new_color = Color.parse(color)
|
||||
setattr(self, self._internal_name, new_color)
|
||||
obj.clear_rule(self.name)
|
||||
elif isinstance(color, Color):
|
||||
obj.set_rule(self.name, color)
|
||||
elif isinstance(color, str):
|
||||
obj.set_rule(self.name, Color.parse(color))
|
||||
|
||||
|
||||
class StyleFlagsProperty:
|
||||
"""Descriptor for getting and set style flag properties (e.g. ``bold italic underline``)."""
|
||||
|
||||
_VALID_PROPERTIES = {
|
||||
"none",
|
||||
"not",
|
||||
"bold",
|
||||
"italic",
|
||||
@@ -725,8 +709,7 @@ class StyleFlagsProperty:
|
||||
}
|
||||
|
||||
def __set_name__(self, owner: Styles, name: str) -> None:
|
||||
self._name = name
|
||||
self._internal_name = f"_rule_{name}"
|
||||
self.name = name
|
||||
|
||||
def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> Style:
|
||||
"""Get the ``Style``
|
||||
@@ -738,7 +721,7 @@ class StyleFlagsProperty:
|
||||
Returns:
|
||||
Style: The ``Style`` object
|
||||
"""
|
||||
return getattr(obj, self._internal_name, None) or Style.null()
|
||||
return obj.get_rule(self.name, Style.null())
|
||||
|
||||
def __set__(self, obj: Styles, style_flags: str | None):
|
||||
"""Set the style using a style flag string
|
||||
@@ -753,7 +736,7 @@ class StyleFlagsProperty:
|
||||
"""
|
||||
obj.refresh()
|
||||
if style_flags is None:
|
||||
setattr(self, self._internal_name, None)
|
||||
obj.clear_rule(self.name)
|
||||
else:
|
||||
words = [word.strip() for word in style_flags.split(" ")]
|
||||
valid_word = self._VALID_PROPERTIES.__contains__
|
||||
@@ -764,16 +747,12 @@ class StyleFlagsProperty:
|
||||
f"valid values are {friendly_list(self._VALID_PROPERTIES)}"
|
||||
)
|
||||
style = Style.parse(style_flags)
|
||||
setattr(obj, self._internal_name, style)
|
||||
obj.set_rule(self.name, style)
|
||||
|
||||
|
||||
class TransitionsProperty:
|
||||
"""Descriptor for getting transitions properties"""
|
||||
|
||||
def __set_name__(self, owner: Styles, name: str) -> None:
|
||||
self._name = name
|
||||
self._internal_name = f"_rule_{name}"
|
||||
|
||||
def __get__(
|
||||
self, obj: Styles, objtype: type[Styles] | None = None
|
||||
) -> dict[str, Transition]:
|
||||
@@ -788,4 +767,10 @@ class TransitionsProperty:
|
||||
e.g. ``{"offset": Transition(...), ...}``. If no transitions have been set, an empty ``dict``
|
||||
is returned.
|
||||
"""
|
||||
return getattr(obj, self._internal_name, None) or {}
|
||||
return obj.get_rule("transitions", {})
|
||||
|
||||
def __set__(self, obj: Styles, transitions: dict[str, Transition] | None) -> None:
|
||||
if transitions is None:
|
||||
obj.clear_rule("transitions")
|
||||
else:
|
||||
obj.set_rule("transitions", transitions.copy())
|
||||
|
||||
@@ -19,7 +19,6 @@ from .._duration import _duration_as_seconds
|
||||
from .._easing import EASING
|
||||
from .._loop import loop_last
|
||||
from ..geometry import Spacing, SpacingDimensions
|
||||
from ..layouts.factory import get_layout, LayoutName, MissingLayout, LAYOUT_MAP
|
||||
|
||||
|
||||
class StylesBuilder:
|
||||
@@ -73,7 +72,7 @@ class StylesBuilder:
|
||||
if name == "token":
|
||||
value = value.lower()
|
||||
if value in VALID_DISPLAY:
|
||||
self.styles._rule_display = cast(Display, value)
|
||||
self.styles._rules["display"] = cast(Display, value)
|
||||
else:
|
||||
self.error(
|
||||
name,
|
||||
@@ -87,7 +86,7 @@ class StylesBuilder:
|
||||
if not tokens:
|
||||
return
|
||||
if len(tokens) == 1:
|
||||
setattr(self.styles, name, Scalar.parse(tokens[0].value))
|
||||
self.styles._rules[name] = Scalar.parse(tokens[0].value)
|
||||
else:
|
||||
self.error(name, tokens[0], "a single scalar is expected")
|
||||
|
||||
@@ -115,7 +114,7 @@ class StylesBuilder:
|
||||
if name == "token":
|
||||
value = value.lower()
|
||||
if value in VALID_VISIBILITY:
|
||||
self.styles._rule_visibility = cast(Visibility, value)
|
||||
self.styles._rules["visibility"] = cast(Visibility, value)
|
||||
else:
|
||||
self.error(
|
||||
name,
|
||||
@@ -141,11 +140,7 @@ class StylesBuilder:
|
||||
self.error(
|
||||
name, tokens[0], f"1, 2, or 4 values expected; received {len(space)}"
|
||||
)
|
||||
setattr(
|
||||
self.styles,
|
||||
f"_rule_{name}",
|
||||
Spacing.unpack(cast(SpacingDimensions, tuple(space))),
|
||||
)
|
||||
self.styles._rules[name] = Spacing.unpack(cast(SpacingDimensions, tuple(space)))
|
||||
|
||||
def process_padding(self, name: str, tokens: list[Token], important: bool) -> None:
|
||||
self._process_space(name, tokens)
|
||||
@@ -153,68 +148,64 @@ class StylesBuilder:
|
||||
def process_margin(self, name: str, tokens: list[Token], important: bool) -> None:
|
||||
self._process_space(name, tokens)
|
||||
|
||||
def _parse_border(self, name: str, tokens: list[Token]) -> tuple[str, Style]:
|
||||
style = Style()
|
||||
def _parse_border(self, name: str, tokens: list[Token]) -> tuple[str, Color]:
|
||||
border_type = "solid"
|
||||
style_tokens: list[str] = []
|
||||
append = style_tokens.append
|
||||
border_color = Color.default()
|
||||
|
||||
for token in tokens:
|
||||
token_name, value, _, _, _, _ = token
|
||||
if token_name == "token":
|
||||
if value in VALID_BORDER:
|
||||
border_type = value
|
||||
else:
|
||||
append(value)
|
||||
border_color = Color.parse(value)
|
||||
|
||||
elif token_name == "color":
|
||||
append(value)
|
||||
border_color = Color.parse(value)
|
||||
else:
|
||||
self.error(name, token, f"unexpected token {value!r} in declaration")
|
||||
style_definition = " ".join(style_tokens)
|
||||
try:
|
||||
style = Style.parse(style_definition)
|
||||
except Exception as error:
|
||||
self.error(name, tokens[0], f"error in {name} declaration; {error}")
|
||||
return (border_type, style)
|
||||
|
||||
def _process_border(self, edge: str, name: str, tokens: list[Token]) -> None:
|
||||
return (border_type, border_color)
|
||||
|
||||
def _process_border_edge(self, edge: str, name: str, tokens: list[Token]) -> None:
|
||||
border = self._parse_border("border", tokens)
|
||||
setattr(self.styles, f"_rule_border_{edge}", border)
|
||||
self.styles._rules[f"border_{edge}"] = border
|
||||
|
||||
def process_border(self, name: str, tokens: list[Token], important: bool) -> None:
|
||||
border = self._parse_border("border", tokens)
|
||||
styles = self.styles
|
||||
styles._rule_border_top = styles._rule_border_right = border
|
||||
styles._rule_border_bottom = styles._rule_border_left = border
|
||||
rules = self.styles._rules
|
||||
rules["border_top"] = rules["border_right"] = border
|
||||
rules["border_bottom"] = rules["border_left"] = border
|
||||
|
||||
def process_border_top(
|
||||
self, name: str, tokens: list[Token], important: bool
|
||||
) -> None:
|
||||
self._process_border("top", name, tokens)
|
||||
self._process_border_edge("top", name, tokens)
|
||||
|
||||
def process_border_right(
|
||||
self, name: str, tokens: list[Token], important: bool
|
||||
) -> None:
|
||||
self._process_border("right", name, tokens)
|
||||
self._process_border_edge("right", name, tokens)
|
||||
|
||||
def process_border_bottom(
|
||||
self, name: str, tokens: list[Token], important: bool
|
||||
) -> None:
|
||||
self._process_border("bottom", name, tokens)
|
||||
self._process_border_edge("bottom", name, tokens)
|
||||
|
||||
def process_border_left(
|
||||
self, name: str, tokens: list[Token], important: bool
|
||||
) -> None:
|
||||
self._process_border("left", name, tokens)
|
||||
self._process_border_edge("left", name, tokens)
|
||||
|
||||
def _process_outline(self, edge: str, name: str, tokens: list[Token]) -> None:
|
||||
border = self._parse_border("outline", tokens)
|
||||
setattr(self.styles, f"_rule_outline_{edge}", border)
|
||||
self.styles._rules[f"outline_{edge}"] = border
|
||||
|
||||
def process_outline(self, name: str, tokens: list[Token], important: bool) -> None:
|
||||
border = self._parse_border("outline", tokens)
|
||||
styles = self.styles
|
||||
styles._rule_outline_top = styles._rule_outline_right = border
|
||||
styles._rule_outline_bottom = styles._rule_outline_left = border
|
||||
rules = self.styles._rules
|
||||
rules["outline_top"] = rules["outline_right"] = border
|
||||
rules["outline_bottom"] = rules["outline_left"] = border
|
||||
|
||||
def process_outline_top(
|
||||
self, name: str, tokens: list[Token], important: bool
|
||||
@@ -257,7 +248,7 @@ class StylesBuilder:
|
||||
|
||||
scalar_x = Scalar.parse(token1.value, Unit.WIDTH)
|
||||
scalar_y = Scalar.parse(token2.value, Unit.HEIGHT)
|
||||
self.styles._rule_offset = ScalarOffset(scalar_x, scalar_y)
|
||||
self.styles._rules["offset"] = ScalarOffset(scalar_x, scalar_y)
|
||||
|
||||
def process_offset_x(self, name: str, tokens: list[Token], important: bool) -> None:
|
||||
if not tokens:
|
||||
@@ -270,7 +261,7 @@ class StylesBuilder:
|
||||
self.error(name, token, f"expected a scalar; found {token.value!r}")
|
||||
x = Scalar.parse(token.value, Unit.WIDTH)
|
||||
y = self.styles.offset.y
|
||||
self.styles._rule_offset = ScalarOffset(x, y)
|
||||
self.styles._rules["offset"] = ScalarOffset(x, y)
|
||||
|
||||
def process_offset_y(self, name: str, tokens: list[Token], important: bool) -> None:
|
||||
if not tokens:
|
||||
@@ -283,17 +274,19 @@ class StylesBuilder:
|
||||
self.error(name, token, f"expected a scalar; found {token.value!r}")
|
||||
y = Scalar.parse(token.value, Unit.HEIGHT)
|
||||
x = self.styles.offset.x
|
||||
self.styles._rule_offset = ScalarOffset(x, y)
|
||||
self.styles._rules["offset"] = ScalarOffset(x, y)
|
||||
|
||||
def process_layout(self, name: str, tokens: list[Token], important: bool) -> None:
|
||||
from ..layouts.factory import get_layout, MissingLayout, LAYOUT_MAP
|
||||
|
||||
if tokens:
|
||||
if len(tokens) != 1:
|
||||
self.error(name, tokens[0], "unexpected tokens in declaration")
|
||||
else:
|
||||
value = tokens[0].value
|
||||
layout_name = cast(LayoutName, value)
|
||||
layout_name = value
|
||||
try:
|
||||
self.styles._rule_layout = get_layout(layout_name)
|
||||
self.styles._rules["layout"] = get_layout(layout_name)
|
||||
except MissingLayout:
|
||||
self.error(
|
||||
name,
|
||||
@@ -330,7 +323,7 @@ class StylesBuilder:
|
||||
for token in tokens:
|
||||
if token.name in ("color", "token"):
|
||||
try:
|
||||
self.styles._rule_text_color = Color.parse(token.value)
|
||||
self.styles._rules["text_color"] = Color.parse(token.value)
|
||||
except Exception as error:
|
||||
self.error(
|
||||
name, token, f"failed to parse color {token.value!r}; {error}"
|
||||
@@ -346,7 +339,7 @@ class StylesBuilder:
|
||||
for token in tokens:
|
||||
if token.name in ("color", "token"):
|
||||
try:
|
||||
self.styles._rule_text_background = Color.parse(token.value)
|
||||
self.styles._rules["text_background"] = Color.parse(token.value)
|
||||
except Exception as error:
|
||||
self.error(
|
||||
name, token, f"failed to parse color {token.value!r}; {error}"
|
||||
@@ -370,7 +363,7 @@ class StylesBuilder:
|
||||
tokens[1],
|
||||
f"unexpected tokens in dock declaration",
|
||||
)
|
||||
self.styles._rule_dock = tokens[0].value if tokens else ""
|
||||
self.styles._rules["dock"] = tokens[0].value if tokens else ""
|
||||
|
||||
def process_docks(self, name: str, tokens: list[Token], important: bool) -> None:
|
||||
docks: list[DockGroup] = []
|
||||
@@ -401,12 +394,12 @@ class StylesBuilder:
|
||||
token,
|
||||
f"unexpected token {token.value!r} in docks declaration",
|
||||
)
|
||||
self.styles._rule_docks = tuple(docks + [DockGroup("_default", "top", 0)])
|
||||
self.styles._rules["docks"] = tuple(docks + [DockGroup("_default", "top", 0)])
|
||||
|
||||
def process_layer(self, name: str, tokens: list[Token], important: bool) -> None:
|
||||
if len(tokens) > 1:
|
||||
self.error(name, tokens[1], f"unexpected tokens in dock-edge declaration")
|
||||
self.styles._rule_layer = tokens[0].value
|
||||
self.styles._rules["layer"] = tokens[0].value
|
||||
|
||||
def process_layers(self, name: str, tokens: list[Token], important: bool) -> None:
|
||||
layers: list[str] = []
|
||||
@@ -414,7 +407,7 @@ class StylesBuilder:
|
||||
if token.name != "token":
|
||||
self.error(name, token, "{token.name} not expected here")
|
||||
layers.append(token.value)
|
||||
self.styles._rule_layers = tuple(layers)
|
||||
self.styles._rules["layers"] = tuple(layers)
|
||||
|
||||
def process_transition(
|
||||
self, name: str, tokens: list[Token], important: bool
|
||||
@@ -479,4 +472,4 @@ class StylesBuilder:
|
||||
pass
|
||||
transitions[css_property] = Transition(duration, easing, delay)
|
||||
|
||||
self.styles._rule_transitions = transitions
|
||||
self.styles._rules["transitions"] = transitions
|
||||
|
||||
@@ -16,7 +16,7 @@ from __future__ import annotations
|
||||
|
||||
import rich.repr
|
||||
|
||||
from typing import Iterable, Iterator, TYPE_CHECKING
|
||||
from typing import Iterator, TYPE_CHECKING
|
||||
|
||||
|
||||
from .match import match
|
||||
@@ -26,6 +26,10 @@ if TYPE_CHECKING:
|
||||
from ..dom import DOMNode
|
||||
|
||||
|
||||
class EmptyQueryError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@rich.repr.auto(angular=True)
|
||||
class DOMQuery:
|
||||
def __init__(
|
||||
@@ -96,8 +100,10 @@ class DOMQuery:
|
||||
Returns:
|
||||
DOMNode: A DOM Node.
|
||||
"""
|
||||
# TODO: Better response to empty query than an IndexError
|
||||
return self._nodes[0]
|
||||
if self._nodes:
|
||||
return self._nodes[0]
|
||||
else:
|
||||
raise EmptyQueryError("Query is empty")
|
||||
|
||||
def add_class(self, *class_names: str) -> DOMQuery:
|
||||
"""Add the given class name(s) to nodes."""
|
||||
@@ -116,3 +122,27 @@ class DOMQuery:
|
||||
for node in self._nodes:
|
||||
node.toggle_class(*class_names)
|
||||
return self
|
||||
|
||||
def set_styles(self, css: str | None = None, **styles: str) -> DOMQuery:
|
||||
"""Set styles on matched nodes.
|
||||
|
||||
Args:
|
||||
css (str, optional): CSS declarations to parser, or None. Defaults to None.
|
||||
"""
|
||||
for node in self._nodes:
|
||||
node.set_styles(css, **styles)
|
||||
return self
|
||||
|
||||
def refresh(self, repaint: bool = True, layout: bool = False) -> DOMQuery:
|
||||
"""Refresh matched nodes.
|
||||
|
||||
Args:
|
||||
repaint (bool): Repaint node(s). defaults to True.
|
||||
layout (bool): Layout node(s). Defaults to False.
|
||||
|
||||
Returns:
|
||||
DOMQuery: Query for chaining.
|
||||
"""
|
||||
for node in self._nodes:
|
||||
node.refresh(repaint=repaint, layout=layout)
|
||||
return self
|
||||
|
||||
@@ -145,6 +145,15 @@ class ScalarOffset(NamedTuple):
|
||||
x: Scalar
|
||||
y: Scalar
|
||||
|
||||
@classmethod
|
||||
def null(cls) -> ScalarOffset:
|
||||
"""Get a null scalar offset (0, 0)."""
|
||||
return NULL_SCALAR
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
x, y = self
|
||||
return bool(x.value or y.value)
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
yield None, str(self.x)
|
||||
yield None, str(self.y)
|
||||
@@ -157,6 +166,9 @@ class ScalarOffset(NamedTuple):
|
||||
)
|
||||
|
||||
|
||||
NULL_SCALAR = ScalarOffset(Scalar.from_number(0), Scalar.from_number(0))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
print(Scalar.parse("3.14fr"))
|
||||
|
||||
@@ -60,7 +60,7 @@ class ScalarAnimation(Animation):
|
||||
return True
|
||||
|
||||
offset = self.start + (self.destination - self.start) * eased_factor
|
||||
current = getattr(self.styles, f"_rule_{self.attribute}")
|
||||
current = self.styles._rules[self.attribute]
|
||||
if current != offset:
|
||||
setattr(self.styles, f"{self.attribute}", offset)
|
||||
|
||||
|
||||
@@ -1,45 +1,96 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from functools import lru_cache
|
||||
from typing import Any, Iterable, NamedTuple, TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any, Iterable, NamedTuple, cast
|
||||
|
||||
import rich.repr
|
||||
from rich.color import Color
|
||||
from rich.style import Style
|
||||
|
||||
from .._animator import Animation, EasingFunction
|
||||
from ..geometry import Spacing
|
||||
from ._style_properties import (
|
||||
BorderProperty,
|
||||
BoxProperty,
|
||||
ColorProperty,
|
||||
DocksProperty,
|
||||
DockProperty,
|
||||
OffsetProperty,
|
||||
NameProperty,
|
||||
DocksProperty,
|
||||
LayoutProperty,
|
||||
NameListProperty,
|
||||
NameProperty,
|
||||
OffsetProperty,
|
||||
ScalarProperty,
|
||||
SpacingProperty,
|
||||
StringEnumProperty,
|
||||
StyleProperty,
|
||||
StyleFlagsProperty,
|
||||
StyleProperty,
|
||||
TransitionsProperty,
|
||||
LayoutProperty,
|
||||
)
|
||||
from .constants import (
|
||||
VALID_DISPLAY,
|
||||
VALID_VISIBILITY,
|
||||
)
|
||||
from .constants import VALID_DISPLAY, VALID_VISIBILITY
|
||||
from .scalar import Scalar, ScalarOffset, Unit
|
||||
from .scalar_animation import ScalarAnimation
|
||||
from .transition import Transition
|
||||
from .types import Display, Edge, Visibility
|
||||
from .types import Specificity3, Specificity4
|
||||
from .._animator import Animation, EasingFunction
|
||||
from ..geometry import Spacing
|
||||
from .types import Display, Edge, Specificity3, Specificity4, Visibility
|
||||
|
||||
if sys.version_info >= (3, 8):
|
||||
from typing import TypedDict
|
||||
else:
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..layout import Layout
|
||||
from ..dom import DOMNode
|
||||
from ..layout import Layout
|
||||
|
||||
|
||||
class RulesMap(TypedDict, total=False):
|
||||
"""A typed dict for CSS rules.
|
||||
|
||||
Any key may be absent, indiciating that rule has not been set.
|
||||
|
||||
Does not define composite rules, that is a rule that is made of a combination of other rules. For instance,
|
||||
the text style is made up of text_color, text_background, and text_style.
|
||||
"""
|
||||
|
||||
display: Display
|
||||
visibility: Visibility
|
||||
layout: "Layout"
|
||||
|
||||
text_color: Color
|
||||
text_background: Color
|
||||
text_style: Style
|
||||
|
||||
padding: Spacing
|
||||
margin: Spacing
|
||||
offset: ScalarOffset
|
||||
|
||||
border_top: tuple[str, Color]
|
||||
border_right: tuple[str, Color]
|
||||
border_bottom: tuple[str, Color]
|
||||
border_left: tuple[str, Color]
|
||||
|
||||
outline_top: tuple[str, Color]
|
||||
outline_right: tuple[str, Color]
|
||||
outline_bottom: tuple[str, Color]
|
||||
outline_left: tuple[str, Color]
|
||||
|
||||
width: Scalar
|
||||
height: Scalar
|
||||
min_width: Scalar
|
||||
min_height: Scalar
|
||||
|
||||
dock: str
|
||||
docks: tuple[DockGroup, ...]
|
||||
|
||||
layers: tuple[str, ...]
|
||||
layer: str
|
||||
|
||||
transitions: dict[str, Transition]
|
||||
|
||||
|
||||
RULE_NAMES = list(RulesMap.__annotations__.keys())
|
||||
|
||||
|
||||
class DockGroup(NamedTuple):
|
||||
@@ -48,51 +99,18 @@ class DockGroup(NamedTuple):
|
||||
z: int
|
||||
|
||||
|
||||
@rich.repr.auto
|
||||
@dataclass
|
||||
class Styles:
|
||||
class StylesBase(ABC):
|
||||
"""A common base class for Styles and RenderStyles"""
|
||||
|
||||
node: DOMNode | None = None
|
||||
|
||||
_rule_display: Display | None = None
|
||||
_rule_visibility: Visibility | None = None
|
||||
_rule_layout: "Layout" | None = None
|
||||
|
||||
_rule_text_color: Color | None = None
|
||||
_rule_text_background: Color | None = None
|
||||
_rule_text_style: Style | None = None
|
||||
|
||||
_rule_padding: Spacing | None = None
|
||||
_rule_margin: Spacing | None = None
|
||||
_rule_offset: ScalarOffset | None = None
|
||||
|
||||
_rule_border_top: tuple[str, Style] | None = None
|
||||
_rule_border_right: tuple[str, Style] | None = None
|
||||
_rule_border_bottom: tuple[str, Style] | None = None
|
||||
_rule_border_left: tuple[str, Style] | None = None
|
||||
|
||||
_rule_outline_top: tuple[str, Style] | None = None
|
||||
_rule_outline_right: tuple[str, Style] | None = None
|
||||
_rule_outline_bottom: tuple[str, Style] | None = None
|
||||
_rule_outline_left: tuple[str, Style] | None = None
|
||||
|
||||
_rule_width: Scalar | None = None
|
||||
_rule_height: Scalar | None = None
|
||||
_rule_min_width: Scalar | None = None
|
||||
_rule_min_height: Scalar | None = None
|
||||
|
||||
_rule_dock: str | None = None
|
||||
_rule_docks: tuple[DockGroup, ...] | None = None
|
||||
|
||||
_rule_layers: tuple[str, ...] | None = None
|
||||
_rule_layer: str | None = None
|
||||
|
||||
_rule_transitions: dict[str, Transition] | None = None
|
||||
|
||||
_layout_required: bool = False
|
||||
_repaint_required: bool = False
|
||||
|
||||
important: set[str] = field(default_factory=set)
|
||||
ANIMATABLE = {
|
||||
"offset",
|
||||
"padding",
|
||||
"margin",
|
||||
"width",
|
||||
"height",
|
||||
"min_width",
|
||||
"min_height",
|
||||
}
|
||||
|
||||
display = StringEnumProperty(VALID_DISPLAY, "block")
|
||||
visibility = StringEnumProperty(VALID_VISIBILITY, "visible")
|
||||
@@ -131,34 +149,219 @@ class Styles:
|
||||
layers = NameListProperty()
|
||||
transitions = TransitionsProperty()
|
||||
|
||||
ANIMATABLE = {
|
||||
"offset",
|
||||
"padding",
|
||||
"margin",
|
||||
"width",
|
||||
"height",
|
||||
"min_width",
|
||||
"min_height",
|
||||
}
|
||||
@abstractmethod
|
||||
def has_rule(self, rule: str) -> bool:
|
||||
"""Check if a rule is set on this Styles object.
|
||||
|
||||
@property
|
||||
def gutter(self) -> Spacing:
|
||||
"""Get the gutter (additional space reserved for margin / padding / border).
|
||||
Args:
|
||||
rule (str): Rule name.
|
||||
|
||||
Returns:
|
||||
Spacing: Space around edges.
|
||||
bool: ``True`` if the rules is present, otherwise ``False``.
|
||||
"""
|
||||
gutter = self.margin + self.padding + self.border.spacing
|
||||
return gutter
|
||||
|
||||
@abstractmethod
|
||||
def clear_rule(self, rule: str) -> None:
|
||||
"""Removes the rule from the Styles object, as if it had never been set.
|
||||
|
||||
Args:
|
||||
rule (str): Rule name.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def get_rules(self) -> RulesMap:
|
||||
"""Get the rules in a mapping.
|
||||
|
||||
Returns:
|
||||
RulesMap: A TypedDict of the rules.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def set_rule(self, rule: str, value: object | None) -> None:
|
||||
"""Set an individual rule.
|
||||
|
||||
Args:
|
||||
rule (str): Name of rule.
|
||||
value (object): Value of rule.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def get_rule(self, rule: str, default: object = None) -> object:
|
||||
"""Get an individual rule.
|
||||
|
||||
Args:
|
||||
rule (str): Name of rule.
|
||||
default (object, optional): Default if rule does not exists. Defaults to None.
|
||||
|
||||
Returns:
|
||||
object: Rule value or default.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def refresh(self, layout: bool = False) -> None:
|
||||
"""Mark the styles are requiring a refresh.
|
||||
|
||||
Args:
|
||||
layout (bool, optional): Also require a layout. Defaults to False.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def check_refresh(self) -> tuple[bool, bool]:
|
||||
"""Check if the Styles must be refreshed.
|
||||
|
||||
Returns:
|
||||
tuple[bool, bool]: (repaint required, layout_required)
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def reset(self) -> None:
|
||||
"""Reset the rules to initial state."""
|
||||
|
||||
@abstractmethod
|
||||
def merge(self, other: StylesBase) -> None:
|
||||
"""Merge values from another Styles.
|
||||
|
||||
Args:
|
||||
other (Styles): A Styles object.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def merge_rules(self, rules: RulesMap) -> None:
|
||||
"""Merge rules in to Styles.
|
||||
|
||||
Args:
|
||||
rules (RulesMap): A mapping of rules.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def is_animatable(cls, rule: str) -> bool:
|
||||
"""Check if a given rule may be animated.
|
||||
|
||||
Args:
|
||||
rule (str): Name of the rule.
|
||||
|
||||
Returns:
|
||||
bool: ``True`` if the rule may be animated, otherwise ``False``.
|
||||
"""
|
||||
return rule in cls.ANIMATABLE
|
||||
|
||||
@classmethod
|
||||
@lru_cache(maxsize=1024)
|
||||
def parse(cls, css: str, path: str) -> Styles:
|
||||
def parse(cls, css: str, path: str, *, node: DOMNode = None) -> Styles:
|
||||
"""Parse CSS and return a Styles object.
|
||||
|
||||
Args:
|
||||
css (str): Textual CSS.
|
||||
path (str): Path or string indicating source of CSS.
|
||||
node (DOMNode, optional): Node to associate with the Styles. Defaults to None.
|
||||
|
||||
Returns:
|
||||
Styles: A Styles instance containing result of parsing CSS.
|
||||
"""
|
||||
from .parse import parse_declarations
|
||||
|
||||
styles = parse_declarations(css, path)
|
||||
styles.node = node
|
||||
return styles
|
||||
|
||||
def get_transition(self, key: str) -> Transition | None:
|
||||
if key in self.ANIMATABLE:
|
||||
return self.transitions.get(key, None)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
@rich.repr.auto
|
||||
@dataclass
|
||||
class Styles(StylesBase):
|
||||
|
||||
node: DOMNode | None = None
|
||||
|
||||
_rules: RulesMap = field(default_factory=dict)
|
||||
|
||||
_layout_required: bool = False
|
||||
_repaint_required: bool = False
|
||||
|
||||
important: set[str] = field(default_factory=set)
|
||||
|
||||
def has_rule(self, rule: str) -> bool:
|
||||
return rule in self._rules
|
||||
|
||||
def clear_rule(self, rule: str) -> None:
|
||||
self._rules.pop(rule, None)
|
||||
|
||||
def get_rules(self) -> RulesMap:
|
||||
return self._rules.copy()
|
||||
|
||||
def set_rule(self, rule: str, value: object | None) -> None:
|
||||
if value is None:
|
||||
self._rules.pop(rule, None)
|
||||
else:
|
||||
self._rules[rule] = value
|
||||
|
||||
def get_rule(self, rule: str, default: object = None) -> object:
|
||||
return self._rules.get(rule, default)
|
||||
|
||||
def refresh(self, layout: bool = False) -> None:
|
||||
self._repaint_required = True
|
||||
self._layout_required = layout
|
||||
|
||||
def check_refresh(self) -> tuple[bool, bool]:
|
||||
"""Check if the Styles must be refreshed.
|
||||
|
||||
Returns:
|
||||
tuple[bool, bool]: (repaint required, layout_required)
|
||||
"""
|
||||
result = (self._repaint_required, self._layout_required)
|
||||
self._repaint_required = self._layout_required = False
|
||||
return result
|
||||
|
||||
def reset(self) -> None:
|
||||
"""
|
||||
Reset internal style rules to ``None``, reverting to default styles.
|
||||
"""
|
||||
self._rules.clear()
|
||||
|
||||
def merge(self, other: Styles) -> None:
|
||||
"""Merge values from another Styles.
|
||||
|
||||
Args:
|
||||
other (Styles): A Styles object.
|
||||
"""
|
||||
|
||||
self._rules.update(other._rules)
|
||||
|
||||
def merge_rules(self, rules: RulesMap) -> None:
|
||||
self._rules.update(rules)
|
||||
|
||||
def extract_rules(
|
||||
self, specificity: Specificity3
|
||||
) -> list[tuple[str, Specificity4, Any]]:
|
||||
"""Extract rules from Styles object, and apply !important css specificity.
|
||||
|
||||
Args:
|
||||
specificity (Specificity3): A node specificity.
|
||||
|
||||
Returns:
|
||||
list[tuple[str, Specificity4, Any]]]: A list containing a tuple of <RULE NAME>, <SPECIFICITY> <RULE VALUE>.
|
||||
"""
|
||||
is_important = self.important.__contains__
|
||||
|
||||
rules = [
|
||||
(rule_name, (int(is_important(rule_name)), *specificity), rule_value)
|
||||
for rule_name, rule_value in self._rules.items()
|
||||
]
|
||||
|
||||
return rules
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
has_rule = self.has_rule
|
||||
for name in RULE_NAMES:
|
||||
if has_rule(name):
|
||||
yield name, getattr(self, name)
|
||||
if self.important:
|
||||
yield "important", self.important
|
||||
|
||||
def __textual_animation__(
|
||||
self,
|
||||
attribute: str,
|
||||
@@ -184,113 +387,60 @@ class Styles:
|
||||
)
|
||||
return None
|
||||
|
||||
def refresh(self, layout: bool = False) -> None:
|
||||
self._repaint_required = True
|
||||
self._layout_required = layout
|
||||
|
||||
def check_refresh(self) -> tuple[bool, bool]:
|
||||
result = (self._repaint_required, self._layout_required)
|
||||
self._repaint_required = self._layout_required = False
|
||||
return result
|
||||
|
||||
@property
|
||||
def has_border(self) -> bool:
|
||||
"""Check in a border is present."""
|
||||
return any(edge for edge, _style in self.border)
|
||||
|
||||
@property
|
||||
def has_padding(self) -> bool:
|
||||
return self._rule_padding is not None
|
||||
|
||||
@property
|
||||
def has_margin(self) -> bool:
|
||||
return self._rule_margin is not None
|
||||
|
||||
@property
|
||||
def has_outline(self) -> bool:
|
||||
"""Check if an outline is present."""
|
||||
return any(edge for edge, _style in self.outline)
|
||||
|
||||
@property
|
||||
def has_offset(self) -> bool:
|
||||
return self._rule_offset is not None
|
||||
|
||||
def get_transition(self, key: str) -> Transition | None:
|
||||
if key in self.ANIMATABLE:
|
||||
return self.transitions.get(key, None)
|
||||
else:
|
||||
return None
|
||||
|
||||
def reset(self) -> None:
|
||||
"""
|
||||
Reset internal style rules to ``None``, reverting to default styles.
|
||||
"""
|
||||
for rule_name in INTERNAL_RULE_NAMES:
|
||||
setattr(self, rule_name, None)
|
||||
|
||||
def extract_rules(
|
||||
self, specificity: Specificity3
|
||||
) -> list[tuple[str, Specificity4, Any]]:
|
||||
is_important = self.important.__contains__
|
||||
rules = [
|
||||
(
|
||||
rule_name,
|
||||
(int(is_important(rule_name)), *specificity),
|
||||
getattr(self, f"_rule_{rule_name}"),
|
||||
)
|
||||
for rule_name in RULE_NAMES
|
||||
if getattr(self, f"_rule_{rule_name}") is not None
|
||||
]
|
||||
return rules
|
||||
|
||||
def apply_rules(self, rules: Iterable[tuple[str, object]], animate: bool = False):
|
||||
if animate or self.node is None:
|
||||
for key, value in rules:
|
||||
setattr(self, f"_rule_{key}", value)
|
||||
else:
|
||||
styles = self
|
||||
is_animatable = styles.ANIMATABLE.__contains__
|
||||
for key, value in rules:
|
||||
current = getattr(styles, f"_rule_{key}")
|
||||
if current == value:
|
||||
continue
|
||||
if is_animatable(key):
|
||||
transition = styles.get_transition(key)
|
||||
if transition is None:
|
||||
setattr(styles, f"_rule_{key}", value)
|
||||
else:
|
||||
duration, easing, delay = transition
|
||||
self.node.app.animator.animate(
|
||||
styles, key, value, duration=duration, easing=easing
|
||||
)
|
||||
else:
|
||||
setattr(styles, f"_rule_{key}", value)
|
||||
|
||||
if self.node is not None:
|
||||
self.node.on_style_change()
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
for rule_name, internal_rule_name in zip(RULE_NAMES, INTERNAL_RULE_NAMES):
|
||||
if getattr(self, internal_rule_name) is not None:
|
||||
yield rule_name, getattr(self, rule_name)
|
||||
if self.important:
|
||||
yield "important", self.important
|
||||
|
||||
@classmethod
|
||||
def combine(cls, style1: Styles, style2: Styles) -> Styles:
|
||||
"""Combine rule with another to produce a new rule.
|
||||
def _get_border_css_lines(
|
||||
self, rules: RulesMap, name: str
|
||||
) -> Iterable[tuple[str, str]]:
|
||||
"""Get pairs of strings containing <RULE NAME>, <RULE VALUE> for border css declarations.
|
||||
|
||||
Args:
|
||||
style1 (Style): A style.
|
||||
style2 (Style): Second style.
|
||||
rules (RulesMap): A rules map.
|
||||
name (str): Name of rules (border or outline)
|
||||
|
||||
Returns:
|
||||
Style: New rule with attributes of style2 overriding style1
|
||||
Iterable[tuple[str, str]]: An iterable of CSS declarations.
|
||||
|
||||
"""
|
||||
result = cls()
|
||||
for name in INTERNAL_RULE_NAMES:
|
||||
setattr(result, name, getattr(style1, name) or getattr(style2, name))
|
||||
return result
|
||||
|
||||
has_rule = rules.__contains__
|
||||
get_rule = rules.__getitem__
|
||||
|
||||
has_top = has_rule(f"{name}_top")
|
||||
has_right = has_rule(f"{name}_right")
|
||||
has_bottom = has_rule(f"{name}_bottom")
|
||||
has_left = has_rule(f"{name}_left")
|
||||
if not any((has_top, has_right, has_bottom, has_left)):
|
||||
# No border related rules
|
||||
return
|
||||
|
||||
if all((has_top, has_right, has_bottom, has_left)):
|
||||
# All rules are set
|
||||
# See if we can set them with a single border: declaration
|
||||
top = get_rule(f"{name}_top")
|
||||
right = get_rule(f"{name}_right")
|
||||
bottom = get_rule(f"{name}_bottom")
|
||||
left = get_rule(f"{name}_left")
|
||||
|
||||
if top == right and right == bottom and bottom == left:
|
||||
border_type, border_color = rules[f"{name}_top"]
|
||||
yield name, f"{border_type} {border_color.name}"
|
||||
return
|
||||
|
||||
# Check for edges
|
||||
if has_top:
|
||||
border_type, border_color = rules[f"{name}_top"]
|
||||
yield f"{name}-top", f"{border_type} {border_color.name}"
|
||||
|
||||
if has_right:
|
||||
border_type, border_color = rules[f"{name}_right"]
|
||||
yield f"{name}-right", f"{border_type} {border_color.name}"
|
||||
|
||||
if has_bottom:
|
||||
border_type, border_color = rules[f"{name}_bottom"]
|
||||
yield f"{name}-bottom", f"{border_type} {border_color.name}"
|
||||
|
||||
if has_left:
|
||||
border_type, border_color = rules[f"{name}_left"]
|
||||
yield f"{name}-left", f"{border_type} {border_color.name}"
|
||||
|
||||
@property
|
||||
def css_lines(self) -> list[str]:
|
||||
@@ -303,88 +453,69 @@ class Styles:
|
||||
else:
|
||||
append(f"{name}: {value};")
|
||||
|
||||
if self._rule_display is not None:
|
||||
append_declaration("display", self._rule_display)
|
||||
if self._rule_visibility is not None:
|
||||
append_declaration("visibility", self._rule_visibility)
|
||||
if self._rule_padding is not None:
|
||||
append_declaration("padding", self._rule_padding.packed)
|
||||
if self._rule_margin is not None:
|
||||
append_declaration("margin", self._rule_margin.packed)
|
||||
rules = self.get_rules()
|
||||
get_rule = rules.get
|
||||
has_rule = rules.__contains__
|
||||
|
||||
if (
|
||||
self._rule_border_top is not None
|
||||
and self._rule_border_top == self._rule_border_right
|
||||
and self._rule_border_right == self._rule_border_bottom
|
||||
and self._rule_border_bottom == self._rule_border_left
|
||||
):
|
||||
_type, style = self._rule_border_top
|
||||
append_declaration("border", f"{_type} {style}")
|
||||
else:
|
||||
if self._rule_border_top is not None:
|
||||
_type, style = self._rule_border_top
|
||||
append_declaration("border-top", f"{_type} {style}")
|
||||
if self._rule_border_right is not None:
|
||||
_type, style = self._rule_border_right
|
||||
append_declaration("border-right", f"{_type} {style}")
|
||||
if self._rule_border_bottom is not None:
|
||||
_type, style = self._rule_border_bottom
|
||||
append_declaration("border-bottom", f"{_type} {style}")
|
||||
if self._rule_border_left is not None:
|
||||
_type, style = self._rule_border_left
|
||||
append_declaration("border-left", f"{_type} {style}")
|
||||
if has_rule("display"):
|
||||
append_declaration("display", rules["display"])
|
||||
if has_rule("visibility"):
|
||||
append_declaration("visibility", rules["visibility"])
|
||||
if has_rule("padding"):
|
||||
append_declaration("padding", rules["padding"].css)
|
||||
if has_rule("margin"):
|
||||
append_declaration("margin", rules["margin"].css)
|
||||
|
||||
if (
|
||||
self._rule_outline_top is not None
|
||||
and self._rule_outline_top == self._rule_outline_right
|
||||
and self._rule_outline_right == self._rule_outline_bottom
|
||||
and self._rule_outline_bottom == self._rule_outline_left
|
||||
):
|
||||
_type, style = self._rule_outline_top
|
||||
append_declaration("outline", f"{_type} {style}")
|
||||
else:
|
||||
if self._rule_outline_top is not None:
|
||||
_type, style = self._rule_outline_top
|
||||
append_declaration("outline-top", f"{_type} {style}")
|
||||
if self._rule_outline_right is not None:
|
||||
_type, style = self._rule_outline_right
|
||||
append_declaration("outline-right", f"{_type} {style}")
|
||||
if self._rule_outline_bottom is not None:
|
||||
_type, style = self._rule_outline_bottom
|
||||
append_declaration("outline-bottom", f"{_type} {style}")
|
||||
if self._rule_outline_left is not None:
|
||||
_type, style = self._rule_outline_left
|
||||
append_declaration("outline-left", f"{_type} {style}")
|
||||
for name, rule in self._get_border_css_lines(rules, "border"):
|
||||
append_declaration(name, rule)
|
||||
|
||||
if self.offset:
|
||||
for name, rule in self._get_border_css_lines(rules, "outline"):
|
||||
append_declaration(name, rule)
|
||||
|
||||
if has_rule("offset"):
|
||||
x, y = self.offset
|
||||
append_declaration("offset", f"{x} {y}")
|
||||
if self._rule_dock:
|
||||
append_declaration("dock-group", self._rule_dock)
|
||||
if self._rule_docks:
|
||||
if has_rule("dock"):
|
||||
append_declaration("dock", rules["dock"])
|
||||
if has_rule("docks"):
|
||||
append_declaration(
|
||||
"docks",
|
||||
" ".join(
|
||||
(f"{name}={edge}/{z}" if z else f"{name}={edge}")
|
||||
for name, edge, z in self._rule_docks
|
||||
for name, edge, z in rules["docks"]
|
||||
),
|
||||
)
|
||||
if self._rule_layers is not None:
|
||||
if has_rule("layers"):
|
||||
append_declaration("layers", " ".join(self.layers))
|
||||
if self._rule_layer is not None:
|
||||
if has_rule("layer"):
|
||||
append_declaration("layer", self.layer)
|
||||
if self._rule_text_color or self._rule_text_background or self._rule_text_style:
|
||||
append_declaration("text", str(self.text))
|
||||
if has_rule("layout"):
|
||||
assert self.layout is not None
|
||||
append_declaration("layout", self.layout.name)
|
||||
|
||||
if self._rule_width is not None:
|
||||
if (
|
||||
has_rule("text_color")
|
||||
and has_rule("text_background")
|
||||
and has_rule("text_style")
|
||||
):
|
||||
append_declaration("text", str(self.text))
|
||||
else:
|
||||
if has_rule("text_color"):
|
||||
append_declaration("text-color", self.text_color.name)
|
||||
if has_rule("text_background"):
|
||||
append_declaration("text-background", self.text_background.name)
|
||||
if has_rule("text_style"):
|
||||
append_declaration("text-style", str(get_rule("text_style")))
|
||||
|
||||
if has_rule("width"):
|
||||
append_declaration("width", str(self.width))
|
||||
if self._rule_height is not None:
|
||||
if has_rule("height"):
|
||||
append_declaration("height", str(self.height))
|
||||
if self._rule_min_width is not None:
|
||||
if has_rule("min_width"):
|
||||
append_declaration("min-width", str(self.min_width))
|
||||
if self._rule_min_height is not None:
|
||||
if has_rule("min_height"):
|
||||
append_declaration("min-height", str(self.min_height))
|
||||
if self._rule_transitions is not None:
|
||||
if has_rule("transitions"):
|
||||
append_declaration(
|
||||
"transition",
|
||||
", ".join(
|
||||
@@ -401,8 +532,89 @@ class Styles:
|
||||
return "\n".join(self.css_lines)
|
||||
|
||||
|
||||
RULE_NAMES = [name[6:] for name in dir(Styles) if name.startswith("_rule_")]
|
||||
INTERNAL_RULE_NAMES = [f"_rule_{name}" for name in RULE_NAMES]
|
||||
@rich.repr.auto
|
||||
class RenderStyles(StylesBase):
|
||||
"""Presents a combined view of two Styles object: a base Styles and inline Styles."""
|
||||
|
||||
def __init__(self, node: DOMNode, base: Styles, inline_styles: Styles) -> None:
|
||||
self.node = node
|
||||
self._base_styles = base
|
||||
self._inline_styles = inline_styles
|
||||
|
||||
@property
|
||||
def base(self) -> Styles:
|
||||
"""Quick access to base (css) style."""
|
||||
return self._base_styles
|
||||
|
||||
@property
|
||||
def inline(self) -> Styles:
|
||||
"""Quick access to the inline styles."""
|
||||
return self._inline_styles
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
for rule_name in RULE_NAMES:
|
||||
if self.has_rule(rule_name):
|
||||
yield rule_name, getattr(self, rule_name)
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Reset the inline styles."""
|
||||
self._inline_styles.reset()
|
||||
|
||||
def refresh(self, layout: bool = False) -> None:
|
||||
self._inline_styles.refresh(layout=layout)
|
||||
|
||||
def merge(self, other: Styles) -> None:
|
||||
"""Merge values from another Styles.
|
||||
|
||||
Args:
|
||||
other (Styles): A Styles object.
|
||||
"""
|
||||
self._inline_styles.merge(other)
|
||||
|
||||
def merge_rules(self, rules: RulesMap) -> None:
|
||||
self._inline_styles.merge_rules(rules)
|
||||
|
||||
def check_refresh(self) -> tuple[bool, bool]:
|
||||
"""Check if the Styles must be refreshed.
|
||||
|
||||
Returns:
|
||||
tuple[bool, bool]: (repaint required, layout_required)
|
||||
"""
|
||||
base_repaint, base_layout = self._base_styles.check_refresh()
|
||||
inline_repaint, inline_layout = self._inline_styles.check_refresh()
|
||||
result = (base_repaint or inline_repaint, base_layout or inline_layout)
|
||||
return result
|
||||
|
||||
def has_rule(self, rule: str) -> bool:
|
||||
"""Check if a rule has been set."""
|
||||
return self._inline_styles.has_rule(rule) or self._base_styles.has_rule(rule)
|
||||
|
||||
def set_rule(self, rule: str, value: object | None) -> None:
|
||||
self._inline_styles.set_rule(rule, value)
|
||||
|
||||
def get_rule(self, rule: str, default: object = None) -> object:
|
||||
if self._inline_styles.has_rule(rule):
|
||||
return self._inline_styles.get_rule(rule, default)
|
||||
return self._base_styles.get_rule(rule, default)
|
||||
|
||||
def clear_rule(self, rule_name: str) -> None:
|
||||
"""Clear a rule (from inline)."""
|
||||
self._inline_styles.clear_rule(rule_name)
|
||||
|
||||
def get_rules(self) -> RulesMap:
|
||||
"""Get rules as a dictionary"""
|
||||
rules = {**self._base_styles._rules, **self._inline_styles._rules}
|
||||
return cast(RulesMap, rules)
|
||||
|
||||
@property
|
||||
def css(self) -> str:
|
||||
"""Get the CSS for the combined styles."""
|
||||
styles = Styles()
|
||||
styles.merge(self._base_styles)
|
||||
styles.merge(self._inline_styles)
|
||||
combined_css = styles.css
|
||||
return combined_css
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
styles = Styles()
|
||||
|
||||
@@ -3,7 +3,9 @@ from __future__ import annotations
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from operator import itemgetter
|
||||
from typing import Iterable
|
||||
import os
|
||||
from typing import cast, Iterable
|
||||
|
||||
|
||||
import rich.repr
|
||||
from rich.console import Group, RenderableType
|
||||
@@ -18,8 +20,10 @@ from .errors import StylesheetError
|
||||
from .match import _check_selectors
|
||||
from .model import RuleSet
|
||||
from .parse import parse
|
||||
from .styles import RulesMap
|
||||
from .types import Specificity3, Specificity4
|
||||
from ..dom import DOMNode
|
||||
from .. import log
|
||||
|
||||
|
||||
class StylesheetParseError(Exception):
|
||||
@@ -131,7 +135,7 @@ class Stylesheet:
|
||||
if _check_selectors(selector_set.selectors, node):
|
||||
yield selector_set.specificity
|
||||
|
||||
def apply(self, node: DOMNode) -> None:
|
||||
def apply(self, node: DOMNode, animate: bool = False) -> None:
|
||||
"""Apply the stylesheet to a DOM node.
|
||||
|
||||
Args:
|
||||
@@ -140,9 +144,7 @@ class Stylesheet:
|
||||
If the same rule is defined multiple times for the node (e.g. multiple
|
||||
classes modifying the same CSS property), then only the most specific
|
||||
rule will be applied.
|
||||
|
||||
Returns:
|
||||
None
|
||||
animate (bool, optional): Animate changed rules. Defaults to ``False``.
|
||||
"""
|
||||
|
||||
# Dictionary of rule attribute names e.g. "text_background" to list of tuples.
|
||||
@@ -155,9 +157,6 @@ class Stylesheet:
|
||||
|
||||
_check_rule = self._check_rule
|
||||
|
||||
# TODO: The line below breaks inline styles and animations
|
||||
node.styles.reset()
|
||||
|
||||
# Collect default node CSS rules
|
||||
for key, default_specificity, value in node._default_rules:
|
||||
rule_attributes[key].append((default_specificity, value))
|
||||
@@ -172,18 +171,61 @@ class Stylesheet:
|
||||
|
||||
# For each rule declared for this node, keep only the most specific one
|
||||
get_first_item = itemgetter(0)
|
||||
node_rules = [
|
||||
(name, max(specificity_rules, key=get_first_item)[1])
|
||||
for name, specificity_rules in rule_attributes.items()
|
||||
]
|
||||
node_rules: RulesMap = cast(
|
||||
RulesMap,
|
||||
{
|
||||
name: max(specificity_rules, key=get_first_item)[1]
|
||||
for name, specificity_rules in rule_attributes.items()
|
||||
},
|
||||
)
|
||||
|
||||
node.styles.apply_rules(node_rules)
|
||||
self.apply_rules(node, node_rules, animate=animate)
|
||||
|
||||
def update(self, root: DOMNode) -> None:
|
||||
@classmethod
|
||||
def apply_rules(cls, node: DOMNode, rules: RulesMap, animate: bool = False) -> None:
|
||||
"""Apply style rules to a node, animating as required.
|
||||
|
||||
Args:
|
||||
node (DOMNode): A DOM node.
|
||||
rules (RulesMap): Mapping of rules.
|
||||
animate (bool, optional): Enable animation. Defaults to False.
|
||||
"""
|
||||
styles = node.styles
|
||||
if animate:
|
||||
|
||||
is_animatable = styles.is_animatable
|
||||
current_rules = styles.get_rules()
|
||||
set_rule = styles.base.set_rule
|
||||
|
||||
for key, value in rules.items():
|
||||
current = current_rules.get(key)
|
||||
if current == value:
|
||||
continue
|
||||
if is_animatable(key):
|
||||
transition = styles.get_transition(key)
|
||||
if transition is None:
|
||||
styles.base.set_rule(key, value)
|
||||
else:
|
||||
duration, easing, delay = transition
|
||||
node.app.animator.animate(
|
||||
node.styles.base,
|
||||
key,
|
||||
value,
|
||||
duration=duration,
|
||||
easing=easing,
|
||||
)
|
||||
else:
|
||||
set_rule(key, value)
|
||||
else:
|
||||
styles.base.merge_rules(rules)
|
||||
|
||||
node.on_style_change()
|
||||
|
||||
def update(self, root: DOMNode, animate: bool = False) -> None:
|
||||
"""Update a node and its children."""
|
||||
apply = self.apply
|
||||
for node in root.walk_children():
|
||||
apply(node)
|
||||
apply(node, animate=animate)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -4,7 +4,7 @@ import sys
|
||||
from typing import Tuple
|
||||
|
||||
|
||||
from rich.style import Style
|
||||
from rich.color import Color
|
||||
|
||||
if sys.version_info >= (3, 8):
|
||||
from typing import Literal
|
||||
@@ -15,6 +15,6 @@ else:
|
||||
Edge = Literal["top", "right", "bottom", "left"]
|
||||
Visibility = Literal["visible", "hidden", "initial", "inherit"]
|
||||
Display = Literal["block", "none"]
|
||||
EdgeStyle = Tuple[str, Style]
|
||||
EdgeStyle = Tuple[str, Color]
|
||||
Specificity3 = Tuple[int, int, int]
|
||||
Specificity4 = Tuple[int, int, int, int]
|
||||
|
||||
@@ -12,7 +12,8 @@ from ._node_list import NodeList
|
||||
from .css._error_tools import friendly_list
|
||||
from .css.constants import VALID_DISPLAY, VALID_VISIBILITY
|
||||
from .css.errors import StyleValueError
|
||||
from .css.styles import Styles
|
||||
from .css.styles import Styles, RenderStyles
|
||||
from .css.parse import parse_declarations
|
||||
from .message_pump import MessagePump
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -31,17 +32,22 @@ class DOMNode(MessagePump):
|
||||
|
||||
"""
|
||||
|
||||
STYLES = ""
|
||||
DEFAULT_STYLES = ""
|
||||
INLINE_STYLES = ""
|
||||
|
||||
def __init__(self, name: str | None = None, id: str | None = None) -> None:
|
||||
self._name = name
|
||||
self._id = id
|
||||
self._classes: set[str] = set()
|
||||
self.children = NodeList()
|
||||
self.styles: Styles = Styles(self)
|
||||
self._css_styles: Styles = Styles(self)
|
||||
self._inline_styles: Styles = Styles.parse(
|
||||
self.INLINE_STYLES, repr(self), node=self
|
||||
)
|
||||
self.styles = RenderStyles(self, self._css_styles, self._inline_styles)
|
||||
default_styles = Styles.parse(self.DEFAULT_STYLES, repr(self))
|
||||
self._default_rules = default_styles.extract_rules((0, 0, 0))
|
||||
super().__init__()
|
||||
self.default_styles = Styles.parse(self.STYLES, repr(self))
|
||||
self._default_rules = self.default_styles.extract_rules((0, 0, 0))
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
yield "name", self._name, None
|
||||
@@ -240,7 +246,7 @@ class DOMNode(MessagePump):
|
||||
from .widget import Widget
|
||||
|
||||
for node in self.walk_children():
|
||||
node.styles = Styles(node=node)
|
||||
node._css_styles.reset()
|
||||
if isinstance(node, Widget):
|
||||
# node.clear_render_cache()
|
||||
node._repaint_required = True
|
||||
@@ -289,23 +295,60 @@ class DOMNode(MessagePump):
|
||||
|
||||
return DOMQuery(self, selector)
|
||||
|
||||
def set_styles(self, css: str | None = None, **styles) -> None:
|
||||
"""Set custom styles on this object."""
|
||||
# TODO: This can be done more efficiently
|
||||
kwarg_css = "\n".join(
|
||||
f"{key.replace('_', '-')}: {value}" for key, value in styles.items()
|
||||
)
|
||||
apply_css = f"{css or ''}\n{kwarg_css}\n"
|
||||
new_styles = parse_declarations(apply_css, f"<custom styles for ${self!r}>")
|
||||
self.styles.merge(new_styles)
|
||||
self.refresh()
|
||||
|
||||
def has_class(self, *class_names: str) -> bool:
|
||||
"""Check if the Node has all the given class names.
|
||||
|
||||
Args:
|
||||
*class_names (str): CSS class names to check.
|
||||
|
||||
Returns:
|
||||
bool: ``True`` if the node has all the given class names, otherwise ``False``.
|
||||
"""
|
||||
return self._classes.issuperset(class_names)
|
||||
|
||||
def add_class(self, *class_names: str) -> None:
|
||||
"""Add class names."""
|
||||
"""Add class names to this Node.
|
||||
|
||||
Args:
|
||||
*class_names (str): CSS class names to add.
|
||||
|
||||
"""
|
||||
self._classes.update(class_names)
|
||||
|
||||
def remove_class(self, *class_names: str) -> None:
|
||||
"""Remove class names"""
|
||||
"""Remove class names from this Node.
|
||||
|
||||
Args:
|
||||
*class_names (str): CSS class names to remove.
|
||||
|
||||
"""
|
||||
self._classes.difference_update(class_names)
|
||||
|
||||
def toggle_class(self, *class_names: str) -> None:
|
||||
"""Toggle class names"""
|
||||
"""Toggle class names on this Node.
|
||||
|
||||
Args:
|
||||
*class_names (str): CSS class names to toggle.
|
||||
|
||||
"""
|
||||
self._classes.symmetric_difference_update(class_names)
|
||||
self.app.stylesheet.update(self.app)
|
||||
self.app.stylesheet.update(self.app, animate=True)
|
||||
|
||||
def has_pseudo_class(self, *class_names: str) -> bool:
|
||||
"""Check for pseudo class (such as hover, focus etc)"""
|
||||
has_pseudo_classes = self.pseudo_classes.issuperset(class_names)
|
||||
return has_pseudo_classes
|
||||
|
||||
def refresh(self, repaint: bool = True, layout: bool = False) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
@@ -484,6 +484,9 @@ class Spacing(NamedTuple):
|
||||
bottom: int = 0
|
||||
left: int = 0
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return self == (0, 0, 0, 0)
|
||||
|
||||
@property
|
||||
def width(self) -> int:
|
||||
"""Total space in width."""
|
||||
@@ -514,6 +517,16 @@ class Spacing(NamedTuple):
|
||||
else:
|
||||
return f"{top}, {right}, {bottom}, {left}"
|
||||
|
||||
@property
|
||||
def css(self) -> str:
|
||||
top, right, bottom, left = self
|
||||
if top == right == bottom == left:
|
||||
return f"{top}"
|
||||
if (top, right) == (bottom, left):
|
||||
return f"{top} {right}"
|
||||
else:
|
||||
return f"{top} {right} {bottom} {left}"
|
||||
|
||||
@classmethod
|
||||
def unpack(cls, pad: SpacingDimensions) -> Spacing:
|
||||
"""Unpack padding specified in CSS style."""
|
||||
|
||||
@@ -58,13 +58,14 @@ class WidgetPlacement(NamedTuple):
|
||||
account for margin.
|
||||
"""
|
||||
region, widget, order = self
|
||||
styles = widget.styles
|
||||
if styles.has_margin:
|
||||
return WidgetPlacement(
|
||||
region=region.shrink(styles.margin),
|
||||
widget=widget,
|
||||
order=order,
|
||||
)
|
||||
if widget is not None:
|
||||
styles = widget.styles
|
||||
if any(styles.margin):
|
||||
return WidgetPlacement(
|
||||
region=region.shrink(styles.margin),
|
||||
widget=widget,
|
||||
order=order,
|
||||
)
|
||||
return self
|
||||
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ class LayoutMap:
|
||||
return
|
||||
|
||||
layout_offset = Offset(0, 0)
|
||||
if widget.styles.has_offset:
|
||||
if any(widget.styles.offset):
|
||||
layout_offset = widget.styles.offset.resolve(region.size, clip.size)
|
||||
|
||||
self.widgets[widget] = RenderRegion(region + layout_offset, order, clip)
|
||||
|
||||
@@ -36,10 +36,17 @@ class Dock(NamedTuple):
|
||||
|
||||
|
||||
class DockLayout(Layout):
|
||||
"""Dock Widgets to edge of screen."""
|
||||
|
||||
name = "dock"
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._docks: list[Dock] | None = None
|
||||
|
||||
def __repr__(self):
|
||||
return "<DockLayout>"
|
||||
|
||||
def get_docks(self, view: View) -> list[Dock]:
|
||||
groups: dict[str, list[Widget]] = defaultdict(list)
|
||||
for child in view.children:
|
||||
@@ -71,17 +78,15 @@ class DockLayout(Layout):
|
||||
|
||||
return (
|
||||
DockOptions(
|
||||
styles.width.cells if styles._rule_width is not None else None,
|
||||
styles.width.fraction if styles._rule_width is not None else 1,
|
||||
styles.min_width.cells if styles._rule_min_width is not None else 1,
|
||||
styles.width.cells if styles.has_rule("width") else None,
|
||||
styles.width.fraction if styles.has_rule("width") else 1,
|
||||
styles.min_width.cells if styles.has_rule("min_width") else 1,
|
||||
)
|
||||
if edge in ("left", "right")
|
||||
else DockOptions(
|
||||
styles.height.cells if styles._rule_height is not None else None,
|
||||
styles.height.fraction if styles._rule_height is not None else 1,
|
||||
styles.min_height.cells
|
||||
if styles._rule_min_height is not None
|
||||
else 1,
|
||||
styles.height.cells if styles.has_rule("height") else None,
|
||||
styles.height.fraction if styles.has_rule("height") else 1,
|
||||
styles.min_height.cells if styles.has_rule("min_height") else 1,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ if sys.version_info >= (3, 8):
|
||||
else:
|
||||
from typing_extensions import Literal
|
||||
|
||||
LayoutName = Literal["dock", "grid", "vertical", "horizontal"]
|
||||
|
||||
LAYOUT_MAP = {
|
||||
"dock": DockLayout,
|
||||
"grid": GridLayout,
|
||||
@@ -24,7 +24,7 @@ class MissingLayout(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def get_layout(name: LayoutName) -> Layout:
|
||||
def get_layout(name: str) -> Layout:
|
||||
"""Get a named layout object.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -311,6 +311,21 @@ class MessagePump:
|
||||
else:
|
||||
return False
|
||||
|
||||
# TODO: Does dispatch_key belong on message pump?
|
||||
async def dispatch_key(self, event: events.Key) -> None:
|
||||
"""Dispatch a key event to method.
|
||||
|
||||
This method will call the method named 'key_<event.key>' if it exists.
|
||||
|
||||
Args:
|
||||
event (events.Key): A key event.
|
||||
"""
|
||||
|
||||
key_method = getattr(self, f"key_{event.key}", None)
|
||||
if key_method is not None:
|
||||
await invoke(key_method, event)
|
||||
event.prevent_default()
|
||||
|
||||
async def on_timer(self, event: events.Timer) -> None:
|
||||
event.prevent_default()
|
||||
event.stop()
|
||||
|
||||
@@ -15,9 +15,10 @@ from .widget import Widget
|
||||
|
||||
@rich.repr.auto
|
||||
class View(Widget):
|
||||
STYLES = """
|
||||
|
||||
DEFAULT_STYLES = """
|
||||
layout: dock;
|
||||
docks: main=top;
|
||||
docks: _default=top;
|
||||
"""
|
||||
|
||||
def __init__(self, name: str | None = None, id: str | None = None) -> None:
|
||||
@@ -32,13 +33,6 @@ class View(Widget):
|
||||
)
|
||||
super().__init__(name=name, id=id)
|
||||
|
||||
def __init_subclass__(
|
||||
cls, layout: Callable[[], Layout] | None = None, **kwargs
|
||||
) -> None:
|
||||
if layout is not None:
|
||||
cls.layout_factory = layout
|
||||
super().__init_subclass__(**kwargs)
|
||||
|
||||
background: Reactive[str] = Reactive("")
|
||||
scroll_x: Reactive[int] = Reactive(0)
|
||||
scroll_y: Reactive[int] = Reactive(0)
|
||||
@@ -49,11 +43,12 @@ class View(Widget):
|
||||
self.app.refresh()
|
||||
|
||||
@property
|
||||
def layout(self) -> Layout:
|
||||
"""Convenience property for accessing ``view.styles.layout``.
|
||||
def layout(self) -> Layout | None:
|
||||
"""Convenience property for accessing ``self.styles.layout``.
|
||||
|
||||
Returns: The Layout associated with this view
|
||||
"""
|
||||
|
||||
return self.styles.layout
|
||||
|
||||
@layout.setter
|
||||
@@ -92,7 +87,7 @@ class View(Widget):
|
||||
return self.app.is_mounted(widget)
|
||||
|
||||
def render(self) -> RenderableType:
|
||||
return self.layout
|
||||
return self.layout or ""
|
||||
|
||||
def get_offset(self, widget: Widget) -> Offset:
|
||||
return self.layout.get_offset(widget)
|
||||
@@ -184,6 +179,8 @@ class View(Widget):
|
||||
watch(self.app, "background", watch_background)
|
||||
|
||||
async def on_idle(self, event: events.Idle) -> None:
|
||||
if self.layout is None:
|
||||
return
|
||||
if self.layout.check_update():
|
||||
self.layout.reset_update()
|
||||
await self.refresh_layout()
|
||||
|
||||
@@ -24,7 +24,7 @@ class WindowView(View, layout=VerticalLayout):
|
||||
*,
|
||||
auto_width: bool = False,
|
||||
gutter: SpacingDimensions = (0, 0),
|
||||
name: str | None = None
|
||||
name: str | None = None,
|
||||
) -> None:
|
||||
layout = VerticalLayout(gutter=gutter, auto_width=auto_width)
|
||||
self.widget = widget if isinstance(widget, Widget) else Static(widget)
|
||||
|
||||
@@ -33,6 +33,7 @@ from .message import Message
|
||||
from .messages import Layout, Update
|
||||
from .reactive import watch
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .view import View
|
||||
|
||||
@@ -55,8 +56,8 @@ class Widget(DOMNode):
|
||||
_counts: ClassVar[dict[str, int]] = {}
|
||||
can_focus: bool = False
|
||||
|
||||
STYLES = """
|
||||
dock: _default;
|
||||
DEFAULT_STYLES = """
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, name: str | None = None, id: str | None = None) -> None:
|
||||
@@ -91,11 +92,6 @@ class Widget(DOMNode):
|
||||
pseudo_classes = self.pseudo_classes
|
||||
if pseudo_classes:
|
||||
yield "pseudo_classes", pseudo_classes
|
||||
yield "outline", self.styles.outline
|
||||
|
||||
def __rich__(self) -> RenderableType:
|
||||
renderable = self.render_styled()
|
||||
return renderable
|
||||
|
||||
def get_pseudo_classes(self) -> Iterable[str]:
|
||||
"""Pseudo classes for a widget"""
|
||||
@@ -152,7 +148,9 @@ class Widget(DOMNode):
|
||||
"""
|
||||
|
||||
renderable = self.render()
|
||||
|
||||
styles = self.styles
|
||||
|
||||
parent_text_style = self.parent.text_style
|
||||
|
||||
text_style = styles.text
|
||||
@@ -160,15 +158,15 @@ class Widget(DOMNode):
|
||||
if renderable_text_style:
|
||||
renderable = Styled(renderable, renderable_text_style)
|
||||
|
||||
if styles.has_padding:
|
||||
if styles.padding:
|
||||
renderable = Padding(
|
||||
renderable, styles.padding, style=renderable_text_style
|
||||
)
|
||||
|
||||
if styles.has_border:
|
||||
if styles.border:
|
||||
renderable = Border(renderable, styles.border, style=renderable_text_style)
|
||||
|
||||
if styles.has_outline:
|
||||
if styles.outline:
|
||||
renderable = Border(
|
||||
renderable,
|
||||
styles.outline,
|
||||
@@ -210,11 +208,11 @@ class Widget(DOMNode):
|
||||
Returns:
|
||||
Spacing: [description]
|
||||
"""
|
||||
gutter = self.styles.gutter
|
||||
styles = self.styles
|
||||
gutter = styles.margin + styles.padding + styles.border.spacing
|
||||
return gutter
|
||||
|
||||
def on_style_change(self) -> None:
|
||||
self.log("style_change", self)
|
||||
self.clear_render_cache()
|
||||
|
||||
def _update_size(self, size: Size) -> None:
|
||||
@@ -294,7 +292,7 @@ class Widget(DOMNode):
|
||||
if not self.check_message_enabled(message):
|
||||
return True
|
||||
if not self.is_running:
|
||||
self.log(self, "IS NOT RUNNING")
|
||||
self.log(self, f"IS NOT RUNNING, {message!r} not sent")
|
||||
return await super().post_message(message)
|
||||
|
||||
async def on_resize(self, event: events.Resize) -> None:
|
||||
@@ -335,19 +333,6 @@ class Widget(DOMNode):
|
||||
async def broker_event(self, event_name: str, event: events.Event) -> bool:
|
||||
return await self.app.broker_event(event_name, event, default_namespace=self)
|
||||
|
||||
async def dispatch_key(self, event: events.Key) -> None:
|
||||
"""Dispatch a key event to method.
|
||||
|
||||
This method will call the method named 'key_<event.key>' if it exists.
|
||||
|
||||
Args:
|
||||
event (events.Key): A key event.
|
||||
"""
|
||||
|
||||
key_method = getattr(self, f"key_{event.key}", None)
|
||||
if key_method is not None:
|
||||
await invoke(key_method, event)
|
||||
|
||||
async def on_mouse_down(self, event: events.MouseUp) -> None:
|
||||
await self.broker_event("mouse.down", event)
|
||||
|
||||
@@ -364,3 +349,7 @@ class Widget(DOMNode):
|
||||
async def on_leave(self, event: events.Leave) -> None:
|
||||
self._mouse_over = False
|
||||
self.app.update_styles()
|
||||
|
||||
async def on_key(self, event: events.Key) -> None:
|
||||
if await self.dispatch_key(event):
|
||||
event.prevent_default()
|
||||
|
||||
@@ -27,7 +27,7 @@ class ScrollView(View):
|
||||
name: str | None = None,
|
||||
style: StyleType = "",
|
||||
fluid: bool = True,
|
||||
gutter: SpacingDimensions = (0, 0)
|
||||
gutter: SpacingDimensions = (0, 0),
|
||||
) -> None:
|
||||
from ..views import WindowView
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from rich.color import Color
|
||||
from rich.style import Style
|
||||
|
||||
from textual.css.styles import Styles
|
||||
from textual.css.styles import Styles, RenderStyles
|
||||
|
||||
|
||||
def test_styles_reset():
|
||||
@@ -9,3 +10,119 @@ def test_styles_reset():
|
||||
assert styles.text_style == Style(bold=False)
|
||||
styles.reset()
|
||||
assert styles.text_style is Style.null()
|
||||
|
||||
|
||||
def test_has_rule():
|
||||
styles = Styles()
|
||||
assert not styles.has_rule("text_style")
|
||||
styles.text_style = "bold"
|
||||
assert styles.has_rule("text_style")
|
||||
styles.text_style = None
|
||||
assert not styles.has_rule("text_style")
|
||||
|
||||
|
||||
def test_clear_rule():
|
||||
styles = Styles()
|
||||
styles.text_style = "bold"
|
||||
assert styles.has_rule("text_style")
|
||||
styles.clear_rule("text_style")
|
||||
assert not styles.has_rule("text_style")
|
||||
|
||||
|
||||
def test_get_rules():
|
||||
styles = Styles()
|
||||
# Empty rules at start
|
||||
assert styles.get_rules() == {}
|
||||
styles.text_style = "bold"
|
||||
assert styles.get_rules() == {"text_style": Style.parse("bold")}
|
||||
styles.display = "none"
|
||||
assert styles.get_rules() == {
|
||||
"text_style": Style.parse("bold"),
|
||||
"display": "none",
|
||||
}
|
||||
|
||||
|
||||
def test_set_rule():
|
||||
styles = Styles()
|
||||
assert styles.get_rules() == {}
|
||||
styles.set_rule("text_style", Style.parse("bold"))
|
||||
assert styles.get_rules() == {"text_style": Style.parse("bold")}
|
||||
|
||||
|
||||
def test_reset():
|
||||
styles = Styles()
|
||||
assert styles.get_rules() == {}
|
||||
styles.set_rule("text_style", Style.parse("bold"))
|
||||
assert styles.get_rules() == {"text_style": Style.parse("bold")}
|
||||
styles.reset()
|
||||
assert styles.get_rules() == {}
|
||||
|
||||
|
||||
def test_merge():
|
||||
styles = Styles()
|
||||
styles.set_rule("text_style", Style.parse("bold"))
|
||||
styles2 = Styles()
|
||||
styles2.set_rule("display", "none")
|
||||
styles.merge(styles2)
|
||||
assert styles.get_rules() == {
|
||||
"text_style": Style.parse("bold"),
|
||||
"display": "none",
|
||||
}
|
||||
|
||||
|
||||
def test_merge_rules():
|
||||
styles = Styles()
|
||||
styles.set_rule("text_style", Style.parse("bold"))
|
||||
styles.merge_rules({"display": "none"})
|
||||
assert styles.get_rules() == {
|
||||
"text_style": Style.parse("bold"),
|
||||
"display": "none",
|
||||
}
|
||||
|
||||
|
||||
def test_render_styles_text():
|
||||
"""Test inline styles override base styles"""
|
||||
base = Styles()
|
||||
inline = Styles()
|
||||
styles_view = RenderStyles(None, base, inline)
|
||||
|
||||
# Both styles are empty
|
||||
assert styles_view.text == Style()
|
||||
|
||||
# Base is bold blue
|
||||
base.text_color = "blue"
|
||||
base.text_style = "bold"
|
||||
assert styles_view.text == Style.parse("bold blue")
|
||||
|
||||
# Base is bold blue, inline is red
|
||||
inline.text_color = "red"
|
||||
assert styles_view.text == Style.parse("bold red")
|
||||
|
||||
# Base is bold yellow, inline is red
|
||||
base.text_color = "yellow"
|
||||
assert styles_view.text == Style.parse("bold red")
|
||||
|
||||
# Base is bold blue
|
||||
inline.text_color = None
|
||||
assert styles_view.text == Style.parse("bold yellow")
|
||||
|
||||
|
||||
def test_render_styles_border():
|
||||
base = Styles()
|
||||
inline = Styles()
|
||||
styles_view = RenderStyles(None, base, inline)
|
||||
|
||||
base.border_top = ("heavy", "red")
|
||||
# Base has border-top: heavy red
|
||||
assert styles_view.border_top == ("heavy", Color.parse("red"))
|
||||
|
||||
inline.border_left = ("rounded", "green")
|
||||
# Base has border-top heavy red, inline has border-left: rounded green
|
||||
assert styles_view.border_top == ("heavy", Color.parse("red"))
|
||||
assert styles_view.border_left == ("rounded", Color.parse("green"))
|
||||
assert styles_view.border == (
|
||||
("heavy", Color.parse("red")),
|
||||
("", Color.default()),
|
||||
("", Color.default()),
|
||||
("rounded", Color.parse("green")),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user