Skip to content

Commit 3794d95

Browse files
committed
change to inflect
1 parent 5f53538 commit 3794d95

File tree

9 files changed

+253
-125
lines changed

9 files changed

+253
-125
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1010
### Added
1111

1212
- Added support for A-F to Digits widget https://github.com/Textualize/textual/pull/5094
13+
- Added `Region.constrain` https://github.com/Textualize/textual/pull/5097
1314

1415
### Changed
1516

1617
- Digits are now thin by default, style with text-style: bold to get bold digits https://github.com/Textualize/textual/pull/5094
1718
- Made `Widget.absolute_offset` public https://github.com/Textualize/textual/pull/5097
19+
- Tooltips are now displayed directly below the mouse cursor https://github.com/Textualize/textual/pull/5097
20+
- `Region.inflect` will now assume that margins overlap https://github.com/Textualize/textual/pull/5097
1821

1922
## [0.82.0] - 2024-10-03
2023

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ pytest-textual-snapshot = "^1.0.0"
7979
[tool.pytest.ini_options]
8080
asyncio_mode = "auto"
8181
testpaths = ["tests"]
82-
addopts = "--strict-markers"
82+
addopts = "--strict-markers -vv"
8383
markers = [
8484
"syntax: marks tests that require syntax highlighting (deselect with '-m \"not syntax\"')",
8585
]

src/textual/_compositor.py

+14-47
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@
4040
if TYPE_CHECKING:
4141
from typing_extensions import TypeAlias
4242

43-
from textual.css.styles import RenderStyles
4443
from textual.screen import Screen
4544
from textual.widget import Widget
4645

@@ -522,47 +521,6 @@ def visible_widgets(self) -> dict[Widget, tuple[Region, Region]]:
522521
}
523522
return self._visible_widgets
524523

