Skip to content

Commit 4d4fb73

Browse files
authored
Merge pull request #5097 from Textualize/constrain-tooltips
constrain both axis
2 parents d7eae41 + 9935b64 commit 4d4fb73

16 files changed

+327
-133
lines changed

CHANGELOG.md

+9-4
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,22 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
88
## Unreleased
99

1010

11+
### Added
12+
13+
- Added support for A-F to Digits widget https://github.com/Textualize/textual/pull/5094
14+
- Added `Region.constrain` https://github.com/Textualize/textual/pull/5097
15+
- Added support for A-F to Digits widget https://github.com/Textualize/textual/pull/5094
16+
1117
### Changed
1218

1319
- `Screen.ALLOW_IN_MAXIMIZED_VIEW` will now default to `App.ALLOW_IN_MAXIMIZED_VIEW` https://github.com/Textualize/textual/pull/5088
1420
- Widgets matching `.-textual-system` will now be included in the maximize view by default https://github.com/Textualize/textual/pull/5088
1521
- Digits are now thin by default, style with text-style: bold to get bold digits https://github.com/Textualize/textual/pull/5094
22+
- Made `Widget.absolute_offset` public https://github.com/Textualize/textual/pull/5097
23+
- Tooltips are now displayed directly below the mouse cursor https://github.com/Textualize/textual/pull/5097
24+
- `Region.inflect` will now assume that margins overlap https://github.com/Textualize/textual/pull/5097
1625
- `Pilot.click` and friends will now accept a widget, in addition to a selector https://github.com/Textualize/textual/pull/5095
1726

18-
### Added
19-
20-
- Added support for A-F to Digits widget https://github.com/Textualize/textual/pull/5094
21-
2227
## [0.82.0] - 2024-10-03
2328

2429
### Fixed

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

