Skip to content

Commit

Permalink
Merge branch 'main' into add-example-apps-to-tests
Browse files Browse the repository at this point in the history
  • Loading branch information
rodrigogiraoserrao committed Mar 28, 2024
2 parents 8b390c9 + 69cc80b commit 3e89e94
Show file tree
Hide file tree
Showing 29 changed files with 697 additions and 122 deletions.
21 changes: 19 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,33 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

### Fixed

- Exceptions inside `Widget.compose` or workers weren't bubbling up in tests https://github.com/Textualize/textual/issues/4282

### Added

- Added `Document.start` and `end` location properties for convenience https://github.com/Textualize/textual/pull/4267

## [0.54.0] - 2024-03-26

### Fixed

- Fixed a crash in `TextArea` when undoing an edit to a selection the selection was made backwards https://github.com/Textualize/textual/issues/4301
- Fixed issue with flickering scrollbars https://github.com/Textualize/textual/pull/4315
- Fixed issue where narrow TextArea would repeatedly wrap due to scrollbar appearing/disappearing https://github.com/Textualize/textual/pull/4334
- Fix progress bar ETA not updating when setting `total` reactive https://github.com/Textualize/textual/pull/4316

### Changed

- ProgressBar won't show ETA until there is at least one second of samples https://github.com/Textualize/textual/pull/4316
- `Input` waits until an edit has been made, after entry to the widget, before offering a suggestion https://github.com/Textualize/textual/pull/4335

## [0.53.1] - 2023-03-18
## [0.53.1] - 2024-03-18

### Fixed

- Fixed issue with data binding https://github.com/Textualize/textual/pull/4308

## [0.53.0] - 2023-03-18
## [0.53.0] - 2024-03-18

### Added

Expand Down Expand Up @@ -1795,6 +1811,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.54.0]: https://github.com/Textualize/textual/compare/v0.53.1...v0.54.0
[0.53.1]: https://github.com/Textualize/textual/compare/v0.53.0...v0.53.1
[0.53.0]: https://github.com/Textualize/textual/compare/v0.52.1...v0.53.0
[0.52.1]: https://github.com/Textualize/textual/compare/v0.52.0...v0.52.1
Expand Down
4 changes: 4 additions & 0 deletions docs/events/load.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
::: textual.events.Load
options:
heading_level: 1

## See also

- [Mount](mount.md)
5 changes: 5 additions & 0 deletions docs/events/mount.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
::: textual.events.Mount
options:
heading_level: 1

## See also

- [Load](load.md)
- [Unmount](unmount.md)
2 changes: 2 additions & 0 deletions docs/events/mouse_capture.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ title: MouseCapture

## See also

- [capture_mouse][textual.widget.Widget.capture_mouse]
- [release_mouse][textual.widget.Widget.release_mouse]
- [MouseRelease](mouse_release.md)
2 changes: 2 additions & 0 deletions docs/events/mouse_release.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ title: MouseRelease

## See also

- [capture_mouse][textual.widget.Widget.capture_mouse]
- [release_mouse][textual.widget.Widget.release_mouse]
- [MouseCapture](mouse_capture.md)
7 changes: 7 additions & 0 deletions docs/events/unmount.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
::: textual.events.Unmount
options:
heading_level: 1

## See also

- [Mount](mount.md)
2 changes: 1 addition & 1 deletion docs/guide/animation.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Animation

Ths chapter discusses how to use Textual's animation system to create visual effects such as movement, blending, and fading.
This chapter discusses how to use Textual's animation system to create visual effects such as movement, blending, and fading.


## Animating styles
Expand Down
4 changes: 4 additions & 0 deletions docs/widgets/text_area.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ This method is the programmatic equivalent of selecting some text and then pasti

Some other convenient methods are available, such as [`insert`][textual.widgets._text_area.TextArea.insert], [`delete`][textual.widgets._text_area.TextArea.delete], and [`clear`][textual.widgets._text_area.TextArea.clear].

!!! tip
The `TextArea.document.end` property returns the location at the end of the
document, which might be convenient when editing programmatically.

### Working with the cursor

