197 lines
6.3 KiB
Python
197 lines
6.3 KiB
Python
|
|
"""
|
||
|
|
`ScrollView` is a base class for [Line API](/guide/widgets#line-api) widgets.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from rich.console import RenderableType
|
||
|
|
|
||
|
|
from textual._animator import EasingFunction
|
||
|
|
from textual._types import AnimationLevel, CallbackType
|
||
|
|
from textual.containers import ScrollableContainer
|
||
|
|
from textual.geometry import Region, Size
|
||
|
|
|
||
|
|
|
||
|
|
class ScrollView(ScrollableContainer):
|
||
|
|
"""
|
||
|
|
A base class for a Widget that handles its own scrolling (i.e. doesn't rely
|
||
|
|
on the compositor to render children).
|
||
|
|
|
||
|
|
!!! note
|
||
|
|
|
||
|
|
This is the typically wrong class for making something scrollable. If you want to make something scroll, set its
|
||
|
|
`overflow` style to auto or scroll. Or use one of the pre-defined scrolling containers such as [VerticalScroll][textual.containers.VerticalScroll].
|
||
|
|
"""
|
||
|
|
|
||
|
|
ALLOW_MAXIMIZE = True
|
||
|
|
|
||
|
|
DEFAULT_CSS = """
|
||
|
|
ScrollView {
|
||
|
|
overflow-y: auto;
|
||
|
|
overflow-x: auto;
|
||
|
|
}
|
||
|
|
"""
|
||
|
|
|
||
|
|
@property
|
||
|
|
def is_scrollable(self) -> bool:
|
||
|
|
"""Always scrollable."""
|
||
|
|
return True
|
||
|
|
|
||
|
|
@property
|
||
|
|
def is_container(self) -> bool:
|
||
|
|
"""Since a ScrollView should be a line-api widget, it won't have children,
|
||
|
|
and therefore isn't a container."""
|
||
|
|
return False
|
||
|
|
|
||
|
|
def watch_scroll_x(self, old_value: float, new_value: float) -> None:
|
||
|
|
if self.show_horizontal_scrollbar:
|
||
|
|
self.horizontal_scrollbar.position = new_value
|
||
|
|
if round(old_value) != round(new_value):
|
||
|
|
self.refresh(self.size.region)
|
||
|
|
|
||
|
|
def watch_scroll_y(self, old_value: float, new_value: float) -> None:
|
||
|
|
if self.show_vertical_scrollbar:
|
||
|
|
self.vertical_scrollbar.position = new_value
|
||
|
|
if round(old_value) != round(new_value):
|
||
|
|
self.refresh(self.size.region)
|
||
|
|
|
||
|
|
def on_mount(self):
|
||
|
|
self._refresh_scrollbars()
|
||
|
|
|
||
|
|
def get_content_width(self, container: Size, viewport: Size) -> int:
|
||
|
|
"""Gets the width of the content area.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
container: Size of the container (immediate parent) widget.
|
||
|
|
viewport: Size of the viewport.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
The optimal width of the content.
|
||
|
|
"""
|
||
|
|
return self.virtual_size.width
|
||
|
|
|
||
|
|
def get_content_height(self, container: Size, viewport: Size, width: int) -> int:
|
||
|
|
"""Gets the height (number of lines) in the content area.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
container: Size of the container (immediate parent) widget.
|
||
|
|
viewport: Size of the viewport.
|
||
|
|
width: Width of renderable.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
The height of the content.
|
||
|
|
"""
|
||
|
|
return self.virtual_size.height
|
||
|
|
|
||
|
|
def _size_updated(
|
||
|
|
self, size: Size, virtual_size: Size, container_size: Size, layout: bool = True
|
||
|
|
) -> bool:
|
||
|
|
"""Called when size is updated.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
size: New size.
|
||
|
|
virtual_size: New virtual size.
|
||
|
|
container_size: New container size.
|
||
|
|
layout: Perform layout if required.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
True if a resize event should be sent, otherwise False.
|
||
|
|
"""
|
||
|
|
if size_changed := self._size != size:
|
||
|
|
self._set_dirty()
|
||
|
|
if (
|
||
|
|
size_changed
|
||
|
|
or virtual_size != self.virtual_size
|
||
|
|
or container_size != self.container_size
|
||
|
|
):
|
||
|
|
self._scrollbar_changes.clear()
|
||
|
|
self._size = size
|
||
|
|
virtual_size = self.virtual_size
|
||
|
|
self._container_size = size - self.styles.gutter.totals
|
||
|
|
self._scroll_update(virtual_size)
|
||
|
|
|
||
|
|
return size_changed or self._container_size != container_size
|
||
|
|
|
||
|
|
def render(self) -> RenderableType:
|
||
|
|
"""Render the scrollable region (if `render_lines` is not implemented).
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Renderable object.
|
||
|
|
"""
|
||
|
|
from rich.panel import Panel
|
||
|
|
|
||
|
|
return Panel(f"{self.scroll_offset} {self.show_vertical_scrollbar}")
|
||
|
|
|
||
|
|
# Custom scroll to which doesn't require call_after_refresh
|
||
|
|
def scroll_to(
|
||
|
|
self,
|
||
|
|
x: float | None = None,
|
||
|
|
y: float | None = None,
|
||
|
|
*,
|
||
|
|
animate: bool = True,
|
||
|
|
speed: float | None = None,
|
||
|
|
duration: float | None = None,
|
||
|
|
easing: EasingFunction | str | None = None,
|
||
|
|
force: bool = False,
|
||
|
|
on_complete: CallbackType | None = None,
|
||
|
|
level: AnimationLevel = "basic",
|
||
|
|
immediate: bool = False,
|
||
|
|
) -> None:
|
||
|
|
"""Scroll to a given (absolute) coordinate, optionally animating.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
x: X coordinate (column) to scroll to, or `None` for no change.
|
||
|
|
y: Y coordinate (row) to scroll to, or `None` for no change.
|
||
|
|
animate: Animate to new scroll position.
|
||
|
|
speed: Speed of scroll if `animate` is `True`; or `None` to use `duration`.
|
||
|
|
duration: Duration of animation, if `animate` is `True` and `speed` is `None`.
|
||
|
|
easing: An easing method for the scrolling animation.
|
||
|
|
force: Force scrolling even when prohibited by overflow styling.
|
||
|
|
on_complete: A callable to invoke when the animation is finished.
|
||
|
|
level: Minimum level required for the animation to take place (inclusive).
|
||
|
|
immediate: If `False` the scroll will be deferred until after a screen refresh,
|
||
|
|
set to `True` to scroll immediately.
|
||
|
|
"""
|
||
|
|
|
||
|
|
self._scroll_to(
|
||
|
|
x,
|
||
|
|
y,
|
||
|
|
animate=animate,
|
||
|
|
speed=speed,
|
||
|
|
duration=duration,
|
||
|
|
easing=easing,
|
||
|
|
force=force,
|
||
|
|
on_complete=on_complete,
|
||
|
|
level=level,
|
||
|
|
)
|
||
|
|
|
||
|
|
def refresh_line(self, y: int) -> None:
|
||
|
|
"""Refresh a single line.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
y: Coordinate of line.
|
||
|
|
"""
|
||
|
|
self.refresh(
|
||
|
|
Region(
|
||
|
|
0,
|
||
|
|
y - self.scroll_offset.y,
|
||
|
|
max(self.virtual_size.width, self.size.width),
|
||
|
|
1,
|
||
|
|
)
|
||
|
|
)
|
||
|
|
|
||
|
|
def refresh_lines(self, y_start: int, line_count: int = 1) -> None:
|
||
|
|
"""Refresh one or more lines.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
y_start: First line to refresh.
|
||
|
|
line_count: Total number of lines to refresh.
|
||
|
|
"""
|
||
|
|
refresh_region = Region(
|
||
|
|
0,
|
||
|
|
y_start - self.scroll_offset.y,
|
||
|
|
max(self.virtual_size.width, self.size.width),
|
||
|
|
line_count,
|
||
|
|
)
|
||
|
|
self.refresh(refresh_region)
|