Merge branch 'main' into datatable-events

This commit is contained in:
darrenburns
2023-01-17 15:12:53 +00:00
committed by GitHub
11 changed files with 138 additions and 37 deletions

View File

@@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Added Widget._refresh_scroll to avoid expensive layout when scrolling https://github.com/Textualize/textual/pull/1524
- `events.Paste` now bubbles https://github.com/Textualize/textual/issues/1434
- Clock color in the `Header` widget now matches the header color https://github.com/Textualize/textual/issues/1459
- Programmatic calls to scroll now optionally scroll even if overflow styling says otherwise (introduces a new `force` parameter to all the `scroll_*` methods) https://github.com/Textualize/textual/issues/1201
- `COMPONENT_CLASSES` are now inherited from base classes https://github.com/Textualize/textual/issues/1399
- Watch methods may now take no parameters
- Added `compute` parameter to reactive
@@ -36,6 +37,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Behavior of widget `Input` when rendering after programmatic value change and related scenarios https://github.com/Textualize/textual/issues/1477 https://github.com/Textualize/textual/issues/1443
- `DataTable.show_cursor` now correctly allows cursor toggling https://github.com/Textualize/textual/pull/1547
- Fixed cursor not being visible on `DataTable` mount when `fixed_columns` were used https://github.com/Textualize/textual/pull/1547
- Fixed TextLog wrapping issue https://github.com/Textualize/textual/issues/1554
- Fixed issue with TextLog not writing anything before layout https://github.com/Textualize/textual/issues/1498
## [0.9.1] - 2022-12-30

View File

