Merge branch 'main' into move-child-no-op

This commit is contained in:
Rodrigo Girão Serrão
2023-05-15 10:31:43 +01:00
committed by GitHub
13 changed files with 2853 additions and 2699 deletions

View File

@@ -6,12 +6,21 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/). and this project adheres to [Semantic Versioning](http://semver.org/).
## Unreleased ## Unrealeased
### Changed ### Changed
- App `title` and `sub_title` attributes can be set to any type https://github.com/Textualize/textual/issues/2521
- Using `Widget.move_child` where the target and the child being moved are the same is now a no-op https://github.com/Textualize/textual/issues/1743 - Using `Widget.move_child` where the target and the child being moved are the same is now a no-op https://github.com/Textualize/textual/issues/1743
### Fixed
- Fixed `ZeroDivisionError` in `resolve_fraction_unit` https://github.com/Textualize/textual/issues/2502
- Fixed `TreeNode.expand` and `TreeNode.expand_all` not posting a `Tree.NodeExpanded` message https://github.com/Textualize/textual/issues/2535
- Fixed `TreeNode.collapse` and `TreeNode.collapse_all` not posting a `Tree.NodeCollapsed` message https://github.com/Textualize/textual/issues/2535
- Fixed `TreeNode.toggle` and `TreeNode.toggle_all` not posting a `Tree.NodeExpanded` or `Tree.NodeCollapsed` message https://github.com/Textualize/textual/issues/2535
- `footer--description` component class was being ignored https://github.com/Textualize/textual/issues/2544
## [0.24.1] - 2023-05-08 ## [0.24.1] - 2023-05-08
### Fixed ### Fixed

BIN
reference/spacing.monopic Normal file

Binary file not shown.

View File

@@ -143,7 +143,7 @@ def resolve_fraction_unit(
resolved: list[Fraction | None] = [None] * len(resolve) resolved: list[Fraction | None] = [None] * len(resolve)
remaining_fraction = Fraction(sum(scalar.value for scalar, _, _ in resolve)) remaining_fraction = Fraction(sum(scalar.value for scalar, _, _ in resolve))
while True: while remaining_fraction > 0:
remaining_space_changed = False remaining_space_changed = False
resolve_fraction = Fraction(remaining_space, remaining_fraction) resolve_fraction = Fraction(remaining_space, remaining_fraction)
for index, (scalar, min_value, max_value) in enumerate(resolve): for index, (scalar, min_value, max_value) in enumerate(resolve):
@@ -166,7 +166,7 @@ def resolve_fraction_unit(
return ( return (
Fraction(remaining_space, remaining_fraction) Fraction(remaining_space, remaining_fraction)
if remaining_space if remaining_space > 0
else Fraction(1) else Fraction(1)
) )

View File

@@ -316,6 +316,7 @@ class App(Generic[ReturnType], DOMNode):
the name of the app if it doesn't. the name of the app if it doesn't.
Assign a new value to this attribute to change the title. Assign a new value to this attribute to change the title.
The new value is always converted to string.
""" """
self.sub_title = self.SUB_TITLE if self.SUB_TITLE is not None else "" self.sub_title = self.SUB_TITLE if self.SUB_TITLE is not None else ""
@@ -328,6 +329,7 @@ class App(Generic[ReturnType], DOMNode):
the file being worker on. the file being worker on.
Assign a new value to this attribute to change the sub-title. Assign a new value to this attribute to change the sub-title.
The new value is always converted to string.
""" """
self._logger = Logger(self._log) self._logger = Logger(self._log)
@@ -406,6 +408,14 @@ class App(Generic[ReturnType], DOMNode):
self.set_class(self.dark, "-dark-mode") self.set_class(self.dark, "-dark-mode")
self.set_class(not self.dark, "-light-mode") self.set_class(not self.dark, "-light-mode")
def validate_title(self, title: Any) -> str:
"""Make sure the title is set to a string."""
return str(title)
def validate_sub_title(self, sub_title: Any) -> str:
"""Make sure the sub-title is set to a string."""
return str(sub_title)
@property @property
def workers(self) -> WorkerManager: def workers(self) -> WorkerManager:
"""The [worker](guide/workers/) manager. """The [worker](guide/workers/) manager.

View File

@@ -887,7 +887,7 @@ class DOMNode(MessagePump):
Example: Example:
Here's how you could detect when the app changes from dark to light mode (and visa versa). Here's how you could detect when the app changes from dark to light mode (and vice versa).
```python ```python
def on_dark_change(old_value:bool, new_value:bool): def on_dark_change(old_value:bool, new_value:bool):

View File

@@ -839,7 +839,7 @@ class Region(NamedTuple):
@lru_cache(maxsize=1024) @lru_cache(maxsize=1024)
def split_horizontal(self, cut: int) -> tuple[Region, Region]: def split_horizontal(self, cut: int) -> tuple[Region, Region]:
"""Split a region in to two, from a given x offset. """Split a region in to two, from a given y offset.
``` ```
┌─────────┐ ┌─────────┐
@@ -852,8 +852,8 @@ class Region(NamedTuple):
``` ```
Args: Args:
cut: An offset from self.x where the cut should be made. May be negative, cut: An offset from self.y where the cut should be made. May be negative,
for the offset to start from the right edge. for the offset to start from the lower edge.
Returns: Returns:
Two regions, which add up to the original (self). Two regions, which add up to the original (self).
@@ -909,7 +909,19 @@ class Region(NamedTuple):
class Spacing(NamedTuple): class Spacing(NamedTuple):
"""The spacing around a renderable, such as padding and border """The spacing around a renderable, such as padding and border
Spacing is defined by four integers for the space at the top, right, bottom, and left of a region, Spacing is defined by four integers for the space at the top, right, bottom, and left of a region.
```
┌ ─ ─ ─ ─ ─ ─ ─▲─ ─ ─ ─ ─ ─ ─ ─ ┐
│ top
│ ┏━━━━━▼━━━━━━┓ │
◀──────▶┃ ┃◀───────▶
│ left ┃ ┃ right │
┃ ┃
│ ┗━━━━━▲━━━━━━┛ │
│ bottom
└ ─ ─ ─ ─ ─ ─ ─▼─ ─ ─ ─ ─ ─ ─ ─ ┘
```
Example: Example:
```python ```python

View File

@@ -379,16 +379,22 @@ class Strip:
""" """
pos = 0 pos = 0
cell_length = self.cell_length
cuts = [cut for cut in cuts if cut <= cell_length]
cache_key = tuple(cuts) cache_key = tuple(cuts)
cached = self._divide_cache.get(cache_key) cached = self._divide_cache.get(cache_key)
if cached is not None: if cached is not None:
return cached return cached
strips: list[Strip] = [] strips: list[Strip]
add_strip = strips.append if cuts == [cell_length]:
for segments, cut in zip(Segment.divide(self._segments, cuts), cuts): strips = [self]
add_strip(Strip(segments, cut - pos)) else:
pos = cut strips = []
add_strip = strips.append
for segments, cut in zip(Segment.divide(self._segments, cuts), cuts):
add_strip(Strip(segments, cut - pos))
pos = cut
self._divide_cache[cache_key] = strips self._divide_cache[cache_key] = strips
return strips return strips

View File

@@ -98,6 +98,7 @@ class Footer(Widget):
highlight_style = self.get_component_rich_style("footer--highlight") highlight_style = self.get_component_rich_style("footer--highlight")
highlight_key_style = self.get_component_rich_style("footer--highlight-key") highlight_key_style = self.get_component_rich_style("footer--highlight-key")
key_style = self.get_component_rich_style("footer--key") key_style = self.get_component_rich_style("footer--key")
description_style = self.get_component_rich_style("footer--description")
bindings = [ bindings = [
binding binding
@@ -122,7 +123,7 @@ class Footer(Widget):
(f" {key_display} ", highlight_key_style if hovered else key_style), (f" {key_display} ", highlight_key_style if hovered else key_style),
( (
f" {binding.description} ", f" {binding.description} ",
highlight_style if hovered else base_style, highlight_style if hovered else base_style + description_style,
), ),
meta={ meta={
"@click": f"app.check_bindings('{binding.key}')", "@click": f"app.check_bindings('{binding.key}')",

View File

@@ -207,6 +207,7 @@ class TreeNode(Generic[TreeDataType]):
""" """
self._expanded = True self._expanded = True
self._updates += 1 self._updates += 1
self._tree.post_message(Tree.NodeExpanded(self._tree, self))
if expand_all: if expand_all:
for child in self.children: for child in self.children:
child._expand(expand_all) child._expand(expand_all)
@@ -239,6 +240,7 @@ class TreeNode(Generic[TreeDataType]):
""" """
self._expanded = False self._expanded = False
self._updates += 1 self._updates += 1
self._tree.post_message(Tree.NodeCollapsed(self._tree, self))
if collapse_all: if collapse_all:
for child in self.children: for child in self.children:
child._collapse(collapse_all) child._collapse(collapse_all)
@@ -1157,10 +1159,8 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
return return
if node.is_expanded: if node.is_expanded:
node.collapse() node.collapse()
self.post_message(self.NodeCollapsed(self, node))
else: else:
node.expand() node.expand()
self.post_message(self.NodeExpanded(self, node))
async def _on_click(self, event: events.Click) -> None: async def _on_click(self, event: events.Click) -> None:
meta = event.style.meta meta = event.style.meta

File diff suppressed because one or more lines are too long

View File

@@ -36,3 +36,33 @@ async def test_hover_update_styles():
# We've hovered, so ensure the pseudoclass is present and background changed # We've hovered, so ensure the pseudoclass is present and background changed
assert button.pseudo_classes == {"enabled", "hover"} assert button.pseudo_classes == {"enabled", "hover"}
assert button.styles.background != initial_background assert button.styles.background != initial_background
def test_setting_title():
app = MyApp()
app.title = None
assert app.title == "None"
app.title = ""
assert app.title == ""
app.title = 0.125
assert app.title == "0.125"
app.title = [True, False, 2]
assert app.title == "[True, False, 2]"
def test_setting_sub_title():
app = MyApp()
app.sub_title = None
assert app.sub_title == "None"
app.sub_title = ""
assert app.sub_title == ""
app.sub_title = 0.125
assert app.sub_title == "0.125"
app.sub_title = [True, False, 2]
assert app.sub_title == "[True, False, 2]"

View File

@@ -1,4 +1,5 @@
from fractions import Fraction from fractions import Fraction
from itertools import chain
import pytest import pytest
@@ -122,3 +123,22 @@ def test_resolve_fraction_unit():
Fraction(32), Fraction(32),
resolve_dimension="width", resolve_dimension="width",
) == Fraction(2) ) == Fraction(2)
def test_resolve_issue_2502():
"""Test https://github.com/Textualize/textual/issues/2502"""
widget = Widget()
widget.styles.width = "1fr"
widget.styles.min_width = 11
assert isinstance(
resolve_fraction_unit(
(widget.styles,),
Size(80, 24),
Size(80, 24),
Fraction(10),
resolve_dimension="width",
),
Fraction,
)

View File

@@ -78,6 +78,20 @@ async def test_tree_node_expanded_message() -> None:
assert pilot.app.messages == [("NodeExpanded", "test-tree")] assert pilot.app.messages == [("NodeExpanded", "test-tree")]
async def tree_node_expanded_by_code_message() -> None:
"""Expanding a node via the API should result in an expanded message being posted."""
async with TreeApp().run_test() as pilot:
pilot.app.query_one(Tree).root.children[0].expand()
assert pilot.app.messages == [("NodeExpanded", "test-tree")]
async def tree_node_all_expanded_by_code_message() -> None:
"""Expanding all nodes via the API should result in expanded messages being posted."""
async with TreeApp().run_test() as pilot:
pilot.app.query_one(Tree).root.children[0].expand_all()
assert pilot.app.messages == [("NodeExpanded", "test-tree")]
async def test_tree_node_collapsed_message() -> None: async def test_tree_node_collapsed_message() -> None:
"""Collapsing a node should result in a collapsed message being emitted.""" """Collapsing a node should result in a collapsed message being emitted."""
async with TreeApp().run_test() as pilot: async with TreeApp().run_test() as pilot:
@@ -89,6 +103,46 @@ async def test_tree_node_collapsed_message() -> None:
] ]
async def tree_node_collapsed_by_code_message() -> None:
"""Collapsing a node via the API should result in a collapsed message being posted."""
async with TreeApp().run_test() as pilot:
pilot.app.query_one(Tree).root.children[0].expand().collapse()
assert pilot.app.messages == [
("NodeExpanded", "test-tree"),
("NodeCollapsed", "test-tree"),
]
async def tree_node_all_collapsed_by_code_message() -> None:
"""Collapsing all nodes via the API should result in collapsed messages being posted."""
async with TreeApp().run_test() as pilot:
pilot.app.query_one(Tree).root.children[0].expand_all().collapse_all()
assert pilot.app.messages == [
("NodeExpanded", "test-tree"),
("NodeCollapsed", "test-tree"),
]
async def tree_node_toggled_by_code_message() -> None:
"""Toggling a node twice via the API should result in expanded and collapsed messages."""
async with TreeApp().run_test() as pilot:
pilot.app.query_one(Tree).root.children[0].toggle().toggle()
assert pilot.app.messages == [
("NodeExpanded", "test-tree"),
("NodeCollapsed", "test-tree"),
]
async def tree_node_all_toggled_by_code_message() -> None:
"""Toggling all nodes twice via the API should result in expanded and collapsed messages."""
async with TreeApp().run_test() as pilot:
pilot.app.query_one(Tree).root.children[0].toggle_all().toggle_all()
assert pilot.app.messages == [
("NodeExpanded", "test-tree"),
("NodeCollapsed", "test-tree"),
]
async def test_tree_node_highlighted_message() -> None: async def test_tree_node_highlighted_message() -> None:
"""Highlighting a node should result in a highlighted message being emitted.""" """Highlighting a node should result in a highlighted message being emitted."""
async with TreeApp().run_test() as pilot: async with TreeApp().run_test() as pilot: