Skip to content

Commit

Permalink
Merge branch 'main' into feat-events-add-control-property-to-enter-an…
Browse files Browse the repository at this point in the history
…d-leave
  • Loading branch information
willmcgugan authored Oct 25, 2024
2 parents 51409e1 + b9be24f commit 9a72e06
Show file tree
Hide file tree
Showing 68 changed files with 2,329 additions and 1,172 deletions.
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,33 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

## Unreleased

### Changed

- Grid will now size children to the maximum height of a row https://github.com/Textualize/textual/pull/5113
- Markdown links will be opened with `App.open_url` automatically https://github.com/Textualize/textual/pull/5113
- The universal selector (`*`) will now not match widgets with the class `-textual-system` (scrollbars, notifications etc) https://github.com/Textualize/textual/pull/5113
- Renamed `Screen.can_view` and `Widget.can_view` to `Screen.can_view_entire` and `Widget.can_view_entire` https://github.com/Textualize/textual/pull/5174

### Added

- Added Link widget https://github.com/Textualize/textual/pull/5113
- Added `open_links` to `Markdown` and `MarkdownViewer` widgets https://github.com/Textualize/textual/pull/5113
- Added `App.DEFAULT_MODE` https://github.com/Textualize/textual/pull/5113
- Added `Containers.HorizontalGroup` and `Containers.VerticalGroup` https://github.com/Textualize/textual/pull/5113
- Added `$`, `£`, ``, `(`, `)` symbols to Digits https://github.com/Textualize/textual/pull/5113
- Added `Button.action` parameter to invoke action when clicked https://github.com/Textualize/textual/pull/5113
- Added `immediate` parameter to scroll methods https://github.com/Textualize/textual/pull/5164
- Added `textual._loop.loop_from_index` https://github.com/Textualize/textual/pull/5164
- Added `min_color` and `max_color` to Sparklines constructor, which take precedence over CSS https://github.com/Textualize/textual/pull/5174
- Added new demo `python -m textual`, not *quite* finished but better than the old one https://github.com/Textualize/textual/pull/5174
- Added `Screen.can_view_partial` and `Widget.can_view_partial` https://github.com/Textualize/textual/pull/5174
- Added `App.is_web` property to indicate if the app is running via a web browser https://github.com/Textualize/textual/pull/5128
- `Enter` and `Leave` events can now be used with the `on` decorator https://github.com/Textualize/textual/pull/5159

### Fixed

- Fixed glitchy ListView https://github.com/Textualize/textual/issues/5163

## [0.84.0] - 2024-10-22

### Fixed
Expand Down
6 changes: 6 additions & 0 deletions docs/api/layout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
title: "textual.layout"
---


::: textual.layout
23 changes: 23 additions & 0 deletions docs/examples/widgets/link.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from textual.app import App, ComposeResult
from textual.widgets import Link


class LabelApp(App):
AUTO_FOCUS = None
CSS = """
Screen {
align: center middle;
}
"""

def compose(self) -> ComposeResult:
yield Link(
"Go to textualize.io",
url="https://textualize.io",
tooltip="Click me",
)


if __name__ == "__main__":
app = LabelApp()
app.run()
7 changes: 7 additions & 0 deletions docs/widget_gallery.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,13 @@ A simple text label.
[Label reference](./widgets/label.md){ .md-button .md-button--primary }


## Link

A clickable link that opens a URL.

[Link reference](./widgets/link.md){ .md-button .md-button--primary }


## ListView

Display a list of items (items may be other widgets).
Expand Down
61 changes: 61 additions & 0 deletions docs/widgets/link.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Link

!!! tip "Added in version 0.84.0"

A widget to display a piece of text that opens a URL when clicked, like a web browser link.

- [x] Focusable
- [ ] Container


## Example

A trivial app with a link.
Clicking the link open's a web-browser—as you might expect!

=== "Output"

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

=== "link.py"

```python
--8<-- "docs/examples/widgets/link.py"
```


## Reactive Attributes

| Name | Type | Default | Description |
| ------ | ----- | ------- | ----------------------------------------- |
| `text` | `str` | `""` | The text of the link. |
| `url` | `str` | `""` | The URL to open when the link is clicked. |


## Messages

This widget sends no messages.

## Bindings

The Link widget defines the following bindings:

::: textual.widgets.Link.BINDINGS
options:
show_root_heading: false
show_root_toc_entry: false


## Component classes

This widget contains no component classes.



---


::: textual.widgets.Link
options:
heading_level: 2
2 changes: 1 addition & 1 deletion docs/widgets/masked_input.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ The example below shows a masked input to ease entering a credit card number.
```{.textual path="docs/examples/widgets/masked_input.py"}
```

=== "checkbox.py"
=== "masked_input.py"

