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/)
|
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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user