Skip to content

Commit 4901bba

Browse files
committed
added get_style_at and get_widget_at
1 parent 41438bd commit 4901bba

File tree

4 files changed

+63
-31
lines changed

4 files changed

+63
-31
lines changed

src/textual/app.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from . import events
1919
from . import actions
2020
from ._animator import Animator
21-
from .geometry import Point
21+
from .geometry import Point, Region
2222
from ._context import active_app
2323
from .keys import Binding
2424
from .driver import Driver
@@ -258,6 +258,9 @@ def display(self, renderable: RenderableType) -> None:
258258
except Exception:
259259
log.exception("display failed")
260260

261+
def get_widget_at(self, x: int, y: int) -> tuple[Widget, Region]:
262+
return self.view.get_widget_at(x, y)
263+
261264
async def on_event(self, event: events.Event) -> None:
262265
if isinstance(event, events.Key):
263266
binding = self._bindings.get(event.key, None)

src/textual/layout.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from ._profile import timer
1919
from ._types import Lines
2020

21-
from .geometry import clamp, Region
21+
from .geometry import clamp, Region, Point
2222

2323
log = logging.getLogger("rich")
2424

@@ -85,12 +85,34 @@ def __iter__(self) -> Iterable[tuple[Widget, Region]]:
8585
for widget, (region, _) in layers:
8686
yield widget, region
8787

88+
def get_offset(self, widget: Widget) -> Point:
89+
try:
90+
return self._layout_map[widget].region.origin
91+
except KeyError:
92+
raise NoWidget("Widget is not in layout")
93+
8894
def get_widget_at(self, x: int, y: int) -> tuple[Widget, Region]:
8995
"""Get the widget under the given point or None."""
9096
for widget, region in self:
9197
if region.contains(x, y):
9298
return widget, region
93-
raise NoWidget
99+
raise NoWidget(f"No widget under screen coordinate ({x}, {y})")
100+
101+
def get_style_at(self, x: int, y: int) -> Style:
102+
try:
103+
widget, region = self.get_widget_at(x, y)
104+
except NoWidget:
105+
return Style.null()
106+
_region, lines = self.renders[widget]
107+
x -= region.x
108+
y -= region.y
109+
line = lines[y]
110+
end = 0
111+
for segment in line:
112+
end += segment.cell_length
113+
if x < end:
114+
return segment.style or Style.null()
115+
return Style.null()
94116

95117
@property
96118
def cuts(self) -> list[list[int]]:

src/textual/view.py

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@
1010
from rich.region import Region as LayoutRegion
1111
import rich.repr
1212
from rich.segment import Segments
13+
from rich.style import Style
1314

1415
from . import events
1516
from ._context import active_app
1617
from .layout import Layout, NoWidget
1718
from .layouts.dock import DockEdge, DockLayout, Dock
18-
from .geometry import Dimensions, Region
19+
from .geometry import Dimensions, Point, Region
1920
from .messages import UpdateMessage, LayoutMessage
2021

2122
from .widget import StaticWidget, Widget, WidgetID, Widget
@@ -57,6 +58,9 @@ def is_mounted(self, widget: Widget) -> bool:
5758
def render(self) -> RenderableType:
5859
return self.layout
5960

61+
def get_offset(self, widget: Widget) -> Point:
62+
return self.layout.get_offset(widget)
63+
6064
async def message_update(self, message: UpdateMessage) -> None:
6165
widget = message.widget
6266
assert isinstance(widget, Widget)
@@ -109,9 +113,15 @@ async def on_resize(self, event: events.Resize) -> None:
109113
self.size = Dimensions(event.width, event.height)
110114
await self.refresh_layout()
111115

116+
def get_widget_at(self, x: int, y: int) -> tuple[Widget, Region]:
117+
return self.layout.get_widget_at(x, y)
118+
119+
def get_style_at(self, x: int, y: int) -> Style:
120+
return self.layout.get_style_at(x, y)
121+
112122
async def _on_mouse_move(self, event: events.MouseMove) -> None:
113123
try:
114-
widget, region = self.layout.get_widget_at(event.x, event.y)
124+
widget, region = self.get_widget_at(event.x, event.y)
115125
except NoWidget:
116126
await self.app.set_mouse_over(None)
117127
else:
@@ -140,26 +150,24 @@ async def forward_event(self, event: events.Event) -> None:
140150
await self._on_mouse_move(event)
141151

142152
elif isinstance(event, events.MouseEvent):
143-
pass
144-
# try:
145-
# widget, region = self.get_widget_at(event.x, event.y, deep=True)
146-
# except NoWidget:
147-
# if isinstance(event, events.MouseDown):
148-
# await self.app.set_focus(None)
149-
# else:
150-
# if isinstance(event, events.MouseDown):
151-
# await self.app.set_focus(widget)
152-
# await widget.forward_event(event.offset(-region.x, -region.y))
153+
154+
try:
155+
widget, region = self.get_widget_at(event.x, event.y)
156+
except NoWidget:
157+
pass
158+
else:
159+
if isinstance(event, events.MouseDown) and widget.can_focus:
160+
await self.app.set_focus(widget)
161+
await widget.forward_event(event.offset(-region.x, -region.y))
153162

154163
elif isinstance(event, (events.MouseScrollDown, events.MouseScrollUp)):
155-
pass
156-
# try:
157-
# widget, _region = self.get_widget_at(event.x, event.y, deep=True)
158-
# except NoWidget:
159-
# return
160-
# scroll_widget = widget or self.focused
161-
# if scroll_widget is not None:
162-
# await scroll_widget.forward_event(event)
164+
try:
165+
widget, _region = self.get_widget_at(event.x, event.y)
166+
except NoWidget:
167+
return
168+
scroll_widget = widget or self.focused
169+
if scroll_widget is not None:
170+
await scroll_widget.forward_event(event)
163171
else:
164172
if self.focused is not None:
165173
await self.focused.forward_event(event)

src/textual/widget.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,10 @@ def reset_check_repaint(self) -> None:
177177
def reset_check_layout(self) -> None:
178178
self._layout_required = False
179179

180+
def get_style_at(self, x: int, y: int) -> Style:
181+
offset_x, offset_y = self.root_view.get_offset(self)
182+
return self.root_view.get_style_at(x + offset_x, y + offset_y)
183+
180184
async def forward_event(self, event: events.Event) -> None:
181185
await self.post_message(event)
182186

@@ -235,14 +239,9 @@ async def focus(self) -> None:
235239
async def capture_mouse(self, capture: bool = True) -> None:
236240
await self.app.capture_mouse(self if capture else None)
237241

238-
def get_style_at(self, x: int, y: int) -> Style:
239-
return
240-
return self.line_cache.get_style_at(x, y)
241-
242-
# async def on_mouse_move(self, event: events.MouseMove) -> None:
243-
# style_under_cursor = self.get_style_at(event.x, event.y)
244-
# if style_under_cursor:
245-
# log.debug("%r", style_under_cursor)
242+
async def on_mouse_move(self, event: events.MouseMove) -> None:
243+
style_under_cursor = self.get_style_at(event.x, event.y)
244+
log.debug("%r", style_under_cursor)
246245

247246
# async def on_mouse_up(self, event: events.MouseUp) -> None:
248247
# style = self.get_style_at(event.x, event.y)

0 commit comments

Comments
 (0)