mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Merge branch 'css' into docs-intro
This commit is contained in:
@@ -2,11 +2,12 @@
|
|||||||
# See https://pre-commit.com/hooks.html for more hooks
|
# See https://pre-commit.com/hooks.html for more hooks
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v3.2.0
|
rev: v4.3.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
|
args: ['--unsafe']
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 22.3.0
|
rev: 22.3.0
|
||||||
hooks:
|
hooks:
|
||||||
|
|||||||
20
docs/examples/styles/content_align.css
Normal file
20
docs/examples/styles/content_align.css
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#box1 {
|
||||||
|
content-align: left top;
|
||||||
|
background: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
#box2 {
|
||||||
|
content-align: center middle;
|
||||||
|
background: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
#box3 {
|
||||||
|
content-align: right bottom;
|
||||||
|
background: blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Static {
|
||||||
|
height: 1fr;
|
||||||
|
padding: 1;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
13
docs/examples/styles/content_align.py
Normal file
13
docs/examples/styles/content_align.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from textual.app import App
|
||||||
|
from textual.widgets import Static
|
||||||
|
|
||||||
|
|
||||||
|
class ContentAlignApp(App):
|
||||||
|
def compose(self):
|
||||||
|
yield Static("With [i]content-align[/] you can...", id="box1")
|
||||||
|
yield Static("...[b]Easily align content[/]...", id="box2")
|
||||||
|
yield Static("...Horizontally [i]and[/] vertically!", id="box3")
|
||||||
|
|
||||||
|
|
||||||
|
app = ContentAlignApp(css_path="content_align.css")
|
||||||
|
app.run()
|
||||||
8
docs/examples/styles/scrollbar_gutter.css
Normal file
8
docs/examples/styles/scrollbar_gutter.css
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
Screen {
|
||||||
|
scrollbar-gutter: stable;
|
||||||
|
}
|
||||||
|
|
||||||
|
#text-box {
|
||||||
|
color: floralwhite;
|
||||||
|
background: darkmagenta;
|
||||||
|
}
|
||||||
18
docs/examples/styles/scrollbar_gutter.py
Normal file
18
docs/examples/styles/scrollbar_gutter.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from textual.app import App
|
||||||
|
from textual.widgets import Static
|
||||||
|
|
||||||
|
TEXT = """I must not fear.
|
||||||
|
Fear is the mind-killer.
|
||||||
|
Fear is the little-death that brings total obliteration.
|
||||||
|
I will face my fear.
|
||||||
|
I will permit it to pass over me and through me.
|
||||||
|
And when it has gone past, I will turn the inner eye to see its path.
|
||||||
|
Where the fear has gone there will be nothing. Only I will remain."""
|
||||||
|
|
||||||
|
|
||||||
|
class ScrollbarGutterApp(App):
|
||||||
|
def compose(self):
|
||||||
|
yield Static(TEXT, id="text-box")
|
||||||
|
|
||||||
|
|
||||||
|
app = ScrollbarGutterApp(css_path="scrollbar_gutter.css")
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
The `border` rule enables the drawing of a box around a widget. A border is set with a border value (see below) followed by a color.
|
The `border` rule enables the drawing of a box around a widget. A border is set with a border value (see below) followed by a color.
|
||||||
|
|
||||||
| Border value | Explanation |
|
| Border value | Explanation |
|
||||||
| ------------ | ------------------------------------------------------- |
|
| ------------ |---------------------------------------------------------|
|
||||||
| `"ascii"` | A border with plus, hyphen, and vertical bar |
|
| `"ascii"` | A border with plus, hyphen, and vertical bar |
|
||||||
| `"blank"` | A blank border (reserves space for a border) |
|
| `"blank"` | A blank border (reserves space for a border) |
|
||||||
| `"dashed"` | Dashed line border |
|
| `"dashed"` | Dashed line border |
|
||||||
|
|||||||
65
docs/styles/content_align.md
Normal file
65
docs/styles/content_align.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# Content-align
|
||||||
|
|
||||||
|
The `content-align` property allows you to align content _inside_ a widget.
|
||||||
|
|
||||||
|
You can specify the alignment of content on both the horizontal and vertical axes.
|
||||||
|
|
||||||
|
## Syntax
|
||||||
|
|
||||||
|
```
|
||||||
|
content-align: <HORIZONTAL> <VERTICAL>;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Values
|
||||||
|
|
||||||
|
#### `HORIZONTAL`
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
|------------------|----------------------------------------------------|
|
||||||
|
| `left` (default) | Align content on the left of the horizontal axis |
|
||||||
|
| `center` | Align content in the center of the horizontal axis |
|
||||||
|
| `right` | Align content on the right of the horizontal axis |
|
||||||
|
|
||||||
|
#### `VERTICAL`
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
|-----------------|--------------------------------------------------|
|
||||||
|
| `top` (default) | Align content at the top of the vertical axis |
|
||||||
|
| `middle` | Align content in the middle of the vertical axis |
|
||||||
|
| `bottom` | Align content at the bottom of the vertical axis |
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
=== "content_align.py"
|
||||||
|
|
||||||
|
```python
|
||||||
|
--8<-- "docs/examples/styles/content_align.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "content_align.css"
|
||||||
|
|
||||||
|
```scss
|
||||||
|
--8<-- "docs/examples/styles/content_align.css"
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "Output"
|
||||||
|
|
||||||
|
```{.textual path="docs/examples/styles/content_align.py"}
|
||||||
|
```
|
||||||
|
|
||||||
|
## CSS
|
||||||
|
|
||||||
|
```sass
|
||||||
|
/* Align content in the very center of a widget */
|
||||||
|
content-align: center middle;
|
||||||
|
/* Align content at the top right of a widget */
|
||||||
|
content-align: right top;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Python
|
||||||
|
```python
|
||||||
|
# Align content in the very center of a widget
|
||||||
|
widget.styles.content_align = ("center", "middle")
|
||||||
|
# Align content at the top right of a widget
|
||||||
|
widget.styles.content_align = ("right", "top")
|
||||||
|
```
|
||||||
53
docs/styles/scrollbar_gutter.md
Normal file
53
docs/styles/scrollbar_gutter.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# Scrollbar-gutter
|
||||||
|
|
||||||
|
The `scrollbar-gutter` rule allows authors to reserve space for the vertical scrollbar.
|
||||||
|
|
||||||
|
Setting the value to `stable` prevents unwanted layout changes when the scrollbar becomes visible.
|
||||||
|
|
||||||
|
## Syntax
|
||||||
|
|
||||||
|
```
|
||||||
|
scrollbar-gutter: [auto|stable];
|
||||||
|
```
|
||||||
|
|
||||||
|
### Values
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
|------------------|--------------------------------------------------|
|
||||||
|
| `auto` (default) | No space is reserved for the vertical scrollbar. |
|
||||||
|
| `stable` | Space is reserved for the vertical scrollbar. |
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
In the example below, notice the gap reserved for the scrollbar on the right side of the
|
||||||
|
terminal window.
|
||||||
|
|
||||||
|
=== "scrollbar_gutter.py"
|
||||||
|
|
||||||
|
```python
|
||||||
|
--8<-- "docs/examples/styles/scrollbar_gutter.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "scrollbar_gutter.css"
|
||||||
|
|
||||||
|
```scss
|
||||||
|
--8<-- "docs/examples/styles/scrollbar_gutter.css"
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "Output"
|
||||||
|
|
||||||
|
```{.textual path="docs/examples/styles/scrollbar_gutter.py"}
|
||||||
|
```
|
||||||
|
|
||||||
|
## CSS
|
||||||
|
|
||||||
|
```sass
|
||||||
|
/* Reserve space for vertical scrollbar */
|
||||||
|
scrollbar-gutter: stable;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Python
|
||||||
|
|
||||||
|
```python
|
||||||
|
self.styles.scrollbar_gutter = "stable"
|
||||||
|
```
|
||||||
@@ -42,6 +42,7 @@ nav:
|
|||||||
- "styles/border.md"
|
- "styles/border.md"
|
||||||
- "styles/box_sizing.md"
|
- "styles/box_sizing.md"
|
||||||
- "styles/color.md"
|
- "styles/color.md"
|
||||||
|
- "styles/content_align.md"
|
||||||
- "styles/display.md"
|
- "styles/display.md"
|
||||||
- "styles/min_height.md"
|
- "styles/min_height.md"
|
||||||
- "styles/max_height.md"
|
- "styles/max_height.md"
|
||||||
@@ -55,6 +56,7 @@ nav:
|
|||||||
- "styles/padding.md"
|
- "styles/padding.md"
|
||||||
- "styles/scrollbar.md"
|
- "styles/scrollbar.md"
|
||||||
- "styles/scrollbar_size.md"
|
- "styles/scrollbar_size.md"
|
||||||
|
- "styles/scrollbar_gutter.md"
|
||||||
- "styles/text_style.md"
|
- "styles/text_style.md"
|
||||||
- "styles/tint.md"
|
- "styles/tint.md"
|
||||||
- "styles/visibility.md"
|
- "styles/visibility.md"
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
|
||||||
from rich.console import RenderableType
|
from rich.console import RenderableType
|
||||||
from rich.panel import Panel
|
|
||||||
|
|
||||||
from textual import events
|
from textual import events
|
||||||
from textual.app import App, ComposeResult
|
from textual.app import App, ComposeResult
|
||||||
from textual.layout import Horizontal, Vertical
|
|
||||||
from textual.widget import Widget
|
from textual.widget import Widget
|
||||||
|
|
||||||
|
|
||||||
@@ -18,37 +18,22 @@ class Box(Widget, can_focus=True):
|
|||||||
super().__init__(*children, id=id, classes=classes)
|
super().__init__(*children, id=id, classes=classes)
|
||||||
|
|
||||||
def render(self) -> RenderableType:
|
def render(self) -> RenderableType:
|
||||||
return Panel("Box")
|
return "Box"
|
||||||
|
|
||||||
|
|
||||||
class JustABox(App):
|
class JustABox(App):
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
yield Horizontal(
|
self.box = Box()
|
||||||
Vertical(
|
yield self.box
|
||||||
Box(id="box1", classes="box"),
|
|
||||||
Box(id="box2", classes="box"),
|
|
||||||
# Box(id="box3", classes="box"),
|
|
||||||
# Box(id="box4", classes="box"),
|
|
||||||
# Box(id="box5", classes="box"),
|
|
||||||
# Box(id="box6", classes="box"),
|
|
||||||
# Box(id="box7", classes="box"),
|
|
||||||
# Box(id="box8", classes="box"),
|
|
||||||
# Box(id="box9", classes="box"),
|
|
||||||
# Box(id="box10", classes="box"),
|
|
||||||
id="left_pane",
|
|
||||||
),
|
|
||||||
Box(id="middle_pane"),
|
|
||||||
Vertical(
|
|
||||||
Box(id="boxa", classes="box"),
|
|
||||||
Box(id="boxb", classes="box"),
|
|
||||||
Box(id="boxc", classes="box"),
|
|
||||||
id="right_pane",
|
|
||||||
),
|
|
||||||
id="horizontal",
|
|
||||||
)
|
|
||||||
|
|
||||||
def key_p(self):
|
def key_a(self):
|
||||||
print(self.query("#horizontal").first().styles.layout)
|
self.animator.animate(
|
||||||
|
self.box.styles,
|
||||||
|
"opacity",
|
||||||
|
value=0.0,
|
||||||
|
duration=2.0,
|
||||||
|
on_complete=self.box.remove,
|
||||||
|
)
|
||||||
|
|
||||||
async def on_key(self, event: events.Key) -> None:
|
async def on_key(self, event: events.Key) -> None:
|
||||||
await self.dispatch_key(event)
|
await self.dispatch_key(event)
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ from typing import Any, Callable, TypeVar
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from . import _clock
|
from . import _clock
|
||||||
|
from ._callback import invoke
|
||||||
from ._easing import DEFAULT_EASING, EASING
|
from ._easing import DEFAULT_EASING, EASING
|
||||||
from ._timer import Timer
|
from ._timer import Timer
|
||||||
from ._types import MessageTarget
|
from ._types import MessageTarget, CallbackType
|
||||||
|
|
||||||
if sys.version_info >= (3, 8):
|
if sys.version_info >= (3, 8):
|
||||||
from typing import Protocol, runtime_checkable
|
from typing import Protocol, runtime_checkable
|
||||||
@@ -30,8 +31,20 @@ class Animatable(Protocol):
|
|||||||
|
|
||||||
|
|
||||||
class Animation(ABC):
|
class Animation(ABC):
|
||||||
|
|
||||||
|
on_complete: CallbackType | None = None
|
||||||
|
"""Callback to run after animation completes"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def __call__(self, time: float) -> bool: # pragma: no cover
|
def __call__(self, time: float) -> bool: # pragma: no cover
|
||||||
|
"""Call the animation, return a boolean indicating whether animation is in-progress or complete.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
time (float): The current timestamp
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the animation has finished, otherwise False.
|
||||||
|
"""
|
||||||
raise NotImplementedError("")
|
raise NotImplementedError("")
|
||||||
|
|
||||||
def __eq__(self, other: object) -> bool:
|
def __eq__(self, other: object) -> bool:
|
||||||
@@ -48,6 +61,7 @@ class SimpleAnimation(Animation):
|
|||||||
end_value: float | Animatable
|
end_value: float | Animatable
|
||||||
final_value: object
|
final_value: object
|
||||||
easing: EasingFunction
|
easing: EasingFunction
|
||||||
|
on_complete: CallbackType | None = None
|
||||||
|
|
||||||
def __call__(self, time: float) -> bool:
|
def __call__(self, time: float) -> bool:
|
||||||
|
|
||||||
@@ -109,6 +123,7 @@ class BoundAnimator:
|
|||||||
duration: float | None = None,
|
duration: float | None = None,
|
||||||
speed: float | None = None,
|
speed: float | None = None,
|
||||||
easing: EasingFunction | str = DEFAULT_EASING,
|
easing: EasingFunction | str = DEFAULT_EASING,
|
||||||
|
on_complete: CallbackType | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
easing_function = EASING[easing] if isinstance(easing, str) else easing
|
easing_function = EASING[easing] if isinstance(easing, str) else easing
|
||||||
return self._animator.animate(
|
return self._animator.animate(
|
||||||
@@ -119,6 +134,7 @@ class BoundAnimator:
|
|||||||
duration=duration,
|
duration=duration,
|
||||||
speed=speed,
|
speed=speed,
|
||||||
easing=easing_function,
|
easing=easing_function,
|
||||||
|
on_complete=on_complete,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -163,6 +179,7 @@ class Animator:
|
|||||||
duration: float | None = None,
|
duration: float | None = None,
|
||||||
speed: float | None = None,
|
speed: float | None = None,
|
||||||
easing: EasingFunction | str = DEFAULT_EASING,
|
easing: EasingFunction | str = DEFAULT_EASING,
|
||||||
|
on_complete: CallbackType | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Animate an attribute to a new value.
|
"""Animate an attribute to a new value.
|
||||||
|
|
||||||
@@ -201,6 +218,7 @@ class Animator:
|
|||||||
duration=duration,
|
duration=duration,
|
||||||
speed=speed,
|
speed=speed,
|
||||||
easing=easing_function,
|
easing=easing_function,
|
||||||
|
on_complete=on_complete,
|
||||||
)
|
)
|
||||||
if animation is None:
|
if animation is None:
|
||||||
start_value = getattr(obj, attribute)
|
start_value = getattr(obj, attribute)
|
||||||
@@ -223,6 +241,7 @@ class Animator:
|
|||||||
end_value=value,
|
end_value=value,
|
||||||
final_value=final_value,
|
final_value=final_value,
|
||||||
easing=easing_function,
|
easing=easing_function,
|
||||||
|
on_complete=on_complete,
|
||||||
)
|
)
|
||||||
assert animation is not None, "animation expected to be non-None"
|
assert animation is not None, "animation expected to be non-None"
|
||||||
|
|
||||||
@@ -233,7 +252,7 @@ class Animator:
|
|||||||
self._animations[animation_key] = animation
|
self._animations[animation_key] = animation
|
||||||
self._timer.resume()
|
self._timer.resume()
|
||||||
|
|
||||||
def __call__(self) -> None:
|
async def __call__(self) -> None:
|
||||||
if not self._animations:
|
if not self._animations:
|
||||||
self._timer.pause()
|
self._timer.pause()
|
||||||
else:
|
else:
|
||||||
@@ -241,7 +260,11 @@ class Animator:
|
|||||||
animation_keys = list(self._animations.keys())
|
animation_keys = list(self._animations.keys())
|
||||||
for animation_key in animation_keys:
|
for animation_key in animation_keys:
|
||||||
animation = self._animations[animation_key]
|
animation = self._animations[animation_key]
|
||||||
if animation(animation_time):
|
animation_complete = animation(animation_time)
|
||||||
|
if animation_complete:
|
||||||
|
completion_callback = animation.on_complete
|
||||||
|
if completion_callback is not None:
|
||||||
|
await invoke(completion_callback)
|
||||||
del self._animations[animation_key]
|
del self._animations[animation_key]
|
||||||
|
|
||||||
def _get_time(self) -> float:
|
def _get_time(self) -> float:
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import sys
|
import sys
|
||||||
from typing import Awaitable, Callable, List, Optional, TYPE_CHECKING
|
from typing import Awaitable, Callable, List, TYPE_CHECKING, Union
|
||||||
|
|
||||||
from rich.segment import Segment
|
from rich.segment import Segment
|
||||||
|
|
||||||
if sys.version_info >= (3, 8):
|
if sys.version_info >= (3, 8):
|
||||||
@@ -11,8 +12,6 @@ else:
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .message import Message
|
from .message import Message
|
||||||
|
|
||||||
Callback = Callable[[], None]
|
|
||||||
|
|
||||||
|
|
||||||
class MessageTarget(Protocol):
|
class MessageTarget(Protocol):
|
||||||
async def post_message(self, message: "Message") -> bool:
|
async def post_message(self, message: "Message") -> bool:
|
||||||
@@ -34,5 +33,5 @@ class EventTarget(Protocol):
|
|||||||
|
|
||||||
|
|
||||||
MessageHandler = Callable[["Message"], Awaitable]
|
MessageHandler = Callable[["Message"], Awaitable]
|
||||||
|
|
||||||
Lines = List[List[Segment]]
|
Lines = List[List[Segment]]
|
||||||
|
CallbackType = Union[Callable[[], Awaitable[None]], Callable[[], None]]
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Callable
|
||||||
|
|
||||||
from .. import events, log
|
from .._types import CallbackType
|
||||||
from ..geometry import Offset
|
from ..geometry import Offset
|
||||||
from .._animator import Animation
|
from .._animator import Animation
|
||||||
from .scalar import ScalarOffset
|
from .scalar import ScalarOffset
|
||||||
@@ -25,6 +25,7 @@ class ScalarAnimation(Animation):
|
|||||||
duration: float | None,
|
duration: float | None,
|
||||||
speed: float | None,
|
speed: float | None,
|
||||||
easing: EasingFunction,
|
easing: EasingFunction,
|
||||||
|
on_complete: CallbackType | None = None,
|
||||||
):
|
):
|
||||||
assert (
|
assert (
|
||||||
speed is not None or duration is not None
|
speed is not None or duration is not None
|
||||||
@@ -35,6 +36,7 @@ class ScalarAnimation(Animation):
|
|||||||
self.attribute = attribute
|
self.attribute = attribute
|
||||||
self.final_value = value
|
self.final_value = value
|
||||||
self.easing = easing
|
self.easing = easing
|
||||||
|
self.on_complete = on_complete
|
||||||
|
|
||||||
size = widget.outer_size
|
size = widget.outer_size
|
||||||
viewport = widget.app.size
|
viewport = widget.app.size
|
||||||
@@ -55,7 +57,6 @@ class ScalarAnimation(Animation):
|
|||||||
eased_factor = self.easing(factor)
|
eased_factor = self.easing(factor)
|
||||||
|
|
||||||
if eased_factor >= 1:
|
if eased_factor >= 1:
|
||||||
offset = self.final_value
|
|
||||||
setattr(self.styles, self.attribute, self.final_value)
|
setattr(self.styles, self.attribute, self.final_value)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from typing import TYPE_CHECKING, Any, Iterable, NamedTuple, cast
|
|||||||
import rich.repr
|
import rich.repr
|
||||||
from rich.style import Style
|
from rich.style import Style
|
||||||
|
|
||||||
|
from textual._types import CallbackType
|
||||||
from .._animator import Animation, EasingFunction
|
from .._animator import Animation, EasingFunction
|
||||||
from ..color import Color
|
from ..color import Color
|
||||||
from ..geometry import Offset, Spacing
|
from ..geometry import Offset, Spacing
|
||||||
@@ -579,9 +580,9 @@ class Styles(StylesBase):
|
|||||||
duration: float | None,
|
duration: float | None,
|
||||||
speed: float | None,
|
speed: float | None,
|
||||||
easing: EasingFunction,
|
easing: EasingFunction,
|
||||||
|
on_complete: CallbackType | None = None,
|
||||||
) -> Animation | None:
|
) -> Animation | None:
|
||||||
from ..widget import Widget
|
# from ..widget import Widget
|
||||||
|
|
||||||
# node = self.node
|
# node = self.node
|
||||||
# assert isinstance(self.node, Widget)
|
# assert isinstance(self.node, Widget)
|
||||||
if isinstance(value, ScalarOffset):
|
if isinstance(value, ScalarOffset):
|
||||||
@@ -594,6 +595,7 @@ class Styles(StylesBase):
|
|||||||
duration=duration,
|
duration=duration,
|
||||||
speed=speed,
|
speed=speed,
|
||||||
easing=easing,
|
easing=easing,
|
||||||
|
on_complete=on_complete,
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -760,7 +762,7 @@ class Styles(StylesBase):
|
|||||||
)
|
)
|
||||||
elif has_rule("align_horizontal"):
|
elif has_rule("align_horizontal"):
|
||||||
append_declaration("align-horizontal", self.align_horizontal)
|
append_declaration("align-horizontal", self.align_horizontal)
|
||||||
elif has_rule("align_horizontal"):
|
elif has_rule("align_vertical"):
|
||||||
append_declaration("align-vertical", self.align_vertical)
|
append_declaration("align-vertical", self.align_vertical)
|
||||||
|
|
||||||
if has_rule("content_align_horizontal") and has_rule("content_align_vertical"):
|
if has_rule("content_align_horizontal") and has_rule("content_align_vertical"):
|
||||||
@@ -772,7 +774,7 @@ class Styles(StylesBase):
|
|||||||
append_declaration(
|
append_declaration(
|
||||||
"content-align-horizontal", self.content_align_horizontal
|
"content-align-horizontal", self.content_align_horizontal
|
||||||
)
|
)
|
||||||
elif has_rule("content_align_horizontal"):
|
elif has_rule("content_align_vertical"):
|
||||||
append_declaration("content-align-vertical", self.content_align_vertical)
|
append_declaration("content-align-vertical", self.content_align_vertical)
|
||||||
|
|
||||||
lines.sort()
|
lines.sort()
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import TYPE_CHECKING, Callable, Awaitable, Union
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import rich.repr
|
import rich.repr
|
||||||
|
|
||||||
|
from ._types import CallbackType
|
||||||
from .message import Message
|
from .message import Message
|
||||||
|
|
||||||
|
|
||||||
@@ -11,9 +12,6 @@ if TYPE_CHECKING:
|
|||||||
from .widget import Widget
|
from .widget import Widget
|
||||||
|
|
||||||
|
|
||||||
CallbackType = Union[Callable[[], Awaitable[None]], Callable[[], None]]
|
|
||||||
|
|
||||||
|
|
||||||
@rich.repr.auto
|
@rich.repr.auto
|
||||||
class Update(Message, verbosity=3):
|
class Update(Message, verbosity=3):
|
||||||
def __init__(self, sender: MessagePump, widget: Widget):
|
def __init__(self, sender: MessagePump, widget: Widget):
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ from ._callback import invoke
|
|||||||
|
|
||||||
from .geometry import Offset, Region, Size
|
from .geometry import Offset, Region, Size
|
||||||
from ._compositor import Compositor, MapGeometry
|
from ._compositor import Compositor, MapGeometry
|
||||||
from .messages import CallbackType
|
from ._types import CallbackType
|
||||||
from ._profile import timer
|
|
||||||
from .reactive import Reactive
|
from .reactive import Reactive
|
||||||
from .renderables.blank import Blank
|
from .renderables.blank import Blank
|
||||||
from ._timer import Timer
|
from ._timer import Timer
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from unittest.mock import Mock
|
from unittest.mock import Mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
from textual._animator import Animator, SimpleAnimation
|
from textual._animator import Animator, SimpleAnimation
|
||||||
from textual._easing import EASING, DEFAULT_EASING
|
from textual._easing import EASING, DEFAULT_EASING
|
||||||
|
|
||||||
@@ -184,8 +182,7 @@ class MockAnimator(Animator):
|
|||||||
return self._time
|
return self._time
|
||||||
|
|
||||||
|
|
||||||
def test_animator():
|
async def test_animator():
|
||||||
|
|
||||||
target = Mock()
|
target = Mock()
|
||||||
animator = MockAnimator(target)
|
animator = MockAnimator(target)
|
||||||
animate_test = AnimateTest()
|
animate_test = AnimateTest()
|
||||||
@@ -206,11 +203,11 @@ def test_animator():
|
|||||||
assert animator._animations[(id(animate_test), "foo")] == expected
|
assert animator._animations[(id(animate_test), "foo")] == expected
|
||||||
assert not animator._on_animation_frame_called
|
assert not animator._on_animation_frame_called
|
||||||
|
|
||||||
animator()
|
await animator()
|
||||||
assert animate_test.foo == 0
|
assert animate_test.foo == 0
|
||||||
|
|
||||||
animator._time = 5
|
animator._time = 5
|
||||||
animator()
|
await animator()
|
||||||
assert animate_test.foo == 50
|
assert animate_test.foo == 50
|
||||||
|
|
||||||
# New animation in the middle of an existing one
|
# New animation in the middle of an existing one
|
||||||
@@ -218,12 +215,11 @@ def test_animator():
|
|||||||
assert animate_test.foo == 50
|
assert animate_test.foo == 50
|
||||||
|
|
||||||
animator._time = 6
|
animator._time = 6
|
||||||
animator()
|
await animator()
|
||||||
assert animate_test.foo == 200
|
assert animate_test.foo == 200
|
||||||
|
|
||||||
|
|
||||||
def test_bound_animator():
|
def test_bound_animator():
|
||||||
|
|
||||||
target = Mock()
|
target = Mock()
|
||||||
animator = MockAnimator(target)
|
animator = MockAnimator(target)
|
||||||
animate_test = AnimateTest()
|
animate_test = AnimateTest()
|
||||||
@@ -245,3 +241,29 @@ def test_bound_animator():
|
|||||||
easing=EASING[DEFAULT_EASING],
|
easing=EASING[DEFAULT_EASING],
|
||||||
)
|
)
|
||||||
assert animator._animations[(id(animate_test), "foo")] == expected
|
assert animator._animations[(id(animate_test), "foo")] == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_animator_on_complete_callback_not_fired_before_duration_ends():
|
||||||
|
callback = Mock()
|
||||||
|
animate_test = AnimateTest()
|
||||||
|
animator = MockAnimator(Mock())
|
||||||
|
|
||||||
|
animator.animate(animate_test, "foo", 200, duration=10, on_complete=callback)
|
||||||
|
|
||||||
|
animator._time = 9
|
||||||
|
animator()
|
||||||
|
|
||||||
|
assert not callback.called
|
||||||
|
|
||||||
|
|
||||||
|
async def test_animator_on_complete_callback_fired_at_duration():
|
||||||
|
callback = Mock()
|
||||||
|
animate_test = AnimateTest()
|
||||||
|
animator = MockAnimator(Mock())
|
||||||
|
|
||||||
|
animator.animate(animate_test, "foo", 200, duration=10, on_complete=callback)
|
||||||
|
|
||||||
|
animator._time = 10
|
||||||
|
await animator()
|
||||||
|
|
||||||
|
callback.assert_called_once_with()
|
||||||
|
|||||||
Reference in New Issue
Block a user