Merge pull request #254 from Textualize/inline-styles-view

Inline styles view
This commit is contained in:
Will McGugan
2022-02-10 09:59:16 +00:00
committed by GitHub
31 changed files with 1224 additions and 652 deletions

View File

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

View File

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

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

View File

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

@@ -0,0 +1,7 @@
App > View {
layout: dock;
}
Widget {
text: on blue;
}

29
sandbox/local_styles.py Normal file
View 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")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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__":

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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