mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
event docs
This commit is contained in:
48
docs/examples/events/custom01.py
Normal file
48
docs/examples/events/custom01.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.color import Color
|
||||||
|
from textual.message import Message, MessageTarget
|
||||||
|
from textual.widgets import Static
|
||||||
|
|
||||||
|
|
||||||
|
class ColorButton(Static):
|
||||||
|
"""A color button."""
|
||||||
|
|
||||||
|
class Selected(Message):
|
||||||
|
"""Color selected message."""
|
||||||
|
|
||||||
|
def __init__(self, sender: MessageTarget, color: Color) -> None:
|
||||||
|
self.color = color
|
||||||
|
super().__init__(sender)
|
||||||
|
|
||||||
|
def __init__(self, color: Color) -> None:
|
||||||
|
self.color = color
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def on_mount(self) -> None:
|
||||||
|
self.styles.margin = (1, 2)
|
||||||
|
self.styles.content_align = ("center", "middle")
|
||||||
|
self.styles.background = Color.parse("#ffffff33")
|
||||||
|
self.styles.border = ("tall", self.color)
|
||||||
|
|
||||||
|
async def on_click(self) -> None:
|
||||||
|
# The emit method sends an event to a widget's parent
|
||||||
|
await self.emit(self.Selected(self, self.color))
|
||||||
|
|
||||||
|
def render(self) -> str:
|
||||||
|
return str(self.color)
|
||||||
|
|
||||||
|
|
||||||
|
class ColorApp(App):
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield ColorButton(Color.parse("#008080"))
|
||||||
|
yield ColorButton(Color.parse("#808000"))
|
||||||
|
yield ColorButton(Color.parse("#E9967A"))
|
||||||
|
yield ColorButton(Color.parse("#121212"))
|
||||||
|
|
||||||
|
def on_color_button_selected(self, message: ColorButton.Selected) -> None:
|
||||||
|
self.screen.styles.animate("background", message.color, duration=0.5)
|
||||||
|
|
||||||
|
|
||||||
|
app = ColorApp()
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run()
|
||||||
@@ -2,7 +2,7 @@ All you need to get started building Textual apps.
|
|||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
Textual requires Python 3.7 or later. 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"
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,13 @@
|
|||||||
|
|
||||||
We've used event handler methods in many of the examples in this guide. This chapter explores events and messages (see below) in more detail.
|
We've used event handler methods in many of the examples in this guide. This chapter explores events and messages (see below) in more detail.
|
||||||
|
|
||||||
|
!!! tip
|
||||||
|
|
||||||
|
See [events](../events/index.md) for a comprehensive reference on the events Textual sends.
|
||||||
|
|
||||||
## Messages
|
## Messages
|
||||||
|
|
||||||
Events are a particular kind of *message* which is sent by Textual in response to input and other state changes. Events are reserved for use by Textual but you can also create custom messages for the purpose of coordinating between widgets in your app.
|
Events are a particular kind of *message* sent by Textual in response to input and other state changes. Events are reserved for use by Textual but you can also create custom messages for the purpose of coordinating between widgets in your app.
|
||||||
|
|
||||||
More on that later, but for now keep in mind that events are also messages, and anything that is true of messages is true of events.
|
More on that later, but for now keep in mind that events are also messages, and anything that is true of messages is true of events.
|
||||||
|
|
||||||
@@ -36,29 +40,23 @@ When the `on_key` method returns, Textual will get the next event off the the qu
|
|||||||
--8<-- "docs/images/events/queue2.excalidraw.svg"
|
--8<-- "docs/images/events/queue2.excalidraw.svg"
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## Creating Messages
|
|
||||||
|
|
||||||
## Handlers
|
## Default behaviors
|
||||||
|
|
||||||
|
|
||||||
### Naming
|
|
||||||
|
|
||||||
Let's explore how Textual decides what method to call for a given event.
|
|
||||||
|
|
||||||
- Start with `"on_"`.
|
|
||||||
- Add the messages namespace (if any) converted from CamelCase to snake_case plus an underscore `"_"`
|
|
||||||
- Add the name of the class converted from CamelCase to snake_case.
|
|
||||||
|
|
||||||
### Default behaviors
|
|
||||||
|
|
||||||
You may be familiar with Python's [super](https://docs.python.org/3/library/functions.html#super) function to call a function defined in a base class. You will not have to do this for Textual event handlers as Textual will automatically call any handler methods defined in the base class.
|
You may be familiar with Python's [super](https://docs.python.org/3/library/functions.html#super) function to call a function defined in a base class. You will not have to do this for Textual event handlers as Textual will automatically call any handler methods defined in the base class.
|
||||||
|
|
||||||
For instance if you define a custom widget, Textual will call its `on_key` handler when you hit a key. Textual will also run any `on_key` methods found in the widget's base classes, including `Widget.on_key` where key bindings are processed. Without this behavior, you would have to remember to call `super().on_key(event)` or key bindings would break.
|
For instance if you define a custom widget, Textual will call its `on_key` handler when you hit a key. Textual will also run any `on_key` methods found in the widget's base classes, including `Widget.on_key` where key bindings are processed. Without this behavior, you would have to remember to call `super().on_key(event)` on all key handlers or key bindings would break.
|
||||||
|
|
||||||
|
### Preventing default behaviors
|
||||||
|
|
||||||
If you don't want this behavior you can call [prevent_default()][textual.message.Message.prevent_default] on the event object. This tells Textual not to call any handlers on base classes.
|
If you don't want this behavior you can call [prevent_default()][textual.message.Message.prevent_default] on the event object. This tells Textual not to call any handlers on base classes.
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
|
||||||
### Bubbling
|
Don't call `prevent_default` lightly. It *may* break some of Textual's standard features.
|
||||||
|
|
||||||
|
|
||||||
|
## Bubbling
|
||||||
|
|
||||||
Messages have a `bubble` attribute. If this is set to `True` then events will be sent to their parent widget. Input events typically bubble so that a widget will have the opportunity to process events after its children.
|
Messages have a `bubble` attribute. If this is set to `True` then events will be sent to their parent widget. Input events typically bubble so that a widget will have the opportunity to process events after its children.
|
||||||
|
|
||||||
@@ -68,13 +66,13 @@ The following diagram shows an (abbreviated) DOM for a UI with a container and t
|
|||||||
--8<-- "docs/images/events/bubble1.excalidraw.svg"
|
--8<-- "docs/images/events/bubble1.excalidraw.svg"
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
After Textual calls `Button.on_key` it _bubbles_ the event to its parent and call `Container.on_key` (if it exists).
|
After Textual calls `Button.on_key` the event _bubbles_ to the buttons parent and will call `Container.on_key` (if it exists).
|
||||||
|
|
||||||
<div class="excalidraw">
|
<div class="excalidraw">
|
||||||
--8<-- "docs/images/events/bubble2.excalidraw.svg"
|
--8<-- "docs/images/events/bubble2.excalidraw.svg"
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
Then it will bubble to the container's parent (the App class).
|
As before, the event bubbles to it's parent (the App class).
|
||||||
|
|
||||||
<div class="excalidraw">
|
<div class="excalidraw">
|
||||||
--8<-- "docs/images/events/bubble3.excalidraw.svg"
|
--8<-- "docs/images/events/bubble3.excalidraw.svg"
|
||||||
@@ -82,17 +80,54 @@ Then it will bubble to the container's parent (the App class).
|
|||||||
|
|
||||||
The App class is always the root of the DOM, so there is no where for the event to bubble to.
|
The App class is always the root of the DOM, so there is no where for the event to bubble to.
|
||||||
|
|
||||||
#### Stopping bubbling
|
### Stopping bubbling
|
||||||
|
|
||||||
Event handlers may stop this bubble behavior by calling the [stop()][textual.message.Message.stop] method on the event or message. You might want to do this if a widget has responded to the event in an authoritative way. For instance if a text input widget as responded to a key event you probably do not want it to also invoke a key binding.
|
Event handlers may stop this bubble behavior by calling the [stop()][textual.message.Message.stop] method on the event or message. You might want to do this if a widget has responded to the event in an authoritative way. For instance if a text input widget responded to a key event you probably do not want it to also invoke a key binding.
|
||||||
|
|
||||||
|
## Custom messages
|
||||||
|
|
||||||
|
You can create custom messages for your application that may be used in the same way as events (recall that events are simply messages reserved for use by Textual).
|
||||||
|
|
||||||
|
The most common reason to do this is if you are building a custom widget and you need to inform a parent widget about a state change.
|
||||||
|
|
||||||
|
Let's look at an example which defines a custom message. The following example creates color buttons which, when clicked, send a custom message.
|
||||||
|
|
||||||
|
=== "custom01.py"
|
||||||
|
|
||||||
|
```python title="custom01.py" hl_lines="10-15 27-29 42-43"
|
||||||
|
--8<-- "docs/examples/events/custom01.py"
|
||||||
|
```
|
||||||
|
=== "Output"
|
||||||
|
|
||||||
|
```{.textual path="docs/examples/events/custom01.py"}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Note the custom message class which extends [Message][textual.message.Message]. The constructor stores a [color][textual.color.Color] object which handler methods will be able to inspect.
|
||||||
|
|
||||||
<hr>
|
The message class is defined within the widget class itself. This is not strictly required but recommended.
|
||||||
TODO: events docs
|
|
||||||
|
- If reduces the amount of imports. If you were to import ColorButton, you have access to the message class via `ColorButton.Selected`.
|
||||||
|
- It creates a namespace for the handler. So rather than `on_selected`, the handler name becomes `on_color_button_selected`. This makes it less likely that your chosen name will clash with another message.
|
||||||
|
|
||||||
|
### Handler naming
|
||||||
|
|
||||||
|
Let's recap on the scheme that Textual uses to map messages classes on to a Python method name.
|
||||||
|
|
||||||
|
- Start with `"on_"`.
|
||||||
|
- Add the messages namespace (if any) converted from CamelCase to snake_case plus an underscore `"_"`
|
||||||
|
- Add the name of the class converted from CamelCase to snake_case.
|
||||||
|
|
||||||
|
<div class="excalidraw">
|
||||||
|
--8<-- "docs/images/events/naming.excalidraw.svg"
|
||||||
|
</div>
|
||||||
|
|
||||||
|
### Sending events
|
||||||
|
|
||||||
|
In the previous example we used [emit()][textual.message_pump.MessagePump.emit] to send an event to it's parent. We could also have used [emit_no_wait()][textual.message_pump.MessagePump.emit_no_wait] for non async code. Sending messages in this way allows you to write custom widgets without needing to know in what context they will be used.
|
||||||
|
|
||||||
|
There are other ways of sending (posting) messages, which you may need to use less frequently.
|
||||||
|
|
||||||
|
- [post_message][textual.message_pump.MessagePump.post_message] To post a message to a particular event.
|
||||||
|
- [post_message_no_wait][textual.message_pump.MessagePump.post_message_no_wait] The non-async version of `post_message`.
|
||||||
|
|
||||||
- What are events
|
|
||||||
- Handling events
|
|
||||||
- Auto calling base classes
|
|
||||||
- Event bubbling
|
|
||||||
- Posting / emitting events
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
16
docs/images/events/naming.excalidraw.svg
Normal file
16
docs/images/events/naming.excalidraw.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 12 KiB |
@@ -59,7 +59,7 @@ Textual is a framework for building applications that run within your terminal.
|
|||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
|
|
||||||
```{.textual path="examples/calculator.py" columns=100 lines=40}
|
```{.textual path="examples/calculator.py" columns=100 lines=41 press="3,.,1,4,5,9,2,_,_"}
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -70,6 +70,9 @@ Textual is a framework for building applications that run within your terminal.
|
|||||||
```{.textual path="docs/examples/tutorial/stopwatch.py" press="tab,enter,_,_"}
|
```{.textual path="docs/examples/tutorial/stopwatch.py" press="tab,enter,_,_"}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```{.textual path="docs/examples/guide/layout/combining_layouts.py"}
|
||||||
|
```
|
||||||
|
|
||||||
```{.textual path="docs/examples/app/widgets01.py"}
|
```{.textual path="docs/examples/app/widgets01.py"}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ By the end of this page you should have a solid understanding of app development
|
|||||||
|
|
||||||
!!! quote
|
!!! quote
|
||||||
|
|
||||||
I've always thought the secret sauce in making a popular framework is for it to be fun.
|
If you want people to build things, make it fun.
|
||||||
|
|
||||||
— **Will McGugan** (creator of Rich and Textual)
|
— **Will McGugan** (creator of Rich and Textual)
|
||||||
|
|
||||||
@@ -329,7 +329,7 @@ The `on_button_pressed` method is an *event handler*. Event handlers are methods
|
|||||||
|
|
||||||
If you run "stopwatch04.py" now you will be able to toggle between the two states by clicking the first button:
|
If you run "stopwatch04.py" now you will be able to toggle between the two states by clicking the first button:
|
||||||
|
|
||||||
```{.textual path="docs/examples/tutorial/stopwatch04.py" title="stopwatch04.py" press="tab,tab,tab,enter,_,_"}
|
```{.textual path="docs/examples/tutorial/stopwatch04.py" title="stopwatch04.py" press="tab,tab,tab,_,enter,_,_,_"}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Reactive attributes
|
## Reactive attributes
|
||||||
|
|||||||
@@ -14,7 +14,6 @@
|
|||||||
|
|
||||||
App > Screen {
|
App > Screen {
|
||||||
|
|
||||||
background: $background;
|
|
||||||
color: $text;
|
color: $text;
|
||||||
layers: base sidebar;
|
layers: base sidebar;
|
||||||
layout: vertical;
|
layout: vertical;
|
||||||
@@ -22,10 +21,11 @@ App > Screen {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#tree-container {
|
#tree-container {
|
||||||
|
background: $panel;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
height: 20;
|
height: 20;
|
||||||
margin: 1 2;
|
margin: 1 2;
|
||||||
background: $surface;
|
|
||||||
padding: 1 2;
|
padding: 1 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,13 +36,18 @@ DirectoryTree {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#table-container {
|
||||||
|
background: $panel;
|
||||||
|
height: auto;
|
||||||
|
margin: 1 2;
|
||||||
|
}
|
||||||
|
|
||||||
DataTable {
|
DataTable {
|
||||||
/*border:heavy red;*/
|
/*border:heavy red;*/
|
||||||
/* tint: 10% green; */
|
/* tint: 10% green; */
|
||||||
/* text-opacity: 50%; */
|
/* text-opacity: 50%; */
|
||||||
padding: 1;
|
background: $surface;
|
||||||
|
padding: 1 2;
|
||||||
margin: 1 2;
|
margin: 1 2;
|
||||||
height: 24;
|
height: 24;
|
||||||
}
|
}
|
||||||
@@ -101,7 +106,7 @@ Tweet {
|
|||||||
/* border: outer $primary; */
|
/* border: outer $primary; */
|
||||||
padding: 1;
|
padding: 1;
|
||||||
border: wide $panel;
|
border: wide $panel;
|
||||||
overflow: auto;
|
|
||||||
/* scrollbar-gutter: stable; */
|
/* scrollbar-gutter: stable; */
|
||||||
align-horizontal: center;
|
align-horizontal: center;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -138,8 +143,14 @@ TweetBody {
|
|||||||
padding: 0 1 0 0;
|
padding: 0 1 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Tweet.scroll-horizontal {
|
||||||
|
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
Tweet.scroll-horizontal TweetBody {
|
Tweet.scroll-horizontal TweetBody {
|
||||||
width: 350;
|
width: 350;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
@@ -182,7 +193,7 @@ Tweet.scroll-horizontal TweetBody {
|
|||||||
|
|
||||||
|
|
||||||
#sidebar .content {
|
#sidebar .content {
|
||||||
layout: vertical
|
layout: vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
OptionItem {
|
OptionItem {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from textual.app import App, ComposeResult
|
|||||||
from textual.reactive import Reactive
|
from textual.reactive import Reactive
|
||||||
from textual.widget import Widget
|
from textual.widget import Widget
|
||||||
from textual.widgets import Static, DataTable, DirectoryTree, Header, Footer
|
from textual.widgets import Static, DataTable, DirectoryTree, Header, Footer
|
||||||
from textual.layout import Container
|
from textual.layout import Container, Vertical
|
||||||
|
|
||||||
CODE = '''
|
CODE = '''
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -68,38 +68,38 @@ lorem_short_text = Text.from_markup(lorem_short)
|
|||||||
lorem_long_text = Text.from_markup(lorem * 2)
|
lorem_long_text = Text.from_markup(lorem * 2)
|
||||||
|
|
||||||
|
|
||||||
class TweetHeader(Widget):
|
class TweetHeader(Static):
|
||||||
def render(self) -> RenderableType:
|
def render(self) -> RenderableType:
|
||||||
return Text("Lorem Impsum", justify="center")
|
return Text("Lorem Impsum", justify="center")
|
||||||
|
|
||||||
|
|
||||||
class TweetBody(Widget):
|
class TweetBody(Static):
|
||||||
short_lorem = Reactive(False)
|
short_lorem = Reactive(False)
|
||||||
|
|
||||||
def render(self) -> Text:
|
def render(self) -> Text:
|
||||||
return lorem_short_text if self.short_lorem else lorem_long_text
|
return lorem_short_text if self.short_lorem else lorem_long_text
|
||||||
|
|
||||||
|
|
||||||
class Tweet(Widget):
|
class Tweet(Vertical):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class OptionItem(Widget):
|
class OptionItem(Static):
|
||||||
def render(self) -> Text:
|
def render(self) -> Text:
|
||||||
return Text("Option")
|
return Text("Option")
|
||||||
|
|
||||||
|
|
||||||
class Error(Widget):
|
class Error(Static):
|
||||||
def render(self) -> Text:
|
def render(self) -> Text:
|
||||||
return Text("This is an error message", justify="center")
|
return Text("This is an error message", justify="center")
|
||||||
|
|
||||||
|
|
||||||
class Warning(Widget):
|
class Warning(Static):
|
||||||
def render(self) -> Text:
|
def render(self) -> Text:
|
||||||
return Text("This is a warning message", justify="center")
|
return Text("This is a warning message", justify="center")
|
||||||
|
|
||||||
|
|
||||||
class Success(Widget):
|
class Success(Static):
|
||||||
def render(self) -> Text:
|
def render(self) -> Text:
|
||||||
return Text("This is a success message", justify="center")
|
return Text("This is a success message", justify="center")
|
||||||
|
|
||||||
@@ -120,17 +120,22 @@ class BasicApp(App, css_path="basic.css"):
|
|||||||
table = DataTable()
|
table = DataTable()
|
||||||
self.scroll_to_target = Tweet(TweetBody())
|
self.scroll_to_target = Tweet(TweetBody())
|
||||||
|
|
||||||
yield Container(
|
yield Vertical(
|
||||||
Tweet(TweetBody()),
|
Tweet(TweetBody()),
|
||||||
Widget(
|
Container(
|
||||||
Static(
|
Static(
|
||||||
Syntax(CODE, "python", line_numbers=True, indent_guides=True),
|
Syntax(
|
||||||
|
CODE,
|
||||||
|
"python",
|
||||||
|
line_numbers=True,
|
||||||
|
indent_guides=True,
|
||||||
|
),
|
||||||
classes="code",
|
classes="code",
|
||||||
),
|
),
|
||||||
classes="scrollable",
|
classes="scrollable",
|
||||||
),
|
),
|
||||||
table,
|
Container(table, id="table-container"),
|
||||||
Widget(DirectoryTree("~/"), id="tree-container"),
|
Container(DirectoryTree("~/"), id="tree-container"),
|
||||||
Error(),
|
Error(),
|
||||||
Tweet(TweetBody(), classes="scrollbar-size-custom"),
|
Tweet(TweetBody(), classes="scrollbar-size-custom"),
|
||||||
Warning(),
|
Warning(),
|
||||||
@@ -143,12 +148,12 @@ class BasicApp(App, css_path="basic.css"):
|
|||||||
Tweet(TweetBody(), classes="scroll-horizontal"),
|
Tweet(TweetBody(), classes="scroll-horizontal"),
|
||||||
)
|
)
|
||||||
yield Widget(
|
yield Widget(
|
||||||
Widget(classes="title"),
|
Static("Title", classes="title"),
|
||||||
Widget(classes="user"),
|
Static("Content", classes="user"),
|
||||||
OptionItem(),
|
OptionItem(),
|
||||||
OptionItem(),
|
OptionItem(),
|
||||||
OptionItem(),
|
OptionItem(),
|
||||||
Widget(classes="content"),
|
Static(classes="content"),
|
||||||
id="sidebar",
|
id="sidebar",
|
||||||
)
|
)
|
||||||
yield Footer()
|
yield Footer()
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ class BoundAnimator:
|
|||||||
def __call__(
|
def __call__(
|
||||||
self,
|
self,
|
||||||
attribute: str,
|
attribute: str,
|
||||||
value: float,
|
value: float | Animatable,
|
||||||
*,
|
*,
|
||||||
final_value: object = ...,
|
final_value: object = ...,
|
||||||
duration: float | None = None,
|
duration: float | None = None,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class MessageTarget(Protocol):
|
|||||||
async def post_message(self, message: "Message") -> bool:
|
async def post_message(self, message: "Message") -> bool:
|
||||||
...
|
...
|
||||||
|
|
||||||
async def post_priority_message(self, message: "Message") -> bool:
|
async def _post_priority_message(self, message: "Message") -> bool:
|
||||||
...
|
...
|
||||||
|
|
||||||
def post_message_no_wait(self, message: "Message") -> bool:
|
def post_message_no_wait(self, message: "Message") -> bool:
|
||||||
|
|||||||
@@ -191,6 +191,11 @@ class Scalar(NamedTuple):
|
|||||||
return "auto"
|
return "auto"
|
||||||
return f"{int(value) if value.is_integer() else value}{self.symbol}"
|
return f"{int(value) if value.is_integer() else value}{self.symbol}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_flexible(self) -> bool:
|
||||||
|
"""Check if this unit is flexible (resolves relative to another dimension)."""
|
||||||
|
return self.unit != Unit.CELLS
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_cells(self) -> bool:
|
def is_cells(self) -> bool:
|
||||||
"""Check if the Scalar is explicit cells."""
|
"""Check if the Scalar is explicit cells."""
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ from typing import TYPE_CHECKING, Any, Iterable, NamedTuple, cast
|
|||||||
import rich.repr
|
import rich.repr
|
||||||
from rich.style import Style
|
from rich.style import Style
|
||||||
|
|
||||||
from textual._types import CallbackType
|
from .._types import CallbackType
|
||||||
from .._animator import Animation, EasingFunction
|
from .._animator import Animation, EasingFunction, BoundAnimator
|
||||||
from ..color import Color
|
from ..color import Color
|
||||||
from ..geometry import Offset, Spacing
|
from ..geometry import Offset, Spacing
|
||||||
from ._style_properties import (
|
from ._style_properties import (
|
||||||
@@ -850,6 +850,7 @@ class RenderStyles(StylesBase):
|
|||||||
self.node = node
|
self.node = node
|
||||||
self._base_styles = base
|
self._base_styles = base
|
||||||
self._inline_styles = inline_styles
|
self._inline_styles = inline_styles
|
||||||
|
self._animate: BoundAnimator | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def base(self) -> Styles:
|
def base(self) -> Styles:
|
||||||
@@ -867,6 +868,23 @@ class RenderStyles(StylesBase):
|
|||||||
assert self.node is not None
|
assert self.node is not None
|
||||||
return self.node.rich_style
|
return self.node.rich_style
|
||||||
|
|
||||||
|
@property
|
||||||
|
def animate(self) -> BoundAnimator:
|
||||||
|
"""Get an animator to animate attributes on this widget.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```python
|
||||||
|
self.animate("brightness", 0.5)
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BoundAnimator: An animator bound to this widget.
|
||||||
|
"""
|
||||||
|
if self._animate is None:
|
||||||
|
self._animate = self.node.app.animator.bind(self)
|
||||||
|
assert self._animate is not None
|
||||||
|
return self._animate
|
||||||
|
|
||||||
def __rich_repr__(self) -> rich.repr.Result:
|
def __rich_repr__(self) -> rich.repr.Result:
|
||||||
for rule_name in RULE_NAMES:
|
for rule_name in RULE_NAMES:
|
||||||
if self.has_rule(rule_name):
|
if self.has_rule(rule_name):
|
||||||
|
|||||||
@@ -26,9 +26,6 @@ class Event(Message):
|
|||||||
return
|
return
|
||||||
yield
|
yield
|
||||||
|
|
||||||
def __init_subclass__(cls, bubble: bool = True, verbose: bool = False) -> None:
|
|
||||||
super().__init_subclass__(bubble=bubble, verbose=verbose)
|
|
||||||
|
|
||||||
|
|
||||||
@rich.repr.auto
|
@rich.repr.auto
|
||||||
class Callback(Event, bubble=False, verbose=True):
|
class Callback(Event, bubble=False, verbose=True):
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import rich.repr
|
|||||||
|
|
||||||
from . import _clock
|
from . import _clock
|
||||||
from .case import camel_to_snake
|
from .case import camel_to_snake
|
||||||
from ._types import MessageTarget
|
from ._types import MessageTarget as MessageTarget
|
||||||
|
|
||||||
|
|
||||||
@rich.repr.auto
|
@rich.repr.auto
|
||||||
@@ -36,7 +36,7 @@ class Message:
|
|||||||
|
|
||||||
def __init__(self, sender: MessageTarget) -> None:
|
def __init__(self, sender: MessageTarget) -> None:
|
||||||
self.sender = sender
|
self.sender = sender
|
||||||
self.name = camel_to_snake(self.__class__.__name__.replace("Message", ""))
|
self.name = camel_to_snake(self.__class__.__name__)
|
||||||
self.time = _clock.get_time_no_wait()
|
self.time = _clock.get_time_no_wait()
|
||||||
self._forwarded = False
|
self._forwarded = False
|
||||||
self._no_default_action = False
|
self._no_default_action = False
|
||||||
|
|||||||
@@ -364,7 +364,7 @@ class MessagePump(metaclass=MessagePumpMeta):
|
|||||||
if isinstance(message, Event):
|
if isinstance(message, Event):
|
||||||
await self.on_event(message)
|
await self.on_event(message)
|
||||||
else:
|
else:
|
||||||
await self.on_message(message)
|
await self._on_message(message)
|
||||||
|
|
||||||
def _get_dispatch_methods(
|
def _get_dispatch_methods(
|
||||||
self, method_name: str, message: Message
|
self, method_name: str, message: Message
|
||||||
@@ -390,9 +390,9 @@ class MessagePump(metaclass=MessagePumpMeta):
|
|||||||
Args:
|
Args:
|
||||||
event (events.Event): An Event object.
|
event (events.Event): An Event object.
|
||||||
"""
|
"""
|
||||||
await self.on_message(event)
|
await self._on_message(event)
|
||||||
|
|
||||||
async def on_message(self, message: Message) -> None:
|
async def _on_message(self, message: Message) -> None:
|
||||||
"""Called to process a message.
|
"""Called to process a message.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -444,7 +444,7 @@ class MessagePump(metaclass=MessagePumpMeta):
|
|||||||
|
|
||||||
# TODO: This may not be needed, or may only be needed by the timer
|
# TODO: This may not be needed, or may only be needed by the timer
|
||||||
# Consider removing or making private
|
# Consider removing or making private
|
||||||
async def post_priority_message(self, message: Message) -> bool:
|
async def _post_priority_message(self, message: Message) -> bool:
|
||||||
"""Post a "priority" messages which will be processes prior to regular messages.
|
"""Post a "priority" messages which will be processes prior to regular messages.
|
||||||
|
|
||||||
Note that you should rarely need this in a regular app. It exists primarily to allow
|
Note that you should rarely need this in a regular app. It exists primarily to allow
|
||||||
@@ -480,6 +480,22 @@ class MessagePump(metaclass=MessagePumpMeta):
|
|||||||
self._message_queue.put_nowait(message)
|
self._message_queue.put_nowait(message)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def forward_message(self, target: MessagePump, message: Message) -> None:
|
||||||
|
"""Forward a message. Ensures that a message is sent after processing all messages
|
||||||
|
in this message pump.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
target (MessagePump): Where to forward the message to.
|
||||||
|
message (Message): The message.
|
||||||
|
"""
|
||||||
|
|
||||||
|
forward = messages.ForwardMessage(self, target, message)
|
||||||
|
self._message_queue.put_nowait(forward)
|
||||||
|
self.check_idle()
|
||||||
|
|
||||||
|
async def _on_forward_message(self, message: messages.ForwardMessage) -> None:
|
||||||
|
await message.target.post_message(message.message)
|
||||||
|
|
||||||
async def _post_message_from_child(self, message: Message) -> bool:
|
async def _post_message_from_child(self, message: Message) -> bool:
|
||||||
if self._closing or self._closed:
|
if self._closing or self._closed:
|
||||||
return False
|
return False
|
||||||
@@ -494,6 +510,14 @@ class MessagePump(metaclass=MessagePumpMeta):
|
|||||||
await invoke(event.callback)
|
await invoke(event.callback)
|
||||||
|
|
||||||
def emit_no_wait(self, message: Message) -> bool:
|
def emit_no_wait(self, message: Message) -> bool:
|
||||||
|
"""Send a message to the _parent_, non async version.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message (Message): A message object.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the message was posted successfully.
|
||||||
|
"""
|
||||||
if self._parent:
|
if self._parent:
|
||||||
return self._parent._post_message_from_child_no_wait(message)
|
return self._parent._post_message_from_child_no_wait(message)
|
||||||
else:
|
else:
|
||||||
@@ -506,7 +530,7 @@ class MessagePump(metaclass=MessagePumpMeta):
|
|||||||
message (Message): A message object.
|
message (Message): A message object.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: _True if the message was posted successfully.
|
bool: True if the message was posted successfully.
|
||||||
"""
|
"""
|
||||||
if self._parent:
|
if self._parent:
|
||||||
return await self._parent._post_message_from_child(message)
|
return await self._parent._post_message_from_child(message)
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class Update(Message, verbose=True):
|
|||||||
|
|
||||||
def can_replace(self, message: Message) -> bool:
|
def can_replace(self, message: Message) -> bool:
|
||||||
# Update messages can replace update for the same widget
|
# Update messages can replace update for the same widget
|
||||||
return isinstance(message, Update) and self == message
|
return isinstance(message, Update) and self.widget == message.widget
|
||||||
|
|
||||||
|
|
||||||
@rich.repr.auto
|
@rich.repr.auto
|
||||||
@@ -59,7 +59,7 @@ class ScrollToRegion(Message, bubble=False):
|
|||||||
|
|
||||||
|
|
||||||
@rich.repr.auto
|
@rich.repr.auto
|
||||||
class StylesUpdated(Message):
|
class StylesUpdated(Message, verbose=True):
|
||||||
def __init__(self, sender: MessagePump) -> None:
|
def __init__(self, sender: MessagePump) -> None:
|
||||||
super().__init__(sender)
|
super().__init__(sender)
|
||||||
|
|
||||||
@@ -79,3 +79,18 @@ class TerminalSupportsSynchronizedOutput(Message):
|
|||||||
Used to make the App aware that the terminal emulator supports synchronised output.
|
Used to make the App aware that the terminal emulator supports synchronised output.
|
||||||
@link https://gist.github.com/christianparpart/d8a62cc1ab659194337d73e399004036
|
@link https://gist.github.com/christianparpart/d8a62cc1ab659194337d73e399004036
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@rich.repr.auto
|
||||||
|
class ForwardMessage(Message):
|
||||||
|
def __init__(
|
||||||
|
self, sender: MessagePump, target: MessagePump, message: Message
|
||||||
|
) -> None:
|
||||||
|
super().__init__(sender)
|
||||||
|
self.target = target
|
||||||
|
self.message = message
|
||||||
|
|
||||||
|
def __rich_repr__(self) -> rich.repr.Result:
|
||||||
|
yield from super().__rich_repr__()
|
||||||
|
yield "target", self.target
|
||||||
|
yield "message", self.message
|
||||||
|
|||||||
@@ -220,6 +220,7 @@ class Screen(Widget):
|
|||||||
self, unclipped_region.size, virtual_size, container_size
|
self, unclipped_region.size, virtual_size, container_size
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
self.app._handle_exception(error)
|
self.app._handle_exception(error)
|
||||||
return
|
return
|
||||||
@@ -279,13 +280,13 @@ class Screen(Widget):
|
|||||||
screen_y=event.screen_y,
|
screen_y=event.screen_y,
|
||||||
style=event.style,
|
style=event.style,
|
||||||
)
|
)
|
||||||
mouse_event.set_forwarded()
|
mouse_event._set_forwarded()
|
||||||
await widget._forward_event(mouse_event)
|
await widget._forward_event(mouse_event)
|
||||||
|
|
||||||
async def _forward_event(self, event: events.Event) -> None:
|
async def _forward_event(self, event: events.Event) -> None:
|
||||||
if event.is_forwarded:
|
if event.is_forwarded:
|
||||||
return
|
return
|
||||||
event.set_forwarded()
|
event._set_forwarded()
|
||||||
if isinstance(event, (events.Enter, events.Leave)):
|
if isinstance(event, (events.Enter, events.Leave)):
|
||||||
await self.post_message(event)
|
await self.post_message(event)
|
||||||
|
|
||||||
@@ -310,7 +311,7 @@ class Screen(Widget):
|
|||||||
return
|
return
|
||||||
event.style = self.get_style_at(event.screen_x, event.screen_y)
|
event.style = self.get_style_at(event.screen_x, event.screen_y)
|
||||||
if widget is self:
|
if widget is self:
|
||||||
event.set_forwarded()
|
event._set_forwarded()
|
||||||
await self.post_message(event)
|
await self.post_message(event)
|
||||||
else:
|
else:
|
||||||
await widget._forward_event(event.offset(-region.x, -region.y))
|
await widget._forward_event(event.offset(-region.x, -region.y))
|
||||||
|
|||||||
@@ -167,4 +167,4 @@ class Timer:
|
|||||||
count=count,
|
count=count,
|
||||||
callback=self._callback,
|
callback=self._callback,
|
||||||
)
|
)
|
||||||
await self.target.post_priority_message(event)
|
await self.target._post_priority_message(event)
|
||||||
|
|||||||
@@ -460,6 +460,7 @@ class Widget(DOMNode):
|
|||||||
self._vertical_scrollbar = scroll_bar = ScrollBar(
|
self._vertical_scrollbar = scroll_bar = ScrollBar(
|
||||||
vertical=True, name="vertical", thickness=self.scrollbar_size_vertical
|
vertical=True, name="vertical", thickness=self.scrollbar_size_vertical
|
||||||
)
|
)
|
||||||
|
self._vertical_scrollbar.display = False
|
||||||
self.app._start_widget(self, scroll_bar)
|
self.app._start_widget(self, scroll_bar)
|
||||||
return scroll_bar
|
return scroll_bar
|
||||||
|
|
||||||
@@ -477,6 +478,7 @@ class Widget(DOMNode):
|
|||||||
self._horizontal_scrollbar = scroll_bar = ScrollBar(
|
self._horizontal_scrollbar = scroll_bar = ScrollBar(
|
||||||
vertical=False, name="horizontal", thickness=self.scrollbar_size_horizontal
|
vertical=False, name="horizontal", thickness=self.scrollbar_size_horizontal
|
||||||
)
|
)
|
||||||
|
self._horizontal_scrollbar.display = False
|
||||||
|
|
||||||
self.app._start_widget(self, scroll_bar)
|
self.app._start_widget(self, scroll_bar)
|
||||||
return scroll_bar
|
return scroll_bar
|
||||||
@@ -1503,7 +1505,7 @@ class Widget(DOMNode):
|
|||||||
return self.screen.get_style_at(x + offset_x, y + offset_y)
|
return self.screen.get_style_at(x + offset_x, y + offset_y)
|
||||||
|
|
||||||
async def _forward_event(self, event: events.Event) -> None:
|
async def _forward_event(self, event: events.Event) -> None:
|
||||||
event.set_forwarded()
|
event._set_forwarded()
|
||||||
await self.post_message(event)
|
await self.post_message(event)
|
||||||
|
|
||||||
def refresh(
|
def refresh(
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
|
|||||||
|
|
||||||
DEFAULT_CSS = """
|
DEFAULT_CSS = """
|
||||||
DataTable {
|
DataTable {
|
||||||
background: $surface;
|
|
||||||
color: $text;
|
color: $text;
|
||||||
}
|
}
|
||||||
DataTable > .datatable--header {
|
DataTable > .datatable--header {
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ class Footer(Widget):
|
|||||||
watch(self.app, "focused", self._focus_changed)
|
watch(self.app, "focused", self._focus_changed)
|
||||||
|
|
||||||
def _focus_changed(self, focused: Widget | None) -> None:
|
def _focus_changed(self, focused: Widget | None) -> None:
|
||||||
self.log("FOCUS CHANGED", focused)
|
|
||||||
self._key_text = None
|
self._key_text = None
|
||||||
self.refresh()
|
self.refresh()
|
||||||
|
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ class TreeNode(Generic[NodeDataType]):
|
|||||||
class TreeControl(Generic[NodeDataType], Static, can_focus=True):
|
class TreeControl(Generic[NodeDataType], Static, can_focus=True):
|
||||||
DEFAULT_CSS = """
|
DEFAULT_CSS = """
|
||||||
TreeControl {
|
TreeControl {
|
||||||
background: $surface;
|
|
||||||
color: $text;
|
color: $text;
|
||||||
height: auto;
|
height: auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
Reference in New Issue
Block a user