calculator example

This commit is contained in:
Will McGugan
2021-07-14 20:08:27 +01:00
parent b665ab330e
commit caf62a97da
15 changed files with 726 additions and 110 deletions

View File

@@ -5,7 +5,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/) The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/). and this project adheres to [Semantic Versioning](http://semver.org/).
## [Unreleased] - yyyy-mm-dd ## [0.1.7] - 2021-07-14
Here we write upgrading notes for brands. It's a team effort to make them as ### Changed
straightforward as possible.
- Added functionality to calculator example.
- Scrollview now shows scrollbars automatically
- New handler system for messages that doesn't require inheritance
- Improved traceback handling

View File

@@ -1,17 +1,27 @@
from decimal import Decimal
from rich.align import Align from rich.align import Align
from rich.console import Console, ConsoleOptions, RenderResult from rich.console import Console, ConsoleOptions, RenderResult, RenderableType
from rich.padding import Padding
from rich.text import Text from rich.text import Text
from textual.app import App from textual.app import App
from textual import events from textual import events
from textual.message import Message
from textual.reactive import Reactive
from textual.view import View from textual.view import View
from textual.widgets import Button, Static from textual.widget import Widget
from textual.widgets import Button
from textual.layouts.grid import GridLayout from textual.layouts.grid import GridLayout
try: try:
from pyfiglet import Figlet from pyfiglet import Figlet
except ImportError: except ImportError:
print("Please install pyfiglet to run this example") print("Please install pyfiglet to run this example")
import sys
sys.exit()
class FigletText: class FigletText:
@@ -24,10 +34,8 @@ class FigletText:
self, console: Console, options: ConsoleOptions self, console: Console, options: ConsoleOptions
) -> RenderResult: ) -> RenderResult:
size = min(options.max_width / 2, options.max_height) size = min(options.max_width / 2, options.max_height)
text = self.text
if size < 4: if size < 4:
yield Text(text, style="bold") yield Text(self.text, style="bold")
else: else:
if size < 7: if size < 7:
font_name = "mini" font_name = "mini"
@@ -37,42 +45,148 @@ class FigletText:
font_name = "standard" font_name = "standard"
else: else:
font_name = "big" font_name = "big"
font = Figlet(font=font_name) font = Figlet(font=font_name, width=options.max_width)
yield Text(font.renderText(text).rstrip("\n"), style="bold") yield Text(font.renderText(self.text).rstrip("\n"), style="bold")
class Numbers(Widget):
"""The digital display of the calculator."""
value: Reactive[str] = Reactive("0")
def render(self) -> RenderableType:
return Padding(
Align.right(FigletText(self.value), vertical="middle"),
(0, 1),
style="white on rgb(51,51,51)",
)
class CalculatorApp(App): class CalculatorApp(App):
async def on_startup(self, event: events.Startup) -> None: """A working calculator app."""
async def on_load(self, event: events.Load) -> None:
"""Sent when the app starts, but before displaying anything."""
self.left = Decimal("0")
self.right = Decimal("0")
self.value = ""
self.operator = "+"
self.numbers = Numbers()
def make_button(text: str, style: str) -> Button:
"""Create a button with the given Figlet label."""
return Button(FigletText(text), style=style, name=text)
dark = "white on rgb(51,51,51)"
light = "black on rgb(165,165,165)"
yellow = "white on rgb(255,159,7)"
button_styles = {
"AC": light,
"C": light,
"+/-": light,
"%": light,
"/": yellow,
"X": yellow,
"-": yellow,
"+": yellow,
"=": yellow,
}
# Make all the buttons
self.buttons = {
name: make_button(name, button_styles.get(name, dark))
for name in "+/-,%,/,7,8,9,X,4,5,6,-,1,2,3,+,.,=".split(",")
}
self.zero = make_button("0", dark)
self.ac = make_button("AC", light)
self.c = make_button("C", light)
self.c.visible = False
async def on_startup(self, event: events.Startup) -> None:
"""Sent when the app has gone full screen."""
# Create the layout which defines where our widgets will go
layout = GridLayout(gap=(2, 1), gutter=1, align=("center", "center")) layout = GridLayout(gap=(2, 1), gutter=1, align=("center", "center"))
await self.push_view(View(layout=layout)) await self.push_view(View(layout=layout))
# Create rows / columns / areas
layout.add_column("col", max_size=30, repeat=4) layout.add_column("col", max_size=30, repeat=4)
layout.add_row("numbers") layout.add_row("numbers", max_size=15)
layout.add_row("row", max_size=15, repeat=5) layout.add_row("row", max_size=15, repeat=5)
layout.add_areas( layout.add_areas(
numbers="col1-start|col4-end,numbers", zero="col1-start|col2-end,row5" clear="col1,row1",
numbers="col1-start|col4-end,numbers",
zero="col1-start|col2-end,row5",
) )
# Place out widgets in to the layout
def make_button(text: str) -> Button: layout.place(clear=self.c)
return Button(FigletText(text), style="white on rgb(51,51,51)")
buttons = {
name: make_button(name)
for name in "AC,+/-,%,/,7,8,9,X,4,5,6,-,1,2,3,+,.,=".split(",")
}
for name in ("AC", "+/-", "%"):
buttons[name].style = "black on rgb(165,165,165)"
for name in "/X-+=":
buttons[name].style = "white on rgb(255,159,7)"
numbers = Align.right(FigletText("0"), vertical="middle")
layout.place( layout.place(
*buttons.values(), *self.buttons.values(), clear=self.ac, numbers=self.numbers, zero=self.zero
numbers=Static(numbers, padding=(0, 1), style="white on rgb(51,51,51)"),
zero=make_button("0"),
) )
async def message_button_pressed(self, message: Message) -> None:
"""A message sent by the button widget"""
assert isinstance(message.sender, Button)
button_name = message.sender.name
def do_math() -> bool:
operator = self.operator
right = self.right
if operator == "+":
self.left += right
elif operator == "-":
self.left -= right
elif operator == "/":
try:
self.left /= right
except ZeroDivisionError:
self.numbers.value = "Error"
return False
elif operator == "X":
self.left *= right
return True
if button_name.isdigit():
self.value = self.value.lstrip("0") + button_name
self.numbers.value = self.value
elif button_name == "+/-":
self.value = str(Decimal(self.value or "0") * -1)
self.numbers.value = self.value
elif button_name == "%":
self.value = str(Decimal(self.value or "0") / Decimal(100))
self.numbers.value = self.value
elif button_name == ".":
if "." not in self.value:
self.value += "."
self.numbers.value = self.value
elif button_name == "AC":
self.value = ""
self.left = self.right = Decimal(0)
self.operator = "+"
self.numbers.value = "0"
elif button_name == "C":
self.value = ""
self.numbers.value = "0"
elif button_name in ("+", "-", "/", "X"):
self.right = Decimal(self.value or "0")
if do_math():
self.numbers.value = str(self.left)
self.value = ""
self.operator = button_name
elif button_name == "=":
if self.value:
self.right = Decimal(self.value or "0")
if do_math():
self.numbers.value = str(self.left)
self.value = ""
show_ac = self.value in ("", "0") and self.numbers.value == "0"
self.c.visible = not show_ac
self.ac.visible = show_ac
CalculatorApp.run(title="Calculator Test") CalculatorApp.run(title="Calculator Test")

