diff --git a/docs/guide/styles.md b/docs/guide/styles.md index 7346ced7f..2c24eb9b0 100644 --- a/docs/guide/styles.md +++ b/docs/guide/styles.md @@ -5,7 +5,7 @@ Textual provides a large number of *styles* you can use to customize how your ap ## Styles object -Every widget class in Textual provides a `styles` object which contains a number of writable attributes. Styles define the position and size of a widget, in addition to color, text style, borders, alignment and much more. +Every widget class in Textual provides a `styles` object which contains a number of writable attributes. You can write to any of these attributes and Textual will update the screen accordingly. Let's look at a simple example which sets the styles on the `screen` (a special widget that represents the screen). @@ -13,13 +13,21 @@ Let's look at a simple example which sets the styles on the `screen` (a special --8<-- "docs/examples/guide/styles/screen.py" ``` -The first line sets `screen.styles.background` to `"darkblue"` which will change the background color to dark blue. There are a few other ways of setting color which we will explore later. +The first line sets [background](../styles/background.md) to `"darkblue"` which will change the background color to dark blue. There are a few other ways of setting color which we will explore later. -The second line sets `screen.styles.border` to a tuple of `("heavy", "white")` which tells Textual to draw a white border with a style of `"heavy"`. Running this code will show the following: +The second line sets [border](../styles/border.md) to a tuple of `("heavy", "white")` which tells Textual to draw a white border with a style of `"heavy"`. Running this code will show the following: ```{.textual path="docs/examples/guide/styles/screen.py"} ``` + + +## Colors + +The [color](../styles/color.md) property set the color of text on a widget. The [background](..styles/background/md) property sets the color of the background (under the text). + + + ## Box Model @@ -29,7 +37,6 @@ The second line sets `screen.styles.border` to a tuple of `("heavy", "white")` w - TODO: Styles docs - What are styles diff --git a/docs/images/styles/border_box.excalidraw.svg b/docs/images/styles/border_box.excalidraw.svg new file mode 100644 index 000000000..0288da325 --- /dev/null +++ b/docs/images/styles/border_box.excalidraw.svg @@ -0,0 +1,16 @@ + + + eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1ba1Pbulx1MDAxNv3eX8Fwvlx1MDAxMlfS1tajM3fu0JZcdTAwMDNcZo/yvC29c4YxsUlcXJw4OFx1MDAwZa9O//vdciB2XHUwMDFlXHUwMDBlXHRcdTAwMDRcdTAwMGW3c9wpJZYs70hrrf2Q+vPd0tJydttcdJc/LC2HN3U/joLUv15ecfevwrRcdTAwMWIlbWpcdTAwMTL5527SS+t5z2aWdbpcdTAwMWbev2/56UWYdWK/XHUwMDFleldRt+fH3axcdTAwMTdEiVdPWu+jLGx1/+1+7vqt8F+dpFx1MDAxNWSpV7ykXHUwMDE2XHUwMDA2UZak/XeFcdhcbttZl0b/L31eWvqZ/yxZl4b1zG834jB/IG8qXHUwMDE5aPjo3d2knVx1MDAxYovItJCCwaBD1P1Mr8vCgFrPyeSwaHG3lnt3NVi3XHUwMDFm90785JtYvTq++97BWvHW8yiOXHUwMDBms9u4P1x1MDAxM3692UtLNnWzNLlcYr9GQdakdj5yf/BcXJBkzoBBc5r0XHUwMDFhzXbY7Vx1MDAwZT2UdPx6lN26e4xccu72Z+HDUnHnxk2BXHUwMDAyj1x1MDAwMVx1MDAxN1x1MDAwNoVEZoVcdTAwMWW03uatzJOGSy6BazTWjlx1MDAxOPYpiWkpyLA/WH5cdTAwMTWWnfn1i1x1MDAwNpnXXHUwMDBlXHUwMDA2fbLUb3c7fkpcdTAwMGJW9Lu+/8r0blx1MDAwZrRcdTAwMTGKLFx1MDAwMWOh+DbNMGo0M+pcIlx1MDAwNfeYQVx1MDAwYlx1MDAxYftvU4U1Yb4snFtrhFVcdTAwMWFcdTAwMDctzobOZpAj5K/RaW36aed+9pa77kPJfmf6Wlx0XsXDvU7g92HAXHUwMDE1mVx1MDAwYmCt4sZcZtrjqH1Bje1eXHUwMDFjXHUwMDE395L6RYGc/O6vlScgVqGsQiygXHUwMDE0aHhcdNKPITbRN8di/3gjg/39w5j5u+mPL58rXHUwMDEw202IfnPjdeSpx+Bcbo+hXHUwMDE1XHUwMDAwPC6Y1dpaXHUwMDBiiFx1MDAwNobgXG7SeFxcXHUwMDExaFx0RpbRquhKvOI5XHUwMDA0dTlcdTAwMTWvf8i6XG7PcVx1MDAxY6uA2jOIUplcdTAwMTLQXHUwMDA2MFx1MDAxNWg9QZAwTFx1MDAwMjCBXHUwMDEzYCot12BcdTAwMTmHxcP0vqHAVWm961+2T5rfNo6DW2vbvV3e0c0gXHUwMDFhjDVcdTAwMDRCP02T6+VBy6/736aQwFhtOL5cblx0pIQqXHUwMDEyKE6rr0hCZibB5eVO5+BgN1Y6uN7aSj91OE+vnybbolK2/W5zsbJN000g08A5XHUwMDE4J5k4rNtgjactXHUwMDA3Ji1aIXS1bodK6+foNuF7nFx1MDAwMlxcmlHMS7TKooRcdTAwMTdQ5mmQPzPywO5cdTAwMThxerR5XHUwMDE23Fx1MDAxZPRuzj9lx5Mhn4U3WVx08SuTh1x1MDAxZOq9MutcdTAwMGLnIZLltKJ8UURcdTAwMWGys8Qhbio5JLhcdTAwMTDkgFx1MDAxOc7uSaZP88vHPuxpJLJyXHUwMDFhiaTi1PwqJJJcdTAwMTNIJGCUQ+TbXHUwMDAxLVxu9rrhzdzKXqx70s5cdTAwMGWju1x1MDAxY1Rs6O6ffiuKc6VcdTAwMWHczpFKln6iZrJqiebKL09pN6Q3u6G4XHUwMDFkemY1jlx1MDAxYe3cvdFTYTpcdTAwMDT2LKK0YdChXHUwMDE1XHUwMDA1Qdmj1OlNPo2Zbs7iXHSSNGpEbT8+mmTnM5yZXHUwMDEyVUTkzKKSuiyaj1x1MDAxMfFiZ3v/dj/cWD84uDqIL+Fkj1x1MDAxZFaFdPU06XZrTT+rN6vIKKvIOGdgN1x1MDAwM1x1MDAxN11kR8KDXG7p4txcZlFcdTAwMTEkesiJY4ZRqmKlxEouzlx1MDAxMNhN5eLjwZ3lkimB4+w0QMJpXsLFTXVcdTAwMTdOulx1MDAxNuUucopNQCmKykyZ03ooXHUwMDBlIO3MKF27W79YY1e39f+Y3jW7+7z+5/Zxb8HuYuFcYtVcZjxKK1x1MDAwNGNSK4VajiBUeFx1MDAwMEKDYFx1MDAxMrnQ1ZnHczNlaT1KxKXkjimU6chxlDLKglx1MDAwMLQy0lBcdTAwMWGihEYxhlb3PVx1MDAwNOclnzczWnNjn4hWMlxixFx1MDAxY2gt2eGn2ceoXHUwMDFkRO3G6CNhO6hoif1u9ilptVwiXHUwMDE3O+wlUTtcdTAwMWLtkY+76tKdZuhcdTAwMDdcdTAwMTNGrmzruOGGI9Dit6VcdTAwMDI9+YfB73+tTOxduaruqk1Y0GLAd+V/5yW2tHr07oDYlkmK2UHN7n72vtfuvraPYP2m0V49jk+busVcdTAwMWFvn9jaY1xco7VcdTAwMWE5jJbAXHUwMDE0o1RK0jRZISUt0ahdXHUwMDBiJTZcdTAwMTlr3UVLTL5kQmJFXpA8JXBExaU1JZXp05pidzRkKi/MfFx1MDAxNVpbZ9Y/tFx1MDAxZetduabuXHUwMDFhW805Od0v0kwgtSlFJ6N1bcWZpr+z53ZrcWs7OIq+XHUwMDFk1m/rl4Hvd0z24+zNc1pYXHUwMDBmKIxDilx1MDAxOSWWy/h5ZofoXHRKs7WVnGshX85ZM48mXHUwMDFiKcWkl1x1MDAxOaNcdTAwMDVM8NacXHRcdTAwMGZcdTAwMTlcdTAwMTOSolxuZFx1MDAwNvlYwdC4wrxE9sqkprCOzVPWrlwidVFAediw6SdcXHvN9U7sb8LGJoq77YvvtYO9tYNSUYUyxXrPWUn+j1x1MDAwMVx1MDAxMYjckSbGXHUwMDEwVyyUujX8XHUwMDBldbKeXHUwMDA0S7NcdTAwMDTckvXswUH+KpP+deRklJhcdTAwMGJVlFo1pPLmcTQtSlWsVqN3XHUwMDA3oVx1MDAwMjGSS27VXHUwMDFjdde99s7pYXP7S+O4d9P89uVkf+vLxv+DrGiBkoGQQovhJJVQ6Vx1MDAxOUPpqZCMgS5V91x1MDAxN60qlGpYXHUwMDE0XHUwMDE0mFkgf680XHUwMDE0XHUwMDBiU1JcdTAwMTXmUTpNvoUrUnxccmOxXHUwMDAy127biil8bVlxXHUwMDBl6C3IXG6pXG6SwiFcblx1MDAxMlhDqV2RR1x1MDAwZlSFu1mUUlHKZ0GBYvib6ko1ptw1jqY5VaWqXHUwMDBlTWNVilxucsmMlmr2WGX60r9VUWHcXHUwMDEzlGoxXHUwMDE0XHUwMDFjXHUwMDE0ze2wqijpKTSSolx1MDAxNKCxXrCwgEVcdTAwMDGnVO9cdTAwMWFVXHIrKVFcdTAwMTJKP6Fu8JyNnOnuYlxiaqN7lyvTxp1cdTAwMWXdTlx1MDAxZPdcdTAwMDH306RcdTAwMGXMXFxbOVx1MDAwNVJcdTAwMWavnFx1MDAxN+M+VM43+qtWXHUwMDAyQFEzN0O9XHUwMDA3JfE4PFx1MDAxZqblcMU8S0rrNVxcLlx1MDAxZvpcdTAwMTajtfF7S6YqQlVFQstqRXDRujaoZ1eEk8OT3bXUtLauT/jOd725XHUwMDFk29pcdTAwMTO3d19PXHUwMDExQCiPXHUwMDBiq1x1MDAxOaDUypRcdTAwMDOvvNZIUYjgSlpAw5XG6mL489NcdTAwMTcpuSC2cyQkU/Q9IX2RSG6UkTrleS6UKlx1MDAwZlx1MDAwZjVcdOa2xySTTzju8PQ4Q7rEz7KnkO83r0lUL6q7amPrOaenr9xCkJVHlyiPocXCXHUwMDEy8Vx1MDAxZqP11t1cdTAwMTlcdTAwMGZa6lx1MDAxNtHf3PpY13vnXHUwMDE3Nz/eOq2VRU9cdTAwMDMh0yDl86Z0UqvPau1Rnlx1MDAwZu74XHUwMDEyR/GSVVx07nFcbiSslJqRO6dcYmPSaTtcdTAwMTeUMOWCMKNcdTAwMTBQjFx1MDAxZmOiXHUwMDE1XHUwMDA1XHUwMDE3XHUwMDEyvupcdTAwMTZcdTAwMDLxmiH+w+vx3rXqZc2bx1d0TmZXVlx1MDAwNrhQU7ZcdTAwMDcpgLekxLNvXHUwMDBmXomjjbPVeiu+vO6sXHUwMDA3h1x1MDAwN63PXCJZfevkllx1MDAxYT1cdTAwMDBKXHUwMDEwNXfTgaXIuX+00XqGUTqj89Mm7OV8Nlx1MDAwN1ej0Kglktu1JXZcdTAwMTZRvVx1MDAwN1xcXHUwMDFiXHUwMDBi5JiFoFx1MDAxMMKOb2a70ihIK17VZ4M7NSlcdTAwMTaxj1BVXHUwMDFiYN3Gx9uvja9pjcvLLfR3mq218lx1MDAxMchyyZGDMFx1MDAxNF1cdJpcdTAwMDaaU2PGS46cuY0jo1x1MDAxNFx1MDAxOPpDgenvWlx1MDAxY6hVgipvXHUwMDFkx9OilKVcXFFcdTAwMWM976xcdTAwMTVcdTAwMDUpZSY9qivXp2efv7ePLk7XcevzaXJcdTAwMWF+u9NvXVdQWk9SlM9p3t35+HFZXHUwMDExmlOe6SqPWqpcdTAwMTG7XHUwMDE2KSvG45qiRVxu5jlRXHUwMDE1JmxPMld51uRfXHUwMDE0hTckg3Ks5miJK5yjKZzB68hcbsVcXMUr/0ZZcSVHWkpaUDBcdTAwMWOtXHUwMDE0xamZQlW4x1x1MDAxNFxu6Y5ngFx1MDAwNil+V1WphpS7auNomlNVqmqOildvZDDQTCljZleV6Wv/RlVFXHUwMDFhilZoalx0XkRcdTAwMGVcdTAwMDQ+fJpJWOVxklx1MDAxM6YpYFx1MDAxNFx1MDAwMl7u0EPppP6UoiNFrGSOYfhcdTAwMDTdeE7VcXokOlx1MDAwNLa5qo7TPdHUcVx1MDAxZpBfrXYvel53vOrYXHUwMDA3b1x0XHUwMDAwf1fRsW/Iu4dZymdo2e90XHUwMDBlM5qfgXhcdTAwMTNwouD+S1x1MDAxNsMtX0Xh9cdcdP9cdTAwMDfoPL/cqPmKOjqHXHUwMDBlNj9/vfv1P6+u9moifQ== + + + + Content areaHeightWidth \ No newline at end of file diff --git a/docs/images/styles/box.excalidraw.svg b/docs/images/styles/box.excalidraw.svg index 67a960567..462f21bf5 100644 --- a/docs/images/styles/box.excalidraw.svg +++ b/docs/images/styles/box.excalidraw.svg @@ -1,6 +1,6 @@ - + - eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1cXGtTXHUwMDFhy1x1MDAxNv2eX2F5voY+/X6k6tYtJVx1MDAxYeMrKj5cdTAwMTJv3bJGXHUwMDE4ZFx1MDAxNFx1MDAxODJcZiqeyn+/u0GZN1x1MDAwMlwiYm6m6uTINDCb3nvtvVa//vmwsrJcdTAwMWH2O+7qp5VV977qNL1a4NytfrT3b92g6/ltaKKD112/XHUwMDE3VFx1MDAwN+9shGGn++nvv1tOcOOGnaZTddGt1+05zW7Yq3k+qvqtv73QbXX/bf/dd1ruvzp+q1x1MDAxNlx1MDAwNih6SMmteaFcdTAwMWZcZp/lNt2W21x1MDAwZbvw7f+B1ysr/1xm/o1ZXHUwMDE3uNXQaV813cFcdTAwMDdcdTAwMDZNkYGE8vTdfb89MNZcdTAwMThoZJpEb/C6n+FxoVuD1jqY7EYt9tZq//Te0Vx1MDAxYvtru61vfnDarbT9erVcdTAwMTY9te41m5Ww31x1MDAxY/aEU230gphN3TDwb9wzr1x1MDAxNjasXan7o8/V/NBcdTAwMWEwalx1MDAwZfzeVaPtdruJXHUwMDBm+Vx1MDAxZKfqhX17XHUwMDBm49HdYS98Wonu3MMrTlxiXCKUXGLJXHUwMDE5JZJLw0bN9lx1MDAwYijWSEtlMGNcdTAwMDJcdTAwMWJJpE6ZVvab4FxmMO0vPLhcItsunerNXHUwMDE1XHUwMDE42K6N3lx1MDAxM1x1MDAwNk6723FcdTAwMDJwWfS+u8dcdTAwMWbNXHJD3EhsmFx1MDAxMqPGhutdNUJoZYIhXHSGSFx1MDAxZGvtulx1MDAwM29cdTAwMTBMXHUwMDE4YUpHv8w+t/O1NoiL/6Y7s+FcdTAwMDSdxz5b7dpcdTAwMTcxm625XHUwMDFisaCKPtzr1Jyh84mUVFNKlFx1MDAxNjR6ZNNr30Bju9dsRvf86k1cdTAwMTQvg7u/Ps5cdTAwMTCnQqqiOKXgMs1cdTAwMDWWXHUwMDEzx+nu1tdm3d+vbtVb/aMr73b3kG52XHUwMDBi4rTrXHUwMDAz6KaO0tSnnlx1MDAwYlL2bIwqiYTRlFHBsVx1MDAxMoLJZIwqhbDgTGhOuaG6OERFndWqfGyI/sWr0q2LbHgyoZBcdTAwMTaCJ1x1MDAwMnBcdTAwMTSelGCkpFx1MDAxMYzQTHhSwI4kguv5h+djQ1x1MDAxNE8xP1e/7f5ofN86qfWNaff2SUc1at7ou1x1MDAxMsHnXHUwMDA0gX+3Omr59fjXclx1MDAwND+LxXYq+InAilx0g5WeOPpvdq+vW93g51ng3lx1MDAwN2bj+8Z5j39e+iwtqLLhZVx1MDAxOOdcdTAwMTJbXHUwMDE4JFx1MDAwMMAoh9CEXGaooM1cdTAwMThTXGZcdTAwMDBXwntekKOpXHUwMDEwiGqGXHUwMDA1kVAxuJZZJFx1MDAxMExcdTAwMTG0XGJcdTAwMDCsMITrXHUwMDE4WJ/wgKWRXHUwMDAwXGIhXHUwMDE3XHUwMDA1iKc4XHUwMDBi3fswiYBhUOycr+2d/tzaOl/fOlx1MDAwZi7uNPPu151cdTAwMThcdTAwMWU+5n/t8MNcdTAwMGZ9vfnlmLPdnftG2fdcdTAwMWFcdTAwMDfVjZsvy4mzxO+PXHUwMDEzNVJcYjGpXHI2VGI6McJuPcz4ptlXvcPP1ZtLc7h9XCK/zVx1MDAxOWFTVpjnXHUwMDAxXHUwMDA2XHUwMDExibhcdTAwMTJaW3xpQFFcdTAwMDJggnCoMJRAq+HQXHUwMDE5vFx1MDAxMGAvJUGSZlx1MDAxMZWtKFx1MDAwNFx1MDAwYklcdTAwMTWj9Fx1MDAxNUrKPIMxcrrfXHUwMDBlK97DgLDgxN1Np+U1+1x0v1xyolx1MDAxNCzdc4Irr1x1MDAxZO/Lrlx1MDAwYs9cdTAwMWMkfp1491rTu7JxvNp068lcdTAwMDBcdTAwMGY90Fxyo+bQj/3yKjzdga9cdTAwMGK+1tK/wlx1MDAwZjx4stM8TloyXHUwMDEztljMfSlsaVBcdTAwMThcdTAwMWPTyaHV77Z3Koe161xyp/F1a/dGtlx1MDAwZm+7jVx1MDAwNVx1MDAxNi88I7YoUlx1MDAxNHNcdTAwMDZyilx1MDAwMXhEXHUwMDAyW5xzxCVUXHLLnizy1KuBS/FcdMGlQINcdTAwMTi8YL7mqcb2bXW7/HWjzdbczo9mKC6O36yOvFxmulx1MDAwN06t5rWvllx1MDAwMbtPpsxcdTAwMDReodM3R7rLUi3gOHJy4ZXPMpZcdTAwMWS9gtFx1FNyJJRcdTAwMDA2aCRcdTAwMTWquDC+mHnyPNVcdTAwMTVcclY8glx1MDAxN3JccmNYy9+oMLJcZrrK0FxmVq1AVzn5XHUwMDEwM/lcdTAwMTCrwqfcYFxmyFperVx1MDAxNldhSZw9J57S0EvYOVx1MDAxNn/jXHUwMDA1ICusoERcdTAwMThqXGKVU1xiwJ2jq8bW5ea+6TeuT6/2j1x1MDAwM/H9/q5cdTAwMDCF1cDvdktcciesNoqQmI7416Oog1FcdTAwMTBtXHUwMDA0IdhIQ1xiS1wikSqOlILyiTE1QCxMMVx1MDAxNidcdTAwMThcdTAwMDZcdTAwMTmLxdmHQjRcdTAwMDc5XHUwMDAxon1hym/o8/PPXHUwMDE3zvrDWrV5UvnSb+1dr1x1MDAxZFx1MDAxZF7zyUrrWOm3t32/Yfz69qZcYo5LP+6bXHUwMDBm3p66WU7pN3x+nvZjsSHfdJFcdTAwMDNogZhjfHKKOr6rp4ZcdTAwMTddXHUwMDE4vICcXCLDsYb6QYxcdTAwMTKxjDJgqdAqgFx1MDAxNEpuhO1cdTAwMTXJXG7R9VKWXG5sWUpccjWMYWmRrLNcdTAwMTDjXHUwMDE0XHQwQVhcdTAwMTKiXHUwMDE1ZpkySLAkXHUwMDEyrIzJyYmBNjB1tjpcYqSZxoyZolx1MDAwZXZDJ1xi1732gKp9imHtaYpnWIB6on/aXHUwMDEzZXzj4cpZS7aub9WpXHUwMDFmXHUwMDAzXHUwMDFjVM5qb8B1kOKCMFxuXHRRYMiWMvaeK6cz6FwicDeDLlwiRnDFJeOP71x1MDAxOEF+1W3XnjdpfD1JmESogZxpraFcdTAwMThcdTAwMTOjedYoZDSWmHBcdTAwMDX+J1x1MDAxOMzKXHUwMDE41XS6YdlvtTxL9Fx1MDAwZXyvXHUwMDFkprt40JdrXHUwMDE271xy18nwY/hR8bZ0YujYb0wm1OivlVxiM4NcdTAwMTejv//7MffdhaFsr1ImiqOv+1x1MDAxMP//tKRd6mK+gMFcYspccpt8Wm98yC0koc1G3KnSyFx1MDAxOEWEXCKUU81ISndLbDOa4phDutImJmTmzVx1MDAxN0zOoFZ2WFhzXHUwMDAxVE7IyDXLzlxyXqGG51xuXHUwMDAznbg7Rnav+0Etzu9jkiDGxVx1MDAxNqG6XHUwMDFmLZlcdTAwMTG/41Q3XHUwMDEwT8WnwO/ORb1U3Tre3ehUdvuXwutcXFx1MDAxZV5ezI7fuSlv+Sx+KUGKXHUwMDE5SrRcdTAwMDRcdTAwMTBcdTAwMDMziVx1MDAwNsaGfFx1MDAxZlxuXG7WXHUwMDE4uIqWUE5EyrDiXHUwMDE5zeJZT8ay4CVcImfULItewoFcIlx1MDAxOShcXFx1MDAwYqb2l0fkbK/14PdP27h8UDnZrOzv9t4rfEeOWVxuXGJH1symLFxiXHUwMDExJH17VIhBJVx1MDAxMqWMmFxcuI/39FIrXHUwMDBiiYjE3Fx1MDAxMMYok1witXpcdTAwMDFajVx1MDAwMVx1MDAxNSZccrUjaC9aYDNcdTAwMGXHTCCjlFwiXHUwMDEyXHUwMDEyXG4jXGbnSHdCXHUwMDExXHUwMDAx6Vx1MDAwZTiGXHUwMDFjbCSJ9dEjyjU4zkihXHUwMDE3qytmx+GEumJ8kYiR+Fx1MDAxMkZMXHUwMDE4XG6dXHUwMDA0+sJoUFx1MDAxNyr2rpGy4KBcdTAwMTFcdTAwMTXoL5CToC3GXHUwMDBii+TPeEfsvjiiXHUwMDA2relYmlx1MDAxM7tcdTAwMTex5ULpuWqBbVKZYkLt4lx1MDAwZXfLZ0d7vb1Gy7Tl/jbtbIh3Qe5ccmRcclxu5F1DPoFQZOlheYGAXHUwMDE5XHUwMDE40JiSXHUwMDEznbZrqmH5MTlFRuPMY4k9XHUwMDAzrCi94Pm0s0vuXHUwMDFlfD3bd262tsqnJ1psV65cdTAwMGbeKTNcdTAwMTg6Y1x0SMHQkNn4gC5GLlRcdTAwMTVccplkXG5aP967y8xcdTAwMDaUQoBcdTAwMDeppJ1mZsYkaT3TXGaBVCeYXHUwMDAzVVCU8Vx1MDAxN02Hj8EuxVx1MDAwNHFI4VxulL9cdTAwMDTurvJWniA7NkaJXHUwMDFkXHUwMDFmXHUwMDEzVJv4nMIjIZBcdTAwMTj0mFx1MDAxMWRcdTAwMDbVvsyEYHxhWImP6mHoPEYgIUNcdTAwMDGEVCdM7F2PhMD2tKZSWYZnuKRPI12/XHUwMDFiIyiOKXuVsuE0JScollx1MDAxYax4zIBQTIGC4ykmMcbP60w1U784SqA0QZZoSVxiMUV5ilx1MDAxMVxiXG5cIoQrXHUwMDA2osxcdTAwMTBwzOstYcOIgmc1XHUwMDExXG5YiIbcQPNcdTAwMDZcdTAwMTCYhVx1MDAwNLdrbYBAKp5dhSOYXHUwMDAyuYT5O00saVxmznFcIlx1MDAwMTSIwlx1MDAwNuql5JLZNYtcIka8XHUwMDFmU45ClpNDcWEgRTDAMZNx3lNeXHUwMDE5XHUwMDEzUvYqZaJpbnmFiMK5XHUwMDA0xiRcdTAwMDZSqydfXHUwMDAwdFDX9/t6v3lZXHUwMDBlr4+Pf3yvqm595z2kXHUwMDE1q+Cw4lx1MDAwNHI6T9JcdTAwMTVQY4hjybghXHUwMDE4OoRcdTAwMTcrjZenXHUwMDE1XHUwMDEwk5DENTdcZmKfx7bERFmFIKWhsGBpp85cYpdZvlwiwFgoXHUwMDA3s6ycXe608m5JQ7Fj7ZV16ZTYXHUwMDFl6KRcdTAwMWNo0zGDk1xmniNcdTAwMDUjk1x1MDAwZk6ub26c3mFXqPstsbl97fyo+rL/toven51gMFQjXHUwMDEwXHUwMDFhoDNcYlx1MDAxNFxiLpNr3lx1MDAwN+uJNNacXHUwMDAzuYesS4rnXHUwMDA3X1xubYFcdTAwMTFJb+2LxiRReqVRNFx1MDAxNqkw5szMsO3v/1x1MDAxN8qJtrniOMeL9lx1MDAxYflvXrg1Jn03mlx1MDAxZJRAiuC/yXHLtn/eVapcdTAwMDbj3tntaaOmyOH6N/a2uFx1MDAxNZPgVlx1MDAxYlx1MDAwNrpT5uCWa4NcdTAwMDBcdTAwMTOUmFfHLdd225mQNFx1MDAwN7ZcdTAwMTgxO2+Zh1tcdTAwMGW4XHUwMDE1Mr5m81x1MDAwZm7fXHUwMDEwt1kv2qtcdTAwMTQ5cF5kmuFCja44o1x1MDAxMMtmcuB+vq9L9/xnqdzwNk4uWp8/n1xcXHUwMDFlbbwtcJ/n0oZcdTAwMWJkXHUwMDE4JXDlIJeBJzhcdTAwMDCDXG6r7F5cdTAwMTW5XHUwMDE4XHUwMDE1brSXXGZRO1x1MDAxMsuyyKVglVwiVPxcdTAwMDHu8nDnlFx1MDAxZu1VirlwbtiNTaql2TJQZYnpNPvYSO2A3F1cdTAwMWVWN79ULq/K5btcdTAwMWaBONlbfvBqhDG14/J5ZZdcdTAwMTCbRolQ+LXLLrWnXHUwMDFk5JNlyVx1MDAxMeRcdTAwMTiaV3btuKtcdTAwMTZS/WHLy1x1MDAwMt6MXHUwMDFm7Vx1MDAxNfPglNgt3ICKXHUwMDBih7DsuFxyljymoJ5Drv/9ZPfUXHKuO6dH9/2Nh5299UZXLj1yKUdMMUFcdTAwMTTnOWVXqkWVXZEzX57dJGM0XHUwMDA1PqbNb7SHLTujvTX8/bGufKtccqKPlozFVqFcdTAwMTiVxZu7mV2sXHUwMDFhX3vxXHUwMDFjtn6cXZ3cVPd2XflcdTAwMTB2KsTcX4Vft5ZcXItypUEniGhcZil5fFx1MDAxNDFcdTAwMWNpK/XoXHUwMDAyqmLx4VFcXCC7MyWP01x1MDAxMiG0wJoueKfMO6+Lr6dGs260VynmwSnLYiF0WeHZb1x1MDAwNCubfjmdfGqn12jfXHUwMDExzyE/N8/ILt8258dtv2hX6bJgVzOOhNZcdTAwMDDdPDVKXGaBskjVXHUwMDAyxpEogs7OXHUwMDA3LkFYp1x1MDAxYUcrT4DQcqrVbzdB/F6Bm/STvUox/01cdNtiJSqKz8LDRmi7XHUwMDE4dfKa29i+uTzslEpn3YetSrnE+mq/VLRddWn4LPxEpFx1MDAwYvksUVx1MDAxMjHDyVx1MDAwMvgsocrO/uaN/4JcdTAwMWG2XHUwMDA0KGdcdTAwMGY445xrTeifgrssQrSU8ePw7siFc8OuLq65zDCqXHUwMDE5m2JcdTAwMTRJuuTu7HD9rNz7eVLvXHUwMDA2lfJcdHbpsmNXUYLsyk6TO3lcdTAwMDOIhlZcdTAwMDXi//Wxi1x1MDAxOUpz4tjsXHJJ4vqp6Fx1MDAxMlwisMDxtXR/wPvGuzCyflx1MDAxY3zu0YNTQrfwhEBaXFx1tWR279/kczfHur7zcL59deR+31jfXHScs4vKbdGWzKVcdTAwMDGu5Fx1MDAxNFm8XG5u8oruYDf1YlZLRFx1MDAxMnvcKWZcdTAwMWFjXHJUaJadWu9nXHUwMDE0aVx1MDAxOFx1MDAwNrGufKtBpKEhs1VFoopcdTAwMGa4JdiAXHUwMDFhJWzyXHUwMDEx2vHHkS7tXCJDSGJQXHUwMDEzXHUwMDE5NUxcdTAwMDN5Ta5d5kIhYlxmlYPt0EJcdTAwMTSfvVx1MDAwMmnQ4S+aXHUwMDE3JYoquFxmMVCpNc5bu0xcdTAwMDWIXHUwMDFhrKix54EqsCezytBIw7A9mHjxVZLMgr1cdTAwMTevXVx1MDAxZX8w2Eps7TJcYkJMIDFcdTAwMTlcciTCYKmyJ7NwZFx1MDAxM5c9013Yk7OFeCpi73XxcmFM2auUXHKnedFtwlx1MDAwYkenXHJ0PlViipmf8cdTLm1eoSBsXGa2M59GyFxiXHUwMDFjw1x1MDAxM1BcYlwiWnNMXHUwMDE0ljp+iPj804rdXHUwMDE4JCmwXHUwMDA0jYWIb2qIXHUwMDA0s0aSYpD2zO4r1oxkXHUwMDBmdVx1MDAwMjOBYyg6w1x1MDAxMVx1MDAwYsuQVoqSx/hTbZPJg2NcdTAwMTBGWlKloWom9lx1MDAxNz3ttVx1MDAxMna1uiRcXFx0Yey5lL/r7utScVDZK1x1MDAxYk5FWeXD4yNWnU6nXHUwMDEygu9HroLw8mqPnCz6nau3nnu3nrPXsD64LFx0XHUwMDFh9LVNXHSu/bX//Prw63+/jFx1MDAxMT4ifQ== + eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1cXGtT28hcdTAwMTL9nl9BsV9j7Ty655GqW7dcdTAwMDKBXHUwMDEwXlx04ZVwayslbIFcdTAwMDW25cgyr6389+2RiS1blrDBJiZ39Vx1MDAwMWxJllozfU6f7pnR36+WlpaT23aw/GZpObip+o2wXHUwMDE2+9fLr93+qyDuhFGLXHUwMDBlifR7J+rG1fTMepK0O2/+/LPpx5dB0m741cC7XG47Xb/RSbq1MPKqUfPPMFx0mp3/ur+7fjP4Tztq1pLYXHUwMDFi3KRcdTAwMTLUwiSKe/dcblx1MDAxYUEzaCVcdTAwMWS6+v/o+9LS3+nfjHVxUE381nkjSH+QXHUwMDFlXHUwMDFhXHUwMDE4XGLSjO7djVqpsVxcglGggGH/jLDzju6XXHUwMDA0NTp8RjZcdTAwMDeDI27X8u3RjW/Wdt9uNz9G8VFnv1x1MDAxNZ1Va4PbnoWNxn5y2+g1hV+td+OMUZ0kji6D47CW1N3dR/b3f1eLXHUwMDEyZ0D/cFx1MDAxY3XP662g01x1MDAxOfpR1ParYXLr9jHW39trhjdLgz03rlxymPJcdTAwMTCVZYxzJZmyqn84vVx1MDAwMDDPMLSccYtcdTAwMWGFhlx1MDAxMdNWo1x1MDAwNvVcdTAwMDaZ9lx1MDAwN0u3gW2nfvXynFxmbNX65ySx3+q0/Zj6bHDe9f1DI7Oe1EYoYVBcdTAwMWErXHUwMDA3z1NcdTAwMGbC83rijFx1MDAxNdxjXHUwMDA2rdTYu9vA2E6Qdlxm2Sk5XWXwkM6E9oda6iN/jbZr3Y/b98233HFfMuY7y9dGXHUwMDFkLOtkmb7/dGZuds1u43Q1uTg4+PqlqjtnW/1rXHJ5pFx1MDAxZsfR9XL/yI/7T1x1MDAwM9O67Zrf8zKuqC2oKYzhdtBcdTAwMWGNsHVJXHUwMDA3W91GY7Avql5cdTAwMGVcdTAwMWMz3fvj9SNcdTAwMTChtCpEhLLUtIxcZppcdTAwMThcdTAwMTHbXHUwMDFiXHUwMDFmXHUwMDFhZ9FudeOsefv5PLza3lx1MDAxM+udXHUwMDAyRHRcIsL31HhcdTAwMTj51UNwkFx1MDAwZqJBXHUwMDEzXHUwMDFhrFx1MDAxMVIgMI0oh9EghPGUYlxca6RzTPbwKFx1MDAxYfBM1qpQioY/oKqCM8wjQaL2XGZcIiijMVx1MDAwZlx1MDAwMoHWXHUwMDEz1irDQEomMFx1MDAwZlx1MDAwMiElKo5gnlx1MDAxN1x1MDAwNNWP21/rXzZcdTAwMGVrt9a2uru8reu18Fx1MDAwNYJcdTAwMDBUIVxiJFxuI0BpPTFcdTAwMDYuty8ump34+3FcdTAwMWPcxHbty9pJXHUwMDE33j0uKojCqOB36rONXG4ohac1oEIrtDLIh2GgyVx1MDAwM40x2loukGsuXHUwMDBiYVx1MDAxMFBbPSUokHvnXHUwMDExwDOe/ZP3paFcYlx1MDAwNkLBc/n8T1dKgptk2Ml7XHUwMDFkv3Xydufo+8bGycrGSfzt2sjwZsXPuPzr8Zft/fju1qy/P1x1MDAwMLm9dVNfjcL6p+ra5fvFhNLQ82fVn1x1MDAxMkUoXHUwMDEyqKSgTpw8klxchUzCut3V3b131ctTu7d5qD7OWFtNXHUwMDE5S1x1MDAxZVx1MDAwNpGy1lx1MDAwM1x1MDAwNFBcXChpjVx1MDAxNUMgQi49Y1x1MDAxNLdcdTAwMTaNol7BQlxmPVVYZTpcIlx1MDAxM0Tyylx0ldCSQtzsXHUwMDExVOaM9PhcdTAwMTamcMZBp0etZD+8S1x1MDAxZIpccu1d95th43ao31IvJUt3/Pg8bGXbslx1MDAxM9A9U4Fjhs5+21xiz51cdTAwMWYvN4KzYVx1MDAwN09CSkb6h5Mo8+RVurtPl4s/1EafXCKKQ7qz3zhcdTAwMTi25FHYojBUhC1ClZGUtkyOrdtOa2t/r3ax5tc/bGxfqtbeVaf+jHlcdTAwMGJ7JLgoQlx0J4FcZpeKXHUwMDAxXHUwMDBlgVx1MDAwYlx1MDAwMDzCXHUwMDFkXHUwMDEzylIuIYXVc0NXJiMqR5emtMWyZ9Zkoa5vXlU3Vz+steTboP21keC3g19cdTAwMTZInobdT36tXHUwMDE2ts5cdTAwMTdcdTAwMDG8P015XFxkZDC6t59kXHRlKFx1MDAxNZdqcviO11x1MDAxOYtcdTAwMGVflLpEYErgnn4mgVx0Y1x1MDAwNKbI3K+HX6JcdTAwMWJKtihQP29wnCfAZFx1MDAwZWCrdJisWqKm8sejzI5HWZV+XHUwMDE1xCU4a4a1WjbZXHUwMDFhhtpDSdIo+obsLIVgeZ5neFx1MDAxMVx1MDAwZVExw4HzyfO8rc/n9Y3T9V17W784Ot89iPHLzXVcdTAwMDFcZqtx1OlU6n5SrVx1MDAxN0FxtMw2P5WaljyMRc6ZVZZzXHRDUFx1MDAxNEJ5lFihXHUwMDExVilupCouXHUwMDAwTlDyKIXiw2VcdTAwMGbLgal8cEU0pIhIvz5vbD15981fuXtbbVx1MDAxY+6/v23uXFy8/bx3XHUwMDAxk8XW0uRvZ/NmzUZnm+tcdTAwMThcdTAwMWZUvt407sJcdTAwMWR9uZjJX+/+47AlUFx1MDAxN4FLkCwzlPSoydFV3tRTo6uwkjJzdKHVXHUwMDFlXGJcblx1MDAxZcwwY8mzh3NAQUdBk4S1rmv4/HJAyT1cdTAwMDFcdTAwMTRMgWIpcEo781x1MDAwMJPGQ04mUmpupGVcdTAwMDJGYcaZ4koolcknJ8ZZaupzXHUwMDA3wU7ix8lK2Eql2ptcZtR+XHUwMDBlXHUwMDFj9aJPXHUwMDE3b4+6uMouQ7Z/3FTNiyt9XHUwMDE0ZfBGYbPaTV3AY1xmLEMw1Fx1MDAxN8Iymznn3G+nTeRxbUFcdTAwMTOT0mlA/X5/Rlx1MDAxZvHLQav2sEnl0SRjUoV5woCVXHUwMDFjycPIxcxcdTAwMDBUfaOEZy2ZQ1lcdTAwMTCdp5VCnTOq4XeS1ajZXGadzvtcdTAwMTSFrWS0idO2fOvgXlx1MDAwZvycPqaHylx1MDAxZVx1MDAxYuWFtrviMJ9cdTAwMGU+LVxyIJN+6X/+6/XYs1x1MDAwYl3ZbZWcXHUwMDE3XHUwMDBmLvcq+39a0c5lyWChMJpbRDH5YGG5zz1cdTAwMGKhPVK4i7T5NelcdTAwMDVuQGRcdTAwMWGlV9RcdTAwMDKP5LyQJFx1MDAxNsjTtFx1MDAxObFrhkWtQbAvSbtccqDLIJ6vKvxkZTCHXGI+TVKQz7pXoriW1fa/Lum+t+RxgoRTrC3CL1iQ0lxuPnj4h+BbLtFmM6ozc+hqwz3SIJxybtBcdTAwMDJGhD6S0OekRTgnyHAt5jfQTyFcdTAwMGKVcqGK2Fx1MDAxYoxcdTAwMThTniaOl3SOXHUwMDA0JslOV+LLiVx1MDAxMZRaSsHgXHUwMDEx0H6qXHUwMDE4XHUwMDExj1x1MDAwMd+IXHUwMDE4XHUwMDE5XHKfM9BcdTAwMDS9aG88o1x1MDAwNKOWsUyRXHUwMDAygaxIyahcdTAwMDbNrNFagZJcdTAwMWFccmVdL1pcdTAwMTJcdTAwMTR6lNsqeWeaUlx1MDAxM1x1MDAxNJOKzNSdR0VcdTAwMDFcIoGIgs/kpbzyWSRcdTAwMGLMKtT0lmng1PYwkuFI5lHOLkmwMSlcdTAwMTWM2jVLVlF0XHUwMDBmSNUweTZkssssrWgjyFZF1ipcdTAwMGUqn+MgXHUwMDE5S6rmMaNgL5NWyqetZUiDOlx1MDAxMixcdTAwMTKSXHUwMDA010icMWi8QfrzwlmkyIPclvedKVkkXHUwMDE1TWNIhMBTnFhcYpLSQmXqrlx1MDAwZnHIyvra0TVcdTAwMGJQ32zg+uaF/7VcdTAwMWGp20VcdTAwMWYpN+CilpagybOIMlx1MDAwN4+bliBccvPI8ajVZVqkZPOcg+hxpe34XG4k90aLkz9Jw3DNXHUwMDE4ULr5u5HG8MVmi+WhYzNcdTAwMDXymF50W7//Zlx1MDAwNFxcYsNC4Fx1MDAxYaSMwko2eUohN79f71ctY93jq6N6TfO9lY9y8YFrPSus5oCONM3wdEkgzuScWsFabbRQ8ytvOv5AjtmxgT5smSdTYThcdTAwMDa3QLjFoTLFv7j9hbjN96LbKoNcdTAwMGWclW43WDzNWVA8dnF+cuS+uzlTwcn3ymo9XFw7/NZ89+7w9PPawiNXXHUwMDBiT1NSJEA4ZOJwXHUwMDFkT0rwLICbRqxJ8vB5XHUwMDE2XHUwMDAzXHUwMDE0R1wiXHRqcaOstTCmrlx1MDAwN+hcdTAwMTFsmEWQjFJWY3NIXHUwMDE2oFCTcHihQC7S5tGXw+2jIL5oXHUwMDFmfb65Xbvb2lmpd1TBMFx1MDAwMCOBRCFcdTAwMDctXHUwMDFht5BDjFx1MDAxZJxcdTAwMTCccjSJXGalXHUwMDFkk/I/XHUwMDFihcxVxleKXSo9nPemWdGKtcW0QulcdTAwMTKlXHUwMDBlZopqXHUwMDAwr33i16d71fX3+6fnq6vXX2M83Fl8WuGeqzu50SjL+ei8XHUwMDFlbTxFXFyuOVx0eSBKn59cIuCe0JpxwziCppRNjilcdTAwMDdcdTAwMDB4RH3WXGJcdTAwMDVcXJFcdTAwMGXI0Yp2s5RR6Vx1MDAxN6rrn0wrpKPdMJsmkDBArlx0TTlWMVx1MDAxZVx1MDAxMo60m3tPJ1r1u9JKsUO5LedKU3JK0bBjiVKhTFkppnByoVLe74vKKNS0ylx1MDAxMFVYjciyk0Z6M1x1MDAwNdHjiNath1x1MDAwMmGVXHUwMDFksWuGpYHBpUtcdTAwMDZcdTAwMWPJXHUwMDAzSMFcdTAwMTKEp2eMp1xmOJbHiiFPm2oqUrm0Lb3uT7efXHUwMDA3zz1qIHOj12tcdTAwMTlcdTAwMDf4VVx1MDAwM5n3lpRcdTAwMTJCUdHBXHUwMDE2z6tcdTAwMDJcdTAwMGVcZqxcdTAwMTCTXHUwMDEzwtfj88PL6s52oO6S9j63N+fJh41FJ1x1MDAwNLc2XHJcdTAwMThS1JGcNNXIslx1MDAxYcEsJZHE1FKRXHUwMDEwnuOMKlKZdG+OQljQSJ/GXHRcZvRcdTAwMTizXHUwMDE0PpFcdTAwMGVcdTAwMGLDTL5cdTAwMDRBmVx1MDAxNzIjfsWcqn9LXHUwMDEwY84u7FW3VfJcdTAwMWQ6ZZwvXHUwMDFjXHUwMDA0yCRcdTAwMDa56ZIkqil/YJOnXHUwMDBl3Xrrmoc+/75+zLdh055cdTAwMWO0oqLJyFx1MDAwYoNrbZiHXHUwMDAyXHUwMDE4o+Y3XHUwMDEyYGTpNVOekJorTlmUIaE5z5KEsdpcdTAwMTiFunezsSVcdPdcIlx1MDAwMlKBXHUwMDFh3Upxw/NcdTAwMDOJinJcdTAwMDdcdTAwMTBG/3bzXHUwMDEzXiqwizrVbZVcXH9OieviKVx1MDAwMtlXiOTWwYJcdTAwMTBcdTAwMTTEJlx1MDAwZtj1zcvTvXalcty529hfrchbvVspmlx1MDAwN70wwFbceK4klVx1MDAwZYqg0MNL9bhlnktcdTAwMWItolx1MDAwNlxyXGJzXFyqZzzBOEhGKGEoM6ovM8anuVx1MDAxMVZcdTAwMTJcdTAwMDNJLSznuZXmxExAIFx1MDAxMy80YFx1MDAxN9VcdTAwMDRcdTAwMGXM2dbdyeb55+DL2spW7Fx1MDAxZn/bv/pWUGp0s8BcdFwigrN0iGuwWmopU2p084yZRVx1MDAwNtJlRPcn/G5FgUqhS6VH8940M1rhxbVGY6RcIiDJySdccqiAX1x1MDAxZu+tXHUwMDFjr3a/XHUwMDFmnnXi/dVDXHUwMDE2iIWnXHUwMDE1KzxLdI3SME2OOEIrxlLjO+9cdTAwMTNaS6uKV1x1MDAxMD6ZVbRHXGLlyoJgjKThmPXAzNNCgVx1MDAxMtqtsDdcdTAwMWN5Plx1MDAwZqBUhSFj8P9KK8yjqEwxgFpAuHmJ0uRcdTAwMTcycO5xNzfLpXdcdTAwMTRXtc0vZPg9aKXQpdxWyXvTlKxSVGzUxVx1MDAwYlwipau/cVx1MDAwM5OvcCjv+UXlXHUwMDE0YTxDop1SO6lNdsp4+nutPGXAXHUwMDEyZkiukCvOj1Qy6WDZW1x1MDAwNVxmI61qlHlcdTAwMDRrPKXcWC5Dh3xtqnJjeVx1MDAxYyq97k/Hn1x1MDAwN9c9qtzYc96MXHUwMDAz/KpqY8+Qx8lcZuCFL1x1MDAxYeHpm/ok2skpofytRlx1MDAwYju/WXpcdTAwMTRq3EtEpJFcdTAwMTaGV01cdTAwMDBqj1srlHZcdTAwMGJIXHUwMDEwi1x1MDAxOUGC9OFJVVx0xt0kQya5MFx1MDAxNlx1MDAxMO24XHUwMDE3jzhJhKiUm/xGIVLm0lx1MDAxN6tIslOvvdAhzennN5e/W6CvLZRnXHUwMDE1Z259u9KKXHUwMDAxZIT8IPVxXHUwMDBiQDlcdTAwMTGuNSRTLKMzc1x1MDAwMuQlyYxKiU+lx/PuNKXSKM5fdFx0sYBcdTAwMWJltZOvXHUwMDBlL3/JzcLyilx1MDAxYsawzE0wsKiGJ0qAXCKpa1xmMK5JXHUwMDE2s5J1lE+nXHUwMDE1XHUwMDAz6Zx+xSiBxex6qkFRxHhKMPf2XHUwMDA3N8JNwjS/XHUwMDFhi8x001x1MDAwMMQjXsGwXGK0Urg4ovTdWMPUXHUwMDAw6WxcItLkhnRixr9cdTAwMDfpXHUwMDBiuoUyioMmoKHC/lxugt8tfalcdTAwMTQ7ldvy7lTEKq/ub7Hst9v7XHT1fb+ryL3C2r0mXHUwMDFiPOfyVVx1MDAxOFxcr4x50+pZujlcdTAwMTGUtrWjhMA97d8/Xv34XHUwMDA3vuEvfiJ9 - MarginPaddingContent areaBorderBackgroundColorHeightWidth \ No newline at end of file + MarginPaddingContent areaBorderHeightWidth \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index c13b73b6b..e15f9e5d9 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -124,6 +124,9 @@ markdown_extensions: - name: textual class: textual format: !!python/name:textual._doc.format_svg + - name: rich + class: rich + format: !!python/name:textual._doc.rich - pymdownx.inlinehilite - pymdownx.superfences - pymdownx.snippets diff --git a/sandbox/will/box.css b/sandbox/will/box.css new file mode 100644 index 000000000..fcc4c3d4a --- /dev/null +++ b/sandbox/will/box.css @@ -0,0 +1,28 @@ +Screen { + background: white; + color:black; +} + +#box1 { + width: 10; + height: 5; + background: red 40%; + box-sizing: content-box; +} + + +#box2 { + width: 10; + height: 5; + padding: 1; + background:blue 40%; + box-sizing: content-box; +} + +#box3 { + width: 10; + height: 5; + background:green 40%; + border: heavy; + box-sizing: content-box; +} diff --git a/sandbox/will/box.py b/sandbox/will/box.py new file mode 100644 index 000000000..ab99d4ee6 --- /dev/null +++ b/sandbox/will/box.py @@ -0,0 +1,13 @@ +from textual.app import App +from textual.widgets import Static + +class BoxApp(App): + + def compose(self): + yield Static("0123456789", id="box1") + yield Static("0123456789", id="box2") + yield Static("0123456789", id="box3") + + +app = BoxApp(css_path="box.css") +app.run() diff --git a/src/textual/_doc.py b/src/textual/_doc.py index febe3e576..b70ce67fc 100644 --- a/src/textual/_doc.py +++ b/src/textual/_doc.py @@ -50,3 +50,32 @@ def format_svg(source, language, css_class, options, md, attrs, **kwargs) -> str import traceback traceback.print_exception(error) + + +def rich(source, language, css_class, options, md, attrs, **kwargs) -> str: + """A superfences formatter to insert a SVG screenshot.""" + + from rich.console import Console + import io + + title = attrs.get("title", "Rich") + + console = Console( + file=io.StringIO(), + record=True, + force_terminal=True, + color_system="truecolor", + ) + error_console = Console(stderr=True) + + globals: dict = {} + try: + exec(source, globals) + except Exception: + error_console.print_exception() + console.bell() + + if "output" in globals: + console.print(globals["output"]) + output_svg = console.export_svg(title=title) + return output_svg diff --git a/src/textual/color.py b/src/textual/color.py index fd420e17c..edb3c076b 100644 --- a/src/textual/color.py +++ b/src/textual/color.py @@ -1,9 +1,31 @@ """ -Manages Color in Textual. +This module contains a powerful Color class which Textual uses to expose colors. + +The only exception would be for Rich renderables, which require a rich.color.Color instance. +You can convert from a Textual color to a Rich color with the [rich_color][textual.color.Color.rich_color] property. + +## Named colors + +The following named colors are used by the [parse][textual.color.Color.parse] method. + +```{.rich title="colors"} +from textual._color_constants import COLOR_NAME_TO_RGB +from rich.table import Table +from rich.text import Text +table = Table("Name", "RGB", "Color", expand=True, highlight=True) + +for name, triplet in sorted(COLOR_NAME_TO_RGB.items()): + if len(triplet) != 3: + continue + r, g, b = triplet + table.add_row( + f'"{name}"', + f"rgb({r}, {g}, {b})", + Text(" ", style=f"on rgb({r},{g},{b})") + ) +output = table +``` -All instances where the developer is presented with a color will use this class. The only -exception should be when passing things to a Rich renderable, which will need to use the -`rich_color` attribute to perform a conversion. """ @@ -87,23 +109,21 @@ split_pairs4: Callable[[str], tuple[str, str, str, str]] = itemgetter( class ColorParseError(Exception): - """A color failed to parse""" + """A color failed to parse. + + Args: + message (str): the error message + suggested_color (str | None): a close color we can suggest. Defaults to None. + """ def __init__(self, message: str, suggested_color: str | None = None): - """ - Creates a new ColorParseError - - Args: - message (str): the error message - suggested_color (str | None): a close color we can suggest. Defaults to None. - """ super().__init__(message) self.suggested_color = suggested_color @rich.repr.auto class Color(NamedTuple): - """A class to represent a single RGB color with alpha.""" + """A class to represent a RGB color with an alpha component.""" r: int """Red component (0-255)""" @@ -310,7 +330,29 @@ class Color(NamedTuple): @classmethod @lru_cache(maxsize=1024 * 4) def parse(cls, color_text: str | Color) -> Color: - """Parse a string containing a CSS-style color. + """Parse a string containing a named color or CSS-style color. + + Colors may be parsed from the following formats: + + Text beginning with a `#` is parsed as hex: + + R, G, and B must be hex digits (0-9A-F) + + - `#RGB` + - `#RRGGBB` + - `#RRGGBBAA` + + Text in the following formats is parsed as decimal values: + + RED, GREEN, and BLUE must be numbers between 0 and 255. + ALPHA should ba a value between 0 and 1. + + - `rgb(RED,GREEN,BLUE)` + - `rgba(RED,GREEN,BLUE,ALPHA)` + - `hsl(RED,GREEN,BLUE)` + - `hsl(RED,GREEN,BLUE,ALPHA)` + + All other text will raise a `ColorParseError`. Args: color_text (str | Color): Text with a valid color format. Color objects will @@ -445,43 +487,6 @@ BLACK = Color(0, 0, 0) TRANSPARENT = Color(0, 0, 0, 0) -class ColorPair(NamedTuple): - """A pair of colors for foreground and background.""" - - foreground: Color - background: Color - - def __rich_repr__(self) -> rich.repr.Result: - yield "foreground", self.foreground - yield "background", self.background - - @property - def style(self) -> Style: - """A Rich style with foreground and background.""" - return self._get_style() - - @lru_cache(maxsize=1024 * 4) - def _get_style(self) -> Style: - """Get a Rich style, foreground adjusted for transparency.""" - r, g, b, a = self.foreground - if a == 0: - return Style.from_color( - self.background.rich_color, self.background.rich_color - ) - elif a == 1: - return Style.from_color( - self.foreground.rich_color, self.background.rich_color - ) - else: - r2, g2, b2, _ = self.background - return Style.from_color( - RichColor.from_rgb( - r + (r2 - r) * a, g + (g2 - g) * a, b + (b2 - b) * a - ), - self.background.rich_color, - ) - - def rgb_to_lab(rgb: Color) -> Lab: """Convert an RGB color to the CIE-L*ab format. @@ -534,27 +539,3 @@ def lab_to_rgb(lab: Lab) -> Color: b = 1.055 * pow(b, 1 / 2.4) - 0.055 if b > 0.0031308 else 12.92 * b return Color(int(r * 255), int(g * 255), int(b * 255)) - - -if __name__ == "__main__": - - from rich import print - - c1 = Color.parse("#112233") - print(c1, c1.hex, c1.css) - - c2 = Color.parse("#11223344") - print(c2) - - c3 = Color.parse("rgb(10,20,30)") - print(c3) - - c4 = Color.parse("rgba(10,20,30,0.5)") - print(c4, c4.hex, c4.css) - - p1 = ColorPair(c4, c1) - print(p1) - - print(p1.style) - - print(Color.parse("dark_blue")) diff --git a/src/textual/css/_style_properties.py b/src/textual/css/_style_properties.py index 175504a85..28fc34db8 100644 --- a/src/textual/css/_style_properties.py +++ b/src/textual/css/_style_properties.py @@ -28,7 +28,7 @@ from ._help_text import ( color_property_help_text, ) from .._border import INVISIBLE_EDGE_TYPES, normalize_border_value -from ..color import Color, ColorPair, ColorParseError +from ..color import Color, ColorParseError from ._error_tools import friendly_list from .constants import NULL_SPACING, VALID_STYLE_FLAGS from .errors import StyleTypeError, StyleValueError @@ -452,27 +452,6 @@ class BorderProperty: obj.refresh(layout=self._layout) -class StyleProperty: - """Descriptor for getting the Rich style.""" - - DEFAULT_STYLE = Style() - - def __get__( - self, obj: StylesBase, objtype: type[StylesBase] | None = None - ) -> Style: - """Get the Style - - Args: - obj (Styles): The ``Styles`` object - objtype (type[Styles]): The ``Styles`` class - - Returns: - A ``Style`` object. - """ - style = ColorPair(obj.color, obj.background).style + obj.text_style - return style - - class SpacingProperty: """Descriptor for getting and setting spacing properties (e.g. padding and margin).""" diff --git a/tests/test_color.py b/tests/test_color.py index 0e1e51ebb..0969b5678 100644 --- a/tests/test_color.py +++ b/tests/test_color.py @@ -2,7 +2,7 @@ import pytest from rich.color import Color as RichColor from rich.text import Text -from textual.color import Color, ColorPair, Lab, lab_to_rgb, rgb_to_lab +from textual.color import Color, Lab, lab_to_rgb, rgb_to_lab def test_rich_color(): @@ -31,22 +31,6 @@ def test_css(): assert Color(10, 20, 30, 0.5).css == "rgba(10,20,30,0.5)" -def test_colorpair_style(): - """Test conversion of colorpair to style.""" - - # Black on white - assert ( - str(ColorPair(Color.parse("#000000"), Color.parse("#ffffff")).style) - == "#000000 on #ffffff" - ) - - # 50% black on white - assert ( - str(ColorPair(Color.parse("rgba(0,0,0,0.5)"), Color.parse("#ffffff")).style) - == "#7f7f7f on #ffffff" - ) - - def test_rgb(): assert Color(10, 20, 30, 0.55).rgb == (10, 20, 30) @@ -221,8 +205,3 @@ def test_rgb_lab_rgb_roundtrip(): assert c_.r == pytest.approx(r, abs=1) assert c_.g == pytest.approx(g, abs=1) assert c_.b == pytest.approx(b, abs=1) - - -def test_color_pair_style(): - pair = ColorPair(Color(220, 220, 220), Color(10, 20, 30)) - assert str(pair.style) == "#dcdcdc on #0a141e"