+26-43
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,38 +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 = styles.constrain
539-
if constrain == "inflect":
540-
inflect_margin = styles.margin
541-
margin_region = region.grow(inflect_margin)
542-
region = region.inflect(
543-
(-1 if margin_region.right > constrain_region.right else 0),
544-
(-1 if margin_region.bottom > constrain_region.bottom else 0),
545-
inflect_margin,
546-
)
547-
region = region.translate_inside(constrain_region, True, True)
548-
elif constrain != "none":
549-
# Constrain to avoid clipping
550-
region = region.translate_inside(
551-
constrain_region,
552-
constrain in ("x", "both"),
553-
constrain in ("y", "both"),
554-
)
555-
return region
556-
557524
def _arrange_root(
558525
self, root: Widget, size: Size, visible_only: bool = True
559526
) -> tuple[CompositorMap, set[Widget]]:
@@ -688,10 +655,17 @@ def add_widget(
688655

689656
widget_order = order + ((layer_index, z, layer_order),)
690657

691-
if overlay and sub_widget.styles.constrain != "none":
692-
widget_region = self._constrain(
693-
sub_widget.styles, widget_region, no_clip
694-
)
658+
if overlay:
659+
styles = sub_widget.styles
660+
has_rule = styles.has_rule
661+
if has_rule("constrain_x") or has_rule("constrain_y"):
662+
widget_region = widget_region.constrain(
663+
styles.constrain_x,
664+
styles.constrain_y,
665+
styles.margin,
666+
no_clip,
667+
)
668+
695669
if widget._cover_widget is None:
696670
add_widget(
697671
sub_widget,
@@ -739,13 +713,22 @@ def add_widget(
739713

740714
widget_region = region + layout_offset
741715

742-
if widget._absolute_offset is not None:
743-
widget_region = widget_region.reset_offset.translate(
744-
widget._absolute_offset + widget.styles.margin.top_left
716+
if widget.absolute_offset is not None:
717+
margin = styles.margin
718+
widget_region = widget_region.at_offset(
719+
widget.absolute_offset + margin.top_left
720+
)
721+
widget_region = widget_region.translate(
722+
styles.offset.resolve(widget_region.grow(margin).size, size)
723+
)
724+
has_rule = styles.has_rule
725+
if has_rule("constrain_x") or has_rule("constrain_y"):
726+
widget_region = widget_region.constrain(
727+
styles.constrain_x,
728+
styles.constrain_y,
729+
styles.margin,
730+
size.region,
745731
)
746-
747-
if styles.constrain != "none":
748-
widget_region = self._constrain(styles, widget_region, no_clip)
749732

750733
map[widget._render_widget] = _MapGeometry(
751734
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_builder.py

+34
Original file line numberDiff line numberDiff line change
@@ -1047,6 +1047,40 @@ def process_overlay(self, name: str, tokens: list[Token]) -> None:
10471047
self.styles._rules[name] = value # type: ignore
10481048

10491049
def process_constrain(self, name: str, tokens: list[Token]) -> None:
1050+
if len(tokens) == 1:
1051+
try:
1052+
value = self._process_enum(name, tokens, VALID_CONSTRAIN)
1053+
except StyleValueError:
1054+
self.error(
1055+
name,
1056+
tokens[0],
1057+
string_enum_help_text(name, VALID_CONSTRAIN, context="css"),
1058+
)
1059+
else:
1060+
self.styles._rules["constrain_x"] = value # type: ignore
1061+
self.styles._rules["constrain_y"] = value # type: ignore
1062+
elif len(tokens) == 2:
1063+
constrain_x, constrain_y = self._process_enum_multiple(
1064+
name, tokens, VALID_CONSTRAIN, 2
1065+
)
1066+
self.styles._rules["constrain_x"] = constrain_x # type: ignore
1067+
self.styles._rules["constrain_y"] = constrain_y # type: ignore
1068+
else:
1069+
self.error(name, tokens[0], "one or two values expected here")
1070+
1071+
def process_constrain_x(self, name: str, tokens: list[Token]) -> None:
1072+
try:
1073+
value = self._process_enum(name, tokens, VALID_CONSTRAIN)
1074+
except StyleValueError:
1075+
self.error(
1076+
name,
1077+
tokens[0],
1078+
string_enum_help_text(name, VALID_CONSTRAIN, context="css"),
1079+
)
1080+
else:
1081+
self.styles._rules[name] = value # type: ignore
1082+
1083+
def process_constrain_y(self, name: str, tokens: list[Token]) -> None:
10501084
try:
10511085
value = self._process_enum(name, tokens, VALID_CONSTRAIN)
10521086
except StyleValueError:

src/textual/css/constants.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@
7474
"nocolor",
7575
}
7676
VALID_OVERLAY: Final = {"none", "screen"}
77-
VALID_CONSTRAIN: Final = {"x", "y", "both", "inflect", "none"}
77+
VALID_CONSTRAIN: Final = {"inflect", "inside", "none"}
7878
VALID_KEYLINE: Final = {"none", "thin", "heavy", "double"}
7979
VALID_HATCH: Final = {"left", "right", "cross", "vertical", "horizontal"}
8080
HATCHES: Final = {

src/textual/css/styles.py

+20-4
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,8 @@ class RulesMap(TypedDict, total=False):
190190
hatch: tuple[str, Color] | Literal["none"]
191191

192192
overlay: Overlay
193-
constrain: Constrain
193+
constrain_x: Constrain
194+
constrain_y: Constrain
194195

195196

196197
RULE_NAMES = list(RulesMap.__annotations__.keys())
@@ -450,7 +451,12 @@ class StylesBase:
450451
overlay = StringEnumProperty(
451452
VALID_OVERLAY, "none", layout=True, refresh_parent=True
452453
)
453-
constrain = 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+
)
454460

455461
def __textual_animation__(
456462
self,
@@ -1172,8 +1178,18 @@ def append_declaration(name: str, value: str) -> None:
11721178
append_declaration("subtitle-text-style", str(self.border_subtitle_style))
11731179
if "overlay" in rules:
11741180
append_declaration("overlay", str(self.overlay))
1175-
if "constrain" in rules:
1176-
append_declaration("constrain", str(self.constrain))
1181+
if "constrain_x" in rules and "constrain_y" in rules:
1182+
if self.constrain_x == self.constrain_y:
1183+
append_declaration("constrain", self.constrain_x)
1184+
else:
1185+
append_declaration(
1186+
"constrain", f"{self.constrain_x} {self.constrain_y}"
1187+
)
1188+
elif "constrain_x" in rules:
1189+
append_declaration("constrain-x", self.constrain_x)
1190+
elif "constrain_y" in rules:
1191+
append_declaration("constrain-y", self.constrain_y)
1192+
11771193
if "keyline" in rules:
11781194
keyline_type, keyline_color = self.keyline
11791195
if keyline_type != "none":

src/textual/css/types.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
Overflow = Literal["scroll", "hidden", "auto"]
3737
EdgeStyle = Tuple[EdgeType, Color]
3838
TextAlign = Literal["left", "start", "center", "right", "end", "justify"]
39-
Constrain = Literal["none", "x", "y", "both"]
39+
Constrain = Literal["none", "inflect", "inside"]
4040
Overlay = Literal["none", "screen"]
4141

4242
Specificity3 = Tuple[int, int, int]

0 commit comments

Comments
 (0)