```python
--8<-- "docs/examples/widgets/masked_input.py"
Expand Down
2 changes: 2 additions & 0 deletions mkdocs-nav.yml
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ nav:
- "widgets/index.md"
- "widgets/input.md"
- "widgets/label.md"
- "widgets/link.md"
- "widgets/list_item.md"
- "widgets/list_view.md"
- "widgets/loading_indicator.md"
Expand Down Expand Up @@ -195,6 +196,7 @@ nav:
- "api/filter.md"
- "api/fuzzy_matcher.md"
- "api/geometry.md"
- "api/layout.md"
- "api/lazy.md"
- "api/logger.md"
- "api/logging.md"
Expand Down
2 changes: 1 addition & 1 deletion src/textual/__main__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from textual.demo import DemoApp
from textual.demo.demo_app import DemoApp

if __name__ == "__main__":
app = DemoApp()
Expand Down
4 changes: 2 additions & 2 deletions src/textual/_arrange.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
from operator import attrgetter
from typing import TYPE_CHECKING, Iterable, Mapping, Sequence

from textual._layout import DockArrangeResult, WidgetPlacement
from textual._partition import partition
from textual.geometry import Region, Size, Spacing
from textual.layout import DockArrangeResult, WidgetPlacement

if TYPE_CHECKING:
from textual.widget import Widget
Expand Down Expand Up @@ -90,7 +90,7 @@ def arrange(

if layout_widgets:
# Arrange layout widgets (i.e. not docked)
layout_placements = widget._layout.arrange(
layout_placements = widget.layout.arrange(
widget,
layout_widgets,
dock_region.size,
Expand Down
43 changes: 42 additions & 1 deletion src/textual/_loop.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

from typing import Iterable, TypeVar
from typing import Iterable, Literal, Sequence, TypeVar

T = TypeVar("T")

Expand Down Expand Up @@ -43,3 +43,44 @@ def loop_first_last(values: Iterable[T]) -> Iterable[tuple[bool, bool, T]]:
first = False
previous_value = value
yield first, True, previous_value


def loop_from_index(
values: Sequence[T],
index: int,
direction: Literal[-1, +1] = +1,
wrap: bool = True,
) -> Iterable[tuple[int, T]]:
"""Iterate over values in a sequence from a given starting index, potentially wrapping the index
if it would go out of bounds.
Note that the first value to be yielded is a step from `index`, and `index` will be yielded *last*.
Args:
values: A sequence of values.
index: Starting index.
direction: Direction to move index (+1 for forward, -1 for backward).
bool: Should the index wrap when out of bounds?
Yields:
A tuple of index and value from the sequence.
"""
# Sanity check for devs who miss the typing errors
assert direction in (-1, +1), "direction must be -1 or +1"
count = len(values)
if wrap:
for _ in range(count):
index = (index + direction) % count
yield (index, values[index])
else:
if direction == +1:
for _ in range(count):
if (index := index + 1) >= count:
break
yield (index, values[index])
else:
for _ in range(count):
if (index := index - 1) < 0:
break
yield (index, values[index])
2 changes: 1 addition & 1 deletion src/textual/_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ def timer(subject: str = "time") -> Generator[None, None, None]:
yield
elapsed = perf_counter() - start
elapsed_ms = elapsed * 1000
log(f"{subject} elapsed {elapsed_ms:.2f}ms")
log(f"{subject} elapsed {elapsed_ms:.4f}ms")
6 changes: 6 additions & 0 deletions src/textual/_resolve.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ def resolve(
gutter: int,
size: Size,
viewport: Size,
min_size: int | None = None,
) -> list[tuple[int, int]]:
"""Resolve a list of dimensions.
Expand Down Expand Up @@ -62,6 +63,11 @@ def resolve(
"list[Fraction]", [fraction for _, fraction in resolved]
)

if min_size is not None:
resolved_fractions = [
max(Fraction(min_size), fraction) for fraction in resolved_fractions
]

fraction_gutter = Fraction(gutter)
offsets = [0] + [
int(fraction)
Expand Down
21 changes: 6 additions & 15 deletions src/textual/_widget_navigation.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@

from __future__ import annotations

from functools import partial
from itertools import count
from typing import Literal, Protocol, Sequence

from typing_extensions import TypeAlias

from textual._loop import loop_from_index


class Disableable(Protocol):
"""Non-widgets that have an enabled/disabled status."""
Expand Down Expand Up @@ -105,7 +106,6 @@ def find_next_enabled(
candidates: Sequence[Disableable],
anchor: int | None,
direction: Direction,
with_anchor: bool = False,
) -> int | None:
"""Find the next enabled object if we're currently at the given anchor.
Expand All @@ -118,8 +118,6 @@ def find_next_enabled(
enabled object.
direction: The direction in which to traverse the candidates when looking for
the next enabled candidate.
with_anchor: Consider the anchor position as the first valid position instead of
the last one.
Returns:
The next enabled object. If none are available, return the anchor.
Expand All @@ -134,17 +132,10 @@ def find_next_enabled(
)
return None

start = anchor + direction if not with_anchor else anchor
key_function = partial(
get_directed_distance,
start=start,
direction=direction,
wrap_at=len(candidates),
)
enabled_candidates = [
index for index, candidate in enumerate(candidates) if not candidate.disabled
]
return min(enabled_candidates, key=key_function, default=anchor)
for index, candidate in loop_from_index(candidates, anchor, direction, wrap=True):
if not candidate.disabled:
return index
return anchor


def find_next_enabled_no_wrap(
Expand Down
Loading

0 comments on commit 9a72e06

Please sign in to comment.