mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
renderer refactor
This commit is contained in:
5
examples/README.md
Normal file
5
examples/README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Examples
|
||||||
|
|
||||||
|
Run any of these examples to demonstrate a features.
|
||||||
|
|
||||||
|
These examples may not be feature complete, but they should be somewhat useful and a good starting point for your own code.
|
||||||
20
examples/test_layout.py
Normal file
20
examples/test_layout.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
from rich import print
|
||||||
|
from rich.console import Console
|
||||||
|
|
||||||
|
from textual.geometry import Offset, Region
|
||||||
|
from textual.widgets import Placeholder
|
||||||
|
|
||||||
|
|
||||||
|
from textual.views import WindowView
|
||||||
|
|
||||||
|
p = Placeholder(height=10)
|
||||||
|
view = WindowView(p)
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
view.layout.reflow(console, 30, 25, Offset(0, 3))
|
||||||
|
|
||||||
|
print(view.layout._layout_map.widgets)
|
||||||
|
|
||||||
|
console.print(view.layout.render(console))
|
||||||
|
|
||||||
|
# console.print(view.layout.render(console, Region(100, 2, 10, 10)))
|
||||||
19
examples/vertical.py
Normal file
19
examples/vertical.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from textual import events
|
||||||
|
from textual.app import App
|
||||||
|
|
||||||
|
from textual.views import WindowView
|
||||||
|
from textual.widgets import Placeholder
|
||||||
|
|
||||||
|
|
||||||
|
class MyApp(App):
|
||||||
|
async def on_mount(self, event: events.Mount) -> None:
|
||||||
|
window1 = WindowView(Placeholder(height=20))
|
||||||
|
# window2 = WindowView(Placeholder(height=20))
|
||||||
|
|
||||||
|
# window1.scroll_x = -10
|
||||||
|
# window1.scroll_y = 5
|
||||||
|
|
||||||
|
await self.view.dock(window1, edge="left")
|
||||||
|
|
||||||
|
|
||||||
|
MyApp.run(log="textual.log")
|
||||||
@@ -1,8 +1,17 @@
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
__all__ = ["log", "panic"]
|
||||||
|
|
||||||
|
|
||||||
def log(*args: Any) -> None:
|
def log(*args: Any) -> None:
|
||||||
from ._context import active_app
|
from ._context import active_app
|
||||||
|
|
||||||
app = active_app.get()
|
app = active_app.get()
|
||||||
app.log(*args)
|
app.log(*args)
|
||||||
|
|
||||||
|
|
||||||
|
def panic(*args: Any) -> None:
|
||||||
|
from ._context import active_app
|
||||||
|
|
||||||
|
app = active_app.get()
|
||||||
|
app.panic(*args)
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ class App(MessagePump):
|
|||||||
self.mouse_over: Widget | None = None
|
self.mouse_over: Widget | None = None
|
||||||
self.mouse_captured: Widget | None = None
|
self.mouse_captured: Widget | None = None
|
||||||
self._driver: Driver | None = None
|
self._driver: Driver | None = None
|
||||||
self._tracebacks: list[Traceback] = []
|
self._exit_renderables: list[RenderableType] = []
|
||||||
|
|
||||||
self._docks: list[Dock] = []
|
self._docks: list[Dock] = []
|
||||||
self._action_targets = {"app", "view"}
|
self._action_targets = {"app", "view"}
|
||||||
@@ -231,16 +231,19 @@ class App(MessagePump):
|
|||||||
if widget is not None:
|
if widget is not None:
|
||||||
await widget.post_message(events.MouseCapture(self, self.mouse_position))
|
await widget.post_message(events.MouseCapture(self, self.mouse_position))
|
||||||
|
|
||||||
def panic(self, traceback: Traceback | None = None) -> None:
|
def panic(self, *renderables: RenderableType) -> None:
|
||||||
"""Exits the app with a traceback.
|
"""Exits the app with a traceback.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
traceback (Traceback, optional): Rich Traceback object or None to generate one
|
traceback (Traceback, optional): Rich Traceback object or None to generate one
|
||||||
for the most recent exception. Defaults to None.
|
for the most recent exception. Defaults to None.
|
||||||
"""
|
"""
|
||||||
if traceback is None:
|
|
||||||
traceback = Traceback(show_locals=True)
|
if not renderables:
|
||||||
self._tracebacks.append(traceback)
|
renderables = (
|
||||||
|
Traceback(show_locals=True, width=None, locals_max_length=5),
|
||||||
|
)
|
||||||
|
self._exit_renderables.extend(renderables)
|
||||||
self.close_messages_no_wait()
|
self.close_messages_no_wait()
|
||||||
|
|
||||||
async def process_messages(self) -> None:
|
async def process_messages(self) -> None:
|
||||||
@@ -283,8 +286,8 @@ class App(MessagePump):
|
|||||||
self.panic()
|
self.panic()
|
||||||
finally:
|
finally:
|
||||||
driver.stop_application_mode()
|
driver.stop_application_mode()
|
||||||
if self._tracebacks:
|
if self._exit_renderables:
|
||||||
for traceback in self._tracebacks:
|
for traceback in self._exit_renderables:
|
||||||
self.error_console.print(traceback)
|
self.error_console.print(traceback)
|
||||||
if self.log_file is not None:
|
if self.log_file is not None:
|
||||||
self.log_file.close()
|
self.log_file.close()
|
||||||
@@ -339,7 +342,7 @@ class App(MessagePump):
|
|||||||
console.file.write("\x1bP=2s\x1b\\")
|
console.file.write("\x1bP=2s\x1b\\")
|
||||||
console.file.flush()
|
console.file.flush()
|
||||||
except Exception:
|
except Exception:
|
||||||
self.panic(Traceback(show_locals=True))
|
self.panic()
|
||||||
|
|
||||||
def display(self, renderable: RenderableType) -> None:
|
def display(self, renderable: RenderableType) -> None:
|
||||||
if not self._closed:
|
if not self._closed:
|
||||||
|
|||||||
@@ -167,6 +167,22 @@ class Region(NamedTuple):
|
|||||||
def __bool__(self) -> bool:
|
def __bool__(self) -> bool:
|
||||||
return bool(self.width and self.height)
|
return bool(self.width and self.height)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def x_extents(self) -> tuple[int, int]:
|
||||||
|
return (self.x, self.x + self.width)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def y_extents(self) -> tuple[int, int]:
|
||||||
|
return (self.y, self.y + self.height)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def x_end(self) -> int:
|
||||||
|
return self.x + self.width
|
||||||
|
|
||||||
|
@property
|
||||||
|
def y_end(self) -> int:
|
||||||
|
return self.y + self.height
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def area(self) -> int:
|
def area(self) -> int:
|
||||||
"""Get the area within the region."""
|
"""Get the area within the region."""
|
||||||
@@ -192,6 +208,14 @@ class Region(NamedTuple):
|
|||||||
x, y, width, height = self
|
x, y, width, height = self
|
||||||
return x, y, x + width, y + height
|
return x, y, x + width, y + height
|
||||||
|
|
||||||
|
@property
|
||||||
|
def x_range(self) -> range:
|
||||||
|
return range(self.x, self.x_end)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def y_range(self) -> range:
|
||||||
|
return range(self.y, self.y_end)
|
||||||
|
|
||||||
def __add__(self, other: Any) -> Region:
|
def __add__(self, other: Any) -> Region:
|
||||||
if isinstance(other, tuple):
|
if isinstance(other, tuple):
|
||||||
ox, oy = other
|
ox, oy = other
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from rich.console import Console, ConsoleOptions, RenderResult, RenderableType
|
|||||||
from rich.segment import Segment, SegmentLines
|
from rich.segment import Segment, SegmentLines
|
||||||
from rich.style import Style
|
from rich.style import Style
|
||||||
|
|
||||||
from . import log
|
from . import log, panic
|
||||||
from ._loop import loop_last
|
from ._loop import loop_last
|
||||||
from .layout_map import LayoutMap
|
from .layout_map import LayoutMap
|
||||||
from ._lines import crop_lines
|
from ._lines import crop_lines
|
||||||
@@ -101,6 +101,9 @@ class Layout(ABC):
|
|||||||
) -> ReflowResult:
|
) -> ReflowResult:
|
||||||
self.reset()
|
self.reset()
|
||||||
|
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
|
||||||
map = self.generate_map(
|
map = self.generate_map(
|
||||||
console,
|
console,
|
||||||
Dimensions(width, height),
|
Dimensions(width, height),
|
||||||
@@ -131,8 +134,6 @@ class Layout(ABC):
|
|||||||
hidden_widgets = old_widgets - new_widgets
|
hidden_widgets = old_widgets - new_widgets
|
||||||
|
|
||||||
self._layout_map = map
|
self._layout_map = map
|
||||||
self.width = width
|
|
||||||
self.height = height
|
|
||||||
|
|
||||||
# Copy renders if the size hasn't changed
|
# Copy renders if the size hasn't changed
|
||||||
new_renders = {
|
new_renders = {
|
||||||
@@ -255,9 +256,10 @@ class Layout(ABC):
|
|||||||
if self.map is not None:
|
if self.map is not None:
|
||||||
for region, order, clip in self.map.values():
|
for region, order, clip in self.map.values():
|
||||||
region = region.intersection(clip)
|
region = region.intersection(clip)
|
||||||
if region and (region in screen_region): # type: ignore
|
if region and (region in screen_region):
|
||||||
for y in range(region.y, region.y + region.height):
|
region_cuts = region.x_extents
|
||||||
cuts_sets[y].update({region.x, region.x + region.width})
|
for y in region.y_range:
|
||||||
|
cuts_sets[y].update(region_cuts)
|
||||||
|
|
||||||
# Sort the cuts for each line
|
# Sort the cuts for each line
|
||||||
self._cuts = [sorted(cut_set) for cut_set in cuts_sets]
|
self._cuts = [sorted(cut_set) for cut_set in cuts_sets]
|
||||||
@@ -265,9 +267,6 @@ class Layout(ABC):
|
|||||||
|
|
||||||
def _get_renders(self, console: Console) -> Iterable[tuple[Region, Region, Lines]]:
|
def _get_renders(self, console: Console) -> Iterable[tuple[Region, Region, Lines]]:
|
||||||
_rich_traceback_guard = True
|
_rich_traceback_guard = True
|
||||||
width = self.width
|
|
||||||
height = self.height
|
|
||||||
screen_region = Region(0, 0, width, height)
|
|
||||||
layout_map = self.map
|
layout_map = self.map
|
||||||
|
|
||||||
if layout_map:
|
if layout_map:
|
||||||
@@ -292,12 +291,12 @@ class Layout(ABC):
|
|||||||
|
|
||||||
if not widget.is_visual:
|
if not widget.is_visual:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
region_lines = self.renders.get(widget)
|
region_lines = self.renders.get(widget)
|
||||||
if region_lines is not None:
|
if region_lines is not None:
|
||||||
yield region_lines
|
region, clip, lines = region_lines
|
||||||
continue
|
else:
|
||||||
|
lines = render(widget, region.width, region.height)
|
||||||
lines = render(widget, region.width, region.height)
|
|
||||||
if region in clip:
|
if region in clip:
|
||||||
self.renders[widget] = (region, clip, lines)
|
self.renders[widget] = (region, clip, lines)
|
||||||
yield region, clip, lines
|
yield region, clip, lines
|
||||||
@@ -308,11 +307,10 @@ class Layout(ABC):
|
|||||||
self.renders[widget] = (region, clip, lines)
|
self.renders[widget] = (region, clip, lines)
|
||||||
splits = [delta_x, delta_x + new_region.width]
|
splits = [delta_x, delta_x + new_region.width]
|
||||||
|
|
||||||
|
lines = lines[delta_y : delta_y + new_region.height]
|
||||||
|
|
||||||
divide = Segment.divide
|
divide = Segment.divide
|
||||||
lines = [
|
lines = [list(divide(line, splits))[1] for line in lines]
|
||||||
list(divide(line, splits))[1]
|
|
||||||
for line in lines[delta_y : delta_y + new_region.height]
|
|
||||||
]
|
|
||||||
yield region, clip, lines
|
yield region, clip, lines
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -331,7 +329,7 @@ class Layout(ABC):
|
|||||||
def render(
|
def render(
|
||||||
self,
|
self,
|
||||||
console: Console,
|
console: Console,
|
||||||
clip: Region = None,
|
crop: Region = None,
|
||||||
) -> SegmentLines:
|
) -> SegmentLines:
|
||||||
"""Render a layout.
|
"""Render a layout.
|
||||||
|
|
||||||
@@ -345,8 +343,10 @@ class Layout(ABC):
|
|||||||
width = self.width
|
width = self.width
|
||||||
height = self.height
|
height = self.height
|
||||||
screen = Region(0, 0, width, height)
|
screen = Region(0, 0, width, height)
|
||||||
clip = clip or screen
|
|
||||||
clip_x, clip_y, clip_x2, clip_y2 = clip.corners
|
crop_region = crop or Region(0, 0, self.width, self.height)
|
||||||
|
|
||||||
|
# clip_x, clip_y, clip_x2, clip_y2 = clip.corners
|
||||||
|
|
||||||
divide = Segment.divide
|
divide = Segment.divide
|
||||||
|
|
||||||
@@ -363,27 +363,45 @@ class Layout(ABC):
|
|||||||
]
|
]
|
||||||
# Go through all the renders in reverse order and fill buckets with no render
|
# Go through all the renders in reverse order and fill buckets with no render
|
||||||
renders = self._get_renders(console)
|
renders = self._get_renders(console)
|
||||||
|
clip_y, clip_y2 = crop_region.y_extents
|
||||||
for region, clip, lines in chain(
|
for region, clip, lines in chain(
|
||||||
renders, [(screen, screen, background_render)]
|
renders, [(screen, screen, background_render)]
|
||||||
):
|
):
|
||||||
for y, line in enumerate(lines, region.y):
|
# clip = clip.intersection(crop_region)
|
||||||
|
render_region = region.intersection(clip)
|
||||||
|
for y, line in enumerate(lines, render_region.y):
|
||||||
if clip_y > y > clip_y2:
|
if clip_y > y > clip_y2:
|
||||||
continue
|
continue
|
||||||
first_cut = clamp(region.x, clip_x, clip_x2)
|
# first_cut = clamp(render_region.x, clip_x, clip_x2)
|
||||||
last_cut = clamp(region.x + region.width, clip_x, clip_x2)
|
# last_cut = clamp(render_region.x + render_region.width, clip_x, clip_x2)
|
||||||
|
first_cut = render_region.x
|
||||||
|
last_cut = render_region.x_end
|
||||||
final_cuts = [cut for cut in cuts[y] if (last_cut >= cut >= first_cut)]
|
final_cuts = [cut for cut in cuts[y] if (last_cut >= cut >= first_cut)]
|
||||||
if len(final_cuts) > 1:
|
# final_cuts = cuts[y]
|
||||||
if final_cuts == [region.x, region.x + region.width]:
|
|
||||||
cut_segments = [line]
|
if final_cuts == render_region.y_extents:
|
||||||
else:
|
cut_segments = [line]
|
||||||
relative_cuts = [cut - region.x for cut in final_cuts]
|
else:
|
||||||
_, *cut_segments = divide(line, relative_cuts)
|
render_x = render_region.x
|
||||||
for cut, segments in zip(final_cuts, cut_segments):
|
relative_cuts = [cut - render_x for cut in final_cuts]
|
||||||
if chops[y][cut] is None:
|
_, *cut_segments = divide(line, relative_cuts)
|
||||||
chops[y][cut] = segments
|
for cut, segments in zip(final_cuts, cut_segments):
|
||||||
|
if chops[y][cut] is None:
|
||||||
|
chops[y][cut] = segments
|
||||||
|
|
||||||
# Assemble the cut renders in to lists of segments
|
# Assemble the cut renders in to lists of segments
|
||||||
output_lines = list(self._assemble_chops(chops[clip_y:clip_y2]))
|
output_lines = list(self._assemble_chops(chops))
|
||||||
|
|
||||||
|
def width_view(line: list[Segment]) -> list[Segment]:
|
||||||
|
if line:
|
||||||
|
div_lines = list(Segment.divide(line, [crop_x, crop_x2]))
|
||||||
|
line = div_lines[1] if len(div_lines) > 1 else div_lines[0]
|
||||||
|
return line
|
||||||
|
|
||||||
|
if crop is not None:
|
||||||
|
crop_x, crop_y, crop_x2, crop_y2 = crop_region.corners
|
||||||
|
output_lines = [width_view(line) for line in output_lines[crop_y:crop_y2]]
|
||||||
|
|
||||||
return SegmentLines(output_lines, new_lines=True)
|
return SegmentLines(output_lines, new_lines=True)
|
||||||
|
|
||||||
def __rich_console__(
|
def __rich_console__(
|
||||||
@@ -401,9 +419,9 @@ class Layout(ABC):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.renders[widget] = (region, clip, new_lines)
|
self.renders[widget] = (region, clip, new_lines)
|
||||||
update_lines = self.render(console, region.intersection(clip)).lines
|
|
||||||
|
|
||||||
clipped_region = region.intersection(clip)
|
update_region = region.intersection(clip)
|
||||||
update = LayoutUpdate(update_lines, clipped_region.x, clipped_region.y)
|
update_lines = self.render(console, update_region).lines
|
||||||
|
update = LayoutUpdate(update_lines, update_region.x, update_region.y)
|
||||||
|
|
||||||
return update
|
return update
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from ..widget import Widget
|
|||||||
class VerticalLayout(Layout):
|
class VerticalLayout(Layout):
|
||||||
def __init__(self, *, z: int = 0, gutter: tuple[int, int] | None = None):
|
def __init__(self, *, z: int = 0, gutter: tuple[int, int] | None = None):
|
||||||
self.z = z
|
self.z = z
|
||||||
self.gutter = gutter or (0, 1)
|
self.gutter = gutter or (1, 1)
|
||||||
self._widgets: list[Widget] = []
|
self._widgets: list[Widget] = []
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
@@ -49,8 +49,9 @@ class VerticalLayout(Layout):
|
|||||||
lines = console.render_lines(
|
lines = console.render_lines(
|
||||||
renderable, console.options.update_width(render_width)
|
renderable, console.options.update_width(render_width)
|
||||||
)
|
)
|
||||||
|
|
||||||
region = Region(x, y, render_width, len(lines))
|
region = Region(x, y, render_width, len(lines))
|
||||||
add_widget(widget, region, viewport)
|
add_widget(widget, region - scroll, viewport)
|
||||||
else:
|
else:
|
||||||
add_widget(
|
add_widget(
|
||||||
widget,
|
widget,
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ class MessagePump:
|
|||||||
except CancelledError:
|
except CancelledError:
|
||||||
raise
|
raise
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
self.app.panic(Traceback(show_locals=True))
|
self.app.panic()
|
||||||
break
|
break
|
||||||
finally:
|
finally:
|
||||||
if isinstance(message, events.Event) and self._message_queue.empty():
|
if isinstance(message, events.Event) and self._message_queue.empty():
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ class View(Widget):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def virtual_size(self) -> Dimensions:
|
def virtual_size(self) -> Dimensions:
|
||||||
return self.layout.map.size
|
return self.layout.map.size if self.layout.map else Dimensions(0, 0)
|
||||||
|
|
||||||
# virtual_width: Reactive[int | None] = Reactive(None)
|
# virtual_width: Reactive[int | None] = Reactive(None)
|
||||||
# virtual_height: Reactive[int | None] = Reactive(None)
|
# virtual_height: Reactive[int | None] = Reactive(None)
|
||||||
|
|||||||
@@ -1,17 +1,26 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from rich.console import RenderableType
|
||||||
|
|
||||||
from ..layouts.vertical import VerticalLayout
|
from ..layouts.vertical import VerticalLayout
|
||||||
from ..view import View
|
from ..view import View
|
||||||
from ..widget import Widget
|
from ..widget import Widget
|
||||||
|
from ..widgets import Static
|
||||||
|
|
||||||
|
|
||||||
class WindowView(View, layout=VerticalLayout):
|
class WindowView(View, layout=VerticalLayout):
|
||||||
def __init__(
|
def __init__(
|
||||||
self, *, gutter: tuple[int, int] = (1, 1), name: str | None = None
|
self,
|
||||||
|
widget: RenderableType | Widget,
|
||||||
|
*,
|
||||||
|
gutter: tuple[int, int] = (1, 1),
|
||||||
|
name: str | None = None
|
||||||
) -> None:
|
) -> None:
|
||||||
self.gutter = gutter
|
self.gutter = gutter
|
||||||
super().__init__(name=name)
|
layout = VerticalLayout()
|
||||||
|
layout.add(widget if isinstance(widget, Widget) else Static(widget))
|
||||||
|
super().__init__(name=name, layout=layout)
|
||||||
|
|
||||||
async def update(self, widget: Widget) -> None:
|
async def update(self, widget: Widget | RenderableType) -> None:
|
||||||
self.layout = VerticalLayout(gutter=self.gutter)
|
self.layout = VerticalLayout(gutter=self.gutter)
|
||||||
self.layout.add(widget)
|
self.layout.add(widget if isinstance(widget, Widget) else Static(widget))
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from rich import box
|
from rich import box
|
||||||
from rich.align import Align
|
from rich.align import Align
|
||||||
from rich.console import RenderableType
|
from rich.console import RenderableType
|
||||||
@@ -19,6 +21,11 @@ class Placeholder(Widget, can_focus=True):
|
|||||||
has_focus: Reactive[bool] = Reactive(False)
|
has_focus: Reactive[bool] = Reactive(False)
|
||||||
mouse_over: Reactive[bool] = Reactive(False)
|
mouse_over: Reactive[bool] = Reactive(False)
|
||||||
style: Reactive[str] = Reactive("")
|
style: Reactive[str] = Reactive("")
|
||||||
|
height: Reactive[int | None] = Reactive(None)
|
||||||
|
|
||||||
|
def __init__(self, *, name: str | None = None, height: int | None = None) -> None:
|
||||||
|
super().__init__(name=name)
|
||||||
|
self.height = height
|
||||||
|
|
||||||
def __rich_repr__(self) -> rich.repr.RichReprResult:
|
def __rich_repr__(self) -> rich.repr.RichReprResult:
|
||||||
yield "name", self.name
|
yield "name", self.name
|
||||||
@@ -34,6 +41,7 @@ class Placeholder(Widget, can_focus=True):
|
|||||||
border_style="green" if self.mouse_over else "blue",
|
border_style="green" if self.mouse_over else "blue",
|
||||||
box=box.HEAVY if self.has_focus else box.ROUNDED,
|
box=box.HEAVY if self.has_focus else box.ROUNDED,
|
||||||
style=self.style,
|
style=self.style,
|
||||||
|
height=self.height,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def on_focus(self, event: events.Focus) -> None:
|
async def on_focus(self, event: events.Focus) -> None:
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ from ..scrollbar import ScrollTo, ScrollBar
|
|||||||
from ..geometry import clamp
|
from ..geometry import clamp
|
||||||
from ..page import Page
|
from ..page import Page
|
||||||
from ..view import View
|
from ..view import View
|
||||||
|
|
||||||
|
|
||||||
from ..reactive import Reactive
|
from ..reactive import Reactive
|
||||||
|
|
||||||
|
|
||||||
@@ -23,10 +25,12 @@ class ScrollView(View):
|
|||||||
style: StyleType = "",
|
style: StyleType = "",
|
||||||
fluid: bool = True,
|
fluid: bool = True,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
from ..views import WindowView
|
||||||
|
|
||||||
self.fluid = fluid
|
self.fluid = fluid
|
||||||
self.vscroll = ScrollBar(vertical=True)
|
self.vscroll = ScrollBar(vertical=True)
|
||||||
self.hscroll = ScrollBar(vertical=False)
|
self.hscroll = ScrollBar(vertical=False)
|
||||||
self.page = Page(renderable or "", style=style)
|
self.window = WindowView(renderable or "")
|
||||||
layout = GridLayout()
|
layout = GridLayout()
|
||||||
layout.add_column("main")
|
layout.add_column("main")
|
||||||
layout.add_column("vscroll", size=1)
|
layout.add_column("vscroll", size=1)
|
||||||
@@ -46,33 +50,33 @@ class ScrollView(View):
|
|||||||
target_y: Reactive[float] = Reactive(0, repaint=False)
|
target_y: Reactive[float] = Reactive(0, repaint=False)
|
||||||
|
|
||||||
def validate_x(self, value: float) -> float:
|
def validate_x(self, value: float) -> float:
|
||||||
return clamp(value, 0, self.page.virtual_size.width - self.size.width)
|
return clamp(value, 0, self.window.virtual_size.width - self.size.width)
|
||||||
|
|
||||||
def validate_target_x(self, value: float) -> float:
|
def validate_target_x(self, value: float) -> float:
|
||||||
return clamp(value, 0, self.page.virtual_size.width - self.size.width)
|
return clamp(value, 0, self.window.virtual_size.width - self.size.width)
|
||||||
|
|
||||||
def validate_y(self, value: float) -> float:
|
def validate_y(self, value: float) -> float:
|
||||||
return clamp(value, 0, self.page.virtual_size.height - self.size.height)
|
return clamp(value, 0, self.window.virtual_size.height - self.size.height)
|
||||||
|
|
||||||
def validate_target_y(self, value: float) -> float:
|
def validate_target_y(self, value: float) -> float:
|
||||||
return clamp(value, 0, self.page.virtual_size.height - self.size.height)
|
return clamp(value, 0, self.window.virtual_size.height - self.size.height)
|
||||||
|
|
||||||
async def watch_x(self, new_value: float) -> None:
|
async def watch_x(self, new_value: float) -> None:
|
||||||
self.page.scroll_x = round(new_value)
|
self.window.scroll_x = round(new_value)
|
||||||
self.hscroll.position = round(new_value)
|
self.hscroll.position = round(new_value)
|
||||||
|
|
||||||
async def watch_y(self, new_value: float) -> None:
|
async def watch_y(self, new_value: float) -> None:
|
||||||
self.page.scroll_y = round(new_value)
|
self.window.scroll_y = round(new_value)
|
||||||
self.vscroll.position = round(new_value)
|
self.vscroll.position = round(new_value)
|
||||||
|
|
||||||
async def update(self, renderabe: RenderableType) -> None:
|
async def update(self, renderabe: RenderableType) -> None:
|
||||||
self.page.update(renderabe)
|
await self.window.update(renderabe)
|
||||||
self.require_repaint()
|
self.require_repaint()
|
||||||
|
|
||||||
async def on_mount(self, event: events.Mount) -> None:
|
async def on_mount(self, event: events.Mount) -> None:
|
||||||
assert isinstance(self.layout, GridLayout)
|
assert isinstance(self.layout, GridLayout)
|
||||||
self.layout.place(
|
self.layout.place(
|
||||||
content=self.page,
|
content=self.window,
|
||||||
vscroll=self.vscroll,
|
vscroll=self.vscroll,
|
||||||
hscroll=self.hscroll,
|
hscroll=self.hscroll,
|
||||||
)
|
)
|
||||||
@@ -132,7 +136,7 @@ class ScrollView(View):
|
|||||||
|
|
||||||
async def key_end(self) -> None:
|
async def key_end(self) -> None:
|
||||||
self.target_x = 0
|
self.target_x = 0
|
||||||
self.target_y = self.page.virtual_size.height - self.size.height
|
self.target_y = self.window.virtual_size.height - self.size.height
|
||||||
self.animate("x", self.target_x, duration=1, easing="out_cubic")
|
self.animate("x", self.target_x, duration=1, easing="out_cubic")
|
||||||
self.animate("y", self.target_y, duration=1, easing="out_cubic")
|
self.animate("y", self.target_y, duration=1, easing="out_cubic")
|
||||||
|
|
||||||
@@ -143,8 +147,9 @@ class ScrollView(View):
|
|||||||
self.animate("y", self.target_y, duration=1, easing="out_cubic")
|
self.animate("y", self.target_y, duration=1, easing="out_cubic")
|
||||||
|
|
||||||
async def on_resize(self, event: events.Resize) -> None:
|
async def on_resize(self, event: events.Resize) -> None:
|
||||||
|
return
|
||||||
if self.fluid:
|
if self.fluid:
|
||||||
self.page.update()
|
self.window.update()
|
||||||
|
|
||||||
async def message_scroll_up(self, message: Message) -> None:
|
async def message_scroll_up(self, message: Message) -> None:
|
||||||
self.page_up()
|
self.page_up()
|
||||||
@@ -169,16 +174,16 @@ class ScrollView(View):
|
|||||||
async def message_page_update(self, message: Message) -> None:
|
async def message_page_update(self, message: Message) -> None:
|
||||||
self.x = self.validate_x(self.x)
|
self.x = self.validate_x(self.x)
|
||||||
self.y = self.validate_y(self.y)
|
self.y = self.validate_y(self.y)
|
||||||
self.vscroll.virtual_size = self.page.virtual_size.height
|
self.vscroll.virtual_size = self.window.virtual_size.height
|
||||||
self.vscroll.window_size = self.size.height
|
self.vscroll.window_size = self.size.height
|
||||||
|
|
||||||
assert isinstance(self.layout, GridLayout)
|
assert isinstance(self.layout, GridLayout)
|
||||||
|
|
||||||
if self.layout.show_column(
|
if self.layout.show_column(
|
||||||
"vscroll", self.page.virtual_size.height > self.size.height
|
"vscroll", self.window.virtual_size.height > self.size.height
|
||||||
):
|
):
|
||||||
self.require_layout()
|
self.require_layout()
|
||||||
if self.layout.show_row(
|
if self.layout.show_row(
|
||||||
"hscroll", self.page.virtual_size.width > self.size.width
|
"hscroll", self.window.virtual_size.width > self.size.width
|
||||||
):
|
):
|
||||||
self.require_layout()
|
self.require_layout()
|
||||||
|
|||||||
Reference in New Issue
Block a user