mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Merge branch 'css' into compositor-granularity
This commit is contained in:
@@ -14,7 +14,7 @@ App.-show-focus *:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.list-item {
|
.list-item {
|
||||||
height: 10;
|
height: 6;
|
||||||
color: #12a0;
|
color: #12a0;
|
||||||
background: #ffffff00;
|
background: #ffffff00;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,17 +50,17 @@ class ReflowResult(NamedTuple):
|
|||||||
resized: set[Widget] # Widgets that have been resized
|
resized: set[Widget] # Widgets that have been resized
|
||||||
|
|
||||||
|
|
||||||
class RenderRegion(NamedTuple):
|
class MapGeometry(NamedTuple):
|
||||||
"""Defines the absolute location of a Widget."""
|
"""Defines the absolute location of a Widget."""
|
||||||
|
|
||||||
region: Region # The region occupied by the widget
|
region: Region # The region occupied by the widget
|
||||||
order: tuple[int, ...] # A tuple of ints defining the painting order
|
order: tuple[int, ...] # A tuple of ints defining the painting order
|
||||||
clip: Region # A region to clip the widget by (if a Widget is within a container)
|
clip: Region # A region to clip the widget by (if a Widget is within a container)
|
||||||
virtual_size: Size # The virtual size (scrollable region) of a widget if it is a container
|
virtual_size: Size # The virtual size (scrollable region) of a widget if it is a container
|
||||||
container_size: Size # The container size (area no occupied by scrollbars)
|
container_size: Size # The container size (area not occupied by scrollbars)
|
||||||
|
|
||||||
|
|
||||||
RenderRegionMap: TypeAlias = "dict[Widget, RenderRegion]"
|
CompositorMap: TypeAlias = "dict[Widget, MapGeometry]"
|
||||||
|
|
||||||
|
|
||||||
@rich.repr.auto
|
@rich.repr.auto
|
||||||
@@ -114,7 +114,7 @@ class Compositor:
|
|||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
# A mapping of Widget on to its "render location" (absolute position / depth)
|
# A mapping of Widget on to its "render location" (absolute position / depth)
|
||||||
self.map: RenderRegionMap = {}
|
self.map: CompositorMap = {}
|
||||||
|
|
||||||
# All widgets considered in the arrangement
|
# All widgets considered in the arrangement
|
||||||
# Note this may be a superset of self.map.keys() as some widgets may be invisible for various reasons
|
# Note this may be a superset of self.map.keys() as some widgets may be invisible for various reasons
|
||||||
@@ -221,7 +221,7 @@ class Compositor:
|
|||||||
resized=resized_widgets,
|
resized=resized_widgets,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _arrange_root(self, root: Widget) -> tuple[RenderRegionMap, set[Widget]]:
|
def _arrange_root(self, root: Widget) -> tuple[CompositorMap, set[Widget]]:
|
||||||
"""Arrange a widgets children based on its layout attribute.
|
"""Arrange a widgets children based on its layout attribute.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -234,7 +234,7 @@ class Compositor:
|
|||||||
|
|
||||||
ORIGIN = Offset(0, 0)
|
ORIGIN = Offset(0, 0)
|
||||||
size = root.size
|
size = root.size
|
||||||
map: RenderRegionMap = {}
|
map: CompositorMap = {}
|
||||||
widgets: set[Widget] = set()
|
widgets: set[Widget] = set()
|
||||||
get_order = attrgetter("order")
|
get_order = attrgetter("order")
|
||||||
|
|
||||||
@@ -303,7 +303,7 @@ class Compositor:
|
|||||||
for chrome_widget, chrome_region in widget._arrange_scrollbars(
|
for chrome_widget, chrome_region in widget._arrange_scrollbars(
|
||||||
container_size
|
container_size
|
||||||
):
|
):
|
||||||
map[chrome_widget] = RenderRegion(
|
map[chrome_widget] = MapGeometry(
|
||||||
chrome_region + container_region.origin + layout_offset,
|
chrome_region + container_region.origin + layout_offset,
|
||||||
order,
|
order,
|
||||||
clip,
|
clip,
|
||||||
@@ -312,7 +312,7 @@ class Compositor:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Add the container widget, which will render a background
|
# Add the container widget, which will render a background
|
||||||
map[widget] = RenderRegion(
|
map[widget] = MapGeometry(
|
||||||
region + layout_offset,
|
region + layout_offset,
|
||||||
order,
|
order,
|
||||||
clip,
|
clip,
|
||||||
@@ -322,7 +322,7 @@ class Compositor:
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
# Add the widget to the map
|
# Add the widget to the map
|
||||||
map[widget] = RenderRegion(
|
map[widget] = MapGeometry(
|
||||||
region + layout_offset, order, clip, region.size, container_size
|
region + layout_offset, order, clip, region.size, container_size
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -392,8 +392,8 @@ class Compositor:
|
|||||||
return segment.style or Style.null()
|
return segment.style or Style.null()
|
||||||
return Style.null()
|
return Style.null()
|
||||||
|
|
||||||
def get_widget_region(self, widget: Widget) -> Region:
|
def find_widget(self, widget: Widget) -> MapGeometry:
|
||||||
"""Get the Region of a Widget contained in this Layout.
|
"""Get information regarding the relative position of a widget in the Compositor.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
widget (Widget): The Widget in this layout you wish to know the Region of.
|
widget (Widget): The Widget in this layout you wish to know the Region of.
|
||||||
@@ -402,11 +402,11 @@ class Compositor:
|
|||||||
NoWidget: If the Widget is not contained in this Layout.
|
NoWidget: If the Widget is not contained in this Layout.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Region: The Region of the Widget.
|
MapGeometry: Widget's composition information.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
region, *_ = self.map[widget]
|
region = self.map[widget]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise errors.NoWidget("Widget is not in layout")
|
raise errors.NoWidget("Widget is not in layout")
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -241,7 +241,13 @@ class StylesBase(ABC):
|
|||||||
Returns:
|
Returns:
|
||||||
Spacing: Space around widget.
|
Spacing: Space around widget.
|
||||||
"""
|
"""
|
||||||
spacing = Spacing() + self.padding + self.border.spacing
|
spacing = self.padding + self.border.spacing
|
||||||
|
return spacing
|
||||||
|
|
||||||
|
@property
|
||||||
|
def content_gutter(self) -> Spacing:
|
||||||
|
"""The spacing that surrounds the content area of the widget."""
|
||||||
|
spacing = self.padding + self.border.spacing + self.margin
|
||||||
return spacing
|
return spacing
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
|||||||
@@ -395,9 +395,9 @@ class Blur(Event, bubble=False):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class DescendantFocus(Event, bubble=True):
|
class DescendantFocus(Event, verbosity=2, bubble=True):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class DescendantBlur(Event, bubble=True):
|
class DescendantBlur(Event, verbosity=2, bubble=True):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -275,6 +275,24 @@ class Region(NamedTuple):
|
|||||||
"""Get the start point of the region."""
|
"""Get the start point of the region."""
|
||||||
return Offset(self.x, self.y)
|
return Offset(self.x, self.y)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bottom_left(self) -> Offset:
|
||||||
|
"""Bottom left offset of the region."""
|
||||||
|
x, y, _width, height = self
|
||||||
|
return Offset(x, y + height)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def top_right(self) -> Offset:
|
||||||
|
"""Top right offset of the region."""
|
||||||
|
x, y, width, _height = self
|
||||||
|
return Offset(x + width, y)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bottom_right(self) -> Offset:
|
||||||
|
"""Bottom right of the region."""
|
||||||
|
x, y, width, height = self
|
||||||
|
return Offset(x + width, y + height)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def size(self) -> Size:
|
def size(self) -> Size:
|
||||||
"""Get the size of the region."""
|
"""Get the size of the region."""
|
||||||
@@ -292,17 +310,17 @@ class Region(NamedTuple):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def x_range(self) -> range:
|
def x_range(self) -> range:
|
||||||
"""A range object for X coordinates"""
|
"""A range object for X coordinates."""
|
||||||
return range(self.x, self.x + self.width)
|
return range(self.x, self.x + self.width)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def y_range(self) -> range:
|
def y_range(self) -> range:
|
||||||
"""A range object for Y coordinates"""
|
"""A range object for Y coordinates."""
|
||||||
return range(self.y, self.y + self.height)
|
return range(self.y, self.y + self.height)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def reset_origin(self) -> Region:
|
def reset_origin(self) -> Region:
|
||||||
"""An region of the same size at the origin."""
|
"""An region of the same size at (0, 0)."""
|
||||||
_, _, width, height = self
|
_, _, width, height = self
|
||||||
return Region(0, 0, width, height)
|
return Region(0, 0, width, height)
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ from rich.style import Style
|
|||||||
from . import events, messages, errors
|
from . import events, messages, errors
|
||||||
|
|
||||||
from .geometry import Offset, Region
|
from .geometry import Offset, Region
|
||||||
from ._compositor import Compositor
|
from ._compositor import Compositor, MapGeometry
|
||||||
from ._timer import Timer
|
|
||||||
from .reactive import Reactive
|
from .reactive import Reactive
|
||||||
from .widget import Widget
|
from .widget import Widget
|
||||||
|
|
||||||
@@ -77,7 +76,7 @@ class Screen(Widget):
|
|||||||
"""
|
"""
|
||||||
return self._compositor.get_style_at(x, y)
|
return self._compositor.get_style_at(x, y)
|
||||||
|
|
||||||
def get_widget_region(self, widget: Widget) -> Region:
|
def find_widget(self, widget: Widget) -> MapGeometry:
|
||||||
"""Get the screen region of a Widget.
|
"""Get the screen region of a Widget.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -86,7 +85,7 @@ class Screen(Widget):
|
|||||||
Returns:
|
Returns:
|
||||||
Region: Region relative to screen.
|
Region: Region relative to screen.
|
||||||
"""
|
"""
|
||||||
return self._compositor.get_widget_region(widget)
|
return self._compositor.find_widget(widget)
|
||||||
|
|
||||||
def on_idle(self, event: events.Idle) -> None:
|
def on_idle(self, event: events.Idle) -> None:
|
||||||
# Check for any widgets marked as 'dirty' (needs a repaint)
|
# Check for any widgets marked as 'dirty' (needs a repaint)
|
||||||
@@ -165,7 +164,7 @@ class Screen(Widget):
|
|||||||
try:
|
try:
|
||||||
if self.app.mouse_captured:
|
if self.app.mouse_captured:
|
||||||
widget = self.app.mouse_captured
|
widget = self.app.mouse_captured
|
||||||
region = self.get_widget_region(widget)
|
region = self.find_widget(widget).region
|
||||||
else:
|
else:
|
||||||
widget, region = self.get_widget_at(event.x, event.y)
|
widget, region = self.get_widget_at(event.x, event.y)
|
||||||
except errors.NoWidget:
|
except errors.NoWidget:
|
||||||
@@ -204,7 +203,7 @@ class Screen(Widget):
|
|||||||
try:
|
try:
|
||||||
if self.app.mouse_captured:
|
if self.app.mouse_captured:
|
||||||
widget = self.app.mouse_captured
|
widget = self.app.mouse_captured
|
||||||
region = self.get_widget_region(widget)
|
region = self.find_widget(widget).region
|
||||||
else:
|
else:
|
||||||
widget, region = self.get_widget_at(event.x, event.y)
|
widget, region = self.get_widget_at(event.x, event.y)
|
||||||
except errors.NoWidget:
|
except errors.NoWidget:
|
||||||
|
|||||||
@@ -298,17 +298,21 @@ class Widget(DOMNode):
|
|||||||
y: float | None = None,
|
y: float | None = None,
|
||||||
*,
|
*,
|
||||||
animate: bool = True,
|
animate: bool = True,
|
||||||
|
speed: float | None = None,
|
||||||
|
duration: float | None = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Scroll to a given (absolute) coordinate, optionally animating.
|
"""Scroll to a given (absolute) coordinate, optionally animating.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
scroll_x (int | None, optional): X coordinate (column) to scroll to, or ``None`` for no change. Defaults to None.
|
x (int | None, optional): X coordinate (column) to scroll to, or ``None`` for no change. Defaults to None.
|
||||||
scroll_y (int | None, optional): Y coordinate (row) to scroll to, or ``None`` for no change. Defaults to None.
|
y (int | None, optional): Y coordinate (row) to scroll to, or ``None`` for no change. Defaults to None.
|
||||||
animate (bool, optional): Animate to new scroll position. Defaults to False.
|
animate (bool, optional): Animate to new scroll position. Defaults to False.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the scroll position changed, otherwise False.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
scrolled_x = False
|
scrolled_x = scrolled_y = False
|
||||||
scrolled_y = False
|
|
||||||
|
|
||||||
if animate:
|
if animate:
|
||||||
# TODO: configure animation speed
|
# TODO: configure animation speed
|
||||||
@@ -316,67 +320,142 @@ class Widget(DOMNode):
|
|||||||
self.scroll_target_x = x
|
self.scroll_target_x = x
|
||||||
if x != self.scroll_x:
|
if x != self.scroll_x:
|
||||||
self.animate(
|
self.animate(
|
||||||
"scroll_x", self.scroll_target_x, speed=80, easing="out_cubic"
|
"scroll_x",
|
||||||
|
self.scroll_target_x,
|
||||||
|
speed=speed,
|
||||||
|
duration=duration,
|
||||||
|
easing="out_cubic",
|
||||||
)
|
)
|
||||||
scrolled_x = True
|
scrolled_x = True
|
||||||
if y is not None:
|
if y is not None:
|
||||||
self.scroll_target_y = y
|
self.scroll_target_y = y
|
||||||
if y != self.scroll_y:
|
if y != self.scroll_y:
|
||||||
self.animate(
|
self.animate(
|
||||||
"scroll_y", self.scroll_target_y, speed=80, easing="out_cubic"
|
"scroll_y",
|
||||||
|
self.scroll_target_y,
|
||||||
|
speed=speed,
|
||||||
|
duration=duration,
|
||||||
|
easing="out_cubic",
|
||||||
)
|
)
|
||||||
scrolled_y = True
|
scrolled_y = True
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if x is not None:
|
if x is not None:
|
||||||
self.scroll_target_x = self.scroll_x = x
|
|
||||||
if x != self.scroll_x:
|
if x != self.scroll_x:
|
||||||
|
self.scroll_target_x = self.scroll_x = x
|
||||||
scrolled_x = True
|
scrolled_x = True
|
||||||
if y is not None:
|
if y is not None:
|
||||||
self.scroll_target_y = self.scroll_y = y
|
|
||||||
if y != self.scroll_y:
|
if y != self.scroll_y:
|
||||||
|
self.scroll_target_y = self.scroll_y = y
|
||||||
scrolled_y = True
|
scrolled_y = True
|
||||||
|
if scrolled_x or scrolled_y:
|
||||||
self.refresh(repaint=False, layout=True)
|
self.refresh(repaint=False, layout=True)
|
||||||
|
|
||||||
return scrolled_x or scrolled_y
|
return scrolled_x or scrolled_y
|
||||||
|
|
||||||
def scroll_home(self, animate: bool = True) -> bool:
|
def scroll_relative(
|
||||||
|
self,
|
||||||
|
x: float | None = None,
|
||||||
|
y: float | None = None,
|
||||||
|
*,
|
||||||
|
animate: bool = True,
|
||||||
|
speed: float | None = None,
|
||||||
|
duration: float | None = None,
|
||||||
|
) -> bool:
|
||||||
|
"""Scroll relative to current position.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
x (int | None, optional): X distance (columns) to scroll, or ``None`` for no change. Defaults to None.
|
||||||
|
y (int | None, optional): Y distance (rows) to scroll, or ``None`` for no change. Defaults to None.
|
||||||
|
animate (bool, optional): Animate to new scroll position. Defaults to False.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the scroll position changed, otherwise False.
|
||||||
|
"""
|
||||||
|
return self.scroll_to(
|
||||||
|
None if x is None else (self.scroll_x + x),
|
||||||
|
None if y is None else (self.scroll_y + y),
|
||||||
|
animate=animate,
|
||||||
|
speed=speed,
|
||||||
|
duration=duration,
|
||||||
|
)
|
||||||
|
|
||||||
|
def scroll_home(self, *, animate: bool = True) -> bool:
|
||||||
return self.scroll_to(0, 0, animate=animate)
|
return self.scroll_to(0, 0, animate=animate)
|
||||||
|
|
||||||
def scroll_end(self, animate: bool = True) -> bool:
|
def scroll_end(self, *, animate: bool = True) -> bool:
|
||||||
return self.scroll_to(0, self.max_scroll_y, animate=animate)
|
return self.scroll_to(0, self.max_scroll_y, animate=animate)
|
||||||
|
|
||||||
def scroll_left(self, animate: bool = True) -> bool:
|
def scroll_left(self, *, animate: bool = True) -> bool:
|
||||||
return self.scroll_to(x=self.scroll_target_x - 1, animate=animate)
|
return self.scroll_to(x=self.scroll_target_x - 1, animate=animate)
|
||||||
|
|
||||||
def scroll_right(self, animate: bool = True) -> bool:
|
def scroll_right(self, *, animate: bool = True) -> bool:
|
||||||
return self.scroll_to(x=self.scroll_target_x + 1, animate=animate)
|
return self.scroll_to(x=self.scroll_target_x + 1, animate=animate)
|
||||||
|
|
||||||
def scroll_up(self, animate: bool = True) -> bool:
|
def scroll_up(self, *, animate: bool = True) -> bool:
|
||||||
return self.scroll_to(y=self.scroll_target_y + 1, animate=animate)
|
return self.scroll_to(y=self.scroll_target_y + 1, animate=animate)
|
||||||
|
|
||||||
def scroll_down(self, animate: bool = True) -> bool:
|
def scroll_down(self, *, animate: bool = True) -> bool:
|
||||||
return self.scroll_to(y=self.scroll_target_y - 1, animate=animate)
|
return self.scroll_to(y=self.scroll_target_y - 1, animate=animate)
|
||||||
|
|
||||||
def scroll_page_up(self, animate: bool = True) -> bool:
|
def scroll_page_up(self, *, animate: bool = True) -> bool:
|
||||||
return self.scroll_to(
|
return self.scroll_to(
|
||||||
y=self.scroll_target_y - self.container_size.height, animate=animate
|
y=self.scroll_target_y - self.container_size.height, animate=animate
|
||||||
)
|
)
|
||||||
|
|
||||||
def scroll_page_down(self, animate: bool = True) -> bool:
|
def scroll_page_down(self, *, animate: bool = True) -> bool:
|
||||||
return self.scroll_to(
|
return self.scroll_to(
|
||||||
y=self.scroll_target_y + self.container_size.height, animate=animate
|
y=self.scroll_target_y + self.container_size.height, animate=animate
|
||||||
)
|
)
|
||||||
|
|
||||||
def scroll_page_left(self, animate: bool = True) -> bool:
|
def scroll_page_left(self, *, animate: bool = True) -> bool:
|
||||||
return self.scroll_to(
|
return self.scroll_to(
|
||||||
x=self.scroll_target_x - self.container_size.width, animate=animate
|
x=self.scroll_target_x - self.container_size.width, animate=animate
|
||||||
)
|
)
|
||||||
|
|
||||||
def scroll_page_right(self, animate: bool = True) -> bool:
|
def scroll_page_right(self, *, animate: bool = True) -> bool:
|
||||||
return self.scroll_to(
|
return self.scroll_to(
|
||||||
x=self.scroll_target_x + self.container_size.width, animate=animate
|
x=self.scroll_target_x + self.container_size.width, animate=animate
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def scroll_to_widget(self, widget: Widget, *, animate: bool = True) -> bool:
|
||||||
|
"""Scroll so that a child widget is in the visible area.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
widget (Widget): A Widget in the children.
|
||||||
|
animate (bool, optional): True to animate, or False to jump. Defaults to True.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the scroll position changed, otherwise False.
|
||||||
|
"""
|
||||||
|
screen = self.screen
|
||||||
|
try:
|
||||||
|
widget_geometry = screen.find_widget(widget)
|
||||||
|
container_geometry = screen.find_widget(self)
|
||||||
|
except errors.NoWidget:
|
||||||
|
return False
|
||||||
|
|
||||||
|
widget_region = widget.content_region + widget_geometry.region.origin
|
||||||
|
container_region = self.content_region + container_geometry.region.origin
|
||||||
|
|
||||||
|
if widget_region in container_region:
|
||||||
|
# Widget is visible, nothing to do
|
||||||
|
return False
|
||||||
|
|
||||||
|
# We can either scroll so the widget is at the top of the container, or so that
|
||||||
|
# it is at the bottom. We want to pick which has the shortest distance
|
||||||
|
top_delta = widget_region.origin - container_region.origin
|
||||||
|
bottom_delta = widget_region.origin - (
|
||||||
|
container_region.origin
|
||||||
|
+ Offset(0, container_region.height - widget_region.height)
|
||||||
|
)
|
||||||
|
|
||||||
|
delta_x = min(top_delta.x, bottom_delta.x, key=abs)
|
||||||
|
delta_y = min(top_delta.y, bottom_delta.y, key=abs)
|
||||||
|
return self.scroll_relative(
|
||||||
|
delta_x or None, delta_y or None, animate=animate, duration=0.2
|
||||||
|
)
|
||||||
|
|
||||||
def __init_subclass__(
|
def __init_subclass__(
|
||||||
cls, can_focus: bool = True, can_focus_children: bool = True
|
cls, can_focus: bool = True, can_focus_children: bool = True
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -517,14 +596,28 @@ class Widget(DOMNode):
|
|||||||
def container_size(self) -> Size:
|
def container_size(self) -> Size:
|
||||||
return self._container_size
|
return self._container_size
|
||||||
|
|
||||||
|
@property
|
||||||
|
def content_region(self) -> Region:
|
||||||
|
"""A region relative to the Widget origin that contains the content."""
|
||||||
|
x, y = self.styles.content_gutter.top_left
|
||||||
|
width, height = self._container_size
|
||||||
|
return Region(x, y, width, height)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def content_offset(self) -> Offset:
|
||||||
|
"""An offset from the Widget origin where the content begins."""
|
||||||
|
x, y = self.styles.content_gutter.top_left
|
||||||
|
return Offset(x, y)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def virtual_size(self) -> Size:
|
def virtual_size(self) -> Size:
|
||||||
return self._virtual_size
|
return self._virtual_size
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def region(self) -> Region:
|
def region(self) -> Region:
|
||||||
|
"""The region occupied by this widget, relative to the Screen."""
|
||||||
try:
|
try:
|
||||||
return self.screen._compositor.get_widget_region(self)
|
return self.screen.find_widget(self).region
|
||||||
except errors.NoWidget:
|
except errors.NoWidget:
|
||||||
return Region()
|
return Region()
|
||||||
|
|
||||||
@@ -766,6 +859,8 @@ class Widget(DOMNode):
|
|||||||
|
|
||||||
def on_descendant_focus(self, event: events.DescendantFocus) -> None:
|
def on_descendant_focus(self, event: events.DescendantFocus) -> None:
|
||||||
self.descendant_has_focus = True
|
self.descendant_has_focus = True
|
||||||
|
if self.is_container and isinstance(event.sender, Widget):
|
||||||
|
self.scroll_to_widget(event.sender, animate=True)
|
||||||
|
|
||||||
def on_descendant_blur(self, event: events.DescendantBlur) -> None:
|
def on_descendant_blur(self, event: events.DescendantBlur) -> None:
|
||||||
self.descendant_has_focus = False
|
self.descendant_has_focus = False
|
||||||
|
|||||||
@@ -143,6 +143,18 @@ def test_region_origin():
|
|||||||
assert Region(1, 2, 3, 4).origin == Offset(1, 2)
|
assert Region(1, 2, 3, 4).origin == Offset(1, 2)
|
||||||
|
|
||||||
|
|
||||||
|
def test_region_bottom_left():
|
||||||
|
assert Region(1, 2, 3, 4).bottom_left == Offset(1, 6)
|
||||||
|
|
||||||
|
|
||||||
|
def test_region_top_right():
|
||||||
|
assert Region(1, 2, 3, 4).top_right == Offset(4, 2)
|
||||||
|
|
||||||
|
|
||||||
|
def test_region_bottom_right():
|
||||||
|
assert Region(1, 2, 3, 4).bottom_right == Offset(4, 6)
|
||||||
|
|
||||||
|
|
||||||
def test_region_add():
|
def test_region_add():
|
||||||
assert Region(1, 2, 3, 4) + (10, 20) == Region(11, 22, 3, 4)
|
assert Region(1, 2, 3, 4) + (10, 20) == Region(11, 22, 3, 4)
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ async def test_composition_of_vertical_container_with_children(
|
|||||||
# root widget checks:
|
# root widget checks:
|
||||||
root_widget = cast(Widget, app.get_child("root"))
|
root_widget = cast(Widget, app.get_child("root"))
|
||||||
assert root_widget.size == expected_screen_size
|
assert root_widget.size == expected_screen_size
|
||||||
root_widget_region = app.screen.get_widget_region(root_widget)
|
root_widget_region = app.screen.find_widget(root_widget).region
|
||||||
assert root_widget_region == (
|
assert root_widget_region == (
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
|
|||||||
Reference in New Issue
Block a user