scroll view

This commit is contained in:
Will McGugan
2022-05-31 21:24:27 +01:00
parent 96c013aa39
commit 51df62adc0
6 changed files with 175 additions and 32 deletions

View File

@@ -326,6 +326,7 @@ class Compositor:
# The region covered by children relative to parent widget
total_region = child_region.reset_origin
if widget.is_container:
# Arrange the layout
placements, arranged_widgets = widget._arrange(child_region.size)
widgets.update(arranged_widgets)
@@ -360,6 +361,7 @@ class Compositor:
container_size,
)
if widget.is_container:
# Add the container widget, which will render a background
map[widget] = MapGeometry(
region + layout_offset,
@@ -368,6 +370,15 @@ class Compositor:
total_region.size,
container_size,
)
else:
map[widget] = MapGeometry(
child_region + layout_offset,
order,
clip,
child_region.size,
container_size,
)
else:
# Add the widget to the map

38
src/textual/_lru_cache.py Normal file
View File

@@ -0,0 +1,38 @@
from typing import TypeVar
import sys
CacheKey = TypeVar("CacheKey")
CacheValue = TypeVar("CacheValue")
if sys.version_info < (3, 9):
from typing_extensions import OrderedDict
else:
from collections import OrderedDict
class LRUCache(OrderedDict[CacheKey, CacheValue]):
"""
A dictionary-like container that stores a given maximum items.
If an additional item is added when the LRUCache is full, the least
recently used key is discarded to make room for the new item.
"""
def __init__(self, cache_size: int) -> None:
self.cache_size = cache_size
super().__init__()
def __setitem__(self, key: CacheKey, value: CacheValue) -> None:
"""Store a new views, potentially discarding an old value."""
if key not in self:
if len(self) >= self.cache_size:
self.popitem(last=False)
super().__setitem__(key, value)
def __getitem__(self, key: CacheKey) -> CacheValue:
"""Gets the item, but also makes it most recent."""
value: CacheValue = super().__getitem__(key)
super().__delitem__(key)
super().__setitem__(key, value)
return value

View File

@@ -11,8 +11,8 @@ class ScrollView(Widget):
ScrollView {
background: blue;
overflow-y: scroll;
overflow-x: scroll;
overflow-y: auto;
overflow-x: auto;
scrollbar-size-vertical: 2;
scrollbar-size-horizontal: 1;
}
@@ -31,7 +31,6 @@ class ScrollView(Widget):
def on_mount(self):
self.virtual_size = Size(200, 200)
self._refresh_scrollbars()
self.refresh(layout=True)
def size_updated(
self, size: Size, virtual_size: Size, container_size: Size
@@ -57,7 +56,9 @@ class ScrollView(Widget):
self.call_later(self.scroll_to, self.scroll_x, self.scroll_y)
def render(self) -> RenderableType:
return f"{self.scroll_offset} {self.show_vertical_scrollbar}"
from rich.panel import Panel
return Panel(f"{self.scroll_offset} {self.show_vertical_scrollbar}")
def watch_scroll_x(self, new_value: float) -> None:
self.horizontal_scrollbar.position = int(new_value)

View File

@@ -622,15 +622,19 @@ class Widget(DOMNode):
horizontal_scrollbar_thickness = self.scrollbar_size_horizontal
vertical_scrollbar_thickness = self.scrollbar_size_vertical
print(self, horizontal_scrollbar_thickness, vertical_scrollbar_thickness)
if self.styles.scrollbar_gutter == "stable":
# Let's _always_ reserve some space, whether the scrollbar is actually displayed or not:
show_vertical_scrollbar = True
vertical_scrollbar_thickness = self.styles.scrollbar_size_vertical
if show_horizontal_scrollbar and show_vertical_scrollbar:
print(1, region)
(region, _, _, _) = region.split(
-vertical_scrollbar_thickness, -horizontal_scrollbar_thickness
)
print(2, region)
elif show_vertical_scrollbar:
region, _ = region.split_vertical(-vertical_scrollbar_thickness)
elif show_horizontal_scrollbar:
@@ -780,7 +784,7 @@ class Widget(DOMNode):
Returns:
bool: ``True`` if there is background color, otherwise ``False``.
"""
return self.is_container and self.styles.background.is_transparent
return self.is_scrollable and self.styles.background.is_transparent
@property
def console(self) -> Console:

View File

@@ -1,3 +1,4 @@
from ._datatable import DataTable
from ._footer import Footer
from ._header import Header
from ._button import Button
@@ -8,6 +9,7 @@ from ._directory_tree import DirectoryTree, FileClick
__all__ = [
"Button",
"DataTable",
"DirectoryTree",
"FileClick",
"Footer",

View File

@@ -0,0 +1,87 @@
from __future__ import annotations
from abc import abstractmethod, ABC
from rich.console import Console, ConsoleOptions, RenderResult
from rich.text import Text
from dataclasses import dataclass
from typing import Awaitable, Callable, Generic, TypeVar
from ..geometry import Size
from .._lru_cache import LRUCache
from ..scroll_view import ScrollView
RowType = TypeVar("RowType")
RowSetter = Callable[[RowType | None], Awaitable[None]]
class DataProvider(ABC):
@abstractmethod
async def start(self) -> None:
pass
@abstractmethod
async def get_size(self) -> int | None:
...
@abstractmethod
async def request_row(self, row_no: int, set_row: RowSetter) -> None:
...
class DictListProvider:
def __init__(self, data: list[list]) -> None:
self.data = data
async def start(self) -> None:
pass
async def get_size(self) -> int | None:
return len(self.data)
async def request_row(self, row_no: int, set_row: RowSetter) -> None:
if row_no > len(self.data):
await set_row(None)
else:
row = self.data[row_no]
await set_row(row)
@dataclass
class Column:
label: Text
width: int
class _TableRenderable:
def __rich_console__(
self, console: Console, options: ConsoleOptions
) -> RenderResult:
pass
class DataTable(ScrollView, Generic[RowType]):
def __init__(
self,
data_provider: DataProvider | None,
*,
name: str | None = None,
id: str | None = None,
classes: str | None = None,
) -> None:
super().__init__(name=name, id=id, classes=classes)
self._data_provider = data_provider
self._columns: list[Column]
self._rows = LRUCache[int, RowType]
self.height = 0
def get_content_height(self, container: Size, viewport: Size, width: int) -> int:
return super().get_content_height(container, viewport, width)
async def set_row(self, row_no: int, row: RowType):
pass