@@ -2,7 +2,7 @@
This directory contains example Textual applications.
To run them, navigate to the examples directory and enter `python` followed buy the name of the Python file.
To run them, navigate to the examples directory and enter `python` followed by the name of the Python file.
```
cd textual/examples

View File

@@ -240,8 +240,20 @@ class App(Generic[ReturnType], DOMNode):
SCREENS: dict[str, Screen | Callable[[], Screen]] = {}
_BASE_PATH: str | None = None
CSS_PATH: CSSPathType = None
TITLE: str | None = None
"""str | None: The default title for the application.
If set to a string, this sets the default title for the application. See
also the `title` attribute.
"""
SUB_TITLE: str | None = None
"""str | None: The default sub-title for the application.
If set to a string, this sets the default sub-title for the application. See
also the `sub_title` attribute.
"""
BINDINGS = [
Binding("ctrl+c", "quit", "Quit", show=False, priority=True),
@@ -303,10 +315,24 @@ class App(Generic[ReturnType], DOMNode):
self._animator = Animator(self)
self._animate = self._animator.bind(self)
self.mouse_position = Offset(0, 0)
self.title = (
self.TITLE if self.TITLE is not None else f"{self.__class__.__name__}"
)
"""The title for the application.
The initial value in a running application will be that set in `TITLE`
(if one is set). Assign new values to this instance attribute to change
the title.
"""
self.sub_title = self.SUB_TITLE if self.SUB_TITLE is not None else ""
"""The sub-title for the application.
The initial value in a running application will be that set in `SUB_TITLE`
(if one is set). Assign new values to this instance attribute to change
the sub-title.
"""
self._logger = Logger(self._log)

View File

@@ -28,17 +28,17 @@ class Binding:
"""The configuration of a key binding."""
key: str
"""str: Key to bind. This can also be a comma-separated list of keys to map multiple keys to a single action."""
"""Key to bind. This can also be a comma-separated list of keys to map multiple keys to a single action."""
action: str
"""str: Action to bind to."""
"""Action to bind to."""
description: str
"""str: Description of action."""
"""Description of action."""
show: bool = True
"""bool: Show the action in Footer, or False to hide."""
"""Show the action in Footer, or False to hide."""
key_display: str | None = None
"""str | None: How the key should be shown in footer."""
"""How the key should be shown in footer."""
priority: bool = False
"""bool: Enable priority binding for this key."""
"""Enable priority binding for this key."""
@rich.repr.auto

View File

@@ -9,7 +9,7 @@ if TYPE_CHECKING:
def match(selector_sets: Iterable[SelectorSet], node: DOMNode) -> bool:
"""Check if a given selector matches any of the given selector sets.
"""Check if a given node matches any of the given selector sets.
Args:
selector_sets (Iterable[SelectorSet]): Iterable of selector sets.

View File

@@ -57,12 +57,14 @@ class Strip:
return cls([Segment(" " * cell_length, style)], cell_length)
@classmethod
def from_lines(cls, lines: list[list[Segment]], cell_length: int) -> list[Strip]:
def from_lines(
cls, lines: list[list[Segment]], cell_length: int | None = None
) -> list[Strip]:
"""Convert lines (lists of segments) to a list of Strips.
Args:
lines (list[list[Segment]]): List of lines, where a line is a list of segments.
cell_length (int): Cell length of lines (must be same).
cell_length (int | None): Cell length of lines (must be same) or None if not known. Defaults to None.
Returns:
list[Strip]: List of strips.

View File

@@ -803,16 +803,14 @@ class Widget(DOMNode):
self.highlight_link_id = hover_style.link_id
def watch_scroll_x(self, old_value: float, new_value: float) -> None:
if self.show_horizontal_scrollbar:
self.horizontal_scrollbar.position = round(new_value)
if round(old_value) != round(new_value):
self._refresh_scroll()
self.horizontal_scrollbar.position = round(new_value)
if round(old_value) != round(new_value):
self._refresh_scroll()
def watch_scroll_y(self, old_value: float, new_value: float) -> None:
if self.show_vertical_scrollbar:
self.vertical_scrollbar.position = round(new_value)
if round(old_value) != round(new_value):
self._refresh_scroll()
self.vertical_scrollbar.position = round(new_value)
if round(old_value) != round(new_value):
self._refresh_scroll()
def validate_scroll_x(self, value: float) -> float:
return clamp(value, 0, self.max_scroll_x)
@@ -1340,6 +1338,7 @@ class Widget(DOMNode):
speed: float | None = None,
duration: float | None = None,
easing: EasingFunction | str | None = None,
force: bool = False,
) -> bool:
"""Scroll to a given (absolute) coordinate, optionally animating.
@@ -1351,10 +1350,13 @@ class Widget(DOMNode):
duration (float | None, optional): Duration of animation, if animate is True and speed is None.
easing (EasingFunction | str | None, optional): An easing method for the scrolling animation. Defaults to "None",
which will result in Textual choosing the default scrolling easing function.
force (bool, optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`.
Returns:
bool: True if the scroll position changed, otherwise False.
"""
maybe_scroll_x = x is not None and (self.allow_horizontal_scroll or force)
maybe_scroll_y = y is not None and (self.allow_vertical_scroll or force)
scrolled_x = scrolled_y = False
if animate:
# TODO: configure animation speed
@@ -1364,7 +1366,7 @@ class Widget(DOMNode):
if easing is None:
easing = DEFAULT_SCROLL_EASING
if x is not None:
if maybe_scroll_x:
self.scroll_target_x = x
if x != self.scroll_x:
self.animate(
@@ -1375,7 +1377,7 @@ class Widget(DOMNode):
easing=easing,
)
scrolled_x = True
if y is not None:
if maybe_scroll_y:
self.scroll_target_y = y
if y != self.scroll_y:
self.animate(
@@ -1388,11 +1390,11 @@ class Widget(DOMNode):
scrolled_y = True
else:
if x is not None:
if maybe_scroll_x:
scroll_x = self.scroll_x
self.scroll_target_x = self.scroll_x = x
scrolled_x = scroll_x != self.scroll_x
if y is not None:
if maybe_scroll_y:
scroll_y = self.scroll_y
self.scroll_target_y = self.scroll_y = y
scrolled_y = scroll_y != self.scroll_y
@@ -1408,6 +1410,7 @@ class Widget(DOMNode):
speed: float | None = None,
duration: float | None = None,
easing: EasingFunction | str | None = None,
force: bool = False,
) -> bool:
"""Scroll relative to current position.
@@ -1419,6 +1422,7 @@ class Widget(DOMNode):
duration (float | None, optional): Duration of animation, if animate is True and speed is None.
easing (EasingFunction | str | None, optional): An easing method for the scrolling animation. Defaults to "None",
which will result in Textual choosing the configured default scrolling easing function.
force (bool, optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`.
Returns:
bool: True if the scroll position changed, otherwise False.
@@ -1430,6 +1434,7 @@ class Widget(DOMNode):
speed=speed,
duration=duration,
easing=easing,
force=force,
)
def scroll_home(
@@ -1439,6 +1444,7 @@ class Widget(DOMNode):
speed: float | None = None,
duration: float | None = None,
easing: EasingFunction | str | None = None,
force: bool = False,
) -> bool:
"""Scroll to home position.
@@ -1448,6 +1454,7 @@ class Widget(DOMNode):
duration (float | None, optional): Duration of animation, if animate is True and speed is None.
easing (EasingFunction | str | None, optional): An easing method for the scrolling animation. Defaults to "None",
which will result in Textual choosing the configured default scrolling easing function.
force (bool, optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`.
Returns:
bool: True if any scrolling was done.
@@ -1455,7 +1462,13 @@ class Widget(DOMNode):
if speed is None and duration is None:
duration = 1.0
return self.scroll_to(
0, 0, animate=animate, speed=speed, duration=duration, easing=easing
0,
0,
animate=animate,
speed=speed,
duration=duration,
easing=easing,
force=force,
)
def scroll_end(
@@ -1465,6 +1478,7 @@ class Widget(DOMNode):
speed: float | None = None,
duration: float | None = None,
easing: EasingFunction | str | None = None,
force: bool = False,
) -> bool:
"""Scroll to the end of the container.
@@ -1474,6 +1488,7 @@ class Widget(DOMNode):
duration (float | None, optional): Duration of animation, if animate is True and speed is None.
easing (EasingFunction | str | None, optional): An easing method for the scrolling animation. Defaults to "None",
which will result in Textual choosing the configured default scrolling easing function.
force (bool, optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`.
Returns:
bool: True if any scrolling was done.
@@ -1488,6 +1503,7 @@ class Widget(DOMNode):
speed=speed,
duration=duration,
easing=easing,
force=force,
)
def scroll_left(
@@ -1497,6 +1513,7 @@ class Widget(DOMNode):
speed: float | None = None,
duration: float | None = None,
easing: EasingFunction | str | None = None,
force: bool = False,
) -> bool:
"""Scroll one cell left.
@@ -1506,6 +1523,7 @@ class Widget(DOMNode):
duration (float | None, optional): Duration of animation, if animate is True and speed is None.
easing (EasingFunction | str | None, optional): An easing method for the scrolling animation. Defaults to "None",
which will result in Textual choosing the configured default scrolling easing function.
force (bool, optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`.
Returns:
bool: True if any scrolling was done.
@@ -1517,6 +1535,7 @@ class Widget(DOMNode):
speed=speed,
duration=duration,
easing=easing,
force=force,
)
def scroll_right(
@@ -1526,6 +1545,7 @@ class Widget(DOMNode):
speed: float | None = None,
duration: float | None = None,
easing: EasingFunction | str | None = None,
force: bool = False,
) -> bool:
"""Scroll on cell right.
@@ -1535,6 +1555,7 @@ class Widget(DOMNode):
duration (float | None, optional): Duration of animation, if animate is True and speed is None.
easing (EasingFunction | str | None, optional): An easing method for the scrolling animation. Defaults to "None",
which will result in Textual choosing the configured default scrolling easing function.
force (bool, optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`.
Returns:
bool: True if any scrolling was done.
@@ -1546,6 +1567,7 @@ class Widget(DOMNode):
speed=speed,
duration=duration,
easing=easing,
force=force,
)
def scroll_down(
@@ -1555,6 +1577,7 @@ class Widget(DOMNode):
speed: float | None = None,
duration: float | None = None,
easing: EasingFunction | str | None = None,
force: bool = False,
) -> bool:
"""Scroll one line down.
@@ -1564,6 +1587,7 @@ class Widget(DOMNode):
duration (float | None, optional): Duration of animation, if animate is True and speed is None.
easing (EasingFunction | str | None, optional): An easing method for the scrolling animation. Defaults to "None",
which will result in Textual choosing the configured default scrolling easing function.
force (bool, optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`.
Returns:
bool: True if any scrolling was done.
@@ -1575,6 +1599,7 @@ class Widget(DOMNode):
speed=speed,
duration=duration,
easing=easing,
force=force,
)
def scroll_up(
@@ -1584,6 +1609,7 @@ class Widget(DOMNode):
speed: float | None = None,
duration: float | None = None,
easing: EasingFunction | str | None = None,
force: bool = False,
) -> bool:
"""Scroll one line up.
@@ -1593,6 +1619,7 @@ class Widget(DOMNode):
duration (float | None, optional): Duration of animation, if animate is True and speed is None.
easing (EasingFunction | str | None, optional): An easing method for the scrolling animation. Defaults to "None",
which will result in Textual choosing the configured default scrolling easing function.
force (bool, optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`.
Returns:
bool: True if any scrolling was done.
@@ -1604,6 +1631,7 @@ class Widget(DOMNode):
speed=speed,
duration=duration,
easing=easing,
force=force,
)
def scroll_page_up(
@@ -1613,6 +1641,7 @@ class Widget(DOMNode):
speed: float | None = None,
duration: float | None = None,
easing: EasingFunction | str | None = None,
force: bool = False,
) -> bool:
"""Scroll one page up.
@@ -1622,6 +1651,7 @@ class Widget(DOMNode):
duration (float | None, optional): Duration of animation, if animate is True and speed is None.
easing (EasingFunction | str | None, optional): An easing method for the scrolling animation. Defaults to "None",
which will result in Textual choosing the configured default scrolling easing function.
force (bool, optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`.
Returns:
bool: True if any scrolling was done.
@@ -1633,6 +1663,7 @@ class Widget(DOMNode):
speed=speed,
duration=duration,
easing=easing,
force=force,
)
def scroll_page_down(
@@ -1642,6 +1673,7 @@ class Widget(DOMNode):
speed: float | None = None,
duration: float | None = None,
easing: EasingFunction | str | None = None,
force: bool = False,
) -> bool:
"""Scroll one page down.
@@ -1651,6 +1683,7 @@ class Widget(DOMNode):
duration (float | None, optional): Duration of animation, if animate is True and speed is None.
easing (EasingFunction | str | None, optional): An easing method for the scrolling animation. Defaults to "None",
which will result in Textual choosing the configured default scrolling easing function.
force (bool, optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`.
Returns:
bool: True if any scrolling was done.
@@ -1662,6 +1695,7 @@ class Widget(DOMNode):
speed=speed,
duration=duration,
easing=easing,
force=force,
)
def scroll_page_left(
@@ -1671,6 +1705,7 @@ class Widget(DOMNode):
speed: float | None = None,
duration: float | None = None,
easing: EasingFunction | str | None = None,
force: bool = False,
) -> bool:
"""Scroll one page left.
@@ -1680,6 +1715,7 @@ class Widget(DOMNode):
duration (float | None, optional): Duration of animation, if animate is True and speed is None.
easing (EasingFunction | str | None, optional): An easing method for the scrolling animation. Defaults to "None",
which will result in Textual choosing the configured default scrolling easing function.
force (bool, optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`.
Returns:
bool: True if any scrolling was done.
@@ -1693,6 +1729,7 @@ class Widget(DOMNode):
speed=speed,
duration=duration,
easing=easing,
force=force,
)
def scroll_page_right(
@@ -1702,6 +1739,7 @@ class Widget(DOMNode):
speed: float | None = None,
duration: float | None = None,
easing: EasingFunction | str | None = None,
force: bool = False,
) -> bool:
"""Scroll one page right.
@@ -1711,6 +1749,7 @@ class Widget(DOMNode):
duration (float | None, optional): Duration of animation, if animate is True and speed is None.
easing (EasingFunction | str | None, optional): An easing method for the scrolling animation. Defaults to "None",
which will result in Textual choosing the configured default scrolling easing function.
force (bool, optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`.
Returns:
bool: True if any scrolling was done.
@@ -1724,6 +1763,7 @@ class Widget(DOMNode):
speed=speed,
duration=duration,
easing=easing,
force=force,
)
def scroll_to_widget(
@@ -1735,6 +1775,7 @@ class Widget(DOMNode):
duration: float | None = None,
easing: EasingFunction | str | None = None,
top: bool = False,
force: bool = False,
) -> bool:
"""Scroll scrolling to bring a widget in to view.
@@ -1746,6 +1787,7 @@ class Widget(DOMNode):
easing (EasingFunction | str | None, optional): An easing method for the scrolling animation. Defaults to "None",
which will result in Textual choosing the configured default scrolling easing function.
top (bool, optional): Scroll widget to top of container. Defaults to False.
force (bool, optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`.
Returns:
bool: True if any scrolling has occurred in any descendant, otherwise False.
@@ -1765,6 +1807,7 @@ class Widget(DOMNode):
duration=duration,
top=top,
easing=easing,
force=force,
)
if scroll_offset:
scrolled = True
@@ -1793,6 +1836,7 @@ class Widget(DOMNode):
duration: float | None = None,
easing: EasingFunction | str | None = None,
top: bool = False,
force: bool = False,
) -> Offset:
"""Scrolls a given region in to view, if required.
@@ -1808,6 +1852,7 @@ class Widget(DOMNode):
easing (EasingFunction | str | None, optional): An easing method for the scrolling animation. Defaults to "None",
which will result in Textual choosing the configured default scrolling easing function.
top (bool, optional): Scroll region to top of container. Defaults to False.
force (bool, optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`.
Returns:
Offset: The distance that was scrolled.
@@ -1835,6 +1880,7 @@ class Widget(DOMNode):
speed=speed,
duration=duration,
easing=easing,
force=force,
)
return delta
@@ -1846,6 +1892,7 @@ class Widget(DOMNode):
duration: float | None = None,
top: bool = False,
easing: EasingFunction | str | None = None,
force: bool = False,
) -> None:
"""Scroll the container to make this widget visible.
@@ -1856,6 +1903,7 @@ class Widget(DOMNode):
top (bool, optional): Scroll to top of container. Defaults to False.
easing (EasingFunction | str | None, optional): An easing method for the scrolling animation. Defaults to "None",
which will result in Textual choosing the configured default scrolling easing function.
force (bool, optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`.
"""
parent = self.parent
if isinstance(parent, Widget):
@@ -1867,6 +1915,7 @@ class Widget(DOMNode):
duration=duration,
top=top,
easing=easing,
force=force,
)
def __init_subclass__(

View File

@@ -195,8 +195,13 @@ class Button(Static, can_focus=True):
self.variant = self.validate_variant(variant)
label: Reactive[RenderableType] = Reactive("")
"""The text label that appears within the button."""
variant = Reactive.init("default")
"""The variant name for the button."""
disabled = Reactive(False)
"""The disabled state of the button; `True` if disabled, `False` if not."""
def __rich_repr__(self) -> rich.repr.Result:
yield from super().__rich_repr__()

View File

@@ -57,9 +57,6 @@ class Checkbox(Widget, can_focus=True):
"checkbox--switch",
}
value = reactive(False, init=False)
slider_pos = reactive(0.0)
def __init__(
self,
value: bool = False,
@@ -84,6 +81,12 @@ class Checkbox(Widget, can_focus=True):
self._reactive_value = value
self._should_animate = animate
value = reactive(False, init=False)
"""The value of the checkbox; `True` for on and `False` for off."""
slider_pos = reactive(0.0)
"""The position of the slider."""
def watch_value(self, value: bool) -> None:
target_slider_pos = 1.0 if value else 0.0
if self._should_animate:

View File

@@ -113,6 +113,17 @@ class Input(Widget, can_focus=True):
id: str | None = None,
classes: str | None = None,
) -> None:
"""Initialise the `Input` widget.
Args:
value (str | None, optional): An optional default value for the input.
placeholder (str, optional): Optional placeholder text for the input.
highlighter (Highlighter | None, optional): An optional highlighter for the input.
password (bool, optional): Flag to say if the field should obfuscate its content. Default is `False`.
name (str | None, optional): Optional name for the input widget.
id (str | None): Optional ID for the widget.
classes (str | None): Optional initial classes for the widget.
"""
super().__init__(name=name, id=id, classes=classes)
if value is not None:
self.value = value
@@ -127,7 +138,7 @@ class Input(Widget, can_focus=True):
@property
def _cursor_offset(self) -> int:
"""Get the cell offset of the cursor."""
"""The cell offset of the cursor."""
offset = self._position_to_cell(self.cursor_position)
if self._cursor_at_end:
offset += 1
@@ -135,7 +146,7 @@ class Input(Widget, can_focus=True):
@property
def _cursor_at_end(self) -> bool:
"""Check if the cursor is at the end"""
"""Flag to indicate if the cursor is at the end"""
return self.cursor_position >= len(self.value)
def validate_cursor_position(self, cursor_position: int) -> int:
@@ -170,7 +181,7 @@ class Input(Widget, can_focus=True):
@property
def cursor_width(self) -> int:
"""Get the width of the input (with extra space for cursor at the end)."""
"""The width of the input (with extra space for cursor at the end)."""
if self.placeholder and not self.value:
return cell_len(self.placeholder)
return self._position_to_cell(len(self.value)) + 1

View File

@@ -19,10 +19,10 @@ from ..strip import Strip
class TextLog(ScrollView, can_focus=True):
DEFAULT_CSS = """
DEFAULT_CSS = """
TextLog{
background: $surface;
color: $text;
color: $text;
overflow-y: scroll;
}
"""
@@ -103,11 +103,11 @@ class TextLog(ScrollView, can_focus=True):
container_width = (
self.scrollable_content_region.width if width is None else width
)
if expand and render_width < container_width:
render_width = container_width
if shrink and render_width > container_width:
render_width = container_width
if container_width:
if expand and render_width < container_width:
render_width = container_width
if shrink and render_width > container_width:
render_width = container_width
segments = self.app.console.render(
renderable, render_options.update_width(render_width)
@@ -120,7 +120,9 @@ class TextLog(ScrollView, can_focus=True):
self.max_width,
max(sum(segment.cell_length for segment in _line) for _line in lines),
)
strips = Strip.from_lines(lines, render_width)
strips = Strip.from_lines(lines)
for strip in strips:
strip.adjust_cell_length(render_width)
self.lines.extend(strips)
if self.max_lines is not None and len(self.lines) > self.max_lines: