Skip to content

Commit

Permalink
Merge branch 'main' of github.com:Textualize/textual into themes
Browse files Browse the repository at this point in the history
  • Loading branch information
darrenburns committed Oct 3, 2024
2 parents edcf706 + d76000d commit c6da6c9
Show file tree
Hide file tree
Showing 17 changed files with 1,187 additions and 68 deletions.
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,24 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## Unreleased
## [0.82.0] - 2024-10-03

### Fixed

- Fixed issue with screen not updating when auto_refresh was enabled https://github.com/Textualize/textual/pull/5063
- Fixed issues regarding loading indicator https://github.com/Textualize/textual/pull/5079
- Fixed issues with inspecting the lazy loaded widgets module https://github.com/Textualize/textual/pull/5080

### Added

- Added `DOMNode.is_on_screen` property https://github.com/Textualize/textual/pull/5063
- Added support for keymaps (user configurable key bindings) https://github.com/Textualize/textual/pull/5038
- Added descriptions to bindings for all internal widgets, and updated casing to be consistent https://github.com/Textualize/textual/pull/5062

### Changed

- Breaking change: `Widget.set_loading` no longer return an awaitable https://github.com/Textualize/textual/pull/5079

## [0.81.0] - 2024-09-25

### Added
Expand Down Expand Up @@ -2419,6 +2425,7 @@ https://textual.textualize.io/blog/2022/11/08/version-040/#version-040
- New handler system for messages that doesn't require inheritance
- Improved traceback handling

[0.82.0]: https://github.com/Textualize/textual/compare/v0.81.0...v0.82.0
[0.81.0]: https://github.com/Textualize/textual/compare/v0.80.1...v0.81.0
[0.80.1]: https://github.com/Textualize/textual/compare/v0.80.0...v0.80.1
[0.80.0]: https://github.com/Textualize/textual/compare/v0.79.0...v0.80.0
Expand Down
12 changes: 12 additions & 0 deletions docs/examples/guide/widgets/counter.tcss
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Counter {
background: $panel-darken-1;
padding: 1 2;
color: $text-muted;

&:focus { /* (1)! */
background: $primary;
color: $text;
text-style: bold;
outline-left: thick $accent;
}
}
27 changes: 27 additions & 0 deletions docs/examples/guide/widgets/counter01.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from textual.app import App, ComposeResult, RenderResult
from textual.reactive import reactive
from textual.widgets import Footer, Static


class Counter(Static, can_focus=True): # (1)!
"""A counter that can be incremented and decremented by pressing keys."""

count = reactive(0)

def render(self) -> RenderResult:
return f"Count: {self.count}"


class CounterApp(App[None]):
CSS_PATH = "counter.tcss"

def compose(self) -> ComposeResult:
yield Counter()
yield Counter()
yield Counter()
yield Footer()


if __name__ == "__main__":
app = CounterApp()
app.run()
35 changes: 35 additions & 0 deletions docs/examples/guide/widgets/counter02.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from textual.app import App, ComposeResult, RenderResult
from textual.reactive import reactive
from textual.widgets import Footer, Static


class Counter(Static, can_focus=True):
"""A counter that can be incremented and decremented by pressing keys."""

BINDINGS = [
("up,k", "change_count(1)", "Increment"), # (1)!
("down,j", "change_count(-1)", "Decrement"),
]

count = reactive(0)

def render(self) -> RenderResult:
return f"Count: {self.count}"

def action_change_count(self, amount: int) -> None: # (2)!
self.count += amount


class CounterApp(App[None]):
CSS_PATH = "counter.tcss"

def compose(self) -> ComposeResult:
yield Counter()
yield Counter()
yield Counter()
yield Footer()


if __name__ == "__main__":
app = CounterApp()
app.run()
10 changes: 10 additions & 0 deletions docs/guide/input.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,16 @@ The app splits the screen in to quarters, with a `RichLog` widget in each quarte

You can move focus by pressing the ++tab++ key to focus the next widget. Pressing ++shift+tab++ moves the focus in the opposite direction.

### Focusable widgets

Each widget has a boolean `can_focus` attribute which determines if it is capable of receiving focus.
Note that `can_focus=True` does not mean the widget will _always_ be focusable.
For example, a disabled widget cannot receive focus even if `can_focus` is `True`.

### Controlling focus

Textual will handle keyboard focus automatically, but you can tell Textual to focus a widget by calling the widget's [focus()][textual.widget.Widget.focus] method.
By default, Textual will focus the first focusable widget when the app starts.

### Focus events

Expand Down Expand Up @@ -154,6 +161,9 @@ Note how the footer displays bindings and makes them clickable.
Multiple keys can be bound to a single action by comma-separating them.
For example, `("r,t", "add_bar('red')", "Add Red")` means both ++r++ and ++t++ are bound to `add_bar('red')`.

When you press a key, Textual will first check for a matching binding in the `BINDINGS` list of the currently focused widget.
If no match is found, it will search upwards through the DOM all the way up to the `App` looking for a match.

