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/) The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/). and this project adheres to [Semantic Versioning](http://semver.org/).
## [0.1.12] - Unreleased
### Added
- Added geometry.Spacing
## [0.1.11] - 2021-09-12 ## [0.1.11] - 2021-09-12
### Changed ### Changed

View File

@@ -20,10 +20,10 @@ class MyApp(App):
async def add_content(): async def add_content():
table = Table(title="Demo") table = Table(title="Demo")
for i in range(40): for i in range(20):
table.add_column(f"Col {i + 1}", style="magenta") table.add_column(f"Col {i + 1}", style="magenta")
for i in range(200): for i in range(100):
table.add_row(*[f"cell {i},{j}" for j in range(40)]) table.add_row(*[f"cell {i},{j}" for j in range(20)])
await body.update(table) await body.update(table)

View File

@@ -12,7 +12,6 @@ from rich.padding import Padding
from rich.text import Text from rich.text import Text
from textual.app import App from textual.app import App
from textual import events
from textual.reactive import Reactive from textual.reactive import Reactive
from textual.views import GridView from textual.views import GridView
from textual.widget import Widget from textual.widget import Widget

View File

@@ -17,7 +17,7 @@ class MyApp(App):
"""Create and dock the widgets.""" """Create and dock the widgets."""
# A scrollview to contain the markdown file # A scrollview to contain the markdown file
body = ScrollView() body = ScrollView(gutter=1)
# Header / footer / dock # Header / footer / dock
await self.view.dock(Header(), edge="top") await self.view.dock(Header(), edge="top")

View File

@@ -1,6 +1,9 @@
from __future__ import annotations 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) T = TypeVar("T", int, float)
@@ -262,7 +265,7 @@ class Region(NamedTuple):
return NotImplemented return NotImplemented
def expand(self, size: tuple[int, int]) -> Region: def expand(self, size: tuple[int, int]) -> Region:
"""Add additional height. """Increase the size of the region by adding a border.
Args: Args:
size (tuple[int, int]): Additional width and height. size (tuple[int, int]): Additional width and height.
@@ -270,9 +273,14 @@ class Region(NamedTuple):
Returns: Returns:
Region: A new region. Region: A new region.
""" """
add_width, add_height = size expand_width, expand_height = size
x, y, width, height = self 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: def overlaps(self, other: Region) -> bool:
"""Check if another region overlaps this region. """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) min(x1, ox1), min(y1, oy1), max(x2, ox2), max(y2, oy2)
) )
return union_region 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 typing import Iterable
from ..geometry import Offset, Region, Size from ..geometry import Offset, Region, Size, Spacing, SpacingDimensions
from ..layout import Layout, WidgetPlacement from ..layout import Layout, WidgetPlacement
from ..widget import Widget from ..widget import Widget
from .._loop import loop_last
class VerticalLayout(Layout): class VerticalLayout(Layout):
@@ -13,11 +14,11 @@ class VerticalLayout(Layout):
*, *,
auto_width: bool = False, auto_width: bool = False,
z: int = 0, z: int = 0,
gutter: tuple[int, int] | None = None gutter: SpacingDimensions = (0, 0, 0, 0)
): ):
self.auto_width = auto_width self.auto_width = auto_width
self.z = z self.z = z
self.gutter = gutter or (0, 0) self.gutter = Spacing.unpack(gutter)
self._widgets: list[Widget] = [] self._widgets: list[Widget] = []
self._max_widget_width = 0 self._max_widget_width = 0
super().__init__() super().__init__()
@@ -36,18 +37,19 @@ class VerticalLayout(Layout):
def arrange(self, size: Size, scroll: Offset) -> Iterable[WidgetPlacement]: def arrange(self, size: Size, scroll: Offset) -> Iterable[WidgetPlacement]:
index = 0 index = 0
width, _height = size width, _height = size
gutter_height, gutter_width = self.gutter gutter = self.gutter
x, y = self.gutter.top_left
render_width = ( render_width = (
max(width, self._max_widget_width) + gutter_width * 2 max(width, self._max_widget_width)
if self.auto_width if self.auto_width
else width - gutter_width * 2 else width - gutter.width
) )
x = gutter_width total_width = render_width
y = gutter_height
total_region = Region() gutter_height = max(gutter.top, gutter.bottom)
for widget in self._widgets:
for last, widget in loop_last(self._widgets):
if ( if (
not widget.render_cache not widget.render_cache
or widget.render_cache.size.width != render_width or widget.render_cache.size.width != render_width
@@ -57,6 +59,6 @@ class VerticalLayout(Layout):
render_height = widget.render_cache.size.height render_height = widget.render_cache.size.height
region = Region(x, y, render_width, render_height) region = Region(x, y, render_width, render_height)
yield WidgetPlacement(region, widget, (self.z, index)) 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 from __future__ import annotations
import rich.repr import rich.repr
from rich.color import Color from rich.color import Color
from rich.console import Console, ConsoleOptions, RenderResult, RenderableType from rich.console import Console, ConsoleOptions, RenderResult, RenderableType
@@ -237,14 +238,20 @@ class ScrollBar(Widget):
x: float | None = None x: float | None = None
y: float | None = None y: float | None = None
if self.vertical: if self.vertical:
y = self.grabbed_position + ( y = round(
(event.screen_y - self.grabbed.y) self.grabbed_position
* (self.virtual_size / self.window_size) + (
(event.screen_y - self.grabbed.y)
* (self.virtual_size / self.window_size)
)
) )
else: else:
x = self.grabbed_position + ( x = round(
(event.screen_x - self.grabbed.x) self.grabbed_position
* (self.virtual_size / self.window_size) + (
(event.screen_x - self.grabbed.x)
* (self.virtual_size / self.window_size)
)
) )
await self.emit(ScrollTo(self, x=x, y=y)) 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 rich.console import RenderableType
from .. import events from .. import events
from ..geometry import Size from ..geometry import Size, SpacingDimensions
from ..layouts.vertical import VerticalLayout from ..layouts.vertical import VerticalLayout
from ..view import View from ..view import View
from ..message import Message from ..message import Message
@@ -23,7 +23,7 @@ class WindowView(View, layout=VerticalLayout):
widget: RenderableType | Widget, widget: RenderableType | Widget,
*, *,
auto_width: bool = False, auto_width: bool = False,
gutter: tuple[int, int] = (0, 1), gutter: SpacingDimensions = (0, 0),
name: str | None = None name: str | None = None
) -> None: ) -> None:
layout = VerticalLayout(gutter=gutter, auto_width=auto_width) 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.align import Align
from rich.console import Console, RenderableType from rich.console import Console, RenderableType
from rich.panel import Panel from rich.panel import Panel
from rich.padding import Padding, PaddingDimensions from rich.padding import Padding
from rich.pretty import Pretty from rich.pretty import Pretty
from rich.segment import Segment
from rich.style import Style from rich.style import Style
from rich.styled import Styled from rich.styled import Styled
from rich.text import TextType from rich.text import TextType
@@ -27,7 +26,7 @@ from . import events
from ._animator import BoundAnimator from ._animator import BoundAnimator
from ._callback import invoke from ._callback import invoke
from ._context import active_app from ._context import active_app
from .geometry import Size from .geometry import Size, Spacing, SpacingDimensions
from .message import Message from .message import Message
from .message_pump import MessagePump from .message_pump import MessagePump
from .messages import Layout, Update from .messages import Layout, Update
@@ -41,15 +40,6 @@ if TYPE_CHECKING:
log = getLogger("rich") 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): class RenderCache(NamedTuple):
size: Size size: Size
lines: Lines lines: Lines
@@ -103,11 +93,11 @@ class Widget(MessagePump):
BOX_MAP = {"normal": box.SQUARE, "round": box.ROUNDED, "bold": box.HEAVY} BOX_MAP = {"normal": box.SQUARE, "round": box.ROUNDED, "bold": box.HEAVY}
def validate_padding(self, padding: PaddingDimensions) -> Spacing: def validate_padding(self, padding: SpacingDimensions) -> Spacing:
return Spacing(*Padding.unpack(padding)) return Spacing.unpack(padding)
def validate_margin(self, padding: PaddingDimensions) -> Spacing: def validate_margin(self, margin: SpacingDimensions) -> Spacing:
return Spacing(*Padding.unpack(padding)) return Spacing.unpack(margin)
def validate_layout_offset_x(self, value) -> int: def validate_layout_offset_x(self, value) -> int:
return int(value) return int(value)

