Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
a9afc1c
new demo WIP
willmcgugan Oct 9, 2024
6d79102
Merge branch 'main' into new-demo
willmcgugan Oct 10, 2024
4248979
projects page
willmcgugan Oct 10, 2024
dcea484
simplify
willmcgugan Oct 10, 2024
f670fbe
regular grid
willmcgugan Oct 11, 2024
e366556
grid height
willmcgugan Oct 12, 2024
7451a4d
more projects
willmcgugan Oct 12, 2024
ca4af0c
more projects
willmcgugan Oct 12, 2024
6298acb
link widget
willmcgugan Oct 13, 2024
5bf30fe
added text column
willmcgugan Oct 13, 2024
dd980b8
fix keyline color
willmcgugan Oct 14, 2024
1e2064a
simplify
willmcgugan Oct 14, 2024
bd4688f
demo update
willmcgugan Oct 15, 2024
901db12
Added open links
willmcgugan Oct 15, 2024
f446576
Added welcome text
willmcgugan Oct 15, 2024
9410b83
Home screen
willmcgugan Oct 15, 2024
a56225a
formatting
willmcgugan Oct 15, 2024
32cefdf
widgets page
willmcgugan Oct 17, 2024
bd041b2
merge
willmcgugan Oct 17, 2024
1e26028
widgets
willmcgugan Oct 17, 2024
03e33b8
more widgets
willmcgugan Oct 18, 2024
c363372
merge
willmcgugan Oct 19, 2024
f9aba5d
universal selector now doesn't match scrollbars
willmcgugan Oct 19, 2024
4afdb3f
maor widgets
willmcgugan Oct 20, 2024
d046758
change
willmcgugan Oct 20, 2024
4f9ad30
updated digits
willmcgugan Oct 20, 2024
0ffcd6f
digit tweaks
willmcgugan Oct 20, 2024
6c63918
currency tweaks
willmcgugan Oct 20, 2024
0ce8988
hover effects
willmcgugan Oct 22, 2024
444a878
hover effects timer
willmcgugan Oct 22, 2024
09b636b
tint fix
willmcgugan Oct 16, 2024
e3bb452
snapshot
willmcgugan Oct 16, 2024
e6c97c7
reorder
willmcgugan Oct 16, 2024
89b112e
typo
willmcgugan Oct 16, 2024
bc69bdf
fix infinite loop in cropping
willmcgugan Oct 22, 2024
6c39533
version bump
willmcgugan Oct 22, 2024
b4fa813
changelog
willmcgugan Oct 22, 2024
536d2dd
depenencies
willmcgugan Oct 22, 2024
4f41dfe
snapshot
willmcgugan Oct 22, 2024
9b8cbf6
snapshot
willmcgugan Oct 22, 2024
4923061
snapshot tests
willmcgugan Oct 22, 2024
8e75510
remove -screen-suspended class
willmcgugan Oct 23, 2024
4a39803
merge
willmcgugan Oct 23, 2024
faae1c6
snapshots
willmcgugan Oct 23, 2024
b1bf8a8
changelog
willmcgugan Oct 23, 2024
2b43743
no hover pause in tests
willmcgugan Oct 24, 2024
92d391e
fix for stars
willmcgugan Oct 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +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

### 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

### 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