### Binding class

The tuple of three strings may be enough for simple bindings, but you can also replace the tuple with a [Binding][textual.binding.Binding] instance which exposes a few more options.
Expand Down
63 changes: 63 additions & 0 deletions docs/guide/widgets.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,69 @@ If the supplied text is too long to fit within the widget, it will be cropped (a
There are a number of styles that influence how titles are displayed (color and alignment).
See the [style reference](../styles/index.md) for details.

## Focus & keybindings

Widgets can have a list of associated key [bindings](../guide/input.md#bindings),
which let them call [actions](../guide/actions.md) in response to key presses.

A widget is able to handle key presses if it or one of its descendants has [focus](../guide/input.md#input-focus).

Widgets aren't focusable by default.
To allow a widget to be focused, we need to set `can_focus=True` when defining a widget subclass.
Here's an example of a simple focusable widget:

=== "counter01.py"

```python title="counter01.py" hl_lines="6"
--8<-- "docs/examples/guide/widgets/counter01.py"
```

1. Allow the widget to receive input focus.

=== "counter.tcss"

```css title="counter.tcss" hl_lines="6-11"
--8<-- "docs/examples/guide/widgets/counter.tcss"
```

1. These styles are applied only when the widget has focus.

=== "Output"

```{.textual path="docs/examples/guide/widgets/counter01.py"}
```


The app above contains three `Counter` widgets, which we can focus by clicking or using ++tab++ and ++shift+tab++.

Now that our counter is focusable, let's add some keybindings to it to allow us to change the count using the keyboard.
To do this, we add a `BINDINGS` class variable to `Counter`, with bindings for ++up++ and ++down++.
These new bindings are linked to the `change_count` action, which updates the `count` reactive attribute.

With our bindings in place, we can now change the count of the _currently focused_ counter using ++up++ and ++down++.

=== "counter02.py"

```python title="counter02.py" hl_lines="9-12 19-20"
--8<-- "docs/examples/guide/widgets/counter02.py"
```

1. Associates presses of ++up++ or ++k++ with the `change_count` action, passing `1` as the argument to increment the count. The final argument ("Increment") is a user-facing label displayed in the footer when this binding is active.
2. Called when the binding is triggered. Take care to add the `action_` prefix to the method name.

=== "counter.tcss"

```css title="counter.tcss"
--8<-- "docs/examples/guide/widgets/counter.tcss"
```

1. These styles are applied only when the widget has focus.

=== "Output"

```{.textual path="docs/examples/guide/widgets/counter02.py" press="up,tab,down,down"}
```

## Rich renderables

In previous examples we've set strings as content for Widgets. You can also use special objects called [renderables](https://rich.readthedocs.io/en/latest/protocol.html) for advanced visuals. You can use any renderable defined in [Rich](https://github.com/Textualize/rich) or third party libraries.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "textual"
version = "0.81.0"
version = "0.82.0"
homepage = "https://github.com/Textualize/textual"
repository = "https://github.com/Textualize/textual"
documentation = "https://textual.textualize.io/"
Expand Down
36 changes: 23 additions & 13 deletions src/textual/_compositor.py
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,17 @@ def add_widget(

get_layer_index = layers_to_index.get

if widget._cover_widget is not None:
map[widget._cover_widget] = _MapGeometry(
region.shrink(widget.styles.gutter),
order,
clip,
region.size,
container_size,
virtual_region,
dock_gutter,
)

# Add all the widgets
for sub_region, _, sub_widget, z, fixed, overlay in reversed(
placements
Expand All @@ -681,18 +692,17 @@ def add_widget(
widget_region = self._constrain(
sub_widget.styles, widget_region, no_clip
)

add_widget(
sub_widget,
sub_region,
widget_region,
((1, 0, 0),) if overlay else widget_order,
layer_order,
no_clip if overlay else sub_clip,
visible,
arrange_result.scroll_spacing,
)

if widget._cover_widget is None:
add_widget(
sub_widget,
sub_region,
widget_region,
((1, 0, 0),) if overlay else widget_order,
layer_order,
no_clip if overlay else sub_clip,
visible,
arrange_result.scroll_spacing,
)
layer_order -= 1

if visible:
Expand Down Expand Up @@ -737,7 +747,7 @@ def add_widget(
if styles.constrain != "none":
widget_region = self._constrain(styles, widget_region, no_clip)

map[widget] = _MapGeometry(
map[widget._render_widget] = _MapGeometry(
widget_region,
order,
clip,
Expand Down
2 changes: 1 addition & 1 deletion src/textual/color.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ def inverse(self) -> Color:
@property
def is_transparent(self) -> bool:
"""Is the color transparent (i.e. has 0 alpha)?"""
return self.a == 0 and self.ansi is not None
return self.a == 0 and self.ansi is None

@property
def clamped(self) -> Color:
Expand Down
Loading

0 comments on commit c6da6c9

Please sign in to comment.