386
poetry.lock generated
View File

@@ -6,6 +6,17 @@ category = "dev"
optional = false optional = false
python-versions = "*" python-versions = "*"
[[package]]
name = "astunparse"
version = "1.6.3"
description = "An AST unparser for Python"
category = "dev"
optional = false
python-versions = "*"
[package.dependencies]
six = ">=1.6.1,<2.0"
[[package]] [[package]]
name = "atomicwrites" name = "atomicwrites"
version = "1.4.0" version = "1.4.0"
@@ -50,6 +61,14 @@ typing-extensions = ">=3.7.4"
colorama = ["colorama (>=0.4.3)"] colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] d = ["aiohttp (>=3.3.2)", "aiohttp-cors"]
[[package]]
name = "cached-property"
version = "1.5.2"
description = "A decorator for caching properties in classes."
category = "dev"
optional = false
python-versions = "*"
[[package]] [[package]]
name = "click" name = "click"
version = "8.0.1" version = "8.0.1"
@@ -92,6 +111,20 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
[package.extras] [package.extras]
toml = ["toml"] toml = ["toml"]
[[package]]
name = "ghp-import"
version = "2.0.1"
description = "Copy your docs directly to the gh-pages branch."
category = "dev"
optional = false
python-versions = "*"
[package.dependencies]
python-dateutil = ">=2.8.1"
[package.extras]
dev = ["twine", "markdown", "flake8"]
[[package]] [[package]]
name = "importlib-metadata" name = "importlib-metadata"
version = "4.6.1" version = "4.6.1"
@@ -117,6 +150,128 @@ category = "dev"
optional = false optional = false
python-versions = "*" python-versions = "*"
[[package]]
name = "jinja2"
version = "3.0.1"
description = "A very fast and expressive template engine."
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
MarkupSafe = ">=2.0"
[package.extras]
i18n = ["Babel (>=2.7)"]
[[package]]
name = "markdown"
version = "3.3.4"
description = "Python implementation of Markdown."
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
[package.extras]
testing = ["coverage", "pyyaml"]
[[package]]
name = "markupsafe"
version = "2.0.1"
description = "Safely add untrusted strings to HTML/XML markup."
category = "dev"
optional = false
python-versions = ">=3.6"
[[package]]
name = "mergedeep"
version = "1.3.4"
description = "A deep merge function for 🐍."
category = "dev"
optional = false
python-versions = ">=3.6"
[[package]]
name = "mkdocs"
version = "1.2.1"
description = "Project documentation with Markdown."
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
click = ">=3.3"
ghp-import = ">=1.0"
importlib-metadata = ">=3.10"
Jinja2 = ">=2.10.1"
Markdown = ">=3.2.1"
mergedeep = ">=1.3.4"
packaging = ">=20.5"
PyYAML = ">=3.10"
pyyaml-env-tag = ">=0.1"
watchdog = ">=2.0"
[package.extras]
i18n = ["babel (>=2.9.0)"]
[[package]]
name = "mkdocs-autorefs"
version = "0.2.1"
description = "Automatically link across pages in MkDocs."
category = "dev"
optional = false
python-versions = ">=3.6,<4.0"
[package.dependencies]
Markdown = ">=3.3,<4.0"
mkdocs = ">=1.1,<2.0"
[[package]]
name = "mkdocs-material"
version = "7.1.10"
description = "A Material Design theme for MkDocs"
category = "dev"
optional = false
python-versions = "*"
[package.dependencies]
markdown = ">=3.2"
mkdocs = ">=1.1"
mkdocs-material-extensions = ">=1.0"
Pygments = ">=2.4"
pymdown-extensions = ">=7.0"
[[package]]
name = "mkdocs-material-extensions"
version = "1.0.1"
description = "Extension pack for Python Markdown."
category = "dev"
optional = false
python-versions = ">=3.5"
[package.dependencies]
mkdocs-material = ">=5.0.0"
[[package]]
name = "mkdocstrings"
version = "0.15.2"
description = "Automatic documentation from sources, for MkDocs."
category = "dev"
optional = false
python-versions = ">=3.6,<4.0"
[package.dependencies]
Jinja2 = ">=2.11.1,<4.0"
Markdown = ">=3.3,<4.0"
MarkupSafe = ">=1.1,<3.0"
mkdocs = ">=1.1.1,<2.0.0"
mkdocs-autorefs = ">=0.1,<0.3"
pymdown-extensions = ">=6.3,<9.0"
pytkdocs = ">=0.2.0,<0.12.0"
[[package]] [[package]]
name = "mypy" name = "mypy"
version = "0.910" version = "0.910"
@@ -192,6 +347,17 @@ category = "main"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
[[package]]
name = "pymdown-extensions"
version = "8.2"
description = "Extension pack for Python Markdown."
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
Markdown = ">=3.2"
[[package]] [[package]]
name = "pyparsing" name = "pyparsing"
version = "2.4.7" version = "2.4.7"
@@ -238,6 +404,52 @@ toml = "*"
[package.extras] [package.extras]
testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"]
[[package]]
name = "python-dateutil"
version = "2.8.2"
description = "Extensions to the standard Python datetime module"
category = "dev"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
[package.dependencies]
six = ">=1.5"
[[package]]
name = "pytkdocs"
version = "0.11.1"
description = "Load Python objects documentation."
category = "dev"
optional = false
python-versions = ">=3.6.1,<4.0.0"
[package.dependencies]
astunparse = {version = ">=1.6.3,<2.0.0", markers = "python_version < \"3.9\""}
cached-property = {version = ">=1.5.2,<2.0.0", markers = "python_version < \"3.8\""}
typing-extensions = {version = ">=3.7.4.3,<4.0.0.0", markers = "python_version < \"3.8\""}
[package.extras]
numpy-style = ["docstring_parser (>=0.7.3,<0.8.0)"]
[[package]]
name = "pyyaml"
version = "5.4.1"
description = "YAML parser and emitter for Python"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
[[package]]
name = "pyyaml-env-tag"
version = "0.1"
description = "A custom YAML tag for referencing environment variables in YAML files. "
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
pyyaml = "*"
[[package]] [[package]]
name = "regex" name = "regex"
version = "2021.7.6" version = "2021.7.6"
@@ -263,6 +475,14 @@ typing-extensions = {version = ">=3.7.4,<4.0.0", markers = "python_version < \"3
[package.extras] [package.extras]
jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"]
[[package]]
name = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.10.2" version = "0.10.2"
@@ -287,6 +507,17 @@ category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
[[package]]
name = "watchdog"
version = "2.1.3"
description = "Filesystem events monitoring"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.extras]
watchmedo = ["PyYAML (>=3.10)", "argh (>=0.24.1)"]
[[package]] [[package]]
name = "zipp" name = "zipp"
version = "3.5.0" version = "3.5.0"
@@ -302,13 +533,17 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.7" python-versions = "^3.7"
content-hash = "137e746aef38f8e8eac2674219503221c2705c80b9ee4b463129b1c47e459460" content-hash = "ac64f9d88e11df57c6fabe30fb9f39aee4ea9925809a8c850a61e0b4e92bf64f"
[metadata.files] [metadata.files]
appdirs = [ appdirs = [
{file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
{file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
] ]
astunparse = [
{file = "astunparse-1.6.3-py2.py3-none-any.whl", hash = "sha256:c2652417f2c8b5bb325c885ae329bdf3f86424075c4fd1a128674bc6fba4b8e8"},
{file = "astunparse-1.6.3.tar.gz", hash = "sha256:5ad93a8456f0d084c3456d059fd9a92cce667963232cbf763eac3bc5b7940872"},
]
atomicwrites = [ atomicwrites = [
{file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
{file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
@@ -320,6 +555,10 @@ attrs = [
black = [ black = [
{file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"},
] ]
cached-property = [
{file = "cached-property-1.5.2.tar.gz", hash = "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130"},
{file = "cached_property-1.5.2-py2.py3-none-any.whl", hash = "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0"},
]
click = [ click = [
{file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"}, {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"},
{file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"}, {file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"},
@@ -386,6 +625,9 @@ coverage = [
{file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"}, {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"},
{file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"},
] ]
ghp-import = [
{file = "ghp-import-2.0.1.tar.gz", hash = "sha256:753de2eace6e0f7d4edfb3cce5e3c3b98cd52aadb80163303d1d036bda7b4483"},
]
importlib-metadata = [ importlib-metadata = [
{file = "importlib_metadata-4.6.1-py3-none-any.whl", hash = "sha256:9f55f560e116f8643ecf2922d9cd3e1c7e8d52e683178fecd9d08f6aa357e11e"}, {file = "importlib_metadata-4.6.1-py3-none-any.whl", hash = "sha256:9f55f560e116f8643ecf2922d9cd3e1c7e8d52e683178fecd9d08f6aa357e11e"},
{file = "importlib_metadata-4.6.1.tar.gz", hash = "sha256:079ada16b7fc30dfbb5d13399a5113110dab1aa7c2bc62f66af75f0b717c8cac"}, {file = "importlib_metadata-4.6.1.tar.gz", hash = "sha256:079ada16b7fc30dfbb5d13399a5113110dab1aa7c2bc62f66af75f0b717c8cac"},
@@ -394,6 +636,74 @@ iniconfig = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
] ]
jinja2 = [
{file = "Jinja2-3.0.1-py3-none-any.whl", hash = "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4"},
{file = "Jinja2-3.0.1.tar.gz", hash = "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4"},
]
markdown = [
{file = "Markdown-3.3.4-py3-none-any.whl", hash = "sha256:96c3ba1261de2f7547b46a00ea8463832c921d3f9d6aba3f255a6f71386db20c"},
{file = "Markdown-3.3.4.tar.gz", hash = "sha256:31b5b491868dcc87d6c24b7e3d19a0d730d59d3e46f4eea6430a321bed387a49"},
]
markupsafe = [
{file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"},
{file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"},
{file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"},
{file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"},
{file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"},
{file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"},
{file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"},
{file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"},
{file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"},
]
mergedeep = [
{file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"},
{file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"},
]
mkdocs = [
{file = "mkdocs-1.2.1-py3-none-any.whl", hash = "sha256:11141126e5896dd9d279b3e4814eb488e409a0990fb638856255020406a8e2e7"},
{file = "mkdocs-1.2.1.tar.gz", hash = "sha256:6e0ea175366e3a50d334597b0bc042b8cebd512398cdd3f6f34842d0ef524905"},
]
mkdocs-autorefs = [
{file = "mkdocs-autorefs-0.2.1.tar.gz", hash = "sha256:b8156d653ed91356e71675ce1fa1186d2b2c2085050012522895c9aa98fca3e5"},
{file = "mkdocs_autorefs-0.2.1-py3-none-any.whl", hash = "sha256:f301b983a34259df90b3fcf7edc234b5e6c7065bd578781e66fd90b8cfbe76be"},
]
mkdocs-material = [
{file = "mkdocs-material-7.1.10.tar.gz", hash = "sha256:890e9be00bfbe4d22ccccbcde1bf9bad67a3ba495f2a7d2422ea4acb5099f014"},
{file = "mkdocs_material-7.1.10-py2.py3-none-any.whl", hash = "sha256:92ff8c4a8e78555ef7b7ed0ba3043421d18971b48d066ea2cefb50e889fc66db"},
]
mkdocs-material-extensions = [
{file = "mkdocs-material-extensions-1.0.1.tar.gz", hash = "sha256:6947fb7f5e4291e3c61405bad3539d81e0b3cd62ae0d66ced018128af509c68f"},
{file = "mkdocs_material_extensions-1.0.1-py3-none-any.whl", hash = "sha256:d90c807a88348aa6d1805657ec5c0b2d8d609c110e62b9dce4daf7fa981fa338"},
]
mkdocstrings = [
{file = "mkdocstrings-0.15.2-py3-none-any.whl", hash = "sha256:8d6cbe64c07ae66739010979ca01d49dd2f64d1a45009f089d217b9cd2a65e36"},
{file = "mkdocstrings-0.15.2.tar.gz", hash = "sha256:c2fee9a3a644647c06eb2044fdfede1073adfd1a55bf6752005d3db10705fe73"},
]
mypy = [ mypy = [
{file = "mypy-0.910-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:a155d80ea6cee511a3694b108c4494a39f42de11ee4e61e72bc424c490e46457"}, {file = "mypy-0.910-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:a155d80ea6cee511a3694b108c4494a39f42de11ee4e61e72bc424c490e46457"},
{file = "mypy-0.910-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b94e4b785e304a04ea0828759172a15add27088520dc7e49ceade7834275bedb"}, {file = "mypy-0.910-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b94e4b785e304a04ea0828759172a15add27088520dc7e49ceade7834275bedb"},
@@ -443,6 +753,10 @@ pygments = [
{file = "Pygments-2.9.0-py3-none-any.whl", hash = "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e"}, {file = "Pygments-2.9.0-py3-none-any.whl", hash = "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e"},
{file = "Pygments-2.9.0.tar.gz", hash = "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f"}, {file = "Pygments-2.9.0.tar.gz", hash = "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f"},
] ]
pymdown-extensions = [
{file = "pymdown-extensions-8.2.tar.gz", hash = "sha256:b6daa94aad9e1310f9c64c8b1f01e4ce82937ab7eb53bfc92876a97aca02a6f4"},
{file = "pymdown_extensions-8.2-py3-none-any.whl", hash = "sha256:141452d8ed61165518f2c923454bf054866b85cf466feedb0eb68f04acdc2560"},
]
pyparsing = [ pyparsing = [
{file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
{file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
@@ -455,6 +769,49 @@ pytest-cov = [
{file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"},
{file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"},
] ]
python-dateutil = [
{file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
{file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
]
pytkdocs = [
{file = "pytkdocs-0.11.1-py3-none-any.whl", hash = "sha256:89ca4926d0acc266235beb24cb0b0591aa6bf7adedfae54bf9421d529d782c8d"},
{file = "pytkdocs-0.11.1.tar.gz", hash = "sha256:1ec7e028fe8361acc1ce909ada4e6beabec28ef31e629618549109e1d58549f0"},
]
pyyaml = [
{file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"},
{file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"},
{file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"},
{file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"},
{file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"},
{file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"},
{file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347"},
{file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541"},
{file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"},
{file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"},
{file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"},
{file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"},
{file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa"},
{file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"},
{file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"},
{file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"},
{file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"},
{file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"},
{file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247"},
{file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc"},
{file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"},
{file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"},
{file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"},
{file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"},
{file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122"},
{file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6"},
{file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"},
{file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"},
{file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"},
]
pyyaml-env-tag = [
{file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"},
{file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"},
]
regex = [ regex = [
{file = "regex-2021.7.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e6a1e5ca97d411a461041d057348e578dc344ecd2add3555aedba3b408c9f874"}, {file = "regex-2021.7.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e6a1e5ca97d411a461041d057348e578dc344ecd2add3555aedba3b408c9f874"},
{file = "regex-2021.7.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:6afe6a627888c9a6cfbb603d1d017ce204cebd589d66e0703309b8048c3b0854"}, {file = "regex-2021.7.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:6afe6a627888c9a6cfbb603d1d017ce204cebd589d66e0703309b8048c3b0854"},
@@ -502,6 +859,10 @@ rich = [
{file = "rich-10.6.0-py3-none-any.whl", hash = "sha256:d3f72827cd5df13b2ef7f1a97f81ec65548d4fdeb92cef653234f227580bbb2a"}, {file = "rich-10.6.0-py3-none-any.whl", hash = "sha256:d3f72827cd5df13b2ef7f1a97f81ec65548d4fdeb92cef653234f227580bbb2a"},
{file = "rich-10.6.0.tar.gz", hash = "sha256:128261b3e2419a4ef9c97066ccc2abbfb49fa7c5e89c3fe4056d00aa5e9c1e65"}, {file = "rich-10.6.0.tar.gz", hash = "sha256:128261b3e2419a4ef9c97066ccc2abbfb49fa7c5e89c3fe4056d00aa5e9c1e65"},
] ]
six = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
toml = [ toml = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
@@ -543,6 +904,29 @@ typing-extensions = [
{file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"}, {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"},
{file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"},
] ]
watchdog = [
{file = "watchdog-2.1.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9628f3f85375a17614a2ab5eac7665f7f7be8b6b0a2a228e6f6a2e91dd4bfe26"},
{file = "watchdog-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:acc4e2d5be6f140f02ee8590e51c002829e2c33ee199036fcd61311d558d89f4"},
{file = "watchdog-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:85b851237cf3533fabbc034ffcd84d0fa52014b3121454e5f8b86974b531560c"},
{file = "watchdog-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a12539ecf2478a94e4ba4d13476bb2c7a2e0a2080af2bb37df84d88b1b01358a"},
{file = "watchdog-2.1.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6fe9c8533e955c6589cfea6f3f0a1a95fb16867a211125236c82e1815932b5d7"},
{file = "watchdog-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d9456f0433845e7153b102fffeb767bde2406b76042f2216838af3b21707894e"},
{file = "watchdog-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fd8c595d5a93abd441ee7c5bb3ff0d7170e79031520d113d6f401d0cf49d7c8f"},
{file = "watchdog-2.1.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0bcfe904c7d404eb6905f7106c54873503b442e8e918cc226e1828f498bdc0ca"},
{file = "watchdog-2.1.3-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bf84bd94cbaad8f6b9cbaeef43080920f4cb0e61ad90af7106b3de402f5fe127"},
{file = "watchdog-2.1.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b8ddb2c9f92e0c686ea77341dcb58216fa5ff7d5f992c7278ee8a392a06e86bb"},
{file = "watchdog-2.1.3-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8805a5f468862daf1e4f4447b0ccf3acaff626eaa57fbb46d7960d1cf09f2e6d"},
{file = "watchdog-2.1.3-py3-none-manylinux2014_armv7l.whl", hash = "sha256:3e305ea2757f81d8ebd8559d1a944ed83e3ab1bdf68bcf16ec851b97c08dc035"},
{file = "watchdog-2.1.3-py3-none-manylinux2014_i686.whl", hash = "sha256:431a3ea70b20962e6dee65f0eeecd768cd3085ea613ccb9b53c8969de9f6ebd2"},
{file = "watchdog-2.1.3-py3-none-manylinux2014_ppc64.whl", hash = "sha256:e4929ac2aaa2e4f1a30a36751160be391911da463a8799460340901517298b13"},
{file = "watchdog-2.1.3-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:201cadf0b8c11922f54ec97482f95b2aafca429c4c3a4bb869a14f3c20c32686"},
{file = "watchdog-2.1.3-py3-none-manylinux2014_s390x.whl", hash = "sha256:3a7d242a7963174684206093846537220ee37ba9986b824a326a8bb4ef329a33"},
{file = "watchdog-2.1.3-py3-none-manylinux2014_x86_64.whl", hash = "sha256:54e057727dd18bd01a3060dbf5104eb5a495ca26316487e0f32a394fd5fe725a"},
{file = "watchdog-2.1.3-py3-none-win32.whl", hash = "sha256:b5fc5c127bad6983eecf1ad117ab3418949f18af9c8758bd10158be3647298a9"},
{file = "watchdog-2.1.3-py3-none-win_amd64.whl", hash = "sha256:44acad6f642996a2b50bb9ce4fb3730dde08f23e79e20cd3d8e2a2076b730381"},
{file = "watchdog-2.1.3-py3-none-win_ia64.whl", hash = "sha256:0bcdf7b99b56a3ae069866c33d247c9994ffde91b620eaf0306b27e099bd1ae0"},
{file = "watchdog-2.1.3.tar.gz", hash = "sha256:e5236a8e8602ab6db4b873664c2d356c365ab3cac96fbdec4970ad616415dd45"},
]
zipp = [ zipp = [
{file = "zipp-3.5.0-py3-none-any.whl", hash = "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3"}, {file = "zipp-3.5.0-py3-none-any.whl", hash = "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3"},
{file = "zipp-3.5.0.tar.gz", hash = "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"}, {file = "zipp-3.5.0.tar.gz", hash = "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"},

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "textual" name = "textual"
version = "0.1.6" version = "0.1.7"
homepage = "https://github.com/willmcgugan/textual" homepage = "https://github.com/willmcgugan/textual"
description = "Text User Interface using Rich" description = "Text User Interface using Rich"
authors = ["Will McGugan <willmcgugan@gmail.com>"] authors = ["Will McGugan <willmcgugan@gmail.com>"]
@@ -31,6 +31,9 @@ pytest = "^6.2.3"
black = "^20.8b1" black = "^20.8b1"
mypy = "^0.910" mypy = "^0.910"
pytest-cov = "^2.12.1" pytest-cov = "^2.12.1"
mkdocs = "^1.2.1"
mkdocstrings = "^0.15.2"
mkdocs-material = "^7.1.10"
[build-system] [build-system]
requires = ["poetry-core>=1.0.0"] requires = ["poetry-core>=1.0.0"]

View File

@@ -23,7 +23,7 @@ from .binding import Bindings, NoBinding
from .geometry import Point, Region from .geometry import Point, Region
from ._context import active_app from ._context import active_app
from ._event_broker import extract_handler_actions, NoHandler from ._event_broker import extract_handler_actions, NoHandler
from .keys import Binding from ._types import MessageTarget
from .driver import Driver from .driver import Driver
from .layouts.dock import DockLayout, Dock from .layouts.dock import DockLayout, Dock
from ._linux_driver import LinuxDriver from ._linux_driver import LinuxDriver
@@ -56,6 +56,12 @@ ViewType = TypeVar("ViewType", bound=View)
# uvloop.install() # uvloop.install()
class PanicMessage(Message):
def __init__(self, sender: MessageTarget, traceback: Traceback) -> None:
self.traceback = traceback
super().__init__(sender)
class ActionError(Exception): class ActionError(Exception):
pass pass
@@ -71,11 +77,19 @@ class App(MessagePump):
def __init__( def __init__(
self, self,
console: Console = None, console: Console | None = None,
screen: bool = True, screen: bool = True,
driver_class: Type[Driver] = None, driver_class: Type[Driver] | None = None,
title: str = "Textual Application", title: str = "Textual Application",
): ):
"""The Textual Application base class
Args:
console (Console, optional): A Rich Console. Defaults to None.
screen (bool, optional): Enable full-screen application mode. Defaults to True.
driver_class (Type[Driver], optional): Driver class, or None to use default. Defaults to None.
title (str, optional): Title of the application. Defaults to "Textual Application".
"""
self.console = console or get_console() self.console = console or get_console()
self._screen = screen self._screen = screen
self.driver_class = driver_class or LinuxDriver self.driver_class = driver_class or LinuxDriver
@@ -88,6 +102,7 @@ class App(MessagePump):
self.mouse_over: Widget | None = None self.mouse_over: Widget | None = None
self.mouse_captured: Widget | None = None self.mouse_captured: Widget | None = None
self._driver: Driver | None = None self._driver: Driver | None = None
self._tracebacks: list[Traceback] = []
self._docks: list[Dock] = [] self._docks: list[Dock] = []
self._action_targets = {"app", "view"} self._action_targets = {"app", "view"}
@@ -202,10 +217,12 @@ class App(MessagePump):
if widget is not None: if widget is not None:
await widget.post_message(events.MouseCaptured(self, self.mouse_position)) await widget.post_message(events.MouseCaptured(self, self.mouse_position))
def panic(self, traceback: Traceback) -> None:
self._tracebacks.append(traceback)
self.close_messages_no_wait()
async def process_messages(self) -> None: async def process_messages(self) -> None:
log.debug("driver=%r", self.driver_class) log.debug("driver=%r", self.driver_class)
# loop = asyncio.get_event_loop()
# loop.add_signal_handler(signal.SIGINT, self.on_keyboard_interupt)
driver = self._driver = self.driver_class(self.console, self) driver = self._driver = self.driver_class(self.console, self)
active_app.set(self) active_app.set(self)
@@ -242,10 +259,10 @@ class App(MessagePump):
view = self._view_stack.pop() view = self._view_stack.pop()
await view.close_messages() await view.close_messages()
except Exception: except Exception:
traceback = Traceback(show_locals=True) self._tracebacks.append(Traceback(show_locals=True))
finally: finally:
driver.stop_application_mode() driver.stop_application_mode()
if traceback is not None: for traceback in self._tracebacks:
self.console.print(traceback) self.console.print(traceback)
def require_repaint(self) -> None: def require_repaint(self) -> None:
@@ -289,7 +306,7 @@ class App(MessagePump):
if sync_available: if sync_available:
console.file.write("\x1bP=2s\x1b\\") console.file.write("\x1bP=2s\x1b\\")
except Exception: except Exception:
log.exception("refresh failed") self.panic(Traceback(show_locals=True))
def display(self, renderable: RenderableType) -> None: def display(self, renderable: RenderableType) -> None:
if not self._closed: if not self._closed:

View File

@@ -79,6 +79,16 @@ class Layout(ABC):
self.height = 0 self.height = 0
self.renders: dict[Widget, tuple[Region, Lines]] = {} self.renders: dict[Widget, tuple[Region, Lines]] = {}
self._cuts: list[list[int]] | None = None self._cuts: list[list[int]] | None = None
self._require_update: bool = True
def check_update(self) -> bool:
return self._require_update
def require_update(self) -> None:
self._require_update = True
def reset_update(self) -> None:
self._require_update = False
def reset(self) -> None: def reset(self) -> None:
self._cuts = None self._cuts = None
@@ -265,10 +275,12 @@ class Layout(ABC):
) -> Iterable[list[Segment]]: ) -> Iterable[list[Segment]]:
for bucket in chops: for bucket in chops:
yield sum( line: list[Segment] = []
(segments for _, segments in sorted(bucket.items()) if segments), extend = line.extend
start=[], for _, segments in sorted(bucket.items()):
) if segments:
extend(segments)
yield line
def render( def render(
self, self,

View File

@@ -9,7 +9,7 @@ import sys
from typing import Iterable, NamedTuple from typing import Iterable, NamedTuple
from .._layout_resolve import layout_resolve from .._layout_resolve import layout_resolve
from .._loop import loop_last
from ..geometry import Dimensions, Point, Region from ..geometry import Dimensions, Point, Region
from ..layout import Layout, OrderedRegion from ..layout import Layout, OrderedRegion
from ..view import View from ..view import View
@@ -80,17 +80,39 @@ class GridLayout(Layout):
super().__init__() super().__init__()
def hide_row(self, row_name: str) -> None: def is_row_visible(self, row_name: str) -> bool:
self.hidden_rows.add(row_name) return row_name not in self.hidden_rows
def show_row(self, row_name: str) -> None: def is_column_visible(self, column_name: str) -> bool:
self.hidden_rows.discard(row_name) return column_name not in self.hidden_columns
def hide_column(self, column_name: str) -> None: def show_row(self, row_name: str, visible: bool = True) -> bool:
self.hidden_rows.add(column_name) changed = False
if visible:
if not self.is_row_visible(row_name):
self.require_update()
changed = True
self.hidden_rows.discard(row_name)
else:
if self.is_row_visible(row_name):
self.require_update()
changed = True
self.hidden_rows.add(row_name)
return changed
def show_column(self, column_name: str) -> None: def show_column(self, column_name: str, visible: bool = True) -> bool:
self.hidden_rows.discard(column_name) changed = False
if visible:
if not self.is_column_visible(column_name):
self.require_update()
changed = True
self.hidden_rows.discard(column_name)
else:
if self.is_column_visible(column_name):
self.require_update()
changed = True
self.hidden_rows.add(column_name)
return changed
def add_column( def add_column(
self, self,

View File

@@ -27,7 +27,7 @@ class Message:
self.name = camel_to_snake(self.__class__.__name__.replace("Message", "")) self.name = camel_to_snake(self.__class__.__name__.replace("Message", ""))
self.time = monotonic() self.time = monotonic()
self._no_default_action = False self._no_default_action = False
self._stop_propagaton = False self._stop_propagation = False
super().__init__() super().__init__()
def __rich_repr__(self) -> rich.repr.RichReprResult: def __rich_repr__(self) -> rich.repr.RichReprResult:
@@ -57,5 +57,5 @@ class Message:
""" """
self._no_default_action = prevent self._no_default_action = prevent
def stop_propagation(self, stop: bool = True) -> None: def stop(self, stop: bool = True) -> None:
self._stop_propagaton = stop self._stop_propagation = stop

View File

@@ -1,19 +1,28 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from asyncio import CancelledError
from functools import partial
import logging import logging
from asyncio import Event, Queue, QueueEmpty, Task from asyncio import Queue, QueueEmpty, Task
from typing import Any, Awaitable, Coroutine, NamedTuple from typing import TYPE_CHECKING, Awaitable, Iterable, Callable
from weakref import WeakSet from weakref import WeakSet
from rich.traceback import Traceback
from . import events from . import events
from ._timer import Timer, TimerCallback from ._timer import Timer, TimerCallback
from ._types import MessageHandler from ._types import MessageHandler
from ._context import active_app
from .message import Message from .message import Message
log = logging.getLogger("rich") log = logging.getLogger("rich")
if TYPE_CHECKING:
from .app import App
class NoParent(Exception): class NoParent(Exception):
pass pass
@@ -44,6 +53,15 @@ class MessagePump:
raise NoParent(f"{self._parent} has no parent") raise NoParent(f"{self._parent} has no parent")
return self._parent return self._parent
@property
def app(self) -> "App":
"""Get the current app."""
return active_app.get()
@property
def is_parent_active(self):
return self._parent and not self._parent._closed and not self._parent._closing
def set_parent(self, parent: MessagePump) -> None: def set_parent(self, parent: MessagePump) -> None:
self._parent = parent self._parent = parent
@@ -120,6 +138,9 @@ class MessagePump:
self._child_tasks.add(asyncio.get_event_loop().create_task(timer.run())) self._child_tasks.add(asyncio.get_event_loop().create_task(timer.run()))
return timer return timer
def close_messages_no_wait(self) -> None:
self._message_queue.put_nowait(None)
async def close_messages(self, wait: bool = True) -> None: async def close_messages(self, wait: bool = True) -> None:
"""Close message queue, and optionally wait for queue to finish processing.""" """Close message queue, and optionally wait for queue to finish processing."""
if self._closed: if self._closed:
@@ -138,12 +159,20 @@ class MessagePump:
self._task = asyncio.create_task(self.process_messages()) self._task = asyncio.create_task(self.process_messages())
async def process_messages(self) -> None: async def process_messages(self) -> None:
try:
return await self._process_messages()
except CancelledError:
pass
async def _process_messages(self) -> None:
"""Process messages until the queue is closed.""" """Process messages until the queue is closed."""
while not self._closed: while not self._closed:
try: try:
message = await self.get_message() message = await self.get_message()
except MessagePumpClosed: except MessagePumpClosed:
break break
except CancelledError:
raise
except Exception as error: except Exception as error:
log.exception("error in get_message()") log.exception("error in get_message()")
raise error from None raise error from None
@@ -161,9 +190,11 @@ class MessagePump:
try: try:
await self.dispatch_message(message) await self.dispatch_message(message)
except Exception as error: except CancelledError:
log.exception("error in dispatch_message")
raise raise
except Exception as error:
self.app.panic(Traceback(show_locals=True))
break
finally: finally:
if isinstance(message, events.Event) and self._message_queue.empty(): if isinstance(message, events.Event) and self._message_queue.empty():
if not self._closed: if not self._closed:
@@ -182,25 +213,39 @@ class MessagePump:
return await self.on_message(message) return await self.on_message(message)
return False return False
async def on_event(self, event: events.Event) -> None: def _get_dispatch_methods(
method_name = f"on_{event.name}" self, method_name: str, message: Message
) -> Iterable[Callable[[Message], Awaitable]]:
for cls in self.__class__.__mro__:
if message._no_default_action:
break
method = getattr(cls, method_name, None)
if method is not None:
yield method.__get__(self, cls)
dispatch_function: MessageHandler = getattr(self, method_name, None) async def on_event(self, event: events.Event) -> None:
if dispatch_function is not None: _rich_traceback_guard = True
await dispatch_function(event)
if event.bubble and self._parent and not event._stop_propagaton: for method in self._get_dispatch_methods(f"on_{event.name}", event):
if event.sender == self._parent: await method(event)
pass
# log.debug("bubbled event abandoned; %r", event) if event.bubble and self._parent and not event._stop_propagation:
elif not self._parent._closed and not self._parent._closing: if event.sender != self._parent and self.is_parent_active:
await self._parent.post_message(event) await self._parent.post_message(event)
async def on_message(self, message: Message) -> None: async def on_message(self, message: Message) -> None:
_rich_traceback_guard = True
method_name = f"message_{message.name}" method_name = f"message_{message.name}"
method = getattr(self, method_name, None)
if method is not None: for method in self._get_dispatch_methods(method_name, message):
await method(message) await method(message)
if message.bubble and self._parent and not message._stop_propagation:
if message.sender == self._parent:
pass
elif not self._parent._closed and not self._parent._closing:
await self._parent.post_message(message)
def post_message_no_wait(self, message: Message) -> bool: def post_message_no_wait(self, message: Message) -> bool:
if self._closing or self._closed: if self._closing or self._closed:
return False return False

View File

@@ -7,7 +7,6 @@ from rich.padding import Padding, PaddingDimensions
from rich.segment import Segment from rich.segment import Segment
from rich.style import StyleType from rich.style import StyleType
from . import events
from .geometry import Dimensions, Point from .geometry import Dimensions, Point
from .message import Message from .message import Message
from .widget import Widget, Reactive from .widget import Widget, Reactive
@@ -56,7 +55,6 @@ class PageRender:
def render(self, console: Console, options: ConsoleOptions) -> None: def render(self, console: Console, options: ConsoleOptions) -> None:
width = self.width or options.max_width or console.width width = self.width or options.max_width or console.width
width *= 2
options = options.update_dimensions(width, None) options = options.update_dimensions(width, None)
style = console.get_style(self.style) style = console.get_style(self.style)
renderable = self.renderable renderable = self.renderable

View File

@@ -152,7 +152,6 @@ class ScrollBarRender:
def __rich_console__( def __rich_console__(
self, console: Console, options: ConsoleOptions self, console: Console, options: ConsoleOptions
) -> RenderResult: ) -> RenderResult:
log.debug("SCROLLBAR RENDER")
size = ( size = (
(options.height or console.height) (options.height or console.height)
if self.vertical if self.vertical
@@ -231,7 +230,6 @@ class ScrollBar(Widget):
async def on_mouse_up(self, event: events.MouseUp) -> None: async def on_mouse_up(self, event: events.MouseUp) -> None:
if self.grabbed: if self.grabbed:
await self.release_mouse() await self.release_mouse()
await super().on_mouse_up(event)
async def on_mouse_captured(self, event: events.MouseCaptured) -> None: async def on_mouse_captured(self, event: events.MouseCaptured) -> None:
self.grabbed = event.mouse_position self.grabbed = event.mouse_position

View File

@@ -44,6 +44,9 @@ class View(Widget):
def __rich_repr__(self) -> rich.repr.RichReprResult: def __rich_repr__(self) -> rich.repr.RichReprResult:
yield "name", self.name yield "name", self.name
def __getitem__(self, widget_name: str) -> Widget:
return self.named_widgets[widget_name]
@property @property
def is_visual(self) -> bool: def is_visual(self) -> bool:
return False return False

View File

@@ -87,11 +87,6 @@ class Widget(MessagePump):
def is_visual(self) -> bool: def is_visual(self) -> bool:
return True return True
@property
def app(self) -> "App":
"""Get the current app."""
return active_app.get()
@property @property
def console(self) -> Console: def console(self) -> Console:
"""Get the current console.""" """Get the current console."""

View File

@@ -7,10 +7,16 @@ from rich.panel import Panel
import rich.repr import rich.repr
from rich.style import StyleType from rich.style import StyleType
from .. import events
from ..message import Message
from ..reactive import Reactive from ..reactive import Reactive
from ..widget import Widget from ..widget import Widget
class ButtonPressed(Message, bubble=True):
pass
class Expand: class Expand:
def __init__(self, renderable: RenderableType) -> None: def __init__(self, renderable: RenderableType) -> None:
self.renderable = renderable self.renderable = renderable
@@ -48,11 +54,15 @@ class Button(Widget):
name: str | None = None, name: str | None = None,
style: StyleType = "white on dark_blue", style: StyleType = "white on dark_blue",
): ):
self.label = label
self.name = name or str(label) self.name = name or str(label)
self.style = style self.style = style
super().__init__() super().__init__(name=name)
self.label = label
label: Reactive[RenderableType] = Reactive("")
def render(self) -> RenderableType: def render(self) -> RenderableType:
return ButtonRenderable(self.label, style=self.style) return ButtonRenderable(self.label, style=self.style)
return Align.center(self.label, vertical="middle", style=self.style)
async def on_click(self, event: events.Click) -> None:
await self.emit(ButtonPressed(self))

View File

@@ -29,18 +29,19 @@ class ScrollView(View):
fluid: bool = True, fluid: bool = True,
) -> None: ) -> None:
self.fluid = fluid self.fluid = fluid
self._vertical_scrollbar = ScrollBar(vertical=True) self.vscroll = ScrollBar(vertical=True)
self._horizontal_scrollbar = ScrollBar(vertical=False) self.hscroll = ScrollBar(vertical=False)
self._page = Page(renderable or "", style=style) self.page = Page(renderable or "", style=style)
layout = GridLayout() layout = GridLayout()
layout.add_column("main") layout.add_column("main")
layout.add_column("vertical", size=1) layout.add_column("vscroll", size=1)
layout.add_row("main") layout.add_row("main")
layout.add_row("horizontal", size=1) layout.add_row("hscroll", size=1)
layout.add_areas( layout.add_areas(
content="main,main", vertical="vertical,main", horizontal="main,horizontal" content="main,main", vscroll="vscroll,main", hscroll="main,hscroll"
) )
# layout.hide_row("horizontal") layout.show_row("hscroll", False)
layout.show_row("vscroll", False)
super().__init__(name=name, layout=layout) super().__init__(name=name, layout=layout)
x: Reactive[float] = Reactive(0) x: Reactive[float] = Reactive(0)
@@ -50,35 +51,35 @@ class ScrollView(View):
target_y: Reactive[float] = Reactive(0) target_y: Reactive[float] = Reactive(0)
def validate_x(self, value: float) -> float: def validate_x(self, value: float) -> float:
return clamp(value, 0, self._page.contents_size.width - self.size.width) return clamp(value, 0, self.page.contents_size.width - self.size.width)
def validate_target_x(self, value: float) -> float: def validate_target_x(self, value: float) -> float:
return clamp(value, 0, self._page.contents_size.width - self.size.width) return clamp(value, 0, self.page.contents_size.width - self.size.width)
def validate_y(self, value: float) -> float: def validate_y(self, value: float) -> float:
return clamp(value, 0, self._page.contents_size.height - self.size.height) return clamp(value, 0, self.page.contents_size.height - self.size.height)
def validate_target_y(self, value: float) -> float: def validate_target_y(self, value: float) -> float:
return clamp(value, 0, self._page.contents_size.height - self.size.height) return clamp(value, 0, self.page.contents_size.height - self.size.height)
async def watch_x(self, new_value: float) -> None: async def watch_x(self, new_value: float) -> None:
self._page.x = round(new_value) self.page.x = round(new_value)
self._horizontal_scrollbar.position = round(new_value) self.hscroll.position = round(new_value)
async def watch_y(self, new_value: float) -> None: async def watch_y(self, new_value: float) -> None:
self._page.y = round(new_value) self.page.y = round(new_value)
self._vertical_scrollbar.position = round(new_value) self.vscroll.position = round(new_value)
async def update(self, renderabe: RenderableType) -> None: async def update(self, renderabe: RenderableType) -> None:
self._page.update(renderabe) self.page.update(renderabe)
self.require_repaint() self.require_repaint()
async def on_mount(self, event: events.Mount) -> None: async def on_mount(self, event: events.Mount) -> None:
assert isinstance(self.layout, GridLayout) assert isinstance(self.layout, GridLayout)
self.layout.place( self.layout.place(
content=self._page, content=self.page,
vertical=self._vertical_scrollbar, vscroll=self.vscroll,
horizontal=self._horizontal_scrollbar, hscroll=self.hscroll,
) )
await self.layout.mount_all(self) await self.layout.mount_all(self)
@@ -133,7 +134,7 @@ class ScrollView(View):
async def key_end(self) -> None: async def key_end(self) -> None:
self.target_x = 0 self.target_x = 0
self.target_y = self._page.contents_size.height - self.size.height self.target_y = self.page.contents_size.height - self.size.height
self.animate("x", self.target_x, duration=1, easing="out_cubic") self.animate("x", self.target_x, duration=1, easing="out_cubic")
self.animate("y", self.target_y, duration=1, easing="out_cubic") self.animate("y", self.target_y, duration=1, easing="out_cubic")
@@ -146,7 +147,7 @@ class ScrollView(View):
async def on_resize(self, event: events.Resize) -> None: async def on_resize(self, event: events.Resize) -> None:
await super().on_resize(event) await super().on_resize(event)
if self.fluid: if self.fluid:
self._page.update() self.page.update()
async def message_scroll_up(self, message: Message) -> None: async def message_scroll_up(self, message: Message) -> None:
self.page_up() self.page_up()
@@ -171,7 +172,17 @@ class ScrollView(View):
async def message_page_update(self, message: Message) -> None: async def message_page_update(self, message: Message) -> None:
self.x = self.validate_x(self.x) self.x = self.validate_x(self.x)
self.y = self.validate_y(self.y) self.y = self.validate_y(self.y)
self._horizontal_scrollbar.virtual_size = self._page.virtual_size.width self.vscroll.virtual_size = self.page.virtual_size.height
self._horizontal_scrollbar.window_size = self.size.width self.vscroll.window_size = self.size.height
self._vertical_scrollbar.virtual_size = self._page.virtual_size.height if self.layout.show_column(
self._vertical_scrollbar.window_size = self.size.height "vscroll", self.page.virtual_size.height > self.size.height
):
self.page.update()
self.hscroll.virtual_size = self.page.virtual_size.width
self.hscroll.window_size = self.size.width
if self.layout.show_row(
"hscroll", self.page.virtual_size.width > self.size.width
):
self.page.update()