mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
scroll view
This commit is contained in:
@@ -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
38
src/textual/_lru_cache.py
Normal 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
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
87
src/textual/widgets/_datatable.py
Normal file
87
src/textual/widgets/_datatable.py
Normal 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
|
||||
Reference in New Issue
Block a user