This commit is contained in:
Will McGugan
2021-07-18 10:31:00 +01:00
parent 4cb9fe1b01
commit f737252911
13 changed files with 154 additions and 99 deletions

View File

@@ -1,7 +1,6 @@
from textual import events
from textual.app import App
from textual.reactive import Reactive
from textual.views import DockView
from textual.widgets import Footer, Placeholder
@@ -9,33 +8,29 @@ class SmoothApp(App):
"""Demonstrates smooth animation"""
async def on_load(self, event: events.Load) -> None:
await self.bind("q,ctrl+c", "quit")
await self.bind("x", "bang")
await self.bind("b", "toggle_sidebar")
"""Bing keys here."""
await self.bind("b", "toggle_sidebar", "Toggle sidebar")
await self.bind("q", "quit", "Quit")
show_bar: Reactive[bool] = Reactive(False)
async def watch_show_bar(self, show_bar: bool) -> None:
"""Called when show_bar changes."""
self.animator.animate(self.bar, "layout_offset_x", 0 if show_bar else -40)
async def action_toggle_sidebar(self) -> None:
"""Called when user hits b key."""
self.show_bar = not self.show_bar
async def on_startup(self, event: events.Startup) -> None:
view = await self.push_view(DockView())
"""Build layout here."""
footer = Footer()
self.bar = Placeholder(name="left")
self.bar.layout_offset_x = -40
footer.add_key("b", "Toggle sidebar")
footer.add_key("q", "Quit")
await view.dock(footer, edge="bottom")
await view.dock(self.bar, edge="left", size=40, z=1)
await view.dock(Placeholder(), Placeholder(), edge="top")
await self.view.dock(footer, edge="bottom")
await self.view.dock(self.bar, edge="left", size=40, z=1)
await self.view.dock(Placeholder(), Placeholder(), edge="top")
SmoothApp.run()

View File

