mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
more docs and compute example
This commit is contained in:
@@ -23,7 +23,7 @@ On modern terminal software (installed by default on most systems), Textual apps
|
|||||||
|
|
||||||
## Compatibility
|
## Compatibility
|
||||||
|
|
||||||
Textual runs on Linux, MacOS, and Windows. Textual requires Python 3.7 or above.
|
Textual runs on Linux, macOS, and Windows. Textual requires Python 3.7 or above.
|
||||||
|
|
||||||
## Installing
|
## Installing
|
||||||
|
|
||||||
|
|||||||
13
docs/examples/guide/reactivity/computed01.css
Normal file
13
docs/examples/guide/reactivity/computed01.css
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
#color-inputs {
|
||||||
|
dock: top;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
Input {
|
||||||
|
width: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
#color {
|
||||||
|
height: 100%;
|
||||||
|
border: tall $secondary;
|
||||||
|
}
|
||||||
47
docs/examples/guide/reactivity/computed01.py
Normal file
47
docs/examples/guide/reactivity/computed01.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.color import Color
|
||||||
|
from textual.containers import Horizontal
|
||||||
|
from textual.reactive import reactive
|
||||||
|
from textual.widgets import Input, Static
|
||||||
|
|
||||||
|
|
||||||
|
class ComputedApp(App):
|
||||||
|
CSS_PATH = "computed01.css"
|
||||||
|
|
||||||
|
red = reactive(0)
|
||||||
|
green = reactive(0)
|
||||||
|
blue = reactive(0)
|
||||||
|
color = reactive(Color.parse("transparent"))
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Horizontal(
|
||||||
|
Input("0", placeholder="Enter red 0-255", id="red"),
|
||||||
|
Input("0", placeholder="Enter green 0-255", id="green"),
|
||||||
|
Input("0", placeholder="Enter blue 0-255", id="blue"),
|
||||||
|
id="color-inputs",
|
||||||
|
)
|
||||||
|
yield Static(id="color")
|
||||||
|
|
||||||
|
def compute_color(self) -> Color: # (1)!
|
||||||
|
return Color(self.red, self.green, self.blue).clamped
|
||||||
|
|
||||||
|
def watch_color(self, color: Color) -> None: # (2)
|
||||||
|
self.query_one("#color").styles.background = color
|
||||||
|
|
||||||
|
def on_input_changed(self, event: Input.Changed) -> None:
|
||||||
|
try:
|
||||||
|
component = int(event.value)
|
||||||
|
except ValueError:
|
||||||
|
self.bell()
|
||||||
|
else:
|
||||||
|
if event.input.id == "red":
|
||||||
|
self.red = component
|
||||||
|
elif event.input.id == "green":
|
||||||
|
self.green = component
|
||||||
|
else:
|
||||||
|
self.blue = component
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = ComputedApp()
|
||||||
|
app.run()
|
||||||
9
docs/examples/guide/reactivity/refresh01.css
Normal file
9
docs/examples/guide/reactivity/refresh01.css
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
Input {
|
||||||
|
dock: top;
|
||||||
|
margin-top: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Name {
|
||||||
|
height: 100%;
|
||||||
|
content-align: center middle;
|
||||||
|
}
|
||||||
29
docs/examples/guide/reactivity/refresh01.py
Normal file
29
docs/examples/guide/reactivity/refresh01.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.reactive import reactive
|
||||||
|
from textual.widget import Widget
|
||||||
|
from textual.widgets import Input
|
||||||
|
|
||||||
|
|
||||||
|
class Name(Widget):
|
||||||
|
"""Generates a greeting."""
|
||||||
|
|
||||||
|
who = reactive("name")
|
||||||
|
|
||||||
|
def render(self) -> str:
|
||||||
|
return f"Hello, {self.who}!"
|
||||||
|
|
||||||
|
|
||||||
|
class WatchApp(App):
|
||||||
|
CSS_PATH = "refresh01.css"
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Input(placeholder="Enter your name")
|
||||||
|
yield Name()
|
||||||
|
|
||||||
|
def on_input_changed(self, event: Input.Changed) -> None:
|
||||||
|
self.query_one(Name).who = event.value
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = WatchApp()
|
||||||
|
app.run()
|
||||||
10
docs/examples/guide/reactivity/refresh02.css
Normal file
10
docs/examples/guide/reactivity/refresh02.css
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
Input {
|
||||||
|
dock: top;
|
||||||
|
margin-top: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Name {
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
border: heavy $secondary;
|
||||||
|
}
|
||||||
29
docs/examples/guide/reactivity/refresh02.py
Normal file
29
docs/examples/guide/reactivity/refresh02.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.reactive import reactive
|
||||||
|
from textual.widget import Widget
|
||||||
|
from textual.widgets import Input
|
||||||
|
|
||||||
|
|
||||||
|
class Name(Widget):
|
||||||
|
"""Generates a greeting."""
|
||||||
|
|
||||||
|
who = reactive("name", layout=True) # (1)!
|
||||||
|
|
||||||
|
def render(self) -> str:
|
||||||
|
return f"Hello, {self.who}!"
|
||||||
|
|
||||||
|
|
||||||
|
class WatchApp(App):
|
||||||
|
CSS_PATH = "refresh02.css"
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Input(placeholder="Enter your name")
|
||||||
|
yield Name()
|
||||||
|
|
||||||
|
def on_input_changed(self, event: Input.Changed) -> None:
|
||||||
|
self.query_one(Name).who = event.value
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = WatchApp()
|
||||||
|
app.run()
|
||||||
4
docs/examples/guide/reactivity/validate01.css
Normal file
4
docs/examples/guide/reactivity/validate01.css
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
#buttons {
|
||||||
|
dock: top;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
38
docs/examples/guide/reactivity/validate01.py
Normal file
38
docs/examples/guide/reactivity/validate01.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.containers import Horizontal
|
||||||
|
from textual.reactive import reactive
|
||||||
|
from textual.widgets import Button, TextLog
|
||||||
|
|
||||||
|
|
||||||
|
class ValidateApp(App):
|
||||||
|
CSS_PATH = "validate01.css"
|
||||||
|
|
||||||
|
count = reactive(0)
|
||||||
|
|
||||||
|
def validate_count(self, count: int) -> int:
|
||||||
|
"""Validate value."""
|
||||||
|
if count < 0:
|
||||||
|
count = 0
|
||||||
|
elif count > 10:
|
||||||
|
count = 10
|
||||||
|
return count
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Horizontal(
|
||||||
|
Button("+1", id="plus", variant="success"),
|
||||||
|
Button("-1", id="minus", variant="error"),
|
||||||
|
id="buttons",
|
||||||
|
)
|
||||||
|
yield TextLog(highlight=True)
|
||||||
|
|
||||||
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||||
|
if event.button.id == "plus":
|
||||||
|
self.count += 1
|
||||||
|
else:
|
||||||
|
self.count -= 1
|
||||||
|
self.query_one(TextLog).write(f"{self.count=}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = ValidateApp()
|
||||||
|
app.run()
|
||||||
21
docs/examples/guide/reactivity/watch01.css
Normal file
21
docs/examples/guide/reactivity/watch01.css
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
Input {
|
||||||
|
dock: top;
|
||||||
|
margin-top: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#colors {
|
||||||
|
grid-size: 2 1;
|
||||||
|
grid-gutter: 2 4;
|
||||||
|
grid-columns: 1fr;
|
||||||
|
margin: 0 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#old {
|
||||||
|
height: 100%;
|
||||||
|
border: wide $secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
#new {
|
||||||
|
height: 100%;
|
||||||
|
border: wide $secondary;
|
||||||
|
}
|
||||||
33
docs/examples/guide/reactivity/watch01.py
Normal file
33
docs/examples/guide/reactivity/watch01.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.color import Color, ColorParseError
|
||||||
|
from textual.containers import Grid
|
||||||
|
from textual.reactive import reactive
|
||||||
|
from textual.widgets import Input, Static
|
||||||
|
|
||||||
|
|
||||||
|
class WatchApp(App):
|
||||||
|
CSS_PATH = "watch01.css"
|
||||||
|
|
||||||
|
color = reactive(Color.parse("transparent")) # (1)!
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Input(placeholder="Enter a color")
|
||||||
|
yield Grid(Static(id="old"), Static(id="new"), id="colors")
|
||||||
|
|
||||||
|
def watch_color(self, old_color: Color, new_color: Color) -> None: # (2)!
|
||||||
|
self.query_one("#old").styles.background = old_color
|
||||||
|
self.query_one("#new").styles.background = new_color
|
||||||
|
|
||||||
|
def on_input_submitted(self, event: Input.Submitted) -> None:
|
||||||
|
try:
|
||||||
|
input_color = Color.parse(event.value)
|
||||||
|
except ColorParseError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
self.query_one(Input).value = ""
|
||||||
|
self.color = input_color # (3)!
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = WatchApp()
|
||||||
|
app.run()
|
||||||
@@ -2,7 +2,7 @@ All you need to get started building Textual apps.
|
|||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
Textual requires Python 3.7 or later (if you have a choice, pick the most recent Python). Textual runs on Linux, MacOS, Windows and probably any OS where Python also runs.
|
Textual requires Python 3.7 or later (if you have a choice, pick the most recent Python). Textual runs on Linux, macOS, Windows and probably any OS where Python also runs.
|
||||||
|
|
||||||
!!! info inline end "Your platform"
|
!!! info inline end "Your platform"
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ Textual requires Python 3.7 or later (if you have a choice, pick the most recent
|
|||||||
|
|
||||||
All Linux distros come with a terminal emulator that can run Textual apps.
|
All Linux distros come with a terminal emulator that can run Textual apps.
|
||||||
|
|
||||||
### :material-apple: MacOS
|
### :material-apple: macOS
|
||||||
|
|
||||||
The default terminal app is limited to 256 colors. We recommend installing a newer terminal such as [iterm2](https://iterm2.com/), [Kitty](https://sw.kovidgoyal.net/kitty/), or [WezTerm](https://wezfurlong.org/wezterm/).
|
The default terminal app is limited to 256 colors. We recommend installing a newer terminal such as [iterm2](https://iterm2.com/), [Kitty](https://sw.kovidgoyal.net/kitty/), or [WezTerm](https://wezfurlong.org/wezterm/).
|
||||||
|
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ Widgets can be as simple as a piece of text, a button, or a fully-fledge compone
|
|||||||
|
|
||||||
### Composing
|
### Composing
|
||||||
|
|
||||||
To add widgets to your app implement a [`compose()`][textual.app.App.compose] method which should return a iterable of Widget instances. A list would work, but it is convenient to yield widgets, making the method a *generator*.
|
To add widgets to your app implement a [`compose()`][textual.app.App.compose] method which should return an iterable of Widget instances. A list would work, but it is convenient to yield widgets, making the method a *generator*.
|
||||||
|
|
||||||
The following example imports a builtin Welcome widget and yields it from compose.
|
The following example imports a builtin Welcome widget and yields it from compose.
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,224 @@
|
|||||||
# Reactivity
|
# Reactivity
|
||||||
|
|
||||||
TODO: Reactivity docs
|
Textual's reactive attributes are attributes _with superpowers_. In this chapter we will look at how reactive attributes can simplify your apps.
|
||||||
|
|
||||||
- What is reactivity
|
!!! quote
|
||||||
- Reactive variables
|
|
||||||
- Demo
|
With great power comes great responsibility.
|
||||||
- repaint vs layout
|
|
||||||
- Validation
|
— Uncle Ben
|
||||||
- Watch methods
|
|
||||||
|
## Reactive attributes
|
||||||
|
|
||||||
|
Textual provides an alternative way of adding attributes to your widget or App, which doesn't require adding them to your class constructor (`__init__`). To create these attributes import [reactive][textual.reactive.reactive] from `textual.reactive`, and assign them in the class scope.
|
||||||
|
|
||||||
|
The following code illustrates how to create reactive attributes:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from textual.reactive import reactive
|
||||||
|
from textual.widget import Widget
|
||||||
|
|
||||||
|
class Reactive(Widget):
|
||||||
|
|
||||||
|
name = reactive("Paul") # (1)!
|
||||||
|
count = reactive(0) # (2)!
|
||||||
|
is_cool = reactive(True) # (3)!
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Create a string attribute with a default of `"Paul"`
|
||||||
|
2. Creates an integer attribute with a default of `0`.
|
||||||
|
3. Creates a boolean attribute with a default of `True`.
|
||||||
|
|
||||||
|
The `reactive` constructor accepts a default value as the first positional argument.
|
||||||
|
|
||||||
|
!!! information
|
||||||
|
|
||||||
|
Textual uses Python's _descriptor protocol_ to create reactive attributes, which is the same protocol used by the builtin `property` decorator.
|
||||||
|
|
||||||
|
You can get and set these attributes in the same way as if you had assigned them in a `__init__` method. For instance `self.name = "Jessica"`, `self.count += 1`, or `print(self.is_cool)`.
|
||||||
|
|
||||||
|
### Dynamic defaults
|
||||||
|
|
||||||
|
You can also set the default to a function (or other callable). Textual will call this function to get the default value. The following code illustrates a reactive value which will be automatically assigned the current time when the widget is created:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from time import time
|
||||||
|
from textual.reactive import reactive
|
||||||
|
from textual.widget import Widget
|
||||||
|
|
||||||
|
class Timer(Widget):
|
||||||
|
|
||||||
|
start_time = reactive(time) # (1)!
|
||||||
|
```
|
||||||
|
|
||||||
|
1. The `time` function returns the current time in seconds.
|
||||||
|
|
||||||
|
### Typing reactive attributes
|
||||||
|
|
||||||
|
There is no need to specify a type hint if a reactive attribute has a default value, as type checkers will assume the attribute is the same type as the default.
|
||||||
|
|
||||||
|
You may want to add explicit type hints if the attribute type is a superset of the default type. For instance if you want to make an attribute optional. Here's how you would create a reactive string attribute which may be `None`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
name: reactive[str | None] = reactive("Paul")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Smart refresh
|
||||||
|
|
||||||
|
The first superpower we will look at is "smart refresh". When you modify a reactive attribute, Textual will make note of the fact that it has changed and refresh automatically.
|
||||||
|
|
||||||
|
!!! information
|
||||||
|
|
||||||
|
If you modify multiple reactive attribute, Textual will only do a single refresh to minimize updates.
|
||||||
|
|
||||||
|
Let's look at an example which illustrates this. In the following app, the value of an input is used to update a "Hello, World!" type greeting.
|
||||||
|
|
||||||
|
=== "refresh01.py"
|
||||||
|
|
||||||
|
```python hl_lines="7-13 24"
|
||||||
|
--8<-- "docs/examples/guide/reactivity/refresh01.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "refresh01.css"
|
||||||
|
|
||||||
|
```sass
|
||||||
|
--8<-- "docs/examples/guide/reactivity/refresh01.css"
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "Output"
|
||||||
|
|
||||||
|
```{.textual path="docs/examples/guide/reactivity/refresh01.py" press="tab,T,e,x,t,u,a,l"}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `Name` widget has a reactive `who` attribute. When the app modifies that attribute, a refresh happens automatically.
|
||||||
|
|
||||||
|
!!! information
|
||||||
|
|
||||||
|
Textual will check if a value has really changed, so assigning the same value wont prompt an unnecessary refresh.
|
||||||
|
|
||||||
|
### Disabling refresh
|
||||||
|
|
||||||
|
If you *don't* want an attribute to prompt a refresh or layout but you still want other reactive superpowers, you can use [var][textual.reactive.var] to create an attribute. You can import `var` from `textual.reactive`.
|
||||||
|
|
||||||
|
The following code illustrates how you create non-refreshing reactive attributes.
|
||||||
|
|
||||||
|
```python
|
||||||
|
class MyWidget(Widget):
|
||||||
|
count = var(0) # (1)!
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Changing `self.count` wont cause a refresh or layout.
|
||||||
|
|
||||||
|
### Layout
|
||||||
|
|
||||||
|
The smart refresh feature will update the content area of a widget, but will not change its size. If modifying an attribute should change the size of the widget, you should set `layout=True` on the reactive attribute. This ensures that your CSS layout will update accordingly.
|
||||||
|
|
||||||
|
The following example modifies "refresh01.py" so that the greeting has an automatic width.
|
||||||
|
|
||||||
|
=== "refresh02.py"
|
||||||
|
|
||||||
|
```python hl_lines="10"
|
||||||
|
--8<-- "docs/examples/guide/reactivity/refresh02.py"
|
||||||
|
```
|
||||||
|
1. This attribute will update the layout when changed.
|
||||||
|
|
||||||
|
=== "refresh02.css"
|
||||||
|
|
||||||
|
```sass hl_lines="7-9"
|
||||||
|
--8<-- "docs/examples/guide/reactivity/refresh02.css"
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "Output"
|
||||||
|
|
||||||
|
```{.textual path="docs/examples/guide/reactivity/refresh02.py" press="tab,n,a,m,e"}
|
||||||
|
```
|
||||||
|
|
||||||
|
If you type in to the input now, the greeting will expand to fit the content. If you were to set `layout=False` on the reactive attribute, you should see that the box remains the same size when you type.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
The next superpower we will look at is _validation_. If you add a method that begins with `validate_` followed by the name of your attribute, it will be called when you assign a value to that attribute. This method should accept the incoming value as a positional argument, and return the value to set (which may be the same or a different value).
|
||||||
|
|
||||||
|
A common use for this is to restrict numbers to a given range. The following example keeps a count. There is a button to increase the count, and a button to decrease it.
|
||||||
|
|
||||||
|
=== "validate01.py"
|
||||||
|
|
||||||
|
```python hl_lines="12-18 30 32"
|
||||||
|
--8<-- "docs/examples/guide/reactivity/validate01.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "validate01.css"
|
||||||
|
|
||||||
|
```sass
|
||||||
|
--8<-- "docs/examples/guide/reactivity/validate01.css"
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "Output"
|
||||||
|
|
||||||
|
```{.textual path="docs/examples/guide/reactivity/validate01.py"}
|
||||||
|
```
|
||||||
|
|
||||||
|
If you click the buttons in the above example it will show the current count. When `self.count` is modified in the button handler, Textual runs `validate_count` which limits self.count` to a maximum of 10, and stops it going below zero.
|
||||||
|
|
||||||
|
## Watch methods
|
||||||
|
|
||||||
|
Watch methods are another superpower. Textual will call watch methods when reactive attributes are modified. Watch methods begin with `watch_` followed by the name of the attribute. If the watch method accepts a positional argument, it will be called with the new assigned value. If the watch method accepts *two* positional arguments, it will be called with both the *old* value and the *new* value.
|
||||||
|
|
||||||
|
The follow app will display any color you type in to the input. Try it with a valid color in Textual CSS. For example `"darkorchid"` or `"#52de44".
|
||||||
|
|
||||||
|
=== "watch01.py"
|
||||||
|
|
||||||
|
```python hl_lines="17-19 28"
|
||||||
|
--8<-- "docs/examples/guide/reactivity/watch01.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Creates a reactive [color][textual.color.Color] attribute.
|
||||||
|
2. Called when `self.color` is changed.
|
||||||
|
3. New color is assigned here.
|
||||||
|
|
||||||
|
=== "watch01.css"
|
||||||
|
|
||||||
|
```sass
|
||||||
|
--8<-- "docs/examples/guide/reactivity/watch01.css"
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "Output"
|
||||||
|
|
||||||
|
```{.textual path="docs/examples/guide/reactivity/watch01.py" press="tab,d,a,r,k,o,r,c,h,i,d"}
|
||||||
|
```
|
||||||
|
|
||||||
|
The color is parsed in `on_input_submitted` and assigned to `self.color`. Because `color` is reactive, Textual also calls `watch_color` with the old and new values.
|
||||||
|
|
||||||
|
## Compute methods
|
||||||
|
|
||||||
|
Compute methods are the final superpower offered by the `reactive` descriptor. Textual runs compute methods to calculate the value of a reactive attribute. Compute methods begin with `compute_` followed by the name of the reactive value.
|
||||||
|
|
||||||
|
You could be forgiven in thinking this sounds a lot like Python's property decorator. The difference is that Textual will cache the value of compute methods, and update them when any other reactive attribute changes.
|
||||||
|
|
||||||
|
The following example uses a computed attribute. It displays three inputs for the each color component (red, green, and blue). If you enter numbers in to these inputs, the background color of another widget changes.
|
||||||
|
|
||||||
|
=== "computed01.py"
|
||||||
|
|
||||||
|
```python hl_lines="12-18 30 32"
|
||||||
|
--8<-- "docs/examples/guide/reactivity/computed01.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Combines color components in to a Color object.
|
||||||
|
2. The compute method is called when the _result_ of `compute_color` changes.
|
||||||
|
|
||||||
|
=== "computed01.css"
|
||||||
|
|
||||||
|
```sass
|
||||||
|
--8<-- "docs/examples/guide/reactivity/computed01.css"
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "Output"
|
||||||
|
|
||||||
|
```{.textual path="docs/examples/guide/reactivity/computed01.py"}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note the `compute_color` method which combines the color components into a [Color][textual.color.Color] object. When the _result_ of this method changes, Textual calls `watch_color` which uses the new color as a background.
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
|
||||||
|
You should avoid doing anything slow or cpu-intensive in a compute method. Textual calls compute methods on an object when _any_ reactive attribute changes, so it can known when it changes.
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
@@ -1,4 +1,5 @@
|
|||||||
# Welcome
|
|
||||||
|
# Introduction
|
||||||
|
|
||||||
Welcome to the [Textual](https://github.com/Textualize/textual) framework documentation. Built with ❤️ by [Textualize.io](https://www.textualize.io)
|
Welcome to the [Textual](https://github.com/Textualize/textual) framework documentation. Built with ❤️ by [Textualize.io](https://www.textualize.io)
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
---
|
||||||
|
hide:
|
||||||
|
- navigation
|
||||||
|
---
|
||||||
|
|
||||||
# Tutorial
|
# Tutorial
|
||||||
|
|
||||||
Welcome to the Textual Tutorial!
|
Welcome to the Textual Tutorial!
|
||||||
@@ -102,7 +107,7 @@ Let's examine stopwatch01.py in more detail.
|
|||||||
--8<-- "docs/examples/tutorial/stopwatch01.py"
|
--8<-- "docs/examples/tutorial/stopwatch01.py"
|
||||||
```
|
```
|
||||||
|
|
||||||
The first line imports the Textual `App` class. The second line imports two builtin widgets: `Footer` which shows available keys and `Header` which shows a title and the current time. Widgets are re-usable components responsible for managing a part of the screen. We will cover how to build such widgets in this tutorial.
|
The first line imports the Textual `App` class, which we will use as the base class for our App. The second line imports two builtin widgets: `Footer` which shows a bar at the bottom of the screen with current keys, and `Header` which shows a title and the current time at the top of the screen. Widgets are re-usable components responsible for managing a part of the screen. We will cover how to build widgets in this tutorial.
|
||||||
|
|
||||||
The following lines define the app itself:
|
The following lines define the app itself:
|
||||||
|
|
||||||
@@ -114,29 +119,29 @@ The App class is where most of the logic of Textual apps is written. It is respo
|
|||||||
|
|
||||||
Here's what the above app defines:
|
Here's what the above app defines:
|
||||||
|
|
||||||
- `BINDINGS` is a list of tuples that maps (or *binds*) keys to actions in your app. The first value in the tuple is the key; the second value is the name of the action; the final value is a short description. We have a single binding which maps the ++d++ key on to the "toggle_dark" action.
|
- `BINDINGS` is a list of tuples that maps (or *binds*) keys to actions in your app. The first value in the tuple is the key; the second value is the name of the action; the final value is a short description. We have a single binding which maps the ++d++ key on to the "toggle_dark" action. See [key bindings](./guide/input.md#bindings) in the guide for details.
|
||||||
|
|
||||||
- `compose()` is where we construct a user interface with widgets. The `compose()` method may return a list of widgets, but it is generally easier to _yield_ them (making this method a generator). In the example code we yield an instance of each of the widget classes we imported, i.e. `Header()` and `Footer()`.
|
- `compose()` is where we construct a user interface with widgets. The `compose()` method may return a list of widgets, but it is generally easier to _yield_ them (making this method a generator). In the example code we yield an instance of each of the widget classes we imported, i.e. `Header()` and `Footer()`.
|
||||||
|
|
||||||
- `action_toggle_dark()` defines an _action_ method. Actions are methods beginning with `action_` followed by the name of the action. The `BINDINGS` list above tells Textual to run this action when the user hits the ++d++ key.
|
- `action_toggle_dark()` defines an _action_ method. Actions are methods beginning with `action_` followed by the name of the action. The `BINDINGS` list above tells Textual to run this action when the user hits the ++d++ key. See [actions](./guide/actions.md) in the guide for details.
|
||||||
|
|
||||||
```python title="stopwatch01.py" hl_lines="20-22"
|
```python title="stopwatch01.py" hl_lines="20-22"
|
||||||
--8<-- "docs/examples/tutorial/stopwatch01.py"
|
--8<-- "docs/examples/tutorial/stopwatch01.py"
|
||||||
```
|
```
|
||||||
|
|
||||||
The final three lines create an instance of the app and call [run()][textual.app.App.run] which puts your terminal in to *application mode* and runs the app until you exit with ++ctrl+c++. This happens within a `__name__ == "__main__"` block so we could run the app with `python stopwatch01.py` or import it as part of a larger project.
|
The final three lines create an instance of the app and calls the [run()][textual.app.App.run] method which puts your terminal in to *application mode* and runs the app until you exit with ++ctrl+c++. This happens within a `__name__ == "__main__"` block so we could run the app with `python stopwatch01.py` or import it as part of a larger project.
|
||||||
|
|
||||||
## Designing a UI with widgets
|
## Designing a UI with widgets
|
||||||
|
|
||||||
The header and footer are builtin widgets. For our Stopwatch application we will need to build custom widgets.
|
Textual comes with a number of builtin widgets, like Header and Footer, which are versatile and re-usable. We will need to build some custom widgets for the stopwatch. Before we dive in to that, let's first sketch a design for the app — so we know what we're aiming for.
|
||||||
|
|
||||||
Let's sketch out a design for our app:
|
|
||||||
|
|
||||||
<div class="excalidraw">
|
<div class="excalidraw">
|
||||||
--8<-- "docs/images/stopwatch.excalidraw.svg"
|
--8<-- "docs/images/stopwatch.excalidraw.svg"
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
We will need to build a `Stopwatch` widget composed of the following _child_ widgets:
|
### Custom widgets
|
||||||
|
|
||||||
|
We need a `Stopwatch` widget composed of the following _child_ widgets:
|
||||||
|
|
||||||
- A "Start" button
|
- A "Start" button
|
||||||
- A "Stop" button
|
- A "Stop" button
|
||||||
@@ -151,15 +156,15 @@ Let's add those to the app. Just a skeleton for now, we will add the rest of the
|
|||||||
--8<-- "docs/examples/tutorial/stopwatch02.py"
|
--8<-- "docs/examples/tutorial/stopwatch02.py"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Extending widget classes
|
We've imported two new widgets in this code: `Button`, which creates a clickable button, and `Static` which is a base class for a simple control. We've also imported `Container` from `textual.containers` which (as the name suggests) is a Widget which contains other widgets.
|
||||||
|
|
||||||
We've imported two new widgets in this code: `Button`, which creates a clickable button, and `Static` which is a base class for a simple control. We've also imported `Container` from `textual.layout`. As the name suggests, `Container` is a Widget which contains other widgets. We will use this container to create a scrolling list of stopwatches.
|
We've defined an empty `TimeDisplay` widget by extending `Static`. We will flesh this out later.
|
||||||
|
|
||||||
We're extending Static as a foundation for our `TimeDisplay` widget. There are no methods on this class yet.
|
The Stopwatch widget also class extends `Static`. This class has a `compose()` method which yields child widgets, consisting of three `Button` objects and a single `TimeDisplay`. These widgets will form the stopwatch in our sketch.
|
||||||
|
|
||||||
The Stopwatch class extends Static to define a new widget. This class has a `compose()` method which yields its child widgets, consisting of three `Button` objects and a single `TimeDisplay`. These are all we need to build a stopwatch as in the sketch.
|
#### The buttons
|
||||||
|
|
||||||
The Button constructor takes a label to be displayed in the button ("Start", "Stop", or "Reset"). Additionally some of the buttons set the following parameters:
|
The Button constructor takes a label to be displayed in the button (`"Start"`, `"Stop"`, or `"Reset"`). Additionally some of the buttons set the following parameters:
|
||||||
|
|
||||||
- `id` is an identifier we can use to tell the buttons apart in code and apply styles. More on that later.
|
- `id` is an identifier we can use to tell the buttons apart in code and apply styles. More on that later.
|
||||||
- `variant` is a string which selects a default style. The "success" variant makes the button green, and the "error" variant makes it red.
|
- `variant` is a string which selects a default style. The "success" variant makes the button green, and the "error" variant makes it red.
|
||||||
@@ -188,21 +193,21 @@ Every widget has a `styles` object with a number of attributes that impact how t
|
|||||||
self.styles.background = "blue"
|
self.styles.background = "blue"
|
||||||
self.styles.color = "white"
|
self.styles.color = "white"
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! info inline end
|
|
||||||
|
|
||||||
Don't worry if you have never worked with CSS before. The dialect of CSS we use is greatly simplified over web based CSS and easy to learn!
|
|
||||||
|
|
||||||
|
|
||||||
While it's possible to set all styles for an app this way, it is rarely necessary. Textual has support for CSS (Cascading Style Sheets), a technology used by web browsers. CSS files are data files loaded by your app which contain information about styles to apply to your widgets.
|
While it's possible to set all styles for an app this way, it is rarely necessary. Textual has support for CSS (Cascading Style Sheets), a technology used by web browsers. CSS files are data files loaded by your app which contain information about styles to apply to your widgets.
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
|
||||||
|
The dialect of CSS used in Textual is greatly simplified over web based CSS and much easier to learn!
|
||||||
|
|
||||||
|
|
||||||
Let's add a CSS file to our application.
|
Let's add a CSS file to our application.
|
||||||
|
|
||||||
```python title="stopwatch03.py" hl_lines="24"
|
```python title="stopwatch03.py" hl_lines="24"
|
||||||
--8<-- "docs/examples/tutorial/stopwatch03.py"
|
--8<-- "docs/examples/tutorial/stopwatch03.py"
|
||||||
```
|
```
|
||||||
|
|
||||||
Adding the `CSS_PATH` class variable tells Textual to load the following file when it starts the app:
|
Adding the `CSS_PATH` class variable tells Textual to load the following file when the app starts:
|
||||||
|
|
||||||
```sass title="stopwatch03.css"
|
```sass title="stopwatch03.css"
|
||||||
--8<-- "docs/examples/tutorial/stopwatch03.css"
|
--8<-- "docs/examples/tutorial/stopwatch03.css"
|
||||||
@@ -213,7 +218,7 @@ If we run the app now, it will look *very* different.
|
|||||||
```{.textual path="docs/examples/tutorial/stopwatch03.py" title="stopwatch03.py"}
|
```{.textual path="docs/examples/tutorial/stopwatch03.py" title="stopwatch03.py"}
|
||||||
```
|
```
|
||||||
|
|
||||||
This app looks much more like our sketch. Textual has read style information from `stopwatch03.css` and applied it to the widgets.
|
This app looks much more like our sketch. let's look at how the Textual uses `stopwatch03.css` to apply styles.
|
||||||
|
|
||||||
### CSS basics
|
### CSS basics
|
||||||
|
|
||||||
@@ -285,6 +290,11 @@ You may have noticed that the stop button (`#stop` in the CSS) has `display: non
|
|||||||
|
|
||||||
We want our Stopwatch widget to have two states: a default state with a Start and Reset button; and a _started_ state with a Stop button. When a stopwatch is started it should also have a green background and bold text.
|
We want our Stopwatch widget to have two states: a default state with a Start and Reset button; and a _started_ state with a Stop button. When a stopwatch is started it should also have a green background and bold text.
|
||||||
|
|
||||||
|
<div class="excalidraw">
|
||||||
|
--8<-- "docs/images/css_stopwatch.excalidraw.svg"
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
We can accomplish this with a CSS _class_. Not to be confused with a Python class, a CSS class is like a tag you can add to a widget to modify its styles.
|
We can accomplish this with a CSS _class_. Not to be confused with a Python class, a CSS class is like a tag you can add to a widget to modify its styles.
|
||||||
|
|
||||||
Here's the new CSS:
|
Here's the new CSS:
|
||||||
@@ -295,10 +305,6 @@ Here's the new CSS:
|
|||||||
|
|
||||||
These new rules are prefixed with `.started`. The `.` indicates that `.started` refers to a CSS class called "started". The new styles will be applied only to widgets that have this CSS class.
|
These new rules are prefixed with `.started`. The `.` indicates that `.started` refers to a CSS class called "started". The new styles will be applied only to widgets that have this CSS class.
|
||||||
|
|
||||||
<div class="excalidraw">
|
|
||||||
--8<-- "docs/images/css_stopwatch.excalidraw.svg"
|
|
||||||
</div>
|
|
||||||
|
|
||||||
Some of the new styles have more than one selector separated by a space. The space indicates that the rule should match the second selector if it is a child of the first. Let's look at one of these styles:
|
Some of the new styles have more than one selector separated by a space. The space indicates that the rule should match the second selector if it is a child of the first. Let's look at one of these styles:
|
||||||
|
|
||||||
```sass
|
```sass
|
||||||
@@ -353,7 +359,7 @@ The first argument to `reactive` may be a default value or a callable that retur
|
|||||||
The `time` attribute has a simple float as the default value, so `self.time` will be `0` on start.
|
The `time` attribute has a simple float as the default value, so `self.time` will be `0` on start.
|
||||||
|
|
||||||
|
|
||||||
The `on_mount` method is an event handler which is called then the widget is first added to the application (or _mounted_). In this method we call [set_interval()][textual.message_pump.MessagePump.set_interval] to create a timer which calls `self.update_time` sixty times a second. This `update_time` method calculates the time elapsed since the widget started and assigns it to `self.time`. Which brings us to one of Reactive's super-powers.
|
The `on_mount` method is an event handler called then the widget is first added to the application (or _mounted_). In this method we call [set_interval()][textual.message_pump.MessagePump.set_interval] to create a timer which calls `self.update_time` sixty times a second. This `update_time` method calculates the time elapsed since the widget started and assigns it to `self.time`. Which brings us to one of Reactive's super-powers.
|
||||||
|
|
||||||
If you implement a method that begins with `watch_` followed by the name of a reactive attribute (making it a _watch method_), that method will be called when the attribute is modified.
|
If you implement a method that begins with `watch_` followed by the name of a reactive attribute (making it a _watch method_), that method will be called when the attribute is modified.
|
||||||
|
|
||||||
@@ -401,7 +407,7 @@ In addition, the `on_button_pressed` method on `Stopwatch` has grown some code t
|
|||||||
|
|
||||||
This code supplies missing features and makes our app useful. We've made the following changes.
|
This code supplies missing features and makes our app useful. We've made the following changes.
|
||||||
|
|
||||||
- The first line retrieves the button's ID, which we will use to decide what to do in response.
|
- The first line retrieves `id` attribute of the button that was pressed. We can use this to decide what to do in response.
|
||||||
- The second line calls `query_one` to get a reference to the `TimeDisplay` widget.
|
- The second line calls `query_one` to get a reference to the `TimeDisplay` widget.
|
||||||
- We call the method on `TimeDisplay` that matches the pressed button.
|
- We call the method on `TimeDisplay` that matches the pressed button.
|
||||||
- We add the "started" class when the Stopwatch is started (`self.add_class("started)`), and remove it (`self.remove_class("started")`) when it is stopped. This will update the Stopwatch visuals via CSS.
|
- We add the "started" class when the Stopwatch is started (`self.add_class("started)`), and remove it (`self.remove_class("started")`) when it is stopped. This will update the Stopwatch visuals via CSS.
|
||||||
@@ -411,15 +417,13 @@ If you run stopwatch06.py you will be able to use the stopwatches independently.
|
|||||||
```{.textual path="docs/examples/tutorial/stopwatch06.py" title="stopwatch06.py" press="tab,enter,_,_,tab,enter,_,tab"}
|
```{.textual path="docs/examples/tutorial/stopwatch06.py" title="stopwatch06.py" press="tab,enter,_,_,tab,enter,_,tab"}
|
||||||
```
|
```
|
||||||
|
|
||||||
The only remaining feature of the Stopwatch app left to implement is the ability to add and remove timers.
|
The only remaining feature of the Stopwatch app left to implement is the ability to add and remove stopwatches.
|
||||||
|
|
||||||
## Dynamic widgets
|
## Dynamic widgets
|
||||||
|
|
||||||
It's convenient to build a user interface with the `compose` method. We may also want to add or remove widgets while the app is running.
|
The Stopwatch app creates widgets when it starts via the `compose` method. We will also need to create new widgets while the app is running, and remove widgets we no longer need. We can do this by calling [mount()][textual.widget.Widget.mount] to add a widget, and [remove()][textual.widget.Widget.remove] to remove a widget.
|
||||||
|
|
||||||
To add a new child widget call `mount()` on the parent. To remove a widget, call its `remove()` method.
|
Let's use these methods to implement adding and removing stopwatches to our app.
|
||||||
|
|
||||||
Let's use these to implement adding and removing stopwatches to our app.
|
|
||||||
|
|
||||||
```python title="stopwatch.py" hl_lines="78-79 88-92 94-98"
|
```python title="stopwatch.py" hl_lines="78-79 88-92 94-98"
|
||||||
--8<-- "docs/examples/tutorial/stopwatch.py"
|
--8<-- "docs/examples/tutorial/stopwatch.py"
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ site_url: https://textual.textualize.io/
|
|||||||
repo_url: https://github.com/textualize/textual/
|
repo_url: https://github.com/textualize/textual/
|
||||||
|
|
||||||
nav:
|
nav:
|
||||||
- "index.md"
|
- Introduction:
|
||||||
- "getting_started.md"
|
- "index.md"
|
||||||
|
- "getting_started.md"
|
||||||
- "tutorial.md"
|
- "tutorial.md"
|
||||||
- Guide:
|
- Guide:
|
||||||
- "guide/index.md"
|
- "guide/index.md"
|
||||||
@@ -154,6 +155,7 @@ theme:
|
|||||||
- navigation.tabs
|
- navigation.tabs
|
||||||
- navigation.indexes
|
- navigation.indexes
|
||||||
- navigation.tabs.sticky
|
- navigation.tabs.sticky
|
||||||
|
- content.code.annotate
|
||||||
palette:
|
palette:
|
||||||
- media: "(prefers-color-scheme: light)"
|
- media: "(prefers-color-scheme: light)"
|
||||||
scheme: default
|
scheme: default
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ textual = "textual.cli.cli:run"
|
|||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.7"
|
python = "^3.7"
|
||||||
rich = "^12.6.0a2"
|
rich = "^12.6.0"
|
||||||
#rich = {path="../rich", develop=true}
|
#rich = {path="../rich", develop=true}
|
||||||
importlib-metadata = "^4.11.3"
|
importlib-metadata = "^4.11.3"
|
||||||
typing-extensions = { version = "^4.0.0", python = "<3.8" }
|
typing-extensions = { version = "^4.0.0", python = "<3.8" }
|
||||||
|
|||||||
@@ -132,12 +132,14 @@ class Reactive(Generic[ReactiveType]):
|
|||||||
if current_value != value or first_set:
|
if current_value != value or first_set:
|
||||||
setattr(obj, f"__first_set_{self.internal_name}", False)
|
setattr(obj, f"__first_set_{self.internal_name}", False)
|
||||||
setattr(obj, self.internal_name, value)
|
setattr(obj, self.internal_name, value)
|
||||||
self._check_watchers(obj, name, current_value)
|
self._check_watchers(obj, name, current_value, first_set=first_set)
|
||||||
if self._layout or self._repaint:
|
if self._layout or self._repaint:
|
||||||
obj.refresh(repaint=self._repaint, layout=self._layout)
|
obj.refresh(repaint=self._repaint, layout=self._layout)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _check_watchers(cls, obj: Reactable, name: str, old_value: Any) -> None:
|
def _check_watchers(
|
||||||
|
cls, obj: Reactable, name: str, old_value: Any, first_set: bool = False
|
||||||
|
) -> None:
|
||||||
|
|
||||||
internal_name = f"_reactive_{name}"
|
internal_name = f"_reactive_{name}"
|
||||||
value = getattr(obj, internal_name)
|
value = getattr(obj, internal_name)
|
||||||
@@ -175,6 +177,11 @@ class Reactive(Generic[ReactiveType]):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not first_set:
|
||||||
|
obj.post_message_no_wait(
|
||||||
|
events.Callback(obj, callback=partial(Reactive._compute, obj))
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def _compute(cls, obj: Reactable) -> None:
|
async def _compute(cls, obj: Reactable) -> None:
|
||||||
_rich_traceback_guard = True
|
_rich_traceback_guard = True
|
||||||
@@ -196,7 +203,7 @@ class reactive(Reactive[ReactiveType]):
|
|||||||
default (ReactiveType | Callable[[], ReactiveType]): A default value or callable that returns a default.
|
default (ReactiveType | Callable[[], ReactiveType]): A default value or callable that returns a default.
|
||||||
layout (bool, optional): Perform a layout on change. Defaults to False.
|
layout (bool, optional): Perform a layout on change. Defaults to False.
|
||||||
repaint (bool, optional): Perform a repaint on change. Defaults to True.
|
repaint (bool, optional): Perform a repaint on change. Defaults to True.
|
||||||
init (bool, optional): Call watchers on initialize (post mount). Defaults to False.
|
init (bool, optional): Call watchers on initialize (post mount). Defaults to True.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@@ -315,7 +315,7 @@ See the [tree.py](https://github.com/willmcgugan/rich/blob/master/examples/tree.
|
|||||||
<details>
|
<details>
|
||||||
<summary>Columns</summary>
|
<summary>Columns</summary>
|
||||||
|
|
||||||
Rich can render content in neat [columns](https://rich.readthedocs.io/en/latest/columns.html) with equal or optimal width. Here's a very basic clone of the (MacOS / Linux) `ls` command which displays a directory listing in columns:
|
Rich can render content in neat [columns](https://rich.readthedocs.io/en/latest/columns.html) with equal or optimal width. Here's a very basic clone of the (macOS / Linux) `ls` command which displays a directory listing in columns:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import os
|
import os
|
||||||
|
|||||||
@@ -9,13 +9,14 @@ from typing import TYPE_CHECKING, ClassVar, Collection, Iterable, NamedTuple, ca
|
|||||||
import rich.repr
|
import rich.repr
|
||||||
from rich.console import (
|
from rich.console import (
|
||||||
Console,
|
Console,
|
||||||
ConsoleRenderable,
|
|
||||||
ConsoleOptions,
|
ConsoleOptions,
|
||||||
RichCast,
|
ConsoleRenderable,
|
||||||
JustifyMethod,
|
JustifyMethod,
|
||||||
RenderableType,
|
RenderableType,
|
||||||
RenderResult,
|
RenderResult,
|
||||||
|
RichCast,
|
||||||
)
|
)
|
||||||
|
from rich.measure import Measurement
|
||||||
from rich.segment import Segment
|
from rich.segment import Segment
|
||||||
from rich.style import Style
|
from rich.style import Style
|
||||||
from rich.text import Text
|
from rich.text import Text
|
||||||
@@ -28,9 +29,9 @@ from ._layout import Layout
|
|||||||
from ._segment_tools import align_lines
|
from ._segment_tools import align_lines
|
||||||
from ._styles_cache import StylesCache
|
from ._styles_cache import StylesCache
|
||||||
from ._types import Lines
|
from ._types import Lines
|
||||||
from .css.scalar import ScalarOffset
|
|
||||||
from .binding import NoBinding
|
from .binding import NoBinding
|
||||||
from .box_model import BoxModel, get_box_model
|
from .box_model import BoxModel, get_box_model
|
||||||
|
from .css.scalar import ScalarOffset
|
||||||
from .dom import DOMNode, NoScreen
|
from .dom import DOMNode, NoScreen
|
||||||
from .geometry import Offset, Region, Size, Spacing, clamp
|
from .geometry import Offset, Region, Size, Spacing, clamp
|
||||||
from .layouts.vertical import VerticalLayout
|
from .layouts.vertical import VerticalLayout
|
||||||
@@ -100,6 +101,11 @@ class _Styled:
|
|||||||
)
|
)
|
||||||
return result_segments
|
return result_segments
|
||||||
|
|
||||||
|
def __rich_measure__(
|
||||||
|
self, console: "Console", options: "ConsoleOptions"
|
||||||
|
) -> Measurement:
|
||||||
|
return self.renderable.__rich_measure__(console, options)
|
||||||
|
|
||||||
|
|
||||||
class RenderCache(NamedTuple):
|
class RenderCache(NamedTuple):
|
||||||
"""Stores results of a previous render."""
|
"""Stores results of a previous render."""
|
||||||
@@ -401,9 +407,9 @@ class Widget(DOMNode):
|
|||||||
renderable = self._render()
|
renderable = self._render()
|
||||||
|
|
||||||
width = measure(console, renderable, container.width)
|
width = measure(console, renderable, container.width)
|
||||||
if self.expand:
|
if not self.expand:
|
||||||
width = max(container.width, width)
|
width = max(container.width, width)
|
||||||
if self.shrink:
|
if not self.shrink:
|
||||||
width = min(width, container.width)
|
width = min(width, container.width)
|
||||||
|
|
||||||
self._content_width_cache = (cache_key, width)
|
self._content_width_cache = (cache_key, width)
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ class Input(Widget, can_focus=True):
|
|||||||
Binding("home", "home", "Home"),
|
Binding("home", "home", "Home"),
|
||||||
Binding("end", "end", "Home"),
|
Binding("end", "end", "Home"),
|
||||||
Binding("ctrl+d", "delete_right", "Delete"),
|
Binding("ctrl+d", "delete_right", "Delete"),
|
||||||
|
Binding("enter", "submit", "Submit"),
|
||||||
]
|
]
|
||||||
|
|
||||||
COMPONENT_CLASSES = {"input--cursor", "input--placeholder"}
|
COMPONENT_CLASSES = {"input--cursor", "input--placeholder"}
|
||||||
@@ -154,6 +155,8 @@ class Input(Widget, can_focus=True):
|
|||||||
self.view_position = self.view_position
|
self.view_position = self.view_position
|
||||||
|
|
||||||
async def watch_value(self, value: str) -> None:
|
async def watch_value(self, value: str) -> None:
|
||||||
|
if self.styles.auto_dimensions:
|
||||||
|
self.refresh(layout=True)
|
||||||
await self.emit(self.Changed(self, value))
|
await self.emit(self.Changed(self, value))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -178,16 +181,18 @@ class Input(Widget, can_focus=True):
|
|||||||
class Changed(Message, bubble=True):
|
class Changed(Message, bubble=True):
|
||||||
"""Value was changed."""
|
"""Value was changed."""
|
||||||
|
|
||||||
def __init__(self, sender: MessageTarget, value: str) -> None:
|
def __init__(self, sender: Input, value: str) -> None:
|
||||||
super().__init__(sender)
|
super().__init__(sender)
|
||||||
self.value = value
|
self.value = value
|
||||||
|
self.input = sender
|
||||||
|
|
||||||
class Submitted(Message, bubble=True):
|
class Submitted(Message, bubble=True):
|
||||||
"""Value was updated via enter key or blur."""
|
"""Value was updated via enter key or blur."""
|
||||||
|
|
||||||
def __init__(self, sender: MessageTarget, value: str) -> None:
|
def __init__(self, sender: Input, value: str) -> None:
|
||||||
super().__init__(sender)
|
super().__init__(sender)
|
||||||
self.value = value
|
self.value = value
|
||||||
|
self.input = sender
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _value(self) -> Text:
|
def _value(self) -> Text:
|
||||||
@@ -233,7 +238,7 @@ class Input(Widget, can_focus=True):
|
|||||||
# Do key bindings first
|
# Do key bindings first
|
||||||
if await self.handle_key(event):
|
if await self.handle_key(event):
|
||||||
event.stop()
|
event.stop()
|
||||||
elif event.key == "tab":
|
elif event.key in ("tab", "shift+tab"):
|
||||||
return
|
return
|
||||||
elif event.is_printable:
|
elif event.is_printable:
|
||||||
event.stop()
|
event.stop()
|
||||||
|
|||||||
@@ -49,8 +49,8 @@ class Static(Widget):
|
|||||||
self,
|
self,
|
||||||
renderable: RenderableType = "",
|
renderable: RenderableType = "",
|
||||||
*,
|
*,
|
||||||
expand: bool = False,
|
expand: bool = True,
|
||||||
shrink: bool = False,
|
shrink: bool = True,
|
||||||
markup: bool = True,
|
markup: bool = True,
|
||||||
name: str | None = None,
|
name: str | None = None,
|
||||||
id: str | None = None,
|
id: str | None = None,
|
||||||
@@ -86,13 +86,12 @@ class Static(Widget):
|
|||||||
"""
|
"""
|
||||||
return self._renderable
|
return self._renderable
|
||||||
|
|
||||||
def update(self, renderable: RenderableType = "", *, layout: bool = True) -> None:
|
def update(self, renderable: RenderableType = "") -> None:
|
||||||
"""Update the widget's content area with new text or Rich renderable.
|
"""Update the widget's content area with new text or Rich renderable.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
renderable (RenderableType, optional): A new rich renderable. Defaults to empty renderable;
|
renderable (RenderableType, optional): A new rich renderable. Defaults to empty renderable;
|
||||||
layout (bool, optional): Perform a layout. Defaults to True.
|
|
||||||
"""
|
"""
|
||||||
_check_renderable(renderable)
|
_check_renderable(renderable)
|
||||||
self.renderable = renderable
|
self.renderable = renderable
|
||||||
self.refresh(layout=layout)
|
self.refresh(layout=True)
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ from __future__ import annotations
|
|||||||
from typing import cast
|
from typing import cast
|
||||||
|
|
||||||
from rich.console import RenderableType
|
from rich.console import RenderableType
|
||||||
|
from rich.highlighter import ReprHighlighter
|
||||||
from rich.pretty import Pretty
|
from rich.pretty import Pretty
|
||||||
from rich.protocol import is_renderable
|
from rich.protocol import is_renderable
|
||||||
from rich.segment import Segment
|
from rich.segment import Segment
|
||||||
|
from rich.text import Text
|
||||||
|
|
||||||
from ..reactive import var
|
from ..reactive import var
|
||||||
from ..geometry import Size, Region
|
from ..geometry import Size, Region
|
||||||
@@ -27,6 +29,7 @@ class TextLog(ScrollView, can_focus=True):
|
|||||||
max_lines: var[int | None] = var(None)
|
max_lines: var[int | None] = var(None)
|
||||||
min_width: var[int] = var(78)
|
min_width: var[int] = var(78)
|
||||||
wrap: var[bool] = var(False)
|
wrap: var[bool] = var(False)
|
||||||
|
highlight: var[bool] = var(False)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -34,6 +37,7 @@ class TextLog(ScrollView, can_focus=True):
|
|||||||
max_lines: int | None = None,
|
max_lines: int | None = None,
|
||||||
min_width: int = 78,
|
min_width: int = 78,
|
||||||
wrap: bool = False,
|
wrap: bool = False,
|
||||||
|
highlight: bool = False,
|
||||||
name: str | None = None,
|
name: str | None = None,
|
||||||
id: str | None = None,
|
id: str | None = None,
|
||||||
classes: str | None = None,
|
classes: str | None = None,
|
||||||
@@ -45,6 +49,8 @@ class TextLog(ScrollView, can_focus=True):
|
|||||||
self.max_width: int = 0
|
self.max_width: int = 0
|
||||||
self.min_width = min_width
|
self.min_width = min_width
|
||||||
self.wrap = wrap
|
self.wrap = wrap
|
||||||
|
self.highlight = highlight
|
||||||
|
self.highlighter = ReprHighlighter()
|
||||||
super().__init__(name=name, id=id, classes=classes)
|
super().__init__(name=name, id=id, classes=classes)
|
||||||
|
|
||||||
def _on_styles_updated(self) -> None:
|
def _on_styles_updated(self) -> None:
|
||||||
@@ -61,7 +67,13 @@ class TextLog(ScrollView, can_focus=True):
|
|||||||
if not is_renderable(content):
|
if not is_renderable(content):
|
||||||
renderable = Pretty(content)
|
renderable = Pretty(content)
|
||||||
else:
|
else:
|
||||||
renderable = cast(RenderableType, content)
|
if isinstance(content, str):
|
||||||
|
if self.highlight:
|
||||||
|
renderable = self.highlighter(content)
|
||||||
|
else:
|
||||||
|
renderable = Text(content)
|
||||||
|
else:
|
||||||
|
renderable = cast(RenderableType, content)
|
||||||
|
|
||||||
console = self.app.console
|
console = self.app.console
|
||||||
width = max(self.min_width, self.size.width or self.min_width)
|
width = max(self.min_width, self.size.width or self.min_width)
|
||||||
|
|||||||
Reference in New Issue
Block a user