mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
fix for scrollview space
This commit is contained in:
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
||||
and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## [0.1.12] - Unreleased
|
||||
|
||||
### Added
|
||||
|
||||
- Added geometry.Spacing
|
||||
|
||||
## [0.1.11] - 2021-09-12
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -20,10 +20,10 @@ class MyApp(App):
|
||||
async def add_content():
|
||||
table = Table(title="Demo")
|
||||
|
||||
for i in range(40):
|
||||
for i in range(20):
|
||||
table.add_column(f"Col {i + 1}", style="magenta")
|
||||
for i in range(200):
|
||||
table.add_row(*[f"cell {i},{j}" for j in range(40)])
|
||||
for i in range(100):
|
||||
table.add_row(*[f"cell {i},{j}" for j in range(20)])
|
||||
|
||||
await body.update(table)
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ from rich.padding import Padding
|
||||
from rich.text import Text
|
||||
|
||||
from textual.app import App
|
||||
from textual import events
|
||||
from textual.reactive import Reactive
|
||||
from textual.views import GridView
|
||||
from textual.widget import Widget
|
||||
|
||||
@@ -17,7 +17,7 @@ class MyApp(App):
|
||||
"""Create and dock the widgets."""
|
||||
|
||||
# A scrollview to contain the markdown file
|
||||
body = ScrollView()
|
||||
body = ScrollView(gutter=1)
|
||||
|
||||
# Header / footer / dock
|
||||
await self.view.dock(Header(), edge="top")
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, NamedTuple, TypeVar
|
||||
from typing import Any, cast, NamedTuple, Tuple, Union, TypeVar
|
||||
|
||||
|
||||
SpacingDimensions = Union[int, Tuple[int], Tuple[int, int], Tuple[int, int, int, int]]
|
||||
|
||||
|
||||
T = TypeVar("T", int, float)
|
||||
@@ -262,7 +265,7 @@ class Region(NamedTuple):
|
||||
return NotImplemented
|
||||
|
||||
def expand(self, size: tuple[int, int]) -> Region:
|
||||
"""Add additional height.
|
||||
"""Increase the size of the region by adding a border.
|
||||
|
||||
Args:
|
||||
size (tuple[int, int]): Additional width and height.
|
||||
@@ -270,9 +273,14 @@ class Region(NamedTuple):
|
||||
Returns:
|
||||
Region: A new region.
|
||||
"""
|
||||
add_width, add_height = size
|
||||
expand_width, expand_height = size
|
||||
x, y, width, height = self
|
||||
return Region(x, y, width + add_width, height + add_height)
|
||||
return Region(
|
||||
x - expand_width,
|
||||
y - expand_height,
|
||||
width + expand_width * 2,
|
||||
height + expand_height * 2,
|
||||
)
|
||||
|
||||
def overlaps(self, other: Region) -> bool:
|
||||
"""Check if another region overlaps this region.
|
||||
@@ -419,3 +427,48 @@ class Region(NamedTuple):
|
||||
min(x1, ox1), min(y1, oy1), max(x2, ox2), max(y2, oy2)
|
||||
)
|
||||
return union_region
|
||||
|
||||
|
||||
class Spacing(NamedTuple):
|
||||
"""The spacing around a renderable."""
|
||||
|
||||
top: int = 0
|
||||
right: int = 0
|
||||
bottom: int = 0
|
||||
left: int = 0
|
||||
|
||||
@property
|
||||
def width(self) -> int:
|
||||
"""Total space in width."""
|
||||
return self.left + self.right
|
||||
|
||||
@property
|
||||
def height(self) -> int:
|
||||
"""Total space in height."""
|
||||
return self.top + self.bottom
|
||||
|
||||
@property
|
||||
def top_left(self) -> tuple[int, int]:
|
||||
"""Top left space."""
|
||||
return (self.left, self.top)
|
||||
|
||||
@property
|
||||
def bottom_right(self) -> tuple[int, int]:
|
||||
"""Bottom right space."""
|
||||
return (self.right, self.bottom)
|
||||
|
||||
@classmethod
|
||||
def unpack(cls, pad: SpacingDimensions) -> Spacing:
|
||||
"""Unpack padding specified in CSS style."""
|
||||
if isinstance(pad, int):
|
||||
return cls(pad, pad, pad, pad)
|
||||
if len(pad) == 1:
|
||||
_pad = pad[0]
|
||||
return cls(_pad, _pad, _pad, _pad)
|
||||
if len(pad) == 2:
|
||||
pad_top, pad_right = cast(Tuple[int, int], pad)
|
||||
return cls(pad_top, pad_right, pad_top, pad_right)
|
||||
if len(pad) == 4:
|
||||
top, right, bottom, left = cast(Tuple[int, int, int, int], pad)
|
||||
return cls(top, right, bottom, left)
|
||||
raise ValueError(f"1, 2 or 4 integers required for spacing; {len(pad)} given")
|
||||
|
||||
@@ -2,9 +2,10 @@ from __future__ import annotations
|
||||
|
||||
from typing import Iterable
|
||||
|
||||
from ..geometry import Offset, Region, Size
|
||||
from ..geometry import Offset, Region, Size, Spacing, SpacingDimensions
|
||||
from ..layout import Layout, WidgetPlacement
|
||||
from ..widget import Widget
|
||||
from .._loop import loop_last
|
||||
|
||||
|
||||
class VerticalLayout(Layout):
|
||||
@@ -13,11 +14,11 @@ class VerticalLayout(Layout):
|
||||
*,
|
||||
auto_width: bool = False,
|
||||
z: int = 0,
|
||||
gutter: tuple[int, int] | None = None
|
||||
gutter: SpacingDimensions = (0, 0, 0, 0)
|
||||
):
|
||||
self.auto_width = auto_width
|
||||
self.z = z
|
||||
self.gutter = gutter or (0, 0)
|
||||
self.gutter = Spacing.unpack(gutter)
|
||||
self._widgets: list[Widget] = []
|
||||
self._max_widget_width = 0
|
||||
super().__init__()
|
||||
@@ -36,18 +37,19 @@ class VerticalLayout(Layout):
|
||||
def arrange(self, size: Size, scroll: Offset) -> Iterable[WidgetPlacement]:
|
||||
index = 0
|
||||
width, _height = size
|
||||
gutter_height, gutter_width = self.gutter
|
||||
gutter = self.gutter
|
||||
x, y = self.gutter.top_left
|
||||
render_width = (
|
||||
max(width, self._max_widget_width) + gutter_width * 2
|
||||
max(width, self._max_widget_width)
|
||||
if self.auto_width
|
||||
else width - gutter_width * 2
|
||||
else width - gutter.width
|
||||
)
|
||||
|
||||
x = gutter_width
|
||||
y = gutter_height
|
||||
total_width = render_width
|
||||
|
||||
total_region = Region()
|
||||
for widget in self._widgets:
|
||||
gutter_height = max(gutter.top, gutter.bottom)
|
||||
|
||||
for last, widget in loop_last(self._widgets):
|
||||
if (
|
||||
not widget.render_cache
|
||||
or widget.render_cache.size.width != render_width
|
||||
@@ -57,6 +59,6 @@ class VerticalLayout(Layout):
|
||||
render_height = widget.render_cache.size.height
|
||||
region = Region(x, y, render_width, render_height)
|
||||
yield WidgetPlacement(region, widget, (self.z, index))
|
||||
total_region = total_region.union(region)
|
||||
y += render_height + (gutter.bottom if last else gutter_height)
|
||||
|
||||
yield WidgetPlacement(total_region)
|
||||
yield WidgetPlacement(Region(0, 0, total_width + gutter.width, y))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
import rich.repr
|
||||
from rich.color import Color
|
||||
from rich.console import Console, ConsoleOptions, RenderResult, RenderableType
|
||||
@@ -237,14 +238,20 @@ class ScrollBar(Widget):
|
||||
x: float | None = None
|
||||
y: float | None = None
|
||||
if self.vertical:
|
||||
y = self.grabbed_position + (
|
||||
(event.screen_y - self.grabbed.y)
|
||||
* (self.virtual_size / self.window_size)
|
||||
y = round(
|
||||
self.grabbed_position
|
||||
+ (
|
||||
(event.screen_y - self.grabbed.y)
|
||||
* (self.virtual_size / self.window_size)
|
||||
)
|
||||
)
|
||||
else:
|
||||
x = self.grabbed_position + (
|
||||
(event.screen_x - self.grabbed.x)
|
||||
* (self.virtual_size / self.window_size)
|
||||
x = round(
|
||||
self.grabbed_position
|
||||
+ (
|
||||
(event.screen_x - self.grabbed.x)
|
||||
* (self.virtual_size / self.window_size)
|
||||
)
|
||||
)
|
||||
await self.emit(ScrollTo(self, x=x, y=y))
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
from rich.console import RenderableType
|
||||
|
||||
from .. import events
|
||||
from ..geometry import Size
|
||||
from ..geometry import Size, SpacingDimensions
|
||||
from ..layouts.vertical import VerticalLayout
|
||||
from ..view import View
|
||||
from ..message import Message
|
||||
@@ -23,7 +23,7 @@ class WindowView(View, layout=VerticalLayout):
|
||||
widget: RenderableType | Widget,
|
||||
*,
|
||||
auto_width: bool = False,
|
||||
gutter: tuple[int, int] = (0, 1),
|
||||
gutter: SpacingDimensions = (0, 0),
|
||||
name: str | None = None
|
||||
) -> None:
|
||||
layout = VerticalLayout(gutter=gutter, auto_width=auto_width)
|
||||
|
||||
@@ -16,9 +16,8 @@ from rich import box
|
||||
from rich.align import Align
|
||||
from rich.console import Console, RenderableType
|
||||
from rich.panel import Panel
|
||||
from rich.padding import Padding, PaddingDimensions
|
||||
from rich.padding import Padding
|
||||
from rich.pretty import Pretty
|
||||
from rich.segment import Segment
|
||||
from rich.style import Style
|
||||
from rich.styled import Styled
|
||||
from rich.text import TextType
|
||||
@@ -27,7 +26,7 @@ from . import events
|
||||
from ._animator import BoundAnimator
|
||||
from ._callback import invoke
|
||||
from ._context import active_app
|
||||
from .geometry import Size
|
||||
from .geometry import Size, Spacing, SpacingDimensions
|
||||
from .message import Message
|
||||
from .message_pump import MessagePump
|
||||
from .messages import Layout, Update
|
||||
@@ -41,15 +40,6 @@ if TYPE_CHECKING:
|
||||
log = getLogger("rich")
|
||||
|
||||
|
||||
class Spacing(NamedTuple):
|
||||
"""The spacing around a renderable."""
|
||||
|
||||
top: int = 0
|
||||
right: int = 0
|
||||
bottom: int = 0
|
||||
left: int = 0
|
||||
|
||||
|
||||
class RenderCache(NamedTuple):
|
||||
size: Size
|
||||
lines: Lines
|
||||
@@ -103,11 +93,11 @@ class Widget(MessagePump):
|
||||
|
||||
BOX_MAP = {"normal": box.SQUARE, "round": box.ROUNDED, "bold": box.HEAVY}
|
||||
|
||||
def validate_padding(self, padding: PaddingDimensions) -> Spacing:
|
||||
return Spacing(*Padding.unpack(padding))
|
||||
def validate_padding(self, padding: SpacingDimensions) -> Spacing:
|
||||
return Spacing.unpack(padding)
|
||||
|
||||
def validate_margin(self, padding: PaddingDimensions) -> Spacing:
|
||||
return Spacing(*Padding.unpack(padding))
|
||||
def validate_margin(self, margin: SpacingDimensions) -> Spacing:
|
||||
return Spacing.unpack(margin)
|
||||
|
||||
def validate_layout_offset_x(self, value) -> int:
|
||||
return int(value)
|
||||
|
||||
@@ -8,7 +8,6 @@ import os.path
|
||||
from rich.console import RenderableType
|
||||
import rich.repr
|
||||
from rich.text import Text
|
||||
from rich.tree import Tree
|
||||
|
||||
from .. import events
|
||||
from ..message import Message
|
||||
|
||||
@@ -5,12 +5,14 @@ from rich.style import StyleType
|
||||
|
||||
|
||||
from .. import events
|
||||
from ..geometry import SpacingDimensions
|
||||
from ..layouts.grid import GridLayout
|
||||
from ..message import Message
|
||||
from ..messages import CursorMove
|
||||
from ..scrollbar import ScrollTo, ScrollBar
|
||||
from ..geometry import clamp
|
||||
from ..view import View
|
||||
|
||||
from ..widget import Widget
|
||||
|
||||
from ..reactive import Reactive
|
||||
@@ -25,6 +27,7 @@ class ScrollView(View):
|
||||
name: str | None = None,
|
||||
style: StyleType = "",
|
||||
fluid: bool = True,
|
||||
gutter: SpacingDimensions = (0, 0)
|
||||
) -> None:
|
||||
from ..views import WindowView
|
||||
|
||||
@@ -32,7 +35,7 @@ class ScrollView(View):
|
||||
self.vscroll = ScrollBar(vertical=True)
|
||||
self.hscroll = ScrollBar(vertical=False)
|
||||
self.window = WindowView(
|
||||
"" if contents is None else contents, auto_width=auto_width
|
||||
"" if contents is None else contents, auto_width=auto_width, gutter=gutter
|
||||
)
|
||||
layout = GridLayout()
|
||||
layout.add_column("main")
|
||||
@@ -66,11 +69,11 @@ class ScrollView(View):
|
||||
|
||||
@property
|
||||
def max_scroll_y(self) -> float:
|
||||
return max(0, self.window.virtual_size.height - self.size.height)
|
||||
return max(0, self.window.virtual_size.height - self.window.size.height)
|
||||
|
||||
@property
|
||||
def max_scroll_x(self) -> float:
|
||||
return max(0, self.window.virtual_size.width - self.size.width)
|
||||
return max(0, self.window.virtual_size.width - self.window.size.width)
|
||||
|
||||
async def watch_x(self, new_value: float) -> None:
|
||||
self.window.scroll_x = round(new_value)
|
||||
@@ -193,7 +196,10 @@ class ScrollView(View):
|
||||
self.animate("x", self.target_x, speed=150, easing="out_cubic")
|
||||
self.animate("y", self.target_y, speed=150, easing="out_cubic")
|
||||
|
||||
def handle_window_change(self, message) -> None:
|
||||
async def handle_window_change(self, message: Message) -> None:
|
||||
|
||||
message.stop()
|
||||
|
||||
virtual_width, virtual_height = self.window.virtual_size
|
||||
width, height = self.size
|
||||
|
||||
@@ -207,10 +213,10 @@ class ScrollView(View):
|
||||
|
||||
assert isinstance(self.layout, GridLayout)
|
||||
|
||||
if self.layout.show_column("vscroll", virtual_height > height):
|
||||
self.refresh()
|
||||
if self.layout.show_row("hscroll", virtual_width > width):
|
||||
self.refresh()
|
||||
vscroll_change = self.layout.show_column("vscroll", virtual_height > height)
|
||||
hscroll_change = self.layout.show_row("hscroll", virtual_width > width)
|
||||
if hscroll_change or vscroll_change:
|
||||
self.refresh(layout=True)
|
||||
|
||||
def handle_cursor_move(self, message: CursorMove) -> None:
|
||||
self.scroll_to_center(message.line)
|
||||
|
||||
Reference in New Issue
Block a user