@@ -108,22 +108,23 @@ class CalculatorApp(App):
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"))
await self.push_view(View(layout=layout))
# Create a grid layout
grid = await self.view.dock_grid(
gap=(2, 1), gutter=1, align=("center", "center")
)
# Create rows / columns / areas
layout.add_column("col", max_size=30, repeat=4)
layout.add_row("numbers", max_size=15)
layout.add_row("row", max_size=15, repeat=5)
layout.add_areas(
grid.add_column("col", max_size=30, repeat=4)
grid.add_row("numbers", max_size=15)
grid.add_row("row", max_size=15, repeat=5)
grid.add_areas(
clear="col1,row1",
numbers="col1-start|col4-end,numbers",
zero="col1-start|col2-end,row5",
)
# Place out widgets in to the layout
layout.place(clear=self.c)
layout.place(
grid.place(clear=self.c)
grid.place(
*self.buttons.values(), clear=self.ac, numbers=self.numbers, zero=self.zero
)

View File

@@ -11,25 +11,24 @@ class GridTest(App):
async def on_startup(self, event: events.Startup) -> None:
layout = GridLayout()
await self.push_view(View(layout=layout))
grid = await self.view.dock_grid()
layout.add_column(fraction=1, name="left", min_size=20)
layout.add_column(size=30, name="center")
layout.add_column(fraction=1, name="right")
grid.add_column(fraction=1, name="left", min_size=20)
grid.add_column(size=30, name="center")
grid.add_column(fraction=1, name="right")
layout.add_row(fraction=1, name="top", min_size=2)
layout.add_row(fraction=2, name="middle")
layout.add_row(fraction=1, name="bottom")
grid.add_row(fraction=1, name="top", min_size=2)
grid.add_row(fraction=2, name="middle")
grid.add_row(fraction=1, name="bottom")
layout.add_areas(
grid.add_areas(
area1="left,top",
area2="center,middle",
area3="left-start|right-end,bottom",
area4="right,top-start|middle-end",
)
layout.place(
grid.place(
area1=Placeholder(name="area1"),
area2=Placeholder(name="area2"),
area3=Placeholder(name="area3"),

View File

@@ -11,16 +11,15 @@ class GridTest(App):
async def on_startup(self, event: events.Startup) -> None:
layout = GridLayout()
await self.push_view(View(layout=layout))
grid = await self.view.dock_grid()
layout.add_column("col", fraction=1, max_size=20)
layout.add_row("row", fraction=1, max_size=10)
layout.set_repeat(True, True)
layout.add_areas(center="col-2-start|col-4-end,row-2-start|row-3-end")
layout.set_align("stretch", "center")
grid.add_column("col", fraction=1, max_size=20)
grid.add_row("row", fraction=1, max_size=10)
grid.set_repeat(True, True)
grid.add_areas(center="col-2-start|col-4-end,row-2-start|row-3-end")
grid.set_align("stretch", "center")
layout.place(*(Placeholder() for _ in range(20)), center=Placeholder())
grid.place(*(Placeholder() for _ in range(20)), center=Placeholder())
GridTest.run(title="Grid Test")

View File

@@ -2,7 +2,6 @@ from rich.markdown import Markdown
from textual import events
from textual.app import App
from textual.views import DockView
from textual.widgets import Header, Footer, Placeholder, ScrollView
@@ -10,25 +9,17 @@ class MyApp(App):
"""An example of a very simple Textual App"""
async def on_load(self, event: events.Load) -> None:
await self.bind("q,ctrl+c", "quit", "Quit")
await self.bind("b", "view.toggle('sidebar')", "Toggle sidebar")
await self.bind("q", "quit", "Quit")
async def on_startup(self, event: events.Startup) -> None:
view = await self.push_view(DockView())
footer = Footer()
header = Header()
body = ScrollView()
sidebar = Placeholder()
footer.add_key("b", "Toggle sidebar")
footer.add_key("q", "Quit")
await view.dock(header, edge="top")
await view.dock(footer, edge="bottom")
await view.dock(sidebar, edge="left", size=30, name="sidebar")
await view.dock(body, edge="right")
await self.view.dock(Header(), edge="top")
await self.view.dock(Footer(), edge="bottom")
await self.view.dock(Placeholder(), edge="left", size=30, name="sidebar")
await self.view.dock(body, edge="right")
async def get_markdown(filename: str) -> None:
with open(filename, "rt") as fh:

32
poetry.lock generated
View File

@@ -369,11 +369,11 @@ pyparsing = ">=2.0.2"
[[package]]
name = "pathspec"
version = "0.8.1"
version = "0.9.0"
description = "Utility library for gitignore style pattern matching of file paths."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
[[package]]
name = "platformdirs"
@@ -547,17 +547,24 @@ version = "10.6.0"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
category = "main"
optional = false
python-versions = ">=3.6,<4.0"
python-versions = "^3.6"
develop = false
[package.dependencies]
colorama = ">=0.4.0,<0.5.0"
commonmark = ">=0.9.0,<0.10.0"
pygments = ">=2.6.0,<3.0.0"
typing-extensions = {version = ">=3.7.4,<4.0.0", markers = "python_version < \"3.8\""}
colorama = "^0.4.0"
commonmark = "^0.9.0"
pygments = "^2.6.0"
typing-extensions = {version = "^3.7.4", markers = "python_version < \"3.8\""}
[package.extras]
jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"]
[package.source]
type = "git"
url = "git@github.com:willmcgugan/rich"
reference = "link-id"
resolved_reference = "c4c00a2d0441519ced7ab2dead931341d9345eda"
[[package]]
name = "six"
version = "1.16.0"
@@ -636,7 +643,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes
[metadata]
lock-version = "1.1"
python-versions = "^3.7"
content-hash = "b78b6843dbfa68dd86e0ec81c9f1980f57eb85c13d1df9b497c34762e8805699"
content-hash = "89e70da124ff666d5f911585eb2032d523499bcfe3c0efad9b2f5367cc64183b"
[metadata.files]
appdirs = [
@@ -865,8 +872,8 @@ packaging = [
{file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"},
]
pathspec = [
{file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"},
{file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"},
{file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"},
{file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"},
]
platformdirs = [
{file = "platformdirs-2.0.2-py2.py3-none-any.whl", hash = "sha256:0b9547541f599d3d242078ae60b927b3e453f0ad52f58b4d4bc3be86aed3ec41"},
@@ -990,10 +997,7 @@ regex = [
{file = "regex-2021.7.6-cp39-cp39-win_amd64.whl", hash = "sha256:4c9c3155fe74269f61e27617529b7f09552fbb12e44b1189cebbdb24294e6e1c"},
{file = "regex-2021.7.6.tar.gz", hash = "sha256:8394e266005f2d8c6f0bc6780001f7afa3ef81a7a2111fa35058ded6fce79e4d"},
]
rich = [
{file = "rich-10.6.0-py3-none-any.whl", hash = "sha256:d3f72827cd5df13b2ef7f1a97f81ec65548d4fdeb92cef653234f227580bbb2a"},
{file = "rich-10.6.0.tar.gz", hash = "sha256:128261b3e2419a4ef9c97066ccc2abbfb49fa7c5e89c3fe4056d00aa5e9c1e65"},
]
rich = []
six = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},

View File

@@ -21,8 +21,8 @@ classifiers = [
[tool.poetry.dependencies]
python = "^3.7"
rich = "^10.6.0"
#rich = {git = "git@github.com:willmcgugan/rich", rev = "height-fixes"}
#rich = "^10.6.0"
rich = {git = "git@github.com:willmcgugan/rich", rev = "link-id"}
typing-extensions = { version = "^3.10.0", python = "<3.8" }
[tool.poetry.dev-dependencies]

View File

View File

@@ -11,13 +11,11 @@ import rich.repr
from rich.screen import Screen
from rich import get_console
from rich.console import Console, RenderableType
from rich.style import Style
from rich.traceback import Traceback
from . import events
from . import actions
from ._animator import Animator
from ._profile import timer
from .binding import Bindings, NoBinding
from .geometry import Point, Region
from . import log
@@ -92,7 +90,7 @@ class App(MessagePump):
self.driver_class = driver_class or LinuxDriver
self._title = title
self._layout = DockLayout()
self._view_stack: list[View] = []
self._view_stack: list[DockView] = []
self.children: set[MessagePump] = set()
self.focused: Widget | None = None
@@ -111,7 +109,7 @@ class App(MessagePump):
self.log_file = open(log, "wt") if log else None
self.bindings.bind("ctrl+c", "quit")
self.bindings.bind("ctrl+c", "quit", show=False)
super().__init__()
@@ -130,7 +128,7 @@ class App(MessagePump):
return self._animator
@property
def view(self) -> View:
def view(self) -> DockView:
return self._view_stack[-1]
def log(self, *args: Any, verbosity: int = 0) -> None:
@@ -143,9 +141,16 @@ class App(MessagePump):
pass
async def bind(
self, keys: str, action: str, description: str = "", show: bool = False
self,
keys: str,
action: str,
description: str = "",
show: bool = True,
key_display: str | None = None,
) -> None:
self.bindings.bind(keys, action, description, show=show)
self.bindings.bind(
keys, action, description, show=show, key_display=key_display
)
@classmethod
def run(
@@ -246,7 +251,7 @@ class App(MessagePump):
log(f"driver={self.driver_class}")
await self.dispatch_message(events.Load(sender=self))
await self.push_view(View())
await self.push_view(DockView())
try:
driver.start_application_mode()
@@ -345,14 +350,18 @@ class App(MessagePump):
def get_widget_at(self, x: int, y: int) -> tuple[Widget, Region]:
return self.view.get_widget_at(x, y)
async def on_event(self, event: events.Event) -> None:
if isinstance(event, events.Key):
async def press(self, key: str) -> bool:
try:
binding = self.bindings.get_key(event.key)
binding = self.bindings.get_key(key)
except NoBinding:
pass
return False
else:
await self.action(binding.action)
return True
async def on_event(self, event: events.Event) -> None:
if isinstance(event, events.Key):
if await self.press(event.key):
return
await super().on_event(event)
@@ -425,6 +434,9 @@ class App(MessagePump):
async def on_resize(self, event: events.Resize) -> None:
await self.view.post_message(event)
async def action_press(self, key: str) -> None:
await self.press(key)
async def action_quit(self) -> None:
await self.shutdown()
@@ -467,9 +479,9 @@ if __name__ == "__main__":
"""Just a test app."""
async def on_load(self, event: events.Load) -> None:
await self.bind("q,ctrl+c", "quit")
await self.bind("x", "bang")
await self.bind("b", "toggle_sidebar")
await self.bind("q,ctrl+c", "quit", "Exit app")
await self.bind("x", "bang", "Test error handling")
await self.bind("b", "toggle_sidebar", "Toggle sidebar")
show_bar: Reactive[bool] = Reactive(False)
@@ -486,8 +498,6 @@ if __name__ == "__main__":
header = Header()
footer = Footer()
self.bar = Placeholder(name="left")
footer.add_key("b", "Toggle sidebar")
footer.add_key("q", "Quit")
await view.dock(header, edge="top")
await view.dock(footer, edge="bottom")

View File

@@ -12,6 +12,7 @@ class Binding:
action: str
description: str
show: bool = False
key_display: str | None = None
class Bindings:
@@ -20,16 +21,24 @@ class Bindings:
def __init__(self) -> None:
self.keys: dict[str, Binding] = {}
@property
def shown_keys(self) -> list[Binding]:
keys = [binding for binding in self.keys.values() if binding.show]
return keys
def bind(
self, keys: str, action: str, description: str = "", show: bool = False
self,
keys: str,
action: str,
description: str = "",
show: bool = True,
key_display: str | None = None,
) -> None:
all_keys = [key.strip() for key in keys.split(",")]
for key in all_keys:
self.keys[key] = Binding(key, action, description, show=show)
self.keys[key] = Binding(
key, action, description, show=show, key_display=key_display
)
def get_key(self, key: str) -> Binding:
try:

View File

@@ -8,6 +8,7 @@ import rich.repr
from rich.style import Style
from . import events
from . import log
from .layout import Layout, NoWidget
from .layouts.dock import DockLayout
from .geometry import Dimensions, Point, Region
@@ -30,6 +31,8 @@ class View(Widget):
self.size = Dimensions(0, 0)
self.widgets: set[Widget] = set()
self.named_widgets: dict[str, Widget] = {}
self._mouse_style: Style = Style()
self._mouse_widget: Widget | None = None
super().__init__(name)
background: Reactive[str] = Reactive("")
@@ -148,6 +151,7 @@ class View(Widget):
await self.refresh_layout()
async def _on_mouse_move(self, event: events.MouseMove) -> None:
try:
if self.app.mouse_captured:
widget = self.app.mouse_captured
@@ -157,8 +161,13 @@ class View(Widget):
except NoWidget:
await self.app.set_mouse_over(None)
else:
await self.app.set_mouse_over(widget)
if event.style is not self._mouse_style and self._mouse_widget:
await self.app.broker_event("leave", event, self._mouse_widget)
await self.app.broker_event("enter", event, widget)
self._mouse_style = event.style
self._mouse_widget = widget
await self.app.set_mouse_over(widget)
await widget.forward_event(
events.MouseMove(
self,

View File

@@ -2,6 +2,7 @@ from __future__ import annotations
from typing import cast, Optional
from ..layouts.dock import DockLayout, Dock, DockEdge
from ..layouts.grid import GridLayout, GridAlign
from ..view import View
from ..widget import Widget
@@ -23,7 +24,7 @@ class DockView(View):
edge: DockEdge = "top",
z: int = 0,
size: int | None | DoNotSet = do_not_set,
name: str | None = None
name: str | None = None,
) -> None:
dock = Dock(edge, widgets, z)
@@ -38,3 +39,29 @@ class DockView(View):
else:
await self.mount(**{name: widget})
await self.refresh_layout()
async def dock_grid(
self,
*,
edge: DockEdge = "top",
z: int = 0,
size: int | None | DoNotSet = do_not_set,
name: str | None = None,
gap: tuple[int, int] | int | None = None,
gutter: tuple[int, int] | int | None = None,
align: tuple[GridAlign, GridAlign] | None = None,
) -> GridLayout:
grid = GridLayout(gap=gap, gutter=gutter, align=align)
view = View(layout=grid)
dock = Dock(edge, (view,), z)
assert isinstance(self.layout, DockLayout)
self.layout.docks.append(dock)
if size is not do_not_set:
view.layout_size = cast(Optional[int], size)
if not self.is_mounted(view):
if name is None:
await self.mount(view)
else:
await self.mount(**{name: view})
return grid

View File

@@ -1,8 +1,8 @@
from rich.console import RenderableType
from rich.style import Style
from rich.text import Text
import rich.repr
from .. import events
from ..widget import Widget
@@ -20,7 +20,6 @@ class Footer(Widget):
self.keys.append((key, label))
def render(self) -> RenderableType:
text = Text(
style="white on dark_green",
no_wrap=True,
@@ -28,7 +27,19 @@ class Footer(Widget):
justify="left",
end="",
)
for key, label in self.keys:
text.append(f" {key.upper()} ", style="default on default")
text.append(f" {label} ")
for binding in self.app.bindings.shown_keys:
key_display = (
binding.key.upper()
if binding.key_display is None
else binding.key_display
)
key_text = Text.assemble(
(f" {key_display} ", "default on default"), f" {binding.description} "
)
key_text.stylize(Style(meta={"@click": f"app.press('{binding.key}')"}))
text.append_text(key_text)
# text.append(f" {key_display} ", style="default on default")
# text.append(f" {binding.description} ")
# text.stylize(Style(meta={"@enter": "app.bell()"}))
return text