View File

@@ -8,7 +8,6 @@ import os.path
from rich.console import RenderableType from rich.console import RenderableType
import rich.repr import rich.repr
from rich.text import Text from rich.text import Text
from rich.tree import Tree
from .. import events from .. import events
from ..message import Message from ..message import Message

View File

@@ -5,12 +5,14 @@ from rich.style import StyleType
from .. import events from .. import events
from ..geometry import SpacingDimensions
from ..layouts.grid import GridLayout from ..layouts.grid import GridLayout
from ..message import Message from ..message import Message
from ..messages import CursorMove from ..messages import CursorMove
from ..scrollbar import ScrollTo, ScrollBar from ..scrollbar import ScrollTo, ScrollBar
from ..geometry import clamp from ..geometry import clamp
from ..view import View from ..view import View
from ..widget import Widget from ..widget import Widget
from ..reactive import Reactive from ..reactive import Reactive
@@ -25,6 +27,7 @@ class ScrollView(View):
name: str | None = None, name: str | None = None,
style: StyleType = "", style: StyleType = "",
fluid: bool = True, fluid: bool = True,
gutter: SpacingDimensions = (0, 0)
) -> None: ) -> None:
from ..views import WindowView from ..views import WindowView
@@ -32,7 +35,7 @@ class ScrollView(View):
self.vscroll = ScrollBar(vertical=True) self.vscroll = ScrollBar(vertical=True)
self.hscroll = ScrollBar(vertical=False) self.hscroll = ScrollBar(vertical=False)
self.window = WindowView( 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 = GridLayout()
layout.add_column("main") layout.add_column("main")
@@ -66,11 +69,11 @@ class ScrollView(View):
@property @property
def max_scroll_y(self) -> float: 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 @property
def max_scroll_x(self) -> float: 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: async def watch_x(self, new_value: float) -> None:
self.window.scroll_x = round(new_value) 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("x", self.target_x, speed=150, easing="out_cubic")
self.animate("y", self.target_y, 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 virtual_width, virtual_height = self.window.virtual_size
width, height = self.size width, height = self.size
@@ -207,10 +213,10 @@ class ScrollView(View):
assert isinstance(self.layout, GridLayout) assert isinstance(self.layout, GridLayout)
if self.layout.show_column("vscroll", virtual_height > height): vscroll_change = self.layout.show_column("vscroll", virtual_height > height)
self.refresh() hscroll_change = self.layout.show_row("hscroll", virtual_width > width)
if self.layout.show_row("hscroll", virtual_width > width): if hscroll_change or vscroll_change:
self.refresh() self.refresh(layout=True)
def handle_cursor_move(self, message: CursorMove) -> None: def handle_cursor_move(self, message: CursorMove) -> None:
self.scroll_to_center(message.line) self.scroll_to_center(message.line)