fix for scrollview space

This commit is contained in:
Will McGugan
2021-09-19 21:09:31 +01:00
parent 4903e7c79f
commit 368971045d
11 changed files with 116 additions and 54 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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")

View File

@@ -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")

View File

@@ -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))

View File

@@ -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,15 +238,21 @@ class ScrollBar(Widget):
x: float | None = None
y: float | None = None
if self.vertical:
y = self.grabbed_position + (
y = round(
self.grabbed_position
+ (
(event.screen_y - self.grabbed.y)
* (self.virtual_size / self.window_size)
)
)
else:
x = self.grabbed_position + (
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))

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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)