mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
input docs an exampels
This commit is contained in:
@@ -8,5 +8,6 @@ The `Key` event is sent to a widget when the user presses a key on the keyboard.
|
||||
## Attributes
|
||||
|
||||
| attribute | type | purpose |
|
||||
| --------- | ---- | ------------------------ |
|
||||
| `key` | str | The key that was pressed |
|
||||
| --------- | ----------- | ----------------------------------------------------------- |
|
||||
| `key` | str | Name of the key that was pressed. |
|
||||
| `char` | str or None | The character that was pressed, or None it isn't printable. |
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
The `MouseMove` event is sent to a widget when the mouse pointer is moved over a widget.
|
||||
|
||||
- [x] Bubbles
|
||||
- [ ] Bubbles
|
||||
- [x] Verbose
|
||||
|
||||
## Attributes
|
||||
|
||||
21
docs/examples/guide/input/key01.py
Normal file
21
docs/examples/guide/input/key01.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.widgets import TextLog
|
||||
from textual import events
|
||||
|
||||
|
||||
class InputApp(App):
|
||||
"""App to display key events."""
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield TextLog()
|
||||
|
||||
def on_key(self, event: events.Key) -> None:
|
||||
self.query_one(TextLog).write(event)
|
||||
|
||||
def key_space(self) -> None:
|
||||
self.bell()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = InputApp()
|
||||
app.run()
|
||||
12
docs/examples/guide/input/key02.css
Normal file
12
docs/examples/guide/input/key02.css
Normal file
@@ -0,0 +1,12 @@
|
||||
Screen {
|
||||
layout: horizontal;
|
||||
}
|
||||
|
||||
KeyLogger {
|
||||
width: 1fr;
|
||||
border: blank;
|
||||
}
|
||||
|
||||
KeyLogger:focus {
|
||||
border: wide $accent;
|
||||
}
|
||||
23
docs/examples/guide/input/key02.py
Normal file
23
docs/examples/guide/input/key02.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.widgets import TextLog
|
||||
from textual import events
|
||||
|
||||
|
||||
class KeyLogger(TextLog):
|
||||
def on_key(self, event: events.Key) -> None:
|
||||
self.write(event)
|
||||
|
||||
|
||||
class InputApp(App):
|
||||
"""App to display key events."""
|
||||
|
||||
CSS_PATH = "input02.css"
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield KeyLogger()
|
||||
yield KeyLogger()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = InputApp()
|
||||
app.run()
|
||||
24
docs/examples/guide/input/mouse01.css
Normal file
24
docs/examples/guide/input/mouse01.css
Normal file
@@ -0,0 +1,24 @@
|
||||
Screen {
|
||||
layers: log ball;
|
||||
}
|
||||
|
||||
TextLog {
|
||||
layer: log;
|
||||
}
|
||||
|
||||
PlayArea {
|
||||
background: transparent;
|
||||
layer: ball;
|
||||
|
||||
}
|
||||
Ball {
|
||||
layer: ball;
|
||||
width: auto;
|
||||
height: 1;
|
||||
background: $secondary;
|
||||
border: tall $secondary;
|
||||
color: $background;
|
||||
box-sizing: content-box;
|
||||
text-style: bold;
|
||||
padding: 0 4;
|
||||
}
|
||||
31
docs/examples/guide/input/mouse01.py
Normal file
31
docs/examples/guide/input/mouse01.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from textual.app import App, ComposeResult
|
||||
|
||||
from textual import events
|
||||
from textual.layout import Container
|
||||
from textual.widgets import Static, TextLog
|
||||
|
||||
|
||||
class PlayArea(Container):
|
||||
def on_mount(self) -> None:
|
||||
self.capture_mouse()
|
||||
|
||||
def on_mouse_move(self, event: events.MouseMove) -> None:
|
||||
self.screen.query_one(TextLog).write(event)
|
||||
self.query_one(Ball).offset = event.offset - (8, 2)
|
||||
|
||||
|
||||
class Ball(Static):
|
||||
pass
|
||||
|
||||
|
||||
class MouseApp(App):
|
||||
CSS_PATH = "mouse01.css"
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield TextLog()
|
||||
yield PlayArea(Ball("Textual"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = MouseApp()
|
||||
app.run()
|
||||
26
docs/guide/input.md
Normal file
26
docs/guide/input.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Input
|
||||
|
||||
This chapter will discuss how to make your app respond to input in the form of key presses and mouse actions.
|
||||
|
||||
!!! quote
|
||||
|
||||
More Input!
|
||||
|
||||
— Johnny Five
|
||||
|
||||
## Key events
|
||||
|
||||
The most fundamental way to receive input in via [Key](./events/key) events. Let's take a closer look at key events with an app that will display key events as you type.
|
||||
|
||||
=== "key01.py"
|
||||
|
||||
```python title="key01.py" hl_lines="12-13"
|
||||
--8<-- "docs/examples/guide/input/key01.py"
|
||||
```
|
||||
|
||||
=== "Output"
|
||||
|
||||
```{.textual path="docs/examples/guide/input/key01.py", press="T,e,x,t,u,a,l,!,_"}
|
||||
```
|
||||
|
||||
Note the key event handler which
|
||||
@@ -14,6 +14,7 @@ nav:
|
||||
- "guide/CSS.md"
|
||||
- "guide/layout.md"
|
||||
- "guide/events.md"
|
||||
- "guide/input.md"
|
||||
- "guide/actions.md"
|
||||
- "guide/reactivity.md"
|
||||
- "guide/widgets.md"
|
||||
|
||||
@@ -275,6 +275,11 @@ class Animator:
|
||||
|
||||
if duration is not None:
|
||||
animation_duration = duration
|
||||
else:
|
||||
if hasattr(value, "get_distance_to"):
|
||||
animation_duration = value.get_distance_to(start_value) / (
|
||||
speed or 50
|
||||
)
|
||||
else:
|
||||
animation_duration = abs(value - start_value) / (speed or 50)
|
||||
|
||||
|
||||
@@ -501,7 +501,18 @@ class Compositor:
|
||||
raise errors.NoWidget("Widget is not in layout")
|
||||
|
||||
def get_widget_at(self, x: int, y: int) -> tuple[Widget, Region]:
|
||||
"""Get the widget under the given point or None."""
|
||||
"""Get the widget under a given coordinate.
|
||||
|
||||
Args:
|
||||
x (int): X Coordinate.
|
||||
y (int): Y Coordinate.
|
||||
|
||||
Raises:
|
||||
errors.NoWidget: If there is not widget underneath (x, y).
|
||||
|
||||
Returns:
|
||||
tuple[Widget, Region]: A tuple of the widget and its region.
|
||||
"""
|
||||
# TODO: Optimize with some line based lookup
|
||||
contains = Region.contains
|
||||
for widget, cropped_region, region, *_ in self:
|
||||
@@ -509,6 +520,21 @@ class Compositor:
|
||||
return widget, region
|
||||
raise errors.NoWidget(f"No widget under screen coordinate ({x}, {y})")
|
||||
|
||||
def get_widgets_at(self, x: int, y: int) -> Iterable[tuple[Widget, Region]]:
|
||||
"""Get all widgets under a given coordinate.
|
||||
|
||||
Args:
|
||||
x (int): X coordinate.
|
||||
y (int): Y coordinate.
|
||||
|
||||
Returns:
|
||||
Iterable[tuple[Widget, Region]]: Sequence of (WIDGET, REGION) tuples.
|
||||
"""
|
||||
contains = Region.contains
|
||||
for widget, cropped_region, region, *_ in self:
|
||||
if contains(cropped_region, x, y) and widget.visible:
|
||||
yield widget, region
|
||||
|
||||
def get_style_at(self, x: int, y: int) -> Style:
|
||||
"""Get the Style at the given cell or Style.null()
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unicodedata
|
||||
import re
|
||||
from typing import Any, Callable, Generator, Iterable
|
||||
|
||||
@@ -228,7 +229,9 @@ class XTermParser(Parser[events.Event]):
|
||||
for event in sequence_to_key_events(character):
|
||||
on_token(event)
|
||||
|
||||
def _sequence_to_key_events(self, sequence: str) -> Iterable[events.Key]:
|
||||
def _sequence_to_key_events(
|
||||
self, sequence: str, _unicode_name=unicodedata.name
|
||||
) -> Iterable[events.Key]:
|
||||
"""Map a sequence of code points on to a sequence of keys.
|
||||
|
||||
Args:
|
||||
@@ -246,4 +249,16 @@ class XTermParser(Parser[events.Event]):
|
||||
self.sender, key.value, sequence if len(sequence) == 1 else None
|
||||
)
|
||||
elif len(sequence) == 1:
|
||||
try:
|
||||
if not sequence.isalnum():
|
||||
name = (
|
||||
_unicode_name(sequence)
|
||||
.lower()
|
||||
.replace("-", "_")
|
||||
.replace(" ", "_")
|
||||
)
|
||||
else:
|
||||
name = sequence
|
||||
yield events.Key(self.sender, name, sequence)
|
||||
except:
|
||||
yield events.Key(self.sender, sequence, sequence)
|
||||
|
||||
@@ -1447,7 +1447,8 @@ class App(Generic[ReturnType], DOMNode):
|
||||
elif event.key == "shift+tab":
|
||||
self.focus_previous()
|
||||
else:
|
||||
await self.press(event.key)
|
||||
if not (await self.press(event.key)):
|
||||
await self.dispatch_key(event)
|
||||
|
||||
async def _on_shutdown_request(self, event: events.ShutdownRequest) -> None:
|
||||
log("shutdown request")
|
||||
|
||||
@@ -16,7 +16,7 @@ a method which evaluates the query, such as first() and last().
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Iterator, TypeVar, overload
|
||||
from typing import Generic, TYPE_CHECKING, Iterator, TypeVar, overload
|
||||
|
||||
import rich.repr
|
||||
|
||||
@@ -42,8 +42,11 @@ class WrongType(QueryError):
|
||||
pass
|
||||
|
||||
|
||||
QueryType = TypeVar("QueryType", bound="Widget")
|
||||
|
||||
|
||||
@rich.repr.auto(angular=True)
|
||||
class DOMQuery:
|
||||
class DOMQuery(Generic[QueryType]):
|
||||
__slots__ = [
|
||||
"_node",
|
||||
"_nodes",
|
||||
@@ -78,7 +81,7 @@ class DOMQuery:
|
||||
return self._node
|
||||
|
||||
@property
|
||||
def nodes(self) -> list[Widget]:
|
||||
def nodes(self) -> list[QueryType]:
|
||||
"""Lazily evaluate nodes."""
|
||||
from ..widget import Widget
|
||||
|
||||
@@ -103,21 +106,21 @@ class DOMQuery:
|
||||
"""True if non-empty, otherwise False."""
|
||||
return bool(self.nodes)
|
||||
|
||||
def __iter__(self) -> Iterator[Widget]:
|
||||
def __iter__(self) -> Iterator[QueryType]:
|
||||
return iter(self.nodes)
|
||||
|
||||
def __reversed__(self) -> Iterator[Widget]:
|
||||
def __reversed__(self) -> Iterator[QueryType]:
|
||||
return reversed(self.nodes)
|
||||
|
||||
@overload
|
||||
def __getitem__(self, index: int) -> Widget:
|
||||
def __getitem__(self, index: int) -> QueryType:
|
||||
...
|
||||
|
||||
@overload
|
||||
def __getitem__(self, index: slice) -> list[Widget]:
|
||||
def __getitem__(self, index: slice) -> list[QueryType]:
|
||||
...
|
||||
|
||||
def __getitem__(self, index: int | slice) -> Widget | list[Widget]:
|
||||
def __getitem__(self, index: int | slice) -> QueryType | list[QueryType]:
|
||||
return self.nodes[index]
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
@@ -133,7 +136,7 @@ class DOMQuery:
|
||||
for selectors in self._excludes
|
||||
)
|
||||
|
||||
def filter(self, selector: str) -> DOMQuery:
|
||||
def filter(self, selector: str) -> DOMQuery[QueryType]:
|
||||
"""Filter this set by the given CSS selector.
|
||||
|
||||
Args:
|
||||
@@ -145,7 +148,7 @@ class DOMQuery:
|
||||
|
||||
return DOMQuery(self.node, filter=selector, parent=self)
|
||||
|
||||
def exclude(self, selector: str) -> DOMQuery:
|
||||
def exclude(self, selector: str) -> DOMQuery[QueryType]:
|
||||
"""Exclude nodes that match a given selector.
|
||||
|
||||
Args:
|
||||
@@ -166,7 +169,9 @@ class DOMQuery:
|
||||
def first(self, expect_type: type[ExpectType]) -> ExpectType:
|
||||
...
|
||||
|
||||
def first(self, expect_type: type[ExpectType] | None = None) -> Widget | ExpectType:
|
||||
def first(
|
||||
self, expect_type: type[ExpectType] | None = None
|
||||
) -> QueryType | ExpectType:
|
||||
"""Get the *first* match node.
|
||||
|
||||
Args:
|
||||
@@ -199,7 +204,9 @@ class DOMQuery:
|
||||
def last(self, expect_type: type[ExpectType]) -> ExpectType:
|
||||
...
|
||||
|
||||
def last(self, expect_type: type[ExpectType] | None = None) -> Widget | ExpectType:
|
||||
def last(
|
||||
self, expect_type: type[ExpectType] | None = None
|
||||
) -> QueryType | ExpectType:
|
||||
"""Get the *last* match node.
|
||||
|
||||
Args:
|
||||
@@ -251,7 +258,7 @@ class DOMQuery:
|
||||
if isinstance(node, filter_type):
|
||||
yield node
|
||||
|
||||
def set_class(self, add: bool, *class_names: str) -> DOMQuery:
|
||||
def set_class(self, add: bool, *class_names: str) -> DOMQuery[QueryType]:
|
||||
"""Set the given class name(s) according to a condition.
|
||||
|
||||
Args:
|
||||
@@ -264,31 +271,33 @@ class DOMQuery:
|
||||
node.set_class(add, *class_names)
|
||||
return self
|
||||
|
||||
def add_class(self, *class_names: str) -> DOMQuery:
|
||||
def add_class(self, *class_names: str) -> DOMQuery[QueryType]:
|
||||
"""Add the given class name(s) to nodes."""
|
||||
for node in self:
|
||||
node.add_class(*class_names)
|
||||
return self
|
||||
|
||||
def remove_class(self, *class_names: str) -> DOMQuery:
|
||||
def remove_class(self, *class_names: str) -> DOMQuery[QueryType]:
|
||||
"""Remove the given class names from the nodes."""
|
||||
for node in self:
|
||||
node.remove_class(*class_names)
|
||||
return self
|
||||
|
||||
def toggle_class(self, *class_names: str) -> DOMQuery:
|
||||
def toggle_class(self, *class_names: str) -> DOMQuery[QueryType]:
|
||||
"""Toggle the given class names from matched nodes."""
|
||||
for node in self:
|
||||
node.toggle_class(*class_names)
|
||||
return self
|
||||
|
||||
def remove(self) -> DOMQuery:
|
||||
def remove(self) -> DOMQuery[QueryType]:
|
||||
"""Remove matched nodes from the DOM"""
|
||||
for node in self:
|
||||
node.remove()
|
||||
return self
|
||||
|
||||
def set_styles(self, css: str | None = None, **update_styles) -> DOMQuery:
|
||||
def set_styles(
|
||||
self, css: str | None = None, **update_styles
|
||||
) -> DOMQuery[QueryType]:
|
||||
"""Set styles on matched nodes.
|
||||
|
||||
Args:
|
||||
@@ -308,7 +317,9 @@ class DOMQuery:
|
||||
node.refresh(layout=True)
|
||||
return self
|
||||
|
||||
def refresh(self, *, repaint: bool = True, layout: bool = False) -> DOMQuery:
|
||||
def refresh(
|
||||
self, *, repaint: bool = True, layout: bool = False
|
||||
) -> DOMQuery[QueryType]:
|
||||
"""Refresh matched nodes.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -327,6 +327,14 @@ class ScalarOffset(NamedTuple):
|
||||
"""Get a null scalar offset (0, 0)."""
|
||||
return NULL_SCALAR
|
||||
|
||||
@classmethod
|
||||
def from_offset(cls, offset: tuple[int, int]) -> ScalarOffset:
|
||||
x, y = offset
|
||||
return cls(
|
||||
Scalar(x, Unit.CELLS, Unit.WIDTH),
|
||||
Scalar(y, Unit.CELLS, Unit.HEIGHT),
|
||||
)
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
x, y = self
|
||||
return bool(x.value or y.value)
|
||||
|
||||
@@ -675,7 +675,17 @@ class DOMNode(MessagePump):
|
||||
return child
|
||||
raise NoMatchingNodesError(f"No child found with id={id!r}")
|
||||
|
||||
def query(self, selector: str | None = None) -> DOMQuery:
|
||||
ExpectType = TypeVar("ExpectType", bound="Widget")
|
||||
|
||||
@overload
|
||||
def query(self, selector: str | None) -> DOMQuery:
|
||||
...
|
||||
|
||||
@overload
|
||||
def query(self, selector: type[ExpectType]) -> DOMQuery[ExpectType]:
|
||||
...
|
||||
|
||||
def query(self, selector: str | type | None = None) -> DOMQuery:
|
||||
"""Get a DOM query matching a selector.
|
||||
|
||||
Args:
|
||||
@@ -686,9 +696,13 @@ class DOMNode(MessagePump):
|
||||
"""
|
||||
from .css.query import DOMQuery
|
||||
|
||||
return DOMQuery(self, filter=selector)
|
||||
query: str | None
|
||||
if isinstance(selector, str) or selector is None:
|
||||
query = selector
|
||||
else:
|
||||
query = selector.__name__
|
||||
|
||||
ExpectType = TypeVar("ExpectType")
|
||||
return DOMQuery(self, filter=query)
|
||||
|
||||
@overload
|
||||
def query_one(self, selector: str) -> Widget:
|
||||
@@ -723,7 +737,7 @@ class DOMNode(MessagePump):
|
||||
query_selector = selector
|
||||
else:
|
||||
query_selector = selector.__name__
|
||||
query = DOMQuery(self, filter=query_selector)
|
||||
query: DOMQuery[Widget] = DOMQuery(self, filter=query_selector)
|
||||
|
||||
if expect_type is None:
|
||||
return query.first()
|
||||
|
||||
@@ -7,7 +7,6 @@ from rich.style import Style
|
||||
|
||||
from ._types import MessageTarget
|
||||
from .geometry import Offset, Size
|
||||
from .keys import KEY_VALUES, Keys
|
||||
from .message import Message
|
||||
|
||||
MouseEventT = TypeVar("MouseEventT", bound="MouseEvent")
|
||||
@@ -317,6 +316,14 @@ class MouseEvent(InputEvent, bubble=True):
|
||||
yield "meta", self.meta, False
|
||||
yield "ctrl", self.ctrl, False
|
||||
|
||||
@property
|
||||
def offset(self) -> Offset:
|
||||
return Offset(self.x, self.y)
|
||||
|
||||
@property
|
||||
def screen_offset(self) -> Offset:
|
||||
return Offset(self.screen_x, self.screen_y)
|
||||
|
||||
@property
|
||||
def style(self) -> Style:
|
||||
return self._style or Style()
|
||||
@@ -325,7 +332,7 @@ class MouseEvent(InputEvent, bubble=True):
|
||||
def style(self, style: Style) -> None:
|
||||
self._style = style
|
||||
|
||||
def offset(self, x: int, y: int) -> MouseEvent:
|
||||
def _apply_offset(self, x: int, y: int) -> MouseEvent:
|
||||
return self.__class__(
|
||||
self.sender,
|
||||
x=self.x + x,
|
||||
@@ -343,7 +350,7 @@ class MouseEvent(InputEvent, bubble=True):
|
||||
|
||||
|
||||
@rich.repr.auto
|
||||
class MouseMove(MouseEvent, bubble=True, verbose=True):
|
||||
class MouseMove(MouseEvent, bubble=False, verbose=True):
|
||||
"""Sent when the mouse cursor moves."""
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import ClassVar
|
||||
from typing import ClassVar, TYPE_CHECKING
|
||||
|
||||
import rich.repr
|
||||
|
||||
@@ -8,6 +8,9 @@ from . import _clock
|
||||
from .case import camel_to_snake
|
||||
from ._types import MessageTarget as MessageTarget
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .widget import Widget
|
||||
|
||||
|
||||
@rich.repr.auto
|
||||
class Message:
|
||||
@@ -109,3 +112,11 @@ class Message:
|
||||
"""
|
||||
self._stop_propagation = stop
|
||||
return self
|
||||
|
||||
async def _bubble_to(self, widget: Widget) -> None:
|
||||
"""Bubble to a widget (typically the parent).
|
||||
|
||||
Args:
|
||||
widget (Widget): Target of bubble.
|
||||
"""
|
||||
await widget.post_message(self)
|
||||
|
||||
@@ -417,7 +417,7 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
# parent is sender, so we stop propagation after parent
|
||||
message.stop()
|
||||
if self.is_parent_active and not self._parent._closing:
|
||||
await self._parent.post_message(message)
|
||||
await message._bubble_to(self._parent)
|
||||
|
||||
def check_idle(self) -> None:
|
||||
"""Prompt the message pump to call idle if the queue is empty."""
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from typing import Iterable
|
||||
|
||||
import rich.repr
|
||||
from rich.console import RenderableType
|
||||
@@ -106,6 +107,18 @@ class Screen(Widget):
|
||||
"""
|
||||
return self._compositor.get_widget_at(x, y)
|
||||
|
||||
def get_widgets_at(self, x: int, y: int) -> Iterable[tuple[Widget, Region]]:
|
||||
"""Get all widgets under a given coordinate.
|
||||
|
||||
Args:
|
||||
x (int): X coordinate.
|
||||
y (int): Y coordinate.
|
||||
|
||||
Returns:
|
||||
Iterable[tuple[Widget, Region]]: Sequence of (WIDGET, REGION) tuples.
|
||||
"""
|
||||
return self._compositor.get_widgets_at(x, y)
|
||||
|
||||
def get_style_at(self, x: int, y: int) -> Style:
|
||||
"""Get the style under a given coordinate.
|
||||
|
||||
@@ -315,7 +328,9 @@ class Screen(Widget):
|
||||
event._set_forwarded()
|
||||
await self.post_message(event)
|
||||
else:
|
||||
await widget._forward_event(event.offset(-region.x, -region.y))
|
||||
await widget._forward_event(
|
||||
event._apply_offset(-region.x, -region.y)
|
||||
)
|
||||
|
||||
elif isinstance(event, (events.MouseScrollDown, events.MouseScrollUp)):
|
||||
try:
|
||||
|
||||
@@ -97,13 +97,13 @@ class ScrollView(Widget):
|
||||
virtual_size (Size): New virtual size.
|
||||
container_size (Size): New container size.
|
||||
"""
|
||||
|
||||
virtual_size = self.virtual_size
|
||||
if self._size != size:
|
||||
if self._size != size or virtual_size != self.virtual_size:
|
||||
self._size = size
|
||||
self._container_size = size
|
||||
virtual_size = self.virtual_size
|
||||
self._container_size = size - self.gutter.totals
|
||||
self._scroll_update(virtual_size)
|
||||
self.scroll_to(self.scroll_x, self.scroll_y)
|
||||
self.refresh()
|
||||
|
||||
def render(self) -> RenderableType:
|
||||
"""Render the scrollable region (if `render_lines` is not implemented).
|
||||
|
||||
@@ -28,6 +28,7 @@ from ._layout import Layout
|
||||
from ._segment_tools import align_lines
|
||||
from ._styles_cache import StylesCache
|
||||
from ._types import Lines
|
||||
from .css.scalar import ScalarOffset
|
||||
from .binding import NoBinding
|
||||
from .box_model import BoxModel, get_box_model
|
||||
from .dom import DOMNode, NoScreen
|
||||
@@ -255,6 +256,14 @@ class Widget(DOMNode):
|
||||
self.allow_horizontal_scroll or self.allow_vertical_scroll
|
||||
)
|
||||
|
||||
@property
|
||||
def offset(self) -> Offset:
|
||||
return self.styles.offset.resolve(self.size, self.app.size)
|
||||
|
||||
@offset.setter
|
||||
def offset(self, offset: Offset) -> None:
|
||||
self.styles.offset = ScalarOffset.from_offset(offset)
|
||||
|
||||
def get_component_rich_style(self, name: str) -> Style:
|
||||
"""Get a *Rich* style for a component.
|
||||
|
||||
@@ -1526,7 +1535,11 @@ class Widget(DOMNode):
|
||||
virtual_size (Size): Virtual (scrollable) size.
|
||||
container_size (Size): Container size (size of parent).
|
||||
"""
|
||||
if self._size != size or self.virtual_size != virtual_size:
|
||||
if (
|
||||
self._size != size
|
||||
or self.virtual_size != virtual_size
|
||||
or self._container_size != container_size
|
||||
):
|
||||
self._size = size
|
||||
self.virtual_size = virtual_size
|
||||
self._container_size = container_size
|
||||
@@ -1543,6 +1556,7 @@ class Widget(DOMNode):
|
||||
"""
|
||||
self._refresh_scrollbars()
|
||||
width, height = self.container_size
|
||||
|
||||
if self.show_vertical_scrollbar:
|
||||
self.vertical_scrollbar.window_virtual_size = virtual_size.height
|
||||
self.vertical_scrollbar.window_size = (
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import cast
|
||||
|
||||
from rich.console import RenderableType
|
||||
from rich.pretty import Pretty
|
||||
from rich.protocol import is_renderable
|
||||
from rich.segment import Segment
|
||||
|
||||
from ..reactive import var
|
||||
@@ -46,19 +50,26 @@ class TextLog(ScrollView, can_focus=True):
|
||||
def _on_styles_updated(self) -> None:
|
||||
self._line_cache.clear()
|
||||
|
||||
def write(self, content: RenderableType) -> None:
|
||||
def write(self, content: RenderableType | object) -> None:
|
||||
"""Write text or a rich renderable.
|
||||
|
||||
Args:
|
||||
content (RenderableType): Rich renderable (or text).
|
||||
"""
|
||||
|
||||
renderable: RenderableType
|
||||
if not is_renderable(content):
|
||||
renderable = Pretty(content)
|
||||
else:
|
||||
renderable = cast(RenderableType, content)
|
||||
|
||||
console = self.app.console
|
||||
width = max(self.min_width, self.size.width or self.min_width)
|
||||
|
||||
render_options = console.options.update_width(width)
|
||||
if not self.wrap:
|
||||
render_options = render_options.update(overflow="ignore", no_wrap=True)
|
||||
segments = self.app.console.render(content, render_options)
|
||||
segments = self.app.console.render(renderable, render_options)
|
||||
lines = list(Segment.split_lines(segments))
|
||||
self.max_width = max(
|
||||
self.max_width,
|
||||
|
||||
Reference in New Issue
Block a user