## [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
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
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
77 changes: 68 additions & 9 deletions src/textual/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ class App(Generic[ReturnType], DOMNode):
overflow-y: auto !important;
align: center middle;
.-maximized {
dock: initial !important;
dock: initial !important;
}
}
/* Fade the header title when app is blurred */
Expand Down Expand Up @@ -413,6 +413,9 @@ class MyApp(App[None]):
...
```
"""
DEFAULT_MODE: ClassVar[str] = "_default"
"""Name of the default mode."""

SCREENS: ClassVar[dict[str, Callable[[], Screen[Any]]]] = {}
"""Screens associated with the app for the lifetime of the app."""

Expand Down Expand Up @@ -490,6 +493,9 @@ class MyApp(App[None]):
SUSPENDED_SCREEN_CLASS: ClassVar[str] = ""
"""Class to apply to suspended screens, or empty string for no class."""

HOVER_EFFECTS_SCROLL_PAUSE: ClassVar[float] = 0.2
"""Seconds to pause hover effects for when scrolling."""

_PSEUDO_CLASSES: ClassVar[dict[str, Callable[[App], bool]]] = {
"focus": lambda app: app.app_focus,
"blur": lambda app: not app.app_focus,
Expand Down Expand Up @@ -587,9 +593,9 @@ def __init__(
self._workers = WorkerManager(self)
self.error_console = Console(markup=False, highlight=False, stderr=True)
self.driver_class = driver_class or self.get_driver_class()
self._screen_stacks: dict[str, list[Screen[Any]]] = {"_default": []}
self._screen_stacks: dict[str, list[Screen[Any]]] = {self.DEFAULT_MODE: []}
"""A stack of screens per mode."""
self._current_mode: str = "_default"
self._current_mode: str = self.DEFAULT_MODE
"""The current mode the app is in."""
self._sync_available = False

Expand Down Expand Up @@ -775,6 +781,11 @@ def __init__(
self._previous_inline_height: int | None = None
"""Size of previous inline update."""

self._paused_hover_effects: bool = False
"""Have the hover effects been paused?"""

self._hover_effects_timer: Timer | None = None

if self.ENABLE_COMMAND_PALETTE:
for _key, binding in self._bindings:
if binding.action in {"command_palette", "app.command_palette"}:
Expand Down Expand Up @@ -2171,8 +2182,12 @@ def _init_mode(self, mode: str) -> AwaitMount:

stack = self._screen_stacks.get(mode, [])
if stack:
await_mount = AwaitMount(stack[0], [])
else:
# Mode already exists
# Return an dummy await
return AwaitMount(stack[0], [])

if mode in self._modes:
# Mode is defined in MODES
_screen = self._modes[mode]
if isinstance(_screen, Screen):
raise TypeError(
Expand All @@ -2183,6 +2198,17 @@ def _init_mode(self, mode: str) -> AwaitMount:
screen, await_mount = self._get_screen(new_screen)
stack.append(screen)
self._load_screen_css(screen)
self.refresh_css()
screen.post_message(events.ScreenResume())
else:
# Mode is not defined
screen = self.get_default_screen()
stack.append(screen)
self._register(self, screen)
screen.post_message(events.ScreenResume())
await_mount = AwaitMount(stack[0], [])

screen._screen_resized(self.size)

self._screen_stacks[mode] = stack
return await_mount
Expand All @@ -2199,7 +2225,12 @@ def switch_mode(self, mode: str) -> AwaitMount:

Raises:
UnknownModeError: If trying to switch to an unknown mode.

"""

if mode == self._current_mode:
return AwaitMount(self.screen, [])

if mode not in self._modes:
raise UnknownModeError(f"No known mode {mode!r}")

Expand Down Expand Up @@ -2673,12 +2704,43 @@ def set_focus(self, widget: Widget | None, scroll_visible: bool = True) -> None:
"""
self.screen.set_focus(widget, scroll_visible)

def _pause_hover_effects(self):
"""Pause any hover effects based on Enter and Leave events for 200ms."""
if not self.HOVER_EFFECTS_SCROLL_PAUSE or self.is_headless:
return
self._paused_hover_effects = True
if self._hover_effects_timer is None:
self._hover_effects_timer = self.set_interval(
self.HOVER_EFFECTS_SCROLL_PAUSE, self._resume_hover_effects
)
else:
self._hover_effects_timer.reset()
self._hover_effects_timer.resume()

def _resume_hover_effects(self):
"""Resume sending Enter and Leave for hover effects."""
if not self.HOVER_EFFECTS_SCROLL_PAUSE or self.is_headless:
return
if self._paused_hover_effects:
self._paused_hover_effects = False
if self._hover_effects_timer is not None:
self._hover_effects_timer.pause()
try:
widget, _ = self.screen.get_widget_at(*self.mouse_position)
except NoWidget:
pass
else:
if widget is not self.mouse_over:
self._set_mouse_over(widget)

def _set_mouse_over(self, widget: Widget | None) -> None:
"""Called when the mouse is over another widget.

Args:
widget: Widget under mouse, or None for no widgets.
"""
if self._paused_hover_effects:
return
if widget is None:
if self.mouse_over is not None:
try:
Expand Down Expand Up @@ -3505,10 +3567,7 @@ async def on_event(self, event: events.Event) -> None:
# Handle input events that haven't been forwarded
# If the event has been forwarded it may have bubbled up back to the App
if isinstance(event, events.Compose):
screen: Screen[Any] = self.get_default_screen()
self._register(self, screen)
self._screen_stack.append(screen)
screen.post_message(events.ScreenResume())
await self._init_mode(self._current_mode)
await super().on_event(event)

elif isinstance(event, events.InputEvent) and not event.is_forwarded:
Expand Down
2 changes: 1 addition & 1 deletion src/textual/color.py
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ def __rich_repr__(self) -> rich.repr.Result:
yield g
yield b
yield "a", a, 1.0
yield "ansi", ansi
yield "ansi", ansi, None

def with_alpha(self, alpha: float) -> Color:
"""Create a new color with the given alpha.
Expand Down
Loading
Loading