#### Moving the cursor
Expand Down
1 change: 1 addition & 0 deletions mkdocs-nav.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ nav:
- "events/screen_resume.md"
- "events/screen_suspend.md"
- "events/show.md"
- "events/unmount.md"
- Styles:
- "styles/index.md"
- "styles/align.md"
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.53.1"
version = "0.54.0"
homepage = "https://github.com/Textualize/textual"
repository = "https://github.com/Textualize/textual"
documentation = "https://textual.textualize.io/"
Expand Down
9 changes: 8 additions & 1 deletion src/textual/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3006,11 +3006,18 @@ async def _broker_event(
return False
else:
event.stop()
if isinstance(action, (str, tuple)):
if isinstance(action, str) or (isinstance(action, tuple) and len(action) == 2):
await self.run_action(action, default_namespace=default_namespace) # type: ignore[arg-type]
elif callable(action):
await action()
else:
if isinstance(action, tuple) and self.debug:
# It's a tuple and made it this far, which means it'll be a
# malformed action. This is a no-op, but let's log that
# anyway.
log.warning(
f"Can't parse @{event_name} action from style meta; check your console markup syntax"
)
return False
return True

Expand Down
22 changes: 22 additions & 0 deletions src/textual/document/_document.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,17 @@ def prepare_query(self, query: str) -> Query | None:
def line_count(self) -> int:
"""Returns the number of lines in the document."""

@property
@abstractmethod
def start(self) -> Location:
"""Returns the location of the start of the document (0, 0)."""
return (0, 0)

@property
@abstractmethod
def end(self) -> Location:
"""Returns the location of the end of the document."""

@overload
def __getitem__(self, line_index: int) -> str: ...

Expand Down Expand Up @@ -331,6 +342,17 @@ def line_count(self) -> int:
"""Returns the number of lines in the document."""
return len(self._lines)

@property
def start(self) -> Location:
"""Returns the location of the start of the document (0, 0)."""
return super().start

@property
def end(self) -> Location:
"""Returns the location of the end of the document."""
last_line = self._lines[-1]
return (self.line_count, len(last_line))

def get_index_from_location(self, location: Location) -> int:
"""Given a location, returns the index from the document's text.
Expand Down
21 changes: 14 additions & 7 deletions src/textual/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ class Load(Event, bubble=False):
"""
Sent when the App is running but *before* the terminal is in application mode.
Use this event to run any set up that doesn't require any visuals such as loading
Use this event to run any setup that doesn't require any visuals such as loading
configuration and binding keys.
- [ ] Bubbles
Expand Down Expand Up @@ -129,6 +129,9 @@ def __rich_repr__(self) -> rich.repr.Result:
class Compose(Event, bubble=False, verbose=True):
"""Sent to a widget to request it to compose and mount children.
This event is used internally by Textual.
You won't typically need to explicitly handle it,
- [ ] Bubbles
- [X] Verbose
"""
Expand All @@ -143,15 +146,15 @@ class Mount(Event, bubble=False, verbose=False):


class Unmount(Event, bubble=False, verbose=False):
"""Sent when a widget is unmounted and may not longer receive messages.
"""Sent when a widget is unmounted and may no longer receive messages.
- [ ] Bubbles
- [ ] Verbose
"""


class Show(Event, bubble=False):
"""Sent when a widget has become visible.
"""Sent when a widget is first displayed.
- [ ] Bubbles
- [ ] Verbose
Expand All @@ -164,13 +167,17 @@ class Hide(Event, bubble=False):
- [ ] Bubbles
- [ ] Verbose
A widget may be hidden by setting its `visible` flag to `False`, if it is no longer in a layout,
or if it has been offset beyond the edges of the terminal.
Sent when any of the following conditions apply:
- The widget is removed from the DOM.
- The widget is no longer displayed because it has been scrolled or clipped from the terminal or its container.
- The widget has its `display` attribute set to `False`.
- The widget's `display` style is set to `"none"`.
"""


class Ready(Event, bubble=False):
"""Sent to the app when the DOM is ready.
"""Sent to the `App` when the DOM is ready and the first frame has been displayed.
- [ ] Bubbles
- [ ] Verbose
Expand Down Expand Up @@ -232,7 +239,7 @@ class Key(InputEvent):
Args:
key: The key that was pressed.
character: A printable character or ``None`` if it is not printable.
character: A printable character or `None` if it is not printable.
"""

__slots__ = ["key", "character", "aliases"]
Expand Down
2 changes: 1 addition & 1 deletion src/textual/screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from .css.parse import parse_selectors
from .css.query import NoMatches, QueryType
from .dom import DOMNode
from .errors import NoWidget
from .geometry import Offset, Region, Size
from .reactive import Reactive
from .renderables.background_screen import BackgroundScreen
Expand All @@ -54,7 +55,6 @@
from .command import Provider

# Unused & ignored imports are needed for the docs to link to these objects:
from .errors import NoWidget # type: ignore # noqa: F401
from .message_pump import MessagePump

# Screen updates will be batched so that they don't happen more often than 60 times per second:
Expand Down
1 change: 1 addition & 0 deletions src/textual/scroll_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ def _size_updated(
or virtual_size != self.virtual_size
or container_size != self.container_size
):
self._scrollbar_changes.clear()
self._size = size
virtual_size = self.virtual_size
self._container_size = size - self.styles.gutter.totals
Expand Down
4 changes: 3 additions & 1 deletion src/textual/signal.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,6 @@ def publish(self) -> None:
try:
callback()
except Exception as error:
log.error(f"error publishing signal to {node} ignored; {error}")
log.error(
f"error publishing signal to {node} ignored (callback={callback}); {error}"
)
55 changes: 29 additions & 26 deletions src/textual/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,14 +361,15 @@ def __init__(

self._styles_cache = StylesCache()
self._rich_style_cache: dict[str, tuple[Style, Style]] = {}
self._stabilize_scrollbar: tuple[Size, str, str] | None = None
"""Used to prevent scrollbar logic getting stuck in an infinite loop."""

self._tooltip: RenderableType | None = None
"""The tooltip content."""
self._absolute_offset: Offset | None = None
"""Force an absolute offset for the widget (used by tooltips)."""

self._scrollbar_changes: set[tuple[bool, bool]] = set()
"""Used to stabilize scrollbars."""

super().__init__(
name=name,
id=id,
Expand Down Expand Up @@ -799,7 +800,6 @@ def _arrange(self, size: Size) -> DockArrangeResult:
def _clear_arrangement_cache(self) -> None:
"""Clear arrangement cache, forcing a new arrange operation."""
self._arrangement_cache.clear()
self._stabilize_scrollbar = None

def _get_virtual_dom(self) -> Iterable[Widget]:
"""Get widgets not part of the DOM.
Expand Down Expand Up @@ -1437,14 +1437,6 @@ def _refresh_scrollbars(self) -> None:
overflow_x = styles.overflow_x
overflow_y = styles.overflow_y

stabilize_scrollbar = (
self.container_size,
overflow_x,
overflow_y,
)
if self._stabilize_scrollbar == stabilize_scrollbar:
return

width, height = self._container_size

show_horizontal = False
Expand All @@ -1463,17 +1455,31 @@ def _refresh_scrollbars(self) -> None:
elif overflow_y == "auto":
show_vertical = self.virtual_size.height > height

# When a single scrollbar is shown, the other dimension changes, so we need to recalculate.
if overflow_x == "auto" and show_vertical and not show_horizontal:
show_horizontal = self.virtual_size.width > (
width - styles.scrollbar_size_vertical
)
if overflow_y == "auto" and show_horizontal and not show_vertical:
show_vertical = self.virtual_size.height > (
height - styles.scrollbar_size_horizontal
)
_show_horizontal = show_horizontal
_show_vertical = show_vertical

if not (
overflow_x == "auto"
and overflow_y == "auto"
and (show_horizontal, show_vertical) in self._scrollbar_changes
):
# When a single scrollbar is shown, the other dimension changes, so we need to recalculate.
if overflow_x == "auto" and show_vertical and not show_horizontal:
show_horizontal = self.virtual_size.width > (
width - styles.scrollbar_size_vertical
)
if overflow_y == "auto" and show_horizontal and not show_vertical:
show_vertical = self.virtual_size.height > (
height - styles.scrollbar_size_horizontal
)

self._stabilize_scrollbar = stabilize_scrollbar
if (
self.show_horizontal_scrollbar != show_horizontal
or self.show_vertical_scrollbar != show_vertical
):
self._scrollbar_changes.add((_show_horizontal, _show_vertical))
else:
self._scrollbar_changes.clear()

self.show_horizontal_scrollbar = show_horizontal
self.show_vertical_scrollbar = show_vertical
Expand Down Expand Up @@ -3322,7 +3328,6 @@ def refresh(
return self
if layout:
self._layout_required = True
self._stabilize_scrollbar = None
for ancestor in self.ancestors:
if not isinstance(ancestor, Widget):
break
Expand Down Expand Up @@ -3613,10 +3618,8 @@ async def _compose(self) -> None:
raise TypeError(
f"{self!r} compose() method returned an invalid result; {error}"
) from error
except Exception:
from rich.traceback import Traceback

self.app.panic(Traceback())
except Exception as error:
self.app._handle_exception(error)
else:
self._extend_compose(widgets)
await self.mount_composed_widgets(widgets)
Expand Down
1 change: 1 addition & 0 deletions src/textual/widgets/_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,7 @@ def _on_focus(self, _: Focus) -> None:
if self.cursor_blink:
self._blink_timer.resume()
self.app.cursor_position = self.cursor_screen_offset
self._suggestion = ""

async def _on_key(self, event: events.Key) -> None:
self._cursor_visible = True
Expand Down
Loading

0 comments on commit 3e89e94

Please sign in to comment.