mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
readme
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
50
README.md
50
README.md
@@ -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:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
@@ -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')")
|
||||||
|
|||||||
@@ -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()
|
|
||||||
@@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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})"
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user