525-
def _constrain(
526-
self, styles: RenderStyles, region: Region, constrain_region: Region
527-
) -> Region:
528-
"""Applies constrain logic to a Region.
529-
530-
Args:
531-
styles: The widget's styles.
532-
region: The region to constrain.
533-
constrain_region: The outer region.
534-
535-
Returns:
536-
New region.
537-
"""
538-
constrain_x = styles.constrain_x
539-
constrain_y = styles.constrain_y
540-
541-
inflect_margin = styles.margin
542-
margin_region = region.grow(inflect_margin)
543-
544-
region = region.inflect(
545-
(
546-
(-1 if margin_region.right > constrain_region.right else 0)
547-
if constrain_x == "inflect"
548-
else 0
549-
),
550-
(
551-
(-1 if margin_region.bottom > constrain_region.bottom else 0)
552-
if constrain_y == "inflect"
553-
else 0
554-
),
555-
inflect_margin,
556-
)
557-
558-
region = region.translate_inside(
559-
constrain_region.shrink(styles.margin),
560-
constrain_x != "none",
561-
constrain_y != "none",
562-
)
563-
564-
return region
565-
566524
def _arrange_root(
567525
self, root: Widget, size: Size, visible_only: bool = True
568526
) -> tuple[CompositorMap, set[Widget]]:
@@ -698,10 +656,14 @@ def add_widget(
698656
widget_order = order + ((layer_index, z, layer_order),)
699657

700658
if overlay:
701-
has_rule = sub_widget.styles.has_rule
659+
styles = sub_widget.styles
660+
has_rule = styles.has_rule
702661
if has_rule("constrain_x") or has_rule("constrain_y"):
703-
widget_region = self._constrain(
704-
sub_widget.styles, widget_region, no_clip
662+
widget_region = widget_region.constrain(
663+
styles.constrain_x,
664+
styles.constrain_y,
665+
styles.margin,
666+
no_clip,
705667
)
706668

707669
if widget._cover_widget is None:
@@ -753,15 +715,20 @@ def add_widget(
753715

754716
if widget.absolute_offset is not None:
755717
margin = styles.margin
756-
widget_region = widget_region.reset_offset.translate(
718+
widget_region = widget_region.at_offset(
757719
widget.absolute_offset + margin.top_left
758720
)
759721
widget_region = widget_region.translate(
760722
styles.offset.resolve(widget_region.grow(margin).size, size)
761723
)
762724
has_rule = styles.has_rule
763725
if has_rule("constrain_x") or has_rule("constrain_y"):
764-
widget_region = self._constrain(styles, widget_region, no_clip)
726+
widget_region = widget_region.constrain(
727+
styles.constrain_x,
728+
styles.constrain_y,
729+
styles.margin,
730+
size.region,
731+
)
765732

766733
map[widget._render_widget] = _MapGeometry(
767734
widget_region,

src/textual/css/_style_properties.py

+7-4
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868

6969
PropertyGetType = TypeVar("PropertyGetType")
7070
PropertySetType = TypeVar("PropertySetType")
71+
EnumType = TypeVar("EnumType", covariant=True)
7172

7273

7374
class GenericProperty(Generic[PropertyGetType, PropertySetType]):
@@ -773,7 +774,7 @@ def __set__(
773774
obj.refresh(layout=True)
774775

775776

776-
class StringEnumProperty:
777+
class StringEnumProperty(Generic[EnumType]):
777778
"""Descriptor for getting and setting string properties and ensuring that the set
778779
value belongs in the set of valid values.
779780
@@ -787,7 +788,7 @@ class StringEnumProperty:
787788
def __init__(
788789
self,
789790
valid_values: set[str],
790-
default: str,
791+
default: EnumType,
791792
layout: bool = False,
792793
refresh_children: bool = False,
793794
refresh_parent: bool = False,
@@ -801,7 +802,9 @@ def __init__(
801802
def __set_name__(self, owner: StylesBase, name: str) -> None:
802803
self.name = name
803804

804-
def __get__(self, obj: StylesBase, objtype: type[StylesBase] | None = None) -> str:
805+
def __get__(
806+
self, obj: StylesBase, objtype: type[StylesBase] | None = None
807+
) -> EnumType:
805808
"""Get the string property, or the default value if it's not set.
806809
807810
Args:
@@ -816,7 +819,7 @@ def __get__(self, obj: StylesBase, objtype: type[StylesBase] | None = None) -> s
816819
def _before_refresh(self, obj: StylesBase, value: str | None) -> None:
817820
"""Do any housekeeping before asking for a layout refresh after a value change."""
818821

819-
def __set__(self, obj: StylesBase, value: str | None = None):
822+
def __set__(self, obj: StylesBase, value: EnumType | None = None):
820823
"""Set the string property and ensure it is in the set of allowed values.
821824
822825
Args:

src/textual/css/styles.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -451,8 +451,12 @@ class StylesBase:
451451
overlay = StringEnumProperty(
452452
VALID_OVERLAY, "none", layout=True, refresh_parent=True
453453
)
454-
constrain_x = StringEnumProperty(VALID_CONSTRAIN, "none")
455-
constrain_y = StringEnumProperty(VALID_CONSTRAIN, "none")
454+
constrain_x: StringEnumProperty[Constrain] = StringEnumProperty(
455+
VALID_CONSTRAIN, "none"
456+
)
457+
constrain_y: StringEnumProperty[Constrain] = StringEnumProperty(
458+
VALID_CONSTRAIN, "none"
459+
)
456460

457461
def __textual_animation__(
458462
self,

src/textual/geometry.py

+103-9
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
TYPE_CHECKING,
1212
Any,
1313
Collection,
14+
Literal,
1415
NamedTuple,
1516
Tuple,
1617
TypeVar,
@@ -986,20 +987,23 @@ def inflect(
986987
A positive value will move the region right or down, a negative value will move
987988
the region left or up. A value of `0` will leave that axis unmodified.
988989
990+
If a margin is provided, it will add space between the resulting region.
991+
992+
Note that if margin is specified it *overlaps*, so the space will be the maximum
993+
of two edges, and not the total.
994+
989995
```
990996
╔══════════╗ │
991997
║ ║
992998
║ Self ║ │
993999
║ ║
9941000
╚══════════╝ │
9951001
996-
─ ─ ─ ─ ─ ─ ─ ─ ┼ ─ ─ ─ ─ ─ ─ ─ ─
997-
998-
│ ┌──────────┐
999-
│ │
1000-
│ │ Result │
1001-
│ │
1002-
│ └──────────┘
1002+
─ ─ ─ ─ ─ ─ ─ ─ ┌──────────┐
1003+
│ │
1004+
│ Result │
1005+
│ │
1006+
└──────────┘
10031007
```
10041008
10051009
Args:
@@ -1013,11 +1017,89 @@ def inflect(
10131017
inflect_margin = NULL_SPACING if margin is None else margin
10141018
x, y, width, height = self
10151019
if x_axis:
1016-
x += (width + inflect_margin.width) * x_axis
1020+
x += (width + inflect_margin.max_width) * x_axis
10171021
if y_axis:
1018-
y += (height + inflect_margin.height) * y_axis
1022+
y += (height + inflect_margin.max_height) * y_axis
10191023
return Region(x, y, width, height)
10201024

1025+
def constrain(
1026+
self,
1027+
constrain_x: Literal["none", "inside", "inflect"],
1028+
constrain_y: Literal["none", "inside", "inflect"],
1029+
margin: Spacing,
1030+
container: Region,
1031+
) -> Region:
1032+
"""Constrain a region to fit within a container, using different methods per axis.
1033+
1034+
Args:
1035+
constrain_x: Constrain method for the X-axis.
1036+
constrain_y: Constrain method for the Y-axis.
1037+
margin: Margin to maintain around region.
1038+
container: Container to constrain to.
1039+
1040+
Returns:
1041+
New widget, that fits inside the container (if possible).
1042+
"""
1043+
margin_region = self.grow(margin)
1044+
region = self
1045+
1046+
def compare_span(
1047+
span_start: int, span_end: int, container_start: int, container_end: int
1048+
) -> int:
1049+
"""Compare a span with a container
1050+
1051+
Args:
1052+
span_start: Start of the span.
1053+
span_end: end of the span.
1054+
container_start: Start of the container.
1055+
container_end: End of the container.
1056+
1057+
Returns:
1058+
0 if the span fits, -1 if it is less that the container, otherwise +1
1059+
"""
1060+
if span_start >= container_start and span_end <= container_end:
1061+
return 0
1062+
if span_start < container_start:
1063+
return -1
1064+
return +1
1065+
1066+
# Apply any inflected constraints
1067+
if constrain_x == "inflect" or constrain_y == "inflect":
1068+
region = region.inflect(
1069+
(
1070+
-compare_span(
1071+
margin_region.x,
1072+
margin_region.right,
1073+
container.x,
1074+
container.right,
1075+
)
1076+
if constrain_x == "inflect"
1077+
else 0
1078+
),
1079+
(
1080+
-compare_span(
1081+
margin_region.y,
1082+
margin_region.bottom,
1083+
container.y,
1084+
container.bottom,
1085+
)
1086+
if constrain_y == "inflect"
1087+
else 0
1088+
),
1089+
margin,
1090+
)
1091+
1092+
# Apply translate inside constrains
1093+
# Note this is also applied, if a previous inflect constrained has been applied
1094+
# This is so that the origin is always inside the container
1095+
region = region.translate_inside(
1096+
container.shrink(margin),
1097+
constrain_x != "none",
1098+
constrain_y != "none",
1099+
)
1100+
1101+
return region
1102+
10211103

10221104
class Spacing(NamedTuple):
10231105
"""Stores spacing around a widget, such as padding and border.
@@ -1072,6 +1154,18 @@ def height(self) -> int:
10721154
"""Total space in the y axis."""
10731155
return self.top + self.bottom
10741156

1157+
@property
1158+
def max_width(self) -> int:
1159+
"""The space between regions in the X direction if margins overlap, i.e. `max(self.left, self.right)`."""
1160+
_top, right, _bottom, left = self
1161+
return left if left > right else right
1162+
1163+
@property
1164+
def max_height(self) -> int:
1165+
"""The space between regions in the Y direction if margins overlap, i.e. `max(self.top, self.bottom)`."""
1166+
top, _right, bottom, _left = self
1167+
return top if top > bottom else bottom
1168+
10751169
@property
10761170
def top_left(self) -> tuple[int, int]:
10771171
"""A pair of integers for the left, and top space."""

src/textual/widgets/_tooltip.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ class Tooltip(Static, inherit_css=False):
77
DEFAULT_CSS = """
88
Tooltip {
99
layer: _tooltips;
10-
margin: 1 2;
10+
margin: 1 0;
1111
padding: 1 2;
1212
background: $background;
1313
width: auto;

0 commit comments

Comments
 (0)