This commit is contained in:
Will McGugan
2021-08-14 17:07:12 +01:00
parent 51d737b272
commit 611f089d9c
9 changed files with 51 additions and 52 deletions

View File

@@ -5,7 +5,14 @@ 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/).
## [0.1.9] - Unreleased ## [0.1.10] - Unreleased
### Changed
- Allowed callbacks to be async or non-async, and for event to be optional.
- Fixed exception in clock example https://github.com/willmcgugan/textual/issues/52
## [0.1.9] - 2021-08-06
### Added ### Added

View File

@@ -56,14 +56,14 @@ from textual.app import App
class Beeper(App): class Beeper(App):
async def on_key(self, event): def on_key(self):
self.console.bell() self.console.bell()
Beeper.run() Beeper.run()
``` ```
Here we can see a textual app with a single `on_key` method which will receive key events. Any key event will result in playing the terminal bell (which will generally emit an irritating beep). Hit Ctrl+C to exit. Here we can see a textual app with a single `on_key` method which will handle key events. Pressing any key will result in playing the terminal bell (generally an irritating beep). Hit Ctrl+C to exit.
Event handlers in Textual are defined by convention, not by inheritance (so you won't find an `on_key` method in the base class). Each event has a `name` attribute which for the key event is simply `"key"`. Textual will call the method named `on_<event.name>` if it exists. Event handlers in Textual are defined by convention, not by inheritance (so you won't find an `on_key` method in the base class). Each event has a `name` attribute which for the key event is simply `"key"`. Textual will call the method named `on_<event.name>` if it exists.
@@ -74,7 +74,7 @@ from textual.app import App
class ColorChanger(App): class ColorChanger(App):
async def on_key(self, event): def on_key(self, event):
if event.key.isdigit(): if event.key.isdigit():
self.background = f"on color({event.key})" self.background = f"on color({event.key})"
@@ -82,7 +82,9 @@ class ColorChanger(App):
ColorChanger.run(log="textual.log") ColorChanger.run(log="textual.log")
``` ```
This example also handles key events, and will set `App.background` if the key is a digit. So pressing the keys 0 to 9 will change the background color to the corresponding [ansi color](https://rich.readthedocs.io/en/latest/appendix/colors.html). You'll notice that the `on_key` method above contains an additional `event` parameter which wasn't present on the beeper example. If the `event` argument is present, Textual will call the handler with an event object. Every event has an associated handler object, in this case it is a KeyEvent which contains additional information regarding which key was pressed.
The key event handler above will set the background attribute if you press the keys 0-9, which turns the terminal to the corresponding [ansi color](https://rich.readthedocs.io/en/latest/appendix/colors.html).
Note that we didn't need to explicitly refresh the screen or draw anything. Setting the `background` attribute to a [Rich style](https://rich.readthedocs.io/en/latest/style.html) is enough for Textual to update the visuals. This is an example of _reactivity_ in Textual. To make changes to the terminal interface you modify the _state_ and let Textual update the UI. Note that we didn't need to explicitly refresh the screen or draw anything. Setting the `background` attribute to a [Rich style](https://rich.readthedocs.io/en/latest/style.html) is enough for Textual to update the visuals. This is an example of _reactivity_ in Textual. To make changes to the terminal interface you modify the _state_ and let Textual update the UI.
@@ -90,17 +92,16 @@ Note that we didn't need to explicitly refresh the screen or draw anything. Sett
To make more interesting apps you will need to make use of _widgets_, which are independent user interface elements. Textual comes with a (growing) library of widgets, but you can develop your own. To make more interesting apps you will need to make use of _widgets_, which are independent user interface elements. Textual comes with a (growing) library of widgets, but you can develop your own.
Let's look at an app which contains widgets. We will be using the built in `Placeholder` widget which you can use to design application layouts before you implement the real content. Let's look at an app which contains widgets. We will be using the built-in `Placeholder` widget which you can use to design application layouts before you implement the real content.
```python ```python
from textual import events
from textual.app import App from textual.app import App
from textual.widgets import Placeholder from textual.widgets import Placeholder
class SimpleApp(App): class SimpleApp(App):
async def on_mount(self, event: events.Mount) -> None: async def on_mount(self) -> None:
await self.view.dock(Placeholder(), edge="left", size=40) await self.view.dock(Placeholder(), edge="left", size=40)
await self.view.dock(Placeholder(), Placeholder(), edge="top") await self.view.dock(Placeholder(), Placeholder(), edge="top")
@@ -108,7 +109,9 @@ class SimpleApp(App):
SimpleApp.run(log="textual.log") SimpleApp.run(log="textual.log")
``` ```
This app contains a single event handler `on_mount`. The mount event is sent when the app or widget is ready to start processing events, and is typically used for initialization. In this case we are going to call `self.view.dock` to add widgets to the interface. This app contains a single event handler `on_mount`. The mount event is sent when the app or widget is ready to start processing events, and is typically used for initialization. You may have noticed that `on_mount` is an `async` function. Since Textual is an asynchronous framework we will need this if we need to call most other methods.
The `on_mount` method makes two calls to `self.view.dock` which adds widgets to tht terminal.
Here's the first line in the mount handler: Here's the first line in the mount handler:
@@ -145,7 +148,6 @@ Let's look at an example with a custom widget:
```python ```python
from rich.panel import Panel from rich.panel import Panel
from textual import events
from textual.app import App from textual.app import App
from textual.reactive import Reactive from textual.reactive import Reactive
from textual.widget import Widget from textual.widget import Widget
@@ -153,22 +155,22 @@ from textual.widget import Widget
class Hover(Widget): class Hover(Widget):
mouse_over: Reactive[bool] = Reactive(False) mouse_over = Reactive(False)
def render(self) -> Panel: def render(self) -> Panel:
return Panel("Hello [b]World[/b]", style=("on red" if self.mouse_over else "")) return Panel("Hello [b]World[/b]", style=("on red" if self.mouse_over else ""))
async def on_enter(self, event: events.Enter) -> None: def on_enter(self) -> None:
self.mouse_over = True self.mouse_over = True
async def on_leave(self, event: events.Leave) -> None: def on_leave(self) -> None:
self.mouse_over = False self.mouse_over = False
class HoverApp(App): class HoverApp(App):
"""Hover widget demonstration.""" """Demonstrates custom widgets"""
async def on_mount(self, event: events.Mount) -> None: async def on_mount(self) -> None:
hovers = (Hover() for _ in range(10)) hovers = (Hover() for _ in range(10))
await self.view.dock(*hovers, edge="top") await self.view.dock(*hovers, edge="top")
@@ -179,26 +181,34 @@ HoverApp.run(log="textual.log")
The `Hover` class is a custom widget which displays a panel containing the classic text "Hello World". The first line in the Hover class may seem a little mysterious at this point: The `Hover` class is a custom widget which displays a panel containing the classic text "Hello World". The first line in the Hover class may seem a little mysterious at this point:
```python ```python
mouse_over: Reactive[bool] = Reactive(False) mouse_over = Reactive(False)
``` ```
This adds a `mouse_over` attribute to your class which is a bool with a default of `False`. The typing part (`Reactive[bool]`) is not required, but will help you find bugs if you are using a tool like [Mypy](https://mypy.readthedocs.io/en/stable/). Adding attributes like this makes them _reactive_, and any changes will result in the widget updating. This adds a `mouse_over` attribute to your class which is a bool with a default of `False`. Adding attributes like this makes them _reactive_: any changes will result in the widget updating.
The following `render()` method is where you define how the widget should be displayed. In the Hover widget we return a Panel containing rich text with a background that changes depending on the value of `mouse_over`. The goal here is to add a mouse hover effect to the widget, which we can achieve by handling two events: `Enter` and `Leave`. These events are sent when the mouse enters or leaves the widget. The following `render()` method is where you define how the widget should be displayed. In the Hover widget we return a [Panel](https://rich.readthedocs.io/en/latest/panel.html) containing rich text with a background that changes depending on the value of `mouse_over`. The goal here is to add a mouse hover effect to the widget, which we can achieve by handling two events: `Enter` and `Leave`. These events are sent when the mouse enters or leaves the widget.
Here are the two event handlers again: Here are the two event handlers again:
```python ```python
async def on_enter(self, event: events.Enter) -> None: def on_enter(self) -> None:
self.mouse_over = True self.mouse_over = True
async def on_leave(self, event: events.Leave) -> None: def on_leave(self) -> None:
self.mouse_over = False self.mouse_over = False
``` ```
Both event handlers set the `mouse_over` attribute which, because it is reactive, will result in the widget's `render()` method being called. Both event handlers set the `mouse_over` attribute which, because it is reactive, will result in the widget's `render()` method being called.
The app class has a `Mount` handler where we _dock_ 10 of these custom widgets from the top edge, stacking them vertically. If you run this script you will see something like the following: The `HoverApp` has a `on_mount` handler which creates 10 Hover widgets and docks them on the top edge, creating a vertical stack.
```python
async def on_mount(self) -> None:
hovers = (Hover() for _ in range(10))
await self.view.dock(*hovers, edge="top")
```
If you run this script you will see something like the following:
![widgets](./imgs/custom.gif) ![widgets](./imgs/custom.gif)

View File

@@ -2,7 +2,7 @@ from textual.app import App
class Colorizer(App): class Colorizer(App):
async def on_load(self, event): async def on_load(self):
await self.bind("r", "color('red')") await self.bind("r", "color('red')")
await self.bind("g", "color('green')") await self.bind("g", "color('green')")
await self.bind("b", "color('blue')") await self.bind("b", "color('blue')")

View File

@@ -1,9 +0,0 @@
from textual.app import App
class Quiter(App):
async def on_load(self, event):
await self.bind("q", "quit")
Quiter.run()

View File

@@ -2,7 +2,7 @@ from textual.app import App
class Beeper(App): class Beeper(App):
async def on_key(self, event): def on_key(self):
self.console.bell() self.console.bell()

View File

@@ -2,7 +2,7 @@ from textual.app import App
class ColorChanger(App): class ColorChanger(App):
async def on_key(self, event): def on_key(self, event):
if event.key.isdigit(): if event.key.isdigit():
self.background = f"on color({event.key})" self.background = f"on color({event.key})"

View File

@@ -1,4 +1,3 @@
from rich.console import RenderableType
from rich.panel import Panel from rich.panel import Panel
from textual.app import App from textual.app import App
@@ -10,7 +9,7 @@ class Hover(Widget):
mouse_over = Reactive(False) mouse_over = Reactive(False)
def render(self) -> RenderableType: def render(self) -> Panel:
return Panel("Hello [b]World[/b]", style=("on red" if self.mouse_over else "")) return Panel("Hello [b]World[/b]", style=("on red" if self.mouse_over else ""))
def on_enter(self) -> None: def on_enter(self) -> None:

View File

@@ -125,7 +125,7 @@ class Size(NamedTuple):
"Dimensions.__contains__ requires an iterable of two integers" "Dimensions.__contains__ requires an iterable of two integers"
) )
width, height = self width, height = self
return bool(width > x >= 0 and height > y >= 0) return width > x >= 0 and height > y >= 0
class Region(NamedTuple): class Region(NamedTuple):
@@ -164,7 +164,7 @@ class Region(NamedTuple):
""" """
x, y = origin x, y = origin
width, height = size width, height = size
return Region(x, y, width, height) return cls(x, y, width, height)
def __bool__(self) -> bool: def __bool__(self) -> bool:
return bool(self.width and self.height) return bool(self.width and self.height)
@@ -182,7 +182,7 @@ class Region(NamedTuple):
return self.x + self.width return self.x + self.width
@property @property
def y_end(self) -> int: def y_max(self) -> int:
return self.y + self.height return self.y + self.height
@property @property
@@ -216,7 +216,7 @@ class Region(NamedTuple):
@property @property
def y_range(self) -> range: def y_range(self) -> range:
return range(self.y, self.y_end) return range(self.y, self.y_max)
def __add__(self, other: Any) -> Region: def __add__(self, other: Any) -> Region:
if isinstance(other, tuple): if isinstance(other, tuple):

View File

@@ -307,7 +307,7 @@ class Layout(ABC):
height = self.height height = self.height
screen = Region(0, 0, width, height) screen = Region(0, 0, width, height)
crop_region = crop or Region(0, 0, self.width, self.height) crop_region = crop.intersection(screen) if crop else screen
_Segment = Segment _Segment = Segment
divide = _Segment.divide divide = _Segment.divide
@@ -326,23 +326,15 @@ class Layout(ABC):
# Go through all the renders in reverse order and fill buckets with no render # Go through all the renders in reverse order and fill buckets with no render
renders = list(self._get_renders(console)) renders = list(self._get_renders(console))
clip_y, clip_y2 = crop_region.y_extents
for region, clip, lines in chain( for region, clip, lines in chain(
renders, [(screen, screen, background_render)] renders, [(screen, screen, background_render)]
): ):
# clip = clip.intersection(crop_region)
render_region = region.intersection(clip) render_region = region.intersection(clip)
for y, line in enumerate(lines, render_region.y): for y, line in zip(render_region.y_range, lines):
if clip_y > y > clip_y2:
continue first_cut, last_cut = render_region.x_extents
# first_cut = clamp(render_region.x, clip_x, clip_x2) final_cuts = [cut for cut in cuts[y] if (last_cut >= cut >= first_cut)]
# last_cut = clamp(render_region.x + render_region.width, clip_x, clip_x2)
first_cut = render_region.x
last_cut = render_region.x_max
final_cuts = [cut for cut in cuts[y] if (last_cut >= cut >= first_cut)]
# final_cuts = cuts[y]
# log(final_cuts, render_region.x_extents)
if len(final_cuts) == 2: if len(final_cuts) == 2:
cut_segments = [line] cut_segments = [line]
else: else: