mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
compositor class
This commit is contained in:
@@ -9,7 +9,7 @@ class BasicApp(App):
|
||||
|
||||
def on_mount(self):
|
||||
"""Build layout here."""
|
||||
self.mount(uber=Widget())
|
||||
self.mount(uber=Placeholder())
|
||||
|
||||
async def on_key(self, event: events.Key) -> None:
|
||||
await self.dispatch_key(event)
|
||||
|
||||
@@ -1,477 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from itertools import chain
|
||||
from operator import attrgetter, itemgetter
|
||||
import sys
|
||||
from typing import Iterator, Iterable, NamedTuple, TYPE_CHECKING
|
||||
|
||||
import rich.repr
|
||||
from rich.console import Console, ConsoleOptions, RenderResult
|
||||
from rich.control import Control
|
||||
from rich.segment import Segment, SegmentLines
|
||||
from rich.style import Style
|
||||
|
||||
from . import log
|
||||
from .geometry import Region, Offset, Size
|
||||
|
||||
from .layout import WidgetPlacement
|
||||
from ._loop import loop_last
|
||||
from ._types import Lines
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
from typing import TypeAlias
|
||||
else: # pragma: no cover
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .widget import Widget
|
||||
|
||||
|
||||
class NoWidget(Exception):
|
||||
"""Raised when there is no widget at the requested coordinate."""
|
||||
|
||||
|
||||
class ReflowResult(NamedTuple):
|
||||
"""The result of a reflow operation. Describes the chances to widgets."""
|
||||
|
||||
hidden: set[Widget]
|
||||
shown: set[Widget]
|
||||
resized: set[Widget]
|
||||
|
||||
|
||||
class RenderRegion(NamedTuple):
|
||||
"""Defines the absolute location of a Widget."""
|
||||
|
||||
region: Region
|
||||
order: tuple[int, ...]
|
||||
clip: Region
|
||||
|
||||
|
||||
RenderRegionMap: TypeAlias = dict[Widget, RenderRegion]
|
||||
|
||||
|
||||
@rich.repr.auto
|
||||
class LayoutUpdate:
|
||||
"""A renderable containing the result of a render for a given region."""
|
||||
|
||||
def __init__(self, lines: Lines, region: Region) -> None:
|
||||
self.lines = lines
|
||||
self.region = region
|
||||
|
||||
def __rich_console__(
|
||||
self, console: Console, options: ConsoleOptions
|
||||
) -> RenderResult:
|
||||
yield Control.home()
|
||||
x = self.region.x
|
||||
new_line = Segment.line()
|
||||
move_to = Control.move_to
|
||||
for last, (y, line) in loop_last(enumerate(self.lines, self.region.y)):
|
||||
yield move_to(x, y)
|
||||
yield from line
|
||||
if not last:
|
||||
yield new_line
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
x, y, width, height = self.region
|
||||
yield "x", x
|
||||
yield "y", y
|
||||
yield "width", width
|
||||
yield "height", height
|
||||
|
||||
|
||||
@rich.repr.auto(angular=True)
|
||||
class Arrangement:
|
||||
"""Responsible for storing information regarding the relative positions of Widgets and rendering them."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
# A mapping of Widget on to its "render location" (absolute position / depth)
|
||||
self.map: RenderRegionMap = {}
|
||||
|
||||
# All widgets considered in the arrangement
|
||||
# Not this may be a supperset of self.map.keys() as some widgets may be invisible for various reasons
|
||||
self.widgets: set[Widget] = set()
|
||||
|
||||
# Dimensions of the arrangement
|
||||
self.width = 0
|
||||
self.height = 0
|
||||
|
||||
self.regions: dict[Widget, tuple[Region, Region]] = {}
|
||||
self._cuts: list[list[int]] | None = None
|
||||
self._require_update: bool = True
|
||||
self.background = ""
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
yield "width", self.width
|
||||
yield "height", self.height
|
||||
yield "widgets", self.widgets
|
||||
|
||||
def check_update(self) -> bool:
|
||||
return self._require_update
|
||||
|
||||
def require_update(self) -> None:
|
||||
self._require_update = True
|
||||
self.reset()
|
||||
self.map.clear()
|
||||
self.widgets.clear()
|
||||
|
||||
def reset_update(self) -> None:
|
||||
self._require_update = False
|
||||
|
||||
def reset(self) -> None:
|
||||
self._cuts = None
|
||||
|
||||
def reflow(self, parent: Widget, size: Size) -> ReflowResult:
|
||||
"""Reflow (layout) widget and its children.
|
||||
|
||||
Args:
|
||||
parent (Widget): The root widget.
|
||||
size (Size): Size of the area to be filled.
|
||||
|
||||
Returns:
|
||||
ReflowResult: Hidden shown and resized widgets
|
||||
"""
|
||||
self.reset()
|
||||
|
||||
self.width = size.width
|
||||
self.height = size.height
|
||||
|
||||
map, virtual_size = self._arrange_root(parent)
|
||||
|
||||
self._require_update = False
|
||||
|
||||
old_widgets = set(self.map.keys())
|
||||
new_widgets = set(map.keys())
|
||||
# Newly visible widgets
|
||||
shown_widgets = new_widgets - old_widgets
|
||||
# Newly hidden widgets
|
||||
hidden_widgets = old_widgets - new_widgets
|
||||
|
||||
self._layout_map = map
|
||||
|
||||
# Copy renders if the size hasn't changed
|
||||
new_renders = {
|
||||
widget: (region, clip) for widget, (region, _order, clip) in map.items()
|
||||
}
|
||||
self.regions = new_renders
|
||||
|
||||
# Widgets with changed size
|
||||
resized_widgets = {
|
||||
widget
|
||||
for widget, (region, *_) in map.items()
|
||||
if widget in old_widgets and widget.size != region.size
|
||||
}
|
||||
|
||||
parent.virtual_size = virtual_size
|
||||
|
||||
return ReflowResult(
|
||||
hidden=hidden_widgets, shown=shown_widgets, resized=resized_widgets
|
||||
)
|
||||
|
||||
def _arrange_root(self, root: Widget) -> tuple[RenderRegionMap, Size]:
|
||||
"""Arrange a widgets children based on its layout attribute.
|
||||
|
||||
Args:
|
||||
root (Widget): Top level widget.
|
||||
|
||||
Returns:
|
||||
map[dict[Widget, RenderRegion], Size]: A mapping of widget on to render region
|
||||
and the "virtual size" (scrollable reason)
|
||||
"""
|
||||
size = Size(self.width, self.height)
|
||||
ORIGIN = Offset(0, 0)
|
||||
|
||||
map: dict[Widget, RenderRegion] = {}
|
||||
|
||||
def add_widget(
|
||||
widget,
|
||||
region: Region,
|
||||
order: tuple[int, ...],
|
||||
clip: Region,
|
||||
):
|
||||
widgets: set[Widget] = set()
|
||||
styles_offset = widget.styles.offset
|
||||
total_region = region
|
||||
layout_offset = (
|
||||
styles_offset.resolve(region.size, clip.size)
|
||||
if styles_offset
|
||||
else ORIGIN
|
||||
)
|
||||
|
||||
map[widget] = RenderRegion(region + layout_offset, order, clip)
|
||||
|
||||
if widget.layout is not None:
|
||||
scroll = widget.scroll
|
||||
total_region = region.size.region
|
||||
sub_clip = clip.intersection(region)
|
||||
|
||||
placements: list[WidgetPlacement] = []
|
||||
add_placement = placements.append
|
||||
iter_arrange = iter(widget.layout.arrange(widget, region.size, scroll))
|
||||
try:
|
||||
while True:
|
||||
add_placement(iter_arrange.__next__())
|
||||
except StopIteration as stop_iteration:
|
||||
widgets.update(stop_iteration.value)
|
||||
|
||||
placements = sorted(
|
||||
[
|
||||
placement.apply_margin()
|
||||
for placement in widget.layout.arrange(
|
||||
widget, region.size, scroll
|
||||
)
|
||||
],
|
||||
key=attrgetter("order"),
|
||||
)
|
||||
for sub_region, sub_widget, z in placements:
|
||||
total_region = total_region.union(sub_region)
|
||||
if sub_widget is not None:
|
||||
add_widget(
|
||||
sub_widget,
|
||||
sub_region + region.origin - scroll,
|
||||
sub_widget.z + (z,),
|
||||
sub_clip,
|
||||
)
|
||||
return total_region.size
|
||||
|
||||
virtual_size = add_widget(root, size.region, (), size.region)
|
||||
return map, virtual_size
|
||||
|
||||
async def mount_all(self, view: "View") -> None:
|
||||
view.mount(*self.widgets)
|
||||
|
||||
def __iter__(self) -> Iterator[tuple[Widget, Region, Region]]:
|
||||
layers = sorted(self.map.items(), key=lambda item: item[1].order, reverse=True)
|
||||
for widget, (region, order, clip) in layers:
|
||||
yield widget, region.intersection(clip), region
|
||||
|
||||
def get_offset(self, widget: Widget) -> Offset:
|
||||
"""Get the offset of a widget."""
|
||||
try:
|
||||
return self.map[widget].region.origin
|
||||
except KeyError:
|
||||
raise NoWidget("Widget is not in layout")
|
||||
|
||||
def get_widget_at(self, x: int, y: int) -> tuple[Widget, Region]:
|
||||
"""Get the widget under the given point or None."""
|
||||
for widget, cropped_region, region in self:
|
||||
if widget.is_visual and cropped_region.contains(x, y):
|
||||
return widget, region
|
||||
raise NoWidget(f"No widget under screen coordinate ({x}, {y})")
|
||||
|
||||
def get_style_at(self, x: int, y: int) -> Style:
|
||||
"""Get the Style at the given cell or Style.null()
|
||||
|
||||
Args:
|
||||
x (int): X position within the Layout
|
||||
y (int): Y position within the Layout
|
||||
|
||||
Returns:
|
||||
Style: The Style at the cell (x, y) within the Layout
|
||||
"""
|
||||
try:
|
||||
widget, region = self.get_widget_at(x, y)
|
||||
except NoWidget:
|
||||
return Style.null()
|
||||
if widget not in self.regions:
|
||||
return Style.null()
|
||||
lines = widget._get_lines()
|
||||
x -= region.x
|
||||
y -= region.y
|
||||
line = lines[y]
|
||||
end = 0
|
||||
for segment in line:
|
||||
end += segment.cell_length
|
||||
if x < end:
|
||||
return segment.style or Style.null()
|
||||
return Style.null()
|
||||
|
||||
def get_widget_region(self, widget: Widget) -> Region:
|
||||
"""Get the Region of a Widget contained in this Layout.
|
||||
|
||||
Args:
|
||||
widget (Widget): The Widget in this layout you wish to know the Region of.
|
||||
|
||||
Raises:
|
||||
NoWidget: If the Widget is not contained in this Layout.
|
||||
|
||||
Returns:
|
||||
Region: The Region of the Widget.
|
||||
|
||||
"""
|
||||
try:
|
||||
region, *_ = self.map[widget]
|
||||
except KeyError:
|
||||
raise NoWidget("Widget is not in layout")
|
||||
else:
|
||||
return region
|
||||
|
||||
@property
|
||||
def cuts(self) -> list[list[int]]:
|
||||
"""Get vertical cuts.
|
||||
|
||||
A cut is every point on a line where a widget starts or ends.
|
||||
|
||||
Returns:
|
||||
list[list[int]]: A list of cuts for every line.
|
||||
"""
|
||||
if self._cuts is not None:
|
||||
return self._cuts
|
||||
width = self.width
|
||||
height = self.height
|
||||
screen_region = Region(0, 0, width, height)
|
||||
cuts_sets = [{0, width} for _ in range(height)]
|
||||
|
||||
if self.map is not None:
|
||||
for region, order, clip in self.map.values():
|
||||
region = region.intersection(clip)
|
||||
if region and (region in screen_region):
|
||||
region_cuts = region.x_extents
|
||||
for y in region.y_range:
|
||||
cuts_sets[y].update(region_cuts)
|
||||
|
||||
# Sort the cuts for each line
|
||||
self._cuts = [sorted(cut_set) for cut_set in cuts_sets]
|
||||
return self._cuts
|
||||
|
||||
def _get_renders(self, console: Console) -> Iterable[tuple[Region, Region, Lines]]:
|
||||
_rich_traceback_guard = True
|
||||
layout_map = self.map
|
||||
|
||||
if layout_map:
|
||||
widget_regions = sorted(
|
||||
(
|
||||
(widget, region, order, clip)
|
||||
for widget, (region, order, clip) in layout_map.items()
|
||||
),
|
||||
key=itemgetter(2),
|
||||
reverse=True,
|
||||
)
|
||||
else:
|
||||
widget_regions = []
|
||||
|
||||
for widget, region, _order, clip in widget_regions:
|
||||
|
||||
if not (widget.is_visual and widget.visible):
|
||||
continue
|
||||
|
||||
lines = widget._get_lines()
|
||||
|
||||
if region in clip:
|
||||
yield region, clip, lines
|
||||
elif clip.overlaps(region):
|
||||
new_region = region.intersection(clip)
|
||||
delta_x = new_region.x - region.x
|
||||
delta_y = new_region.y - region.y
|
||||
splits = [delta_x, delta_x + new_region.width]
|
||||
lines = lines[delta_y : delta_y + new_region.height]
|
||||
divide = Segment.divide
|
||||
lines = [list(divide(line, splits))[1] for line in lines]
|
||||
yield region, clip, lines
|
||||
|
||||
@classmethod
|
||||
def _assemble_chops(
|
||||
cls, chops: list[dict[int, list[Segment] | None]]
|
||||
) -> Iterable[Iterable[Segment]]:
|
||||
|
||||
from_iterable = chain.from_iterable
|
||||
for bucket in chops:
|
||||
yield from_iterable(
|
||||
line for _, line in sorted(bucket.items()) if line is not None
|
||||
)
|
||||
|
||||
def render(
|
||||
self,
|
||||
console: Console,
|
||||
*,
|
||||
crop: Region = None,
|
||||
) -> SegmentLines:
|
||||
"""Render a layout.
|
||||
|
||||
Args:
|
||||
console (Console): Console instance.
|
||||
clip (Optional[Region]): Region to clip to.
|
||||
|
||||
Returns:
|
||||
SegmentLines: A renderable
|
||||
"""
|
||||
width = self.width
|
||||
height = self.height
|
||||
screen = Region(0, 0, width, height)
|
||||
|
||||
crop_region = crop.intersection(screen) if crop else screen
|
||||
|
||||
_Segment = Segment
|
||||
divide = _Segment.divide
|
||||
|
||||
# Maps each cut on to a list of segments
|
||||
cuts = self.cuts
|
||||
chops: list[dict[int, list[Segment] | None]] = [
|
||||
{cut: None for cut in cut_set} for cut_set in cuts
|
||||
]
|
||||
|
||||
# TODO: Provide an option to update the background
|
||||
background_style = console.get_style(self.background)
|
||||
background_render = [
|
||||
[_Segment(" " * width, background_style)] for _ in range(height)
|
||||
]
|
||||
# Go through all the renders in reverse order and fill buckets with no render
|
||||
renders = list(self._get_renders(console))
|
||||
|
||||
for region, clip, lines in chain(
|
||||
renders, [(screen, screen, background_render)]
|
||||
):
|
||||
render_region = region.intersection(clip)
|
||||
for y, line in zip(render_region.y_range, lines):
|
||||
|
||||
first_cut, last_cut = render_region.x_extents
|
||||
final_cuts = [cut for cut in cuts[y] if (last_cut >= cut >= first_cut)]
|
||||
|
||||
if len(final_cuts) == 2:
|
||||
cut_segments = [line]
|
||||
else:
|
||||
render_x = render_region.x
|
||||
relative_cuts = [cut - render_x for cut in final_cuts]
|
||||
_, *cut_segments = divide(line, relative_cuts)
|
||||
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
|
||||
crop_x, crop_y, crop_x2, crop_y2 = crop_region.corners
|
||||
output_lines = self._assemble_chops(chops[crop_y:crop_y2])
|
||||
|
||||
def width_view(line: list[Segment]) -> list[Segment]:
|
||||
if line:
|
||||
div_lines = list(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 and (crop_x, crop_x2) != (0, self.width):
|
||||
render_lines = [width_view(line) for line in output_lines]
|
||||
else:
|
||||
render_lines = list(output_lines)
|
||||
|
||||
return SegmentLines(render_lines, new_lines=True)
|
||||
|
||||
def __rich_console__(
|
||||
self, console: Console, options: ConsoleOptions
|
||||
) -> RenderResult:
|
||||
yield self.render(console)
|
||||
|
||||
def update_widget(self, console: Console, widget: Widget) -> LayoutUpdate | None:
|
||||
if widget not in self.regions:
|
||||
return None
|
||||
|
||||
region, clip = self.regions[widget]
|
||||
|
||||
if not region.size:
|
||||
return None
|
||||
|
||||
widget.clear_render_cache()
|
||||
|
||||
update_region = region.intersection(clip)
|
||||
update_lines = self.render(console, crop=update_region).lines
|
||||
update = LayoutUpdate(update_lines, update_region)
|
||||
log(update)
|
||||
return update
|
||||
@@ -213,10 +213,10 @@ class App(DOMNode):
|
||||
@classmethod
|
||||
def run(
|
||||
cls,
|
||||
console: Console = None,
|
||||
console: Console | None = None,
|
||||
screen: bool = True,
|
||||
driver: Type[Driver] = None,
|
||||
loop: AbstractEventLoop = None,
|
||||
driver: Type[Driver] | None = None,
|
||||
loop: AbstractEventLoop | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Run the app.
|
||||
@@ -519,7 +519,6 @@ class App(DOMNode):
|
||||
self.panic()
|
||||
|
||||
def display(self, renderable: RenderableType) -> None:
|
||||
sync_available = os.environ.get("TERM_PROGRAM", "") != "Apple_Terminal"
|
||||
if not self._closed:
|
||||
console = self.console
|
||||
try:
|
||||
|
||||
@@ -19,6 +19,7 @@ from .message_pump import MessagePump
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .css.query import DOMQuery
|
||||
from .view import View
|
||||
|
||||
|
||||
class NoParent(Exception):
|
||||
@@ -71,6 +72,19 @@ class DOMNode(MessagePump):
|
||||
assert isinstance(self._parent, DOMNode)
|
||||
return self._parent
|
||||
|
||||
@property
|
||||
def view(self) -> "View":
|
||||
"""Get the current view."""
|
||||
# Get the node by looking up a chain of parents
|
||||
# Note that self.view may not be the same as self.app.view
|
||||
from .view import View
|
||||
|
||||
node = self
|
||||
while node and not isinstance(node, View):
|
||||
node = node._parent
|
||||
assert isinstance(node, View)
|
||||
return node
|
||||
|
||||
@property
|
||||
def id(self) -> str | None:
|
||||
"""The ID of this node, or None if the node has no ID.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import ClassVar, Generator, Iterable, NamedTuple, TYPE_CHECKING
|
||||
from typing import ClassVar, Generator, Iterable, NamedTuple, Sequence, TYPE_CHECKING
|
||||
|
||||
|
||||
from .geometry import Region, Offset, Size
|
||||
@@ -48,7 +48,7 @@ class Layout(ABC):
|
||||
@abstractmethod
|
||||
def arrange(
|
||||
self, parent: View, size: Size, scroll: Offset
|
||||
) -> Generator[WidgetPlacement, None, set[Widget]]:
|
||||
) -> tuple[list[WidgetPlacement], set[Widget]]:
|
||||
"""Generate a layout map that defines where on the screen the widgets will be drawn.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from typing import Generator, Iterable, TYPE_CHECKING, NamedTuple, Sequence
|
||||
from typing import TYPE_CHECKING, NamedTuple, Sequence
|
||||
|
||||
from .._layout_resolve import layout_resolve
|
||||
from ..css.types import Edge
|
||||
@@ -61,7 +61,7 @@ class DockLayout(Layout):
|
||||
|
||||
def arrange(
|
||||
self, parent: Widget, size: Size, scroll: Offset
|
||||
) -> Generator[WidgetPlacement, None, set[Widget]]:
|
||||
) -> tuple[list[WidgetPlacement], set[Widget]]:
|
||||
|
||||
width, height = size
|
||||
layout_region = Region(0, 0, width, height)
|
||||
@@ -87,8 +87,8 @@ class DockLayout(Layout):
|
||||
)
|
||||
)
|
||||
|
||||
Placement = WidgetPlacement
|
||||
|
||||
placements: list[WidgetPlacement] = []
|
||||
add_placement = placements.append
|
||||
arranged_widgets: set[Widget] = set()
|
||||
|
||||
for edge, widgets, z in docks:
|
||||
@@ -112,7 +112,9 @@ class DockLayout(Layout):
|
||||
if not new_size:
|
||||
break
|
||||
total += new_size
|
||||
yield Placement(Region(x, render_y, width, new_size), widget, z)
|
||||
add_placement(
|
||||
WidgetPlacement(Region(x, render_y, width, new_size), widget, z)
|
||||
)
|
||||
render_y += new_size
|
||||
remaining = max(0, remaining - new_size)
|
||||
region = Region(x, y + total, width, height - total)
|
||||
@@ -127,8 +129,10 @@ class DockLayout(Layout):
|
||||
if not new_size:
|
||||
break
|
||||
total += new_size
|
||||
yield Placement(
|
||||
Region(x, render_y - new_size, width, new_size), widget, z
|
||||
add_placement(
|
||||
WidgetPlacement(
|
||||
Region(x, render_y - new_size, width, new_size), widget, z
|
||||
)
|
||||
)
|
||||
render_y -= new_size
|
||||
remaining = max(0, remaining - new_size)
|
||||
@@ -144,7 +148,11 @@ class DockLayout(Layout):
|
||||
if not new_size:
|
||||
break
|
||||
total += new_size
|
||||
yield Placement(Region(render_x, y, new_size, height), widget, z)
|
||||
add_placement(
|
||||
WidgetPlacement(
|
||||
Region(render_x, y, new_size, height), widget, z
|
||||
)
|
||||
)
|
||||
render_x += new_size
|
||||
remaining = max(0, remaining - new_size)
|
||||
region = Region(x + total, y, width - total, height)
|
||||
@@ -159,8 +167,10 @@ class DockLayout(Layout):
|
||||
if not new_size:
|
||||
break
|
||||
total += new_size
|
||||
yield Placement(
|
||||
Region(render_x - new_size, y, new_size, height), widget, z
|
||||
add_placement(
|
||||
WidgetPlacement(
|
||||
Region(render_x - new_size, y, new_size, height), widget, z
|
||||
)
|
||||
)
|
||||
render_x -= new_size
|
||||
remaining = max(0, remaining - new_size)
|
||||
@@ -168,4 +178,4 @@ class DockLayout(Layout):
|
||||
|
||||
layers[z] = region
|
||||
|
||||
return arranged_widgets
|
||||
return placements, arranged_widgets
|
||||
|
||||
@@ -16,6 +16,7 @@ from .message import Message
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .app import App
|
||||
from .view import View
|
||||
|
||||
|
||||
class NoParent(Exception):
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .widget import Widget
|
||||
|
||||
from rich.console import RenderableType
|
||||
import rich.repr
|
||||
from rich.style import Style
|
||||
|
||||
|
||||
from . import events, messages
|
||||
|
||||
from .geometry import Offset, Region
|
||||
from ._compositor import Compositor
|
||||
from .widget import Widget
|
||||
from .renderables.gradient import VerticalGradient
|
||||
|
||||
|
||||
@rich.repr.auto
|
||||
@@ -15,3 +23,143 @@ class View(Widget):
|
||||
docks: _default=top;
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, name: str | None = None, id: str | None = None) -> None:
|
||||
super().__init__(name=name, id=id)
|
||||
self._compositor = Compositor()
|
||||
|
||||
@property
|
||||
def is_visual(self) -> bool:
|
||||
return False
|
||||
|
||||
def render(self) -> RenderableType:
|
||||
return VerticalGradient("#11998e", "#38ef7d")
|
||||
|
||||
def get_offset(self, widget: Widget) -> Offset:
|
||||
"""Get the absolute offset of a given Widget.
|
||||
|
||||
Args:
|
||||
widget (Widget): A widget
|
||||
|
||||
Returns:
|
||||
Offset: The widget's offset relative to the top left of the terminal.
|
||||
"""
|
||||
return self._compositor.get_offset(widget)
|
||||
|
||||
def get_widget_at(self, x: int, y: int) -> tuple[Widget, Region]:
|
||||
"""Get the widget at a given coordinate.
|
||||
|
||||
Args:
|
||||
x (int): X Coordinate.
|
||||
y (int): Y Coordinate.
|
||||
|
||||
Returns:
|
||||
tuple[Widget, Region]: Widget and screen region.
|
||||
"""
|
||||
return self._compositor.get_widget_at(x, y)
|
||||
|
||||
def get_style_add(self, x: int, y: int) -> Style:
|
||||
"""Get the style under a given coordinate.
|
||||
|
||||
Args:
|
||||
x (int): X Coordinate.
|
||||
y (int): Y Coordinate.
|
||||
|
||||
Returns:
|
||||
Style: Rich Style object
|
||||
"""
|
||||
return self._compositor.get_style_at(x, y)
|
||||
|
||||
def get_widget_region(self, widget: Widget) -> Region:
|
||||
"""Get the screen region of a Widget.
|
||||
|
||||
Args:
|
||||
widget (Widget): A Widget within the composition.
|
||||
|
||||
Returns:
|
||||
Region: Region relative to screen.
|
||||
"""
|
||||
return self._compositor.get_widget_region(widget)
|
||||
|
||||
async def refresh_layout(self) -> None:
|
||||
|
||||
# await self._compositor.mount_all(self)
|
||||
|
||||
if not self.size:
|
||||
return
|
||||
|
||||
try:
|
||||
hidden, shown, resized = self._compositor.reflow(self, self.size)
|
||||
|
||||
for widget in hidden:
|
||||
widget.post_message_no_wait(events.Hide(self))
|
||||
for widget in shown:
|
||||
widget.post_message_no_wait(events.Show(self))
|
||||
|
||||
send_resize = shown
|
||||
send_resize.update(resized)
|
||||
|
||||
for widget, region, unclipped_region in self._compositor:
|
||||
widget._update_size(unclipped_region.size)
|
||||
if widget in send_resize:
|
||||
widget.post_message_no_wait(
|
||||
events.Resize(self, unclipped_region.size)
|
||||
)
|
||||
|
||||
except Exception:
|
||||
self.app.panic()
|
||||
|
||||
self.app.refresh()
|
||||
|
||||
async def handle_update(self, message: messages.Update) -> None:
|
||||
message.stop()
|
||||
widget = message.widget
|
||||
assert isinstance(widget, Widget)
|
||||
|
||||
display_update = self._compositor.update_widget(self.console, widget)
|
||||
if display_update is not None:
|
||||
self.app.display(display_update)
|
||||
|
||||
async def handle_layout(self, message: messages.Layout) -> None:
|
||||
message.stop()
|
||||
await self.refresh_layout()
|
||||
self.app.refresh()
|
||||
|
||||
async def on_resize(self, event: events.Resize) -> None:
|
||||
event.stop()
|
||||
self._update_size(event.size)
|
||||
await self.refresh_layout()
|
||||
|
||||
async def on_idle(self, event: events.Idle) -> None:
|
||||
if self._compositor.check_update():
|
||||
self._compositor.reset_update()
|
||||
await self.refresh_layout()
|
||||
|
||||
async def _on_mouse_move(self, event: events.MouseMove) -> None:
|
||||
|
||||
try:
|
||||
if self.app.mouse_captured:
|
||||
widget = self.app.mouse_captured
|
||||
region = self.get_widget_region(widget)
|
||||
else:
|
||||
widget, region = self.get_widget_at(event.x, event.y)
|
||||
except NoWidget:
|
||||
await self.app.set_mouse_over(None)
|
||||
else:
|
||||
await self.app.set_mouse_over(widget)
|
||||
await widget.forward_event(
|
||||
events.MouseMove(
|
||||
self,
|
||||
event.x - region.x,
|
||||
event.y - region.y,
|
||||
event.delta_x,
|
||||
event.delta_y,
|
||||
event.button,
|
||||
event.shift,
|
||||
event.meta,
|
||||
event.ctrl,
|
||||
screen_x=event.screen_x,
|
||||
screen_y=event.screen_y,
|
||||
style=event.style,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -59,7 +59,7 @@ class Widget(DOMNode):
|
||||
can_focus: bool = False
|
||||
|
||||
DEFAULT_STYLES = """
|
||||
|
||||
dock: _default
|
||||
"""
|
||||
|
||||
def __init__(self, name: str | None = None, id: str | None = None) -> None:
|
||||
@@ -116,6 +116,7 @@ class Widget(DOMNode):
|
||||
"""
|
||||
|
||||
renderable = self.render()
|
||||
self.log(renderable)
|
||||
|
||||
styles = self.styles
|
||||
|
||||
|
||||
Reference in New Issue
Block a user