mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
text log widget
This commit is contained in:
27
docs/examples/events/bubble01.py
Normal file
27
docs/examples/events/bubble01.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
from textual.app import App, ComposeResult
|
||||||
|
|
||||||
|
from textual.widgets import Static, TextLog
|
||||||
|
|
||||||
|
|
||||||
|
class BubbleApp(App):
|
||||||
|
|
||||||
|
CSS = """
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
Static("Foo", id="static")
|
||||||
|
yield TextLog()
|
||||||
|
|
||||||
|
def on_key(self) -> None:
|
||||||
|
log = self.query_one(TextLog)
|
||||||
|
self.query_one(TextLog).write(self.tree)
|
||||||
|
log.write(repr((log.size, log.virtual_size)))
|
||||||
|
|
||||||
|
|
||||||
|
app = BubbleApp()
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run()
|
||||||
@@ -4,11 +4,9 @@ We've used event handler methods in many of the examples in this guide. This cha
|
|||||||
|
|
||||||
## Messages
|
## Messages
|
||||||
|
|
||||||
Events are a particular kind of *message* which is sent by Textual in response to input and other state changes. Events are reserved for use by Textual but you can create messages for the purpose of coordinating between widgets in your app.
|
Events are a particular kind of *message* which is sent by Textual in response to input and other state changes. Events are reserved for use by Textual but you can also create custom messages for the purpose of coordinating between widgets in your app.
|
||||||
|
|
||||||
More on that later, but for now keep in mind that events are also messages, and anything that is true of messages is also true of events.
|
More on that later, but for now keep in mind that events are also messages, and anything that is true of messages is true of events.
|
||||||
|
|
||||||
Event classes (as used in event handlers) extend the [Event][textual.events.Event] class, which itself extends the [Message][textual.message.Message] class.
|
|
||||||
|
|
||||||
## Message Queue
|
## Message Queue
|
||||||
|
|
||||||
@@ -20,7 +18,7 @@ This processing of messages is done within an asyncio Task which is started when
|
|||||||
|
|
||||||
If you aren't yet familiar with asyncio, you can consider this part to be black box and trust that Textual will get events to your handler methods.
|
If you aren't yet familiar with asyncio, you can consider this part to be black box and trust that Textual will get events to your handler methods.
|
||||||
|
|
||||||
By way of an example, let's consider what happens if the user types "Text" in to a text input widget. When the user hits the ++t++ key it is translated in to a [key][textual.events.Key] event and sent to the widget's message queue. Ditto for ++e++, ++x++, and ++t++.
|
By way of an example, let's consider what happens if you were to type "Text" in to a text input widget. When you hit the ++t++ key it is translated in to a [key][textual.events.Key] event and sent to the widget's message queue. Ditto for ++e++, ++x++, and ++t++.
|
||||||
|
|
||||||
The widget's task will pick the first key event from the queue (for the ++t++ key) and call the `on_key` handler to update the display.
|
The widget's task will pick the first key event from the queue (for the ++t++ key) and call the `on_key` handler to update the display.
|
||||||
|
|
||||||
@@ -38,9 +36,32 @@ When the `on_key` method returns, Textual will get the next event off the the qu
|
|||||||
--8<-- "docs/images/events/queue2.excalidraw.svg"
|
--8<-- "docs/images/events/queue2.excalidraw.svg"
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
## Creating Messages
|
||||||
|
|
||||||
## Handlers
|
## Handlers
|
||||||
|
|
||||||
|
|
||||||
|
### Naming
|
||||||
|
|
||||||
|
Let's explore how Textual decides what method to call for a given event.
|
||||||
|
|
||||||
|
- Start with `"on_"`.
|
||||||
|
- Add the messages namespace (if any) converted from CamelCase to snake_case plus an underscore `"_"`
|
||||||
|
- Add the name of the class converted from CamelCase to snake_case.
|
||||||
|
|
||||||
|
### Default behaviors
|
||||||
|
|
||||||
|
You may be familiar with using Python's [super](https://docs.python.org/3/library/functions.html#super) function to call a function defined in a base class. You will not have to do this for Textual event handlers as Textual will automatically call any handler methods defined in the base class *after* the current handler has run. This allows textual to run any default behavior for the given event.
|
||||||
|
|
||||||
|
For instance if a widget defines an `on_key` handler it will run when the user hits a key. Textual will also run `Widget.on_key`, which allows Textual to respond to any key bindings. This is generally desirable, but you can prevent Textual from running the base class handler by calling [prevent_default()][textual.message.Message.prevent_default] on the event object.
|
||||||
|
|
||||||
|
For the case of key events, you may want to prevent the default behavior for keys that you handle by calling `event.prevent_default()`, but allow the base class to handle all other keys.
|
||||||
|
|
||||||
|
### Bubbling
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
TODO: events docs
|
TODO: events docs
|
||||||
|
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ class Reactive(Generic[ReactiveType]):
|
|||||||
for key in obj.__class__.__dict__.keys():
|
for key in obj.__class__.__dict__.keys():
|
||||||
if startswith(key, "_init_"):
|
if startswith(key, "_init_"):
|
||||||
name = key[6:]
|
name = key[6:]
|
||||||
|
if not hasattr(obj, name):
|
||||||
default = getattr(obj, key)
|
default = getattr(obj, key)
|
||||||
setattr(obj, name, default() if callable(default) else default)
|
setattr(obj, name, default() if callable(default) else default)
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,9 @@ class ScrollView(Widget):
|
|||||||
"""
|
"""
|
||||||
return self.virtual_size.height
|
return self.virtual_size.height
|
||||||
|
|
||||||
|
def watch_virtual_size(self, virtual_size: Size) -> None:
|
||||||
|
self._scroll_update(virtual_size)
|
||||||
|
|
||||||
def _size_updated(
|
def _size_updated(
|
||||||
self, size: Size, virtual_size: Size, container_size: Size
|
self, size: Size, virtual_size: Size, container_size: Size
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -78,6 +81,7 @@ class ScrollView(Widget):
|
|||||||
virtual_size (Size): New virtual size.
|
virtual_size (Size): New virtual size.
|
||||||
container_size (Size): New container size.
|
container_size (Size): New container size.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
virtual_size = self.virtual_size
|
virtual_size = self.virtual_size
|
||||||
if self._size != size:
|
if self._size != size:
|
||||||
self._size = size
|
self._size = size
|
||||||
|
|||||||
@@ -922,135 +922,241 @@ class Widget(DOMNode):
|
|||||||
duration=duration,
|
duration=duration,
|
||||||
)
|
)
|
||||||
|
|
||||||
def scroll_home(self, *, animate: bool = True) -> bool:
|
def scroll_home(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
animate: bool = True,
|
||||||
|
speed: float | None = None,
|
||||||
|
duration: float | None = None,
|
||||||
|
) -> bool:
|
||||||
"""Scroll to home position.
|
"""Scroll to home position.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
animate (bool, optional): Animate scroll. Defaults to True.
|
animate (bool, optional): Animate scroll. Defaults to True.
|
||||||
|
speed (float | None, optional): Speed of scroll if animate is True. Or None to use duration.
|
||||||
|
duration (float | None, optional): Duration of animation, if animate is True and speed is False.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if any scrolling was done.
|
bool: True if any scrolling was done.
|
||||||
"""
|
"""
|
||||||
return self.scroll_to(0, 0, animate=animate, duration=1)
|
if speed is None and duration is None:
|
||||||
|
duration = 1.0
|
||||||
|
return self.scroll_to(0, 0, animate=animate, speed=speed, duration=duration)
|
||||||
|
|
||||||
def scroll_end(self, *, animate: bool = True) -> bool:
|
def scroll_end(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
animate: bool = True,
|
||||||
|
speed: float | None = None,
|
||||||
|
duration: float | None = None,
|
||||||
|
) -> bool:
|
||||||
"""Scroll to the end of the container.
|
"""Scroll to the end of the container.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
animate (bool, optional): Animate scroll. Defaults to True.
|
animate (bool, optional): Animate scroll. Defaults to True.
|
||||||
|
speed (float | None, optional): Speed of scroll if animate is True. Or None to use duration.
|
||||||
|
duration (float | None, optional): Duration of animation, if animate is True and speed is False.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if any scrolling was done.
|
bool: True if any scrolling was done.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return self.scroll_to(0, self.max_scroll_y, animate=animate, duration=1)
|
if speed is None and duration is None:
|
||||||
|
duration = 1.0
|
||||||
|
return self.scroll_to(
|
||||||
|
0, self.max_scroll_y, animate=animate, speed=speed, duration=duration
|
||||||
|
)
|
||||||
|
|
||||||
def scroll_left(self, *, animate: bool = True) -> bool:
|
def scroll_left(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
animate: bool = True,
|
||||||
|
speed: float | None = None,
|
||||||
|
duration: float | None = None,
|
||||||
|
) -> bool:
|
||||||
"""Scroll one cell left.
|
"""Scroll one cell left.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
animate (bool, optional): Animate scroll. Defaults to True.
|
animate (bool, optional): Animate scroll. Defaults to True.
|
||||||
|
speed (float | None, optional): Speed of scroll if animate is True. Or None to use duration.
|
||||||
|
duration (float | None, optional): Duration of animation, if animate is True and speed is False.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if any scrolling was done.
|
bool: True if any scrolling was done.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return self.scroll_to(x=self.scroll_target_x - 1, animate=animate)
|
return self.scroll_to(
|
||||||
|
x=self.scroll_target_x - 1, animate=animate, speed=speed, duration=duration
|
||||||
|
)
|
||||||
|
|
||||||
def scroll_right(self, *, animate: bool = True) -> bool:
|
def scroll_right(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
animate: bool = True,
|
||||||
|
speed: float | None = None,
|
||||||
|
duration: float | None = None,
|
||||||
|
) -> bool:
|
||||||
"""Scroll on cell right.
|
"""Scroll on cell right.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
animate (bool, optional): Animate scroll. Defaults to True.
|
animate (bool, optional): Animate scroll. Defaults to True.
|
||||||
|
speed (float | None, optional): Speed of scroll if animate is True. Or None to use duration.
|
||||||
|
duration (float | None, optional): Duration of animation, if animate is True and speed is False.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if any scrolling was done.
|
bool: True if any scrolling was done.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return self.scroll_to(x=self.scroll_target_x + 1, animate=animate)
|
return self.scroll_to(
|
||||||
|
x=self.scroll_target_x + 1, animate=animate, speed=speed, duration=duration
|
||||||
|
)
|
||||||
|
|
||||||
def scroll_down(self, *, animate: bool = True) -> bool:
|
def scroll_down(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
animate: bool = True,
|
||||||
|
speed: float | None = None,
|
||||||
|
duration: float | None = None,
|
||||||
|
) -> bool:
|
||||||
"""Scroll one line down.
|
"""Scroll one line down.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
animate (bool, optional): Animate scroll. Defaults to True.
|
animate (bool, optional): Animate scroll. Defaults to True.
|
||||||
|
speed (float | None, optional): Speed of scroll if animate is True. Or None to use duration.
|
||||||
|
duration (float | None, optional): Duration of animation, if animate is True and speed is False.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if any scrolling was done.
|
bool: True if any scrolling was done.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return self.scroll_to(y=self.scroll_target_y + 1, animate=animate)
|
return self.scroll_to(
|
||||||
|
y=self.scroll_target_y + 1, animate=animate, speed=speed, duration=duration
|
||||||
|
)
|
||||||
|
|
||||||
def scroll_up(self, *, animate: bool = True) -> bool:
|
def scroll_up(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
animate: bool = True,
|
||||||
|
speed: float | None = None,
|
||||||
|
duration: float | None = None,
|
||||||
|
) -> bool:
|
||||||
"""Scroll one line up.
|
"""Scroll one line up.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
animate (bool, optional): Animate scroll. Defaults to True.
|
animate (bool, optional): Animate scroll. Defaults to True.
|
||||||
|
speed (float | None, optional): Speed of scroll if animate is True. Or None to use duration.
|
||||||
|
duration (float | None, optional): Duration of animation, if animate is True and speed is False.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if any scrolling was done.
|
bool: True if any scrolling was done.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return self.scroll_to(y=self.scroll_target_y - 1, animate=animate)
|
return self.scroll_to(
|
||||||
|
y=self.scroll_target_y - 1, animate=animate, speed=speed, duration=duration
|
||||||
|
)
|
||||||
|
|
||||||
def scroll_page_up(self, *, animate: bool = True) -> bool:
|
def scroll_page_up(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
animate: bool = True,
|
||||||
|
speed: float | None = None,
|
||||||
|
duration: float | None = None,
|
||||||
|
) -> bool:
|
||||||
"""Scroll one page up.
|
"""Scroll one page up.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
animate (bool, optional): Animate scroll. Defaults to True.
|
animate (bool, optional): Animate scroll. Defaults to True.
|
||||||
|
speed (float | None, optional): Speed of scroll if animate is True. Or None to use duration.
|
||||||
|
duration (float | None, optional): Duration of animation, if animate is True and speed is False.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if any scrolling was done.
|
bool: True if any scrolling was done.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return self.scroll_to(
|
return self.scroll_to(
|
||||||
y=self.scroll_target_y - self.container_size.height, animate=animate
|
y=self.scroll_target_y - self.container_size.height,
|
||||||
|
animate=animate,
|
||||||
|
speed=speed,
|
||||||
|
duration=duration,
|
||||||
)
|
)
|
||||||
|
|
||||||
def scroll_page_down(self, *, animate: bool = True) -> bool:
|
def scroll_page_down(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
animate: bool = True,
|
||||||
|
speed: float | None = None,
|
||||||
|
duration: float | None = None,
|
||||||
|
) -> bool:
|
||||||
"""Scroll one page down.
|
"""Scroll one page down.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
animate (bool, optional): Animate scroll. Defaults to True.
|
animate (bool, optional): Animate scroll. Defaults to True.
|
||||||
|
speed (float | None, optional): Speed of scroll if animate is True. Or None to use duration.
|
||||||
|
duration (float | None, optional): Duration of animation, if animate is True and speed is False.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if any scrolling was done.
|
bool: True if any scrolling was done.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return self.scroll_to(
|
return self.scroll_to(
|
||||||
y=self.scroll_target_y + self.container_size.height, animate=animate
|
y=self.scroll_target_y + self.container_size.height,
|
||||||
|
animate=animate,
|
||||||
|
speed=speed,
|
||||||
|
duration=duration,
|
||||||
)
|
)
|
||||||
|
|
||||||
def scroll_page_left(self, *, animate: bool = True) -> bool:
|
def scroll_page_left(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
animate: bool = True,
|
||||||
|
speed: float | None = None,
|
||||||
|
duration: float | None = None,
|
||||||
|
) -> bool:
|
||||||
"""Scroll one page left.
|
"""Scroll one page left.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
animate (bool, optional): Animate scroll. Defaults to True.
|
animate (bool, optional): Animate scroll. Defaults to True.
|
||||||
|
speed (float | None, optional): Speed of scroll if animate is True. Or None to use duration.
|
||||||
|
duration (float | None, optional): Duration of animation, if animate is True and speed is False.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if any scrolling was done.
|
bool: True if any scrolling was done.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
if speed is None and duration is None:
|
||||||
|
duration = 0.3
|
||||||
return self.scroll_to(
|
return self.scroll_to(
|
||||||
x=self.scroll_target_x - self.container_size.width,
|
x=self.scroll_target_x - self.container_size.width,
|
||||||
animate=animate,
|
animate=animate,
|
||||||
duration=0.3,
|
speed=speed,
|
||||||
|
duration=duration,
|
||||||
)
|
)
|
||||||
|
|
||||||
def scroll_page_right(self, *, animate: bool = True) -> bool:
|
def scroll_page_right(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
animate: bool = True,
|
||||||
|
speed: float | None = None,
|
||||||
|
duration: float | None = None,
|
||||||
|
) -> bool:
|
||||||
"""Scroll one page right.
|
"""Scroll one page right.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
animate (bool, optional): Animate scroll. Defaults to True.
|
animate (bool, optional): Animate scroll. Defaults to True.
|
||||||
|
speed (float | None, optional): Speed of scroll if animate is True. Or None to use duration.
|
||||||
|
duration (float | None, optional): Duration of animation, if animate is True and speed is False.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if any scrolling was done.
|
bool: True if any scrolling was done.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
if speed is None and duration is None:
|
||||||
|
duration = 0.3
|
||||||
return self.scroll_to(
|
return self.scroll_to(
|
||||||
x=self.scroll_target_x + self.container_size.width,
|
x=self.scroll_target_x + self.container_size.width,
|
||||||
animate=animate,
|
animate=animate,
|
||||||
duration=0.3,
|
speed=speed,
|
||||||
|
duration=duration,
|
||||||
)
|
)
|
||||||
|
|
||||||
def scroll_to_widget(self, widget: Widget, *, animate: bool = True) -> bool:
|
def scroll_to_widget(self, widget: Widget, *, animate: bool = True) -> bool:
|
||||||
@@ -1294,6 +1400,13 @@ class Widget(DOMNode):
|
|||||||
self.virtual_size = virtual_size
|
self.virtual_size = virtual_size
|
||||||
self._container_size = container_size
|
self._container_size = container_size
|
||||||
if self.is_scrollable:
|
if self.is_scrollable:
|
||||||
|
self._scroll_update(virtual_size)
|
||||||
|
self.refresh(layout=True)
|
||||||
|
self.scroll_to(self.scroll_x, self.scroll_y)
|
||||||
|
else:
|
||||||
|
self.refresh()
|
||||||
|
|
||||||
|
def _scroll_update(self, virtual_size):
|
||||||
self._refresh_scrollbars()
|
self._refresh_scrollbars()
|
||||||
width, height = self.container_size
|
width, height = self.container_size
|
||||||
if self.show_vertical_scrollbar:
|
if self.show_vertical_scrollbar:
|
||||||
@@ -1303,16 +1416,10 @@ class Widget(DOMNode):
|
|||||||
)
|
)
|
||||||
if self.show_horizontal_scrollbar:
|
if self.show_horizontal_scrollbar:
|
||||||
self.horizontal_scrollbar.window_virtual_size = virtual_size.width
|
self.horizontal_scrollbar.window_virtual_size = virtual_size.width
|
||||||
self.horizontal_scrollbar.window_size = (
|
self.horizontal_scrollbar.window_size = width - self.scrollbar_size_vertical
|
||||||
width - self.scrollbar_size_vertical
|
|
||||||
)
|
|
||||||
|
|
||||||
self.scroll_x = self.validate_scroll_x(self.scroll_x)
|
self.scroll_x = self.validate_scroll_x(self.scroll_x)
|
||||||
self.scroll_y = self.validate_scroll_y(self.scroll_y)
|
self.scroll_y = self.validate_scroll_y(self.scroll_y)
|
||||||
self.refresh(layout=True)
|
|
||||||
self.scroll_to(self.scroll_x, self.scroll_y)
|
|
||||||
else:
|
|
||||||
self.refresh()
|
|
||||||
|
|
||||||
def _render_content(self) -> None:
|
def _render_content(self) -> None:
|
||||||
"""Render all lines."""
|
"""Render all lines."""
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ __all__ = [
|
|||||||
"Pretty",
|
"Pretty",
|
||||||
"Static",
|
"Static",
|
||||||
"TextInput",
|
"TextInput",
|
||||||
|
"TextLog",
|
||||||
"TreeControl",
|
"TreeControl",
|
||||||
"Welcome",
|
"Welcome",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -8,5 +8,6 @@ from ._placeholder import Placeholder as Placeholder
|
|||||||
from ._pretty import Pretty as Pretty
|
from ._pretty import Pretty as Pretty
|
||||||
from ._static import Static as Static
|
from ._static import Static as Static
|
||||||
from ._text_input import TextInput as TextInput
|
from ._text_input import TextInput as TextInput
|
||||||
|
from ._text_log import TextLog as TextLog
|
||||||
from ._tree_control import TreeControl as TreeControl
|
from ._tree_control import TreeControl as TreeControl
|
||||||
from ._welcome import Welcome as Welcome
|
from ._welcome import Welcome as Welcome
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ from ..geometry import clamp, Region, Size, Spacing
|
|||||||
from ..reactive import Reactive
|
from ..reactive import Reactive
|
||||||
from .._profile import timer
|
from .._profile import timer
|
||||||
from ..scroll_view import ScrollView
|
from ..scroll_view import ScrollView
|
||||||
from ..widget import Widget
|
|
||||||
from .. import messages
|
from .. import messages
|
||||||
|
|
||||||
|
|
||||||
@@ -155,6 +155,7 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
*,
|
||||||
name: str | None = None,
|
name: str | None = None,
|
||||||
id: str | None = None,
|
id: str | None = None,
|
||||||
classes: str | None = None,
|
classes: str | None = None,
|
||||||
@@ -522,18 +523,6 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
|
|||||||
|
|
||||||
return self._render_line(y, scroll_x, scroll_x + width, style)
|
return self._render_line(y, scroll_x, scroll_x + width, style)
|
||||||
|
|
||||||
def render_lines(self, crop: Region) -> Lines:
|
|
||||||
"""Render the widget in to lines.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
crop (Region): Region within visible area to.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Lines: A list of list of segments
|
|
||||||
"""
|
|
||||||
lines = self._styles_cache.render_widget(self, crop)
|
|
||||||
return lines
|
|
||||||
|
|
||||||
def on_mouse_move(self, event: events.MouseMove):
|
def on_mouse_move(self, event: events.MouseMove):
|
||||||
meta = event.style.meta
|
meta = event.style.meta
|
||||||
if meta:
|
if meta:
|
||||||
|
|||||||
98
src/textual/widgets/_text_log.py
Normal file
98
src/textual/widgets/_text_log.py
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from rich.console import RenderableType
|
||||||
|
from rich.segment import Segment
|
||||||
|
|
||||||
|
from ..reactive import var
|
||||||
|
from ..geometry import Size, Region
|
||||||
|
from ..scroll_view import ScrollView
|
||||||
|
from .._cache import LRUCache
|
||||||
|
from .._segment_tools import line_crop
|
||||||
|
from .._types import Lines
|
||||||
|
|
||||||
|
|
||||||
|
class TextLog(ScrollView, can_focus=True):
|
||||||
|
DEFAULT_CSS = """
|
||||||
|
TextLog{
|
||||||
|
background: $surface;
|
||||||
|
color: $text;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
max_lines: var[int | None] = var(None)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
max_lines: int | None = None,
|
||||||
|
name: str | None = None,
|
||||||
|
id: str | None = None,
|
||||||
|
classes: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
self.max_lines = max_lines
|
||||||
|
self.lines: list[list[Segment]] = []
|
||||||
|
self._line_cache: LRUCache[tuple[int, int, int, int], list[Segment]]
|
||||||
|
self._line_cache = LRUCache(1024)
|
||||||
|
self.max_width: int = 0
|
||||||
|
super().__init__(name=name, id=id, classes=classes)
|
||||||
|
|
||||||
|
def write(self, content: RenderableType) -> None:
|
||||||
|
"""Write text or a rich renderable.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content (RenderableType): Rich renderable (or text).
|
||||||
|
"""
|
||||||
|
console = self.app.console
|
||||||
|
width = self.size.width or 80
|
||||||
|
lines = self.app.console.render_lines(
|
||||||
|
content, console.options.update_width(width)
|
||||||
|
)
|
||||||
|
self.max_width = max(
|
||||||
|
self.max_width,
|
||||||
|
max(sum(segment.cell_length for segment in _line) for _line in lines),
|
||||||
|
)
|
||||||
|
self.lines.extend(lines)
|
||||||
|
|
||||||
|
if self.max_lines is not None:
|
||||||
|
self.lines = self.lines[-self.max_lines :]
|
||||||
|
self.virtual_size = Size(self.max_width, len(self.lines))
|
||||||
|
self.scroll_end(animate=True, speed=100)
|
||||||
|
|
||||||
|
def clear(self) -> None:
|
||||||
|
"""Clear the text log."""
|
||||||
|
del self.lines[:]
|
||||||
|
self.max_width = 0
|
||||||
|
self.virtual_size = Size(self.max_width, len(self.lines))
|
||||||
|
|
||||||
|
def render_line(self, y: int) -> list[Segment]:
|
||||||
|
scroll_x, scroll_y = self.scroll_offset
|
||||||
|
return self._render_line(scroll_y + y, scroll_x, self.size.width)
|
||||||
|
|
||||||
|
def render_lines(self, crop: Region) -> Lines:
|
||||||
|
"""Render the widget in to lines.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
crop (Region): Region within visible area to.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Lines: A list of list of segments
|
||||||
|
"""
|
||||||
|
lines = self._styles_cache.render_widget(self, crop)
|
||||||
|
return lines
|
||||||
|
|
||||||
|
def _render_line(self, y: int, scroll_x: int, width: int) -> list[Segment]:
|
||||||
|
|
||||||
|
if y >= len(self.lines):
|
||||||
|
return [Segment(" " * width, self.rich_style)]
|
||||||
|
|
||||||
|
key = (y, scroll_x, width, self.max_width)
|
||||||
|
if key in self._line_cache:
|
||||||
|
return self._line_cache[key]
|
||||||
|
|
||||||
|
line = self.lines[y]
|
||||||
|
line = Segment.adjust_line_length(
|
||||||
|
line, max(self.max_width, width), self.rich_style
|
||||||
|
)
|
||||||
|
line = line_crop(line, scroll_x, scroll_x + width, self.max_width)
|
||||||
|
self._line_cache[key] = line
|
||||||
|
return line
|
||||||
Reference in New Issue
Block a user