mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
emol
This commit is contained in:
@@ -42,7 +42,6 @@ class MyApp(App):
|
||||
)
|
||||
self.app.sub_title = os.path.basename(message.path)
|
||||
await self.body.update(syntax)
|
||||
# self.body.layout_offset_y = -5
|
||||
self.body.home()
|
||||
|
||||
|
||||
|
||||
@@ -331,3 +331,20 @@ class Region(NamedTuple):
|
||||
_clamp(y2, cy1, cy2),
|
||||
)
|
||||
return new_region
|
||||
|
||||
def union(self, region: Region) -> Region:
|
||||
"""Get a new region that contains both regions.
|
||||
|
||||
Args:
|
||||
region (Region): [description]
|
||||
|
||||
Returns:
|
||||
Region: [description]
|
||||
"""
|
||||
x1, y1, x2, y2 = self.corners
|
||||
ox1, oy1, ox2, oy2 = region.corners
|
||||
|
||||
union_region = Region.from_corners(
|
||||
min(x1, ox1), min(y1, oy1), max(x2, ox2), max(y2, oy2)
|
||||
)
|
||||
return union_region
|
||||
|
||||
@@ -9,11 +9,6 @@ from .geometry import Region, Dimensions
|
||||
from .widget import Widget
|
||||
|
||||
|
||||
class Order(NamedTuple):
|
||||
layer: int
|
||||
z: int
|
||||
|
||||
|
||||
class RenderRegion(NamedTuple):
|
||||
region: Region
|
||||
order: tuple[int, ...]
|
||||
@@ -22,9 +17,13 @@ class RenderRegion(NamedTuple):
|
||||
|
||||
class LayoutMap:
|
||||
def __init__(self, size: Dimensions) -> None:
|
||||
self.size = size
|
||||
self.region = size.region
|
||||
self.widgets: dict[Widget, RenderRegion] = {}
|
||||
|
||||
@property
|
||||
def size(self) -> Dimensions:
|
||||
return self.region.size
|
||||
|
||||
def __getitem__(self, widget: Widget) -> RenderRegion:
|
||||
return self.widgets[widget]
|
||||
|
||||
@@ -52,6 +51,7 @@ class LayoutMap:
|
||||
|
||||
region += widget.layout_offset
|
||||
self.widgets[widget] = RenderRegion(region, order, clip)
|
||||
self.region = self.region.union(region.intersection(clip))
|
||||
|
||||
if isinstance(widget, View):
|
||||
sub_map = widget.layout.generate_map(console, region.size, region)
|
||||
|
||||
@@ -8,9 +8,9 @@ from typing import Iterable, TYPE_CHECKING, Sequence
|
||||
from rich.console import Console
|
||||
|
||||
from .._layout_resolve import layout_resolve
|
||||
from ..geometry import Region, Point, Dimensions
|
||||
from ..geometry import Region, Dimensions
|
||||
from ..layout import Layout
|
||||
from ..layout_map import LayoutMap, Order
|
||||
from ..layout_map import LayoutMap
|
||||
|
||||
if sys.version_info >= (3, 8):
|
||||
from typing import Literal
|
||||
|
||||
@@ -1,58 +1,60 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Iterable
|
||||
|
||||
from rich.console import Console
|
||||
|
||||
|
||||
from ..geometry import Point, Region, Dimensions
|
||||
from ..geometry import Region, Dimensions
|
||||
from ..layout import Layout
|
||||
from ..layout_map import LayoutMap
|
||||
from ..widget import Widget
|
||||
from ..view import View
|
||||
|
||||
|
||||
class VerticalLayout(Layout):
|
||||
def __init__(self, gutter: tuple[int, int] = (0, 1)):
|
||||
self.gutter = gutter or (0, 0)
|
||||
def __init__(self, *, z: int = 0, gutter: tuple[int, int] | None = None):
|
||||
self.z = z
|
||||
self.gutter = gutter or (0, 1)
|
||||
self._widgets: list[Widget] = []
|
||||
super().__init__()
|
||||
|
||||
def add(self, widget: Widget) -> None:
|
||||
self._widgets.append(widget)
|
||||
|
||||
def clear(self) -> None:
|
||||
del self._widgets[:]
|
||||
|
||||
def get_widgets(self) -> Iterable[Widget]:
|
||||
return self._widgets
|
||||
|
||||
def generate_map(
|
||||
self, console: Console, size: Dimensions, viewport: Region
|
||||
) -> WidgetMap:
|
||||
) -> LayoutMap:
|
||||
offset = viewport.origin
|
||||
index = 0
|
||||
width, height = size
|
||||
gutter_width, gutter_height = self.gutter
|
||||
render_width = width - gutter_width * 2
|
||||
x = gutter_width
|
||||
y = gutter_height
|
||||
map: dict[Widget, RenderRegion] = {}
|
||||
map: LayoutMap = LayoutMap(size)
|
||||
|
||||
def add_widget(widget: Widget, region: Region):
|
||||
order = (0, 0)
|
||||
region = region + widget.layout_offset
|
||||
map[widget] = RenderRegion(region, order, offset)
|
||||
if isinstance(widget, View):
|
||||
sub_map = widget.layout.generate_map(
|
||||
console,
|
||||
Dimensions(region.width, region.height),
|
||||
region.origin + offset,
|
||||
)
|
||||
map.update(sub_map)
|
||||
def add_widget(widget: Widget, region: Region, clip: Region) -> None:
|
||||
map.add_widget(console, widget, region, (self.z, index), clip)
|
||||
|
||||
for widget in self._widgets:
|
||||
region_lines = self.renders.get(widget)
|
||||
if region_lines is None:
|
||||
|
||||
try:
|
||||
region, clip, lines = self.renders[widget]
|
||||
except KeyError:
|
||||
renderable = widget.render()
|
||||
lines = console.render_lines(
|
||||
renderable, console.options.update_width(render_width)
|
||||
)
|
||||
region = Region(x, y, render_width, len(lines))
|
||||
add_widget(widget, region)
|
||||
add_widget(widget, region, viewport)
|
||||
else:
|
||||
region, lines = region_lines
|
||||
add_widget(widget, Region(x, y, region.width, region.height))
|
||||
add_widget(widget, Region(x, y, region.width, region.height), clip)
|
||||
y += region.height + gutter_height
|
||||
widget_map = WidgetMap(Dimensions(width, y), map)
|
||||
return widget_map
|
||||
|
||||
return map
|
||||
|
||||
@@ -101,24 +101,20 @@ class Page(Widget):
|
||||
self._page = PageRender(self, renderable, style=style)
|
||||
super().__init__(name=name)
|
||||
|
||||
x: Reactive[int] = Reactive(0)
|
||||
y: Reactive[int] = Reactive(0)
|
||||
scroll_x: Reactive[int] = Reactive(0)
|
||||
scroll_y: Reactive[int] = Reactive(0)
|
||||
|
||||
@property
|
||||
def contents_size(self) -> Dimensions:
|
||||
return self._page.size
|
||||
|
||||
def validate_x(self, value: int) -> int:
|
||||
def validate_scroll_x(self, value: int) -> int:
|
||||
return max(0, value)
|
||||
|
||||
def validate_y(self, value: int) -> int:
|
||||
def validate_scroll_y(self, value: int) -> int:
|
||||
return max(0, value)
|
||||
|
||||
async def watch_x(self, new: int) -> None:
|
||||
async def watch_scroll_x(self, new: int) -> None:
|
||||
x, y = self._page.offset
|
||||
self._page.offset = Point(new, y)
|
||||
|
||||
async def watch_y(self, new: int) -> None:
|
||||
async def watch_scroll_y(self, new: int) -> None:
|
||||
x, y = self._page.offset
|
||||
self._page.offset = Point(x, new)
|
||||
|
||||
|
||||
@@ -45,40 +45,46 @@ class View(Widget):
|
||||
super().__init_subclass__(**kwargs)
|
||||
|
||||
background: Reactive[str] = Reactive("")
|
||||
offset_x: Reactive[int] = Reactive(0)
|
||||
offset_y: Reactive[int] = Reactive(0)
|
||||
virtual_width: Reactive[int | None] = Reactive(None)
|
||||
virtual_height: Reactive[int | None] = Reactive(None)
|
||||
|
||||
async def watch_background(self, value: str) -> None:
|
||||
self.layout.background = value
|
||||
|
||||
scroll_x: Reactive[int] = Reactive(0)
|
||||
scroll_y: Reactive[int] = Reactive(0)
|
||||
|
||||
@property
|
||||
def virtual_size(self) -> Dimensions:
|
||||
virtual_width = self.virtual_width
|
||||
virtual_height = self.virtual_height
|
||||
return Dimensions(
|
||||
(virtual_width if virtual_width is not None else self.size.width),
|
||||
(virtual_height if virtual_height is not None else self.size.height),
|
||||
)
|
||||
return self.layout.map.size
|
||||
|
||||
@virtual_size.setter
|
||||
def virtual_size(self, size: tuple[int, int]) -> None:
|
||||
width, height = size
|
||||
self.virtual_width = width
|
||||
self.virtual_height = height
|
||||
# virtual_width: Reactive[int | None] = Reactive(None)
|
||||
# virtual_height: Reactive[int | None] = Reactive(None)
|
||||
|
||||
@property
|
||||
def offset(self) -> Point:
|
||||
return Point(self.offset_x, self.offset_y)
|
||||
# @property
|
||||
# def virtual_size(self) -> Dimensions:
|
||||
# virtual_width = self.virtual_width
|
||||
# virtual_height = self.virtual_height
|
||||
# return Dimensions(
|
||||
# (virtual_width if virtual_width is not None else self.size.width),
|
||||
# (virtual_height if virtual_height is not None else self.size.height),
|
||||
# )
|
||||
|
||||
@property
|
||||
def viewport(self) -> Region:
|
||||
virtual_width = self.virtual_width
|
||||
virtual_height = self.virtual_height
|
||||
width = virtual_width if virtual_width is not None else self.size.width
|
||||
height = virtual_height if virtual_height is not None else self.size.height
|
||||
return Region(self.offset_x, self.offset_y, width, height)
|
||||
# @virtual_size.setter
|
||||
# def virtual_size(self, size: tuple[int, int]) -> None:
|
||||
# width, height = size
|
||||
# self.virtual_width = width
|
||||
# self.virtual_height = height
|
||||
|
||||
# @property
|
||||
# def offset(self) -> Point:
|
||||
# return Point(self.offset_x, self.offset_y)
|
||||
|
||||
# @property
|
||||
# def viewport(self) -> Region:
|
||||
# virtual_width = self.virtual_width
|
||||
# virtual_height = self.virtual_height
|
||||
# width = virtual_width if virtual_width is not None else self.size.width
|
||||
# height = virtual_height if virtual_height is not None else self.size.height
|
||||
# return Region(self.offset_x, self.offset_y, width, height)
|
||||
|
||||
def __rich_console__(
|
||||
self, console: Console, options: ConsoleOptions
|
||||
@@ -143,10 +149,10 @@ class View(Widget):
|
||||
return
|
||||
|
||||
width, height = self.console.size
|
||||
virtual_width, virtual_height = self.virtual_size
|
||||
viewport = Region(self.offset_x, self.offset_y, width, height)
|
||||
# virtual_width, virtual_height = self.virtual_size
|
||||
viewport = Region(self.scroll_x, self.scroll_y, width, height)
|
||||
hidden, shown, resized = self.layout.reflow(
|
||||
self.console, virtual_width, virtual_height, viewport
|
||||
self.console, width, height, viewport
|
||||
)
|
||||
self.app.refresh()
|
||||
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
from ._dock_view import DockView, Dock, DockEdge
|
||||
from ._grid_view import GridView
|
||||
from ._window_view import WindowView
|
||||
|
||||
17
src/textual/views/_window_view.py
Normal file
17
src/textual/views/_window_view.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from ..layouts.vertical import VerticalLayout
|
||||
from ..view import View
|
||||
from ..widget import Widget
|
||||
|
||||
|
||||
class WindowView(View, layout=VerticalLayout):
|
||||
def __init__(
|
||||
self, *, gutter: tuple[int, int] = (1, 1), name: str | None = None
|
||||
) -> None:
|
||||
self.gutter = gutter
|
||||
super().__init__(name=name)
|
||||
|
||||
async def update(self, widget: Widget) -> None:
|
||||
self.layout = VerticalLayout(gutter=self.gutter)
|
||||
self.layout.add(widget)
|
||||
@@ -46,23 +46,23 @@ class ScrollView(View):
|
||||
target_y: Reactive[float] = Reactive(0, repaint=False)
|
||||
|
||||
def validate_x(self, value: float) -> float:
|
||||
return clamp(value, 0, self.page.contents_size.width - self.size.width)
|
||||
return clamp(value, 0, self.page.virtual_size.width - self.size.width)
|
||||
|
||||
def validate_target_x(self, value: float) -> float:
|
||||
return clamp(value, 0, self.page.contents_size.width - self.size.width)
|
||||
return clamp(value, 0, self.page.virtual_size.width - self.size.width)
|
||||
|
||||
def validate_y(self, value: float) -> float:
|
||||
return clamp(value, 0, self.page.contents_size.height - self.size.height)
|
||||
return clamp(value, 0, self.page.virtual_size.height - self.size.height)
|
||||
|
||||
def validate_target_y(self, value: float) -> float:
|
||||
return clamp(value, 0, self.page.contents_size.height - self.size.height)
|
||||
return clamp(value, 0, self.page.virtual_size.height - self.size.height)
|
||||
|
||||
async def watch_x(self, new_value: float) -> None:
|
||||
self.page.x = round(new_value)
|
||||
self.page.scroll_x = round(new_value)
|
||||
self.hscroll.position = round(new_value)
|
||||
|
||||
async def watch_y(self, new_value: float) -> None:
|
||||
self.page.y = round(new_value)
|
||||
self.page.scroll_y = round(new_value)
|
||||
self.vscroll.position = round(new_value)
|
||||
|
||||
async def update(self, renderabe: RenderableType) -> None:
|
||||
@@ -132,7 +132,7 @@ class ScrollView(View):
|
||||
|
||||
async def key_end(self) -> None:
|
||||
self.target_x = 0
|
||||
self.target_y = self.page.contents_size.height - self.size.height
|
||||
self.target_y = self.page.virtual_size.height - self.size.height
|
||||
self.animate("x", self.target_x, duration=1, easing="out_cubic")
|
||||
self.animate("y", self.target_y, duration=1, easing="out_cubic")
|
||||
|
||||
|
||||
@@ -176,3 +176,7 @@ def test_region_intersection():
|
||||
)
|
||||
|
||||
assert not Region(10, 10, 20, 30).intersection(Region(50, 50, 100, 200))
|
||||
|
||||
|
||||
def test_region_union():
|
||||
assert Region(5, 5, 10, 10).union(Region(20, 30, 10, 5)) == Region(5, 5, 25, 30)
|
||||
|
||||
Reference in New Issue
Block a user