Skip to content

Commit 4cd2efd

Browse files
committed
clickable style
1 parent 0635d6b commit 4cd2efd

File tree

7 files changed

+73
-31
lines changed

7 files changed

+73
-31
lines changed

src/textual/_line_cache.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55

66
from typing import Iterable
77

8+
from rich.cells import cell_len
89
from rich.console import Console, ConsoleOptions, RenderableType, RenderResult
910
from rich.control import Control
1011
from rich.segment import Segment
12+
from rich.style import Style
1113

1214
from ._loop import loop_last
1315

@@ -57,3 +59,15 @@ def render(self, x: int, y: int, width: int, height: int) -> Iterable[Segment]:
5759
if not last:
5860
yield new_line
5961
self._dirty[:] = [False] * len(self.lines)
62+
63+
def get_style_at(self, x: int, y: int) -> Style:
64+
try:
65+
line = self.lines[y]
66+
except IndexError:
67+
return Style.null()
68+
end = 0
69+
for segment in line:
70+
end += cell_len(segment.text)
71+
if x < end:
72+
return segment.style or Style.null()
73+
return Style.null()

src/textual/app.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,9 @@ async def action_quit(self) -> None:
276276
async def action_bang(self) -> None:
277277
1 / 0
278278

279+
async def action_bell(self) -> None:
280+
self.console.bell()
281+
279282

280283
if __name__ == "__main__":
281284
import asyncio

src/textual/events.py

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
11
from __future__ import annotations
22

3-
from dataclasses import dataclass, field
4-
import re
5-
from enum import auto, Enum
6-
from time import monotonic
7-
from typing import ClassVar, TYPE_CHECKING
3+
from typing import TYPE_CHECKING
84

95
from rich.repr import rich_repr, RichReprResult
106

117
from .message import Message
12-
from ._types import Callback, MessageTarget
8+
from ._types import MessageTarget
139
from .keys import Keys
1410

1511

@@ -137,15 +133,31 @@ def __rich_repr__(self) -> RichReprResult:
137133
yield "meta", self.meta, False
138134
yield "ctrl", self.ctrl, False
139135

136+
def offset(self, x: int, y: int):
137+
return self.__class__(
138+
self.sender,
139+
x=self.x + x,
140+
y=self.y + y,
141+
button=self.button,
142+
shift=self.shift,
143+
meta=self.meta,
144+
ctrl=self.ctrl,
145+
screen_x=self.screen_x,
146+
screen_y=self.screen_y,
147+
)
140148

149+
150+
@rich_repr
141151
class MouseMove(MouseEvent):
142152
pass
143153

144154

155+
@rich_repr
145156
class MouseDown(MouseEvent):
146157
pass
147158

148159

160+
@rich_repr
149161
class MouseUp(MouseEvent):
150162
pass
151163

@@ -199,14 +211,14 @@ class Leave(Event):
199211
pass
200212

201213

202-
class Focus(Event, type=EventType.FOCUS):
214+
class Focus(Event):
203215
pass
204216

205217

206-
class Blur(Event, type=EventType.BLUR):
218+
class Blur(Event):
207219
pass
208220

209221

210-
class Update(Event, type=EventType.UPDATE):
222+
class Update(Event):
211223
def can_batch(self, event: Message) -> bool:
212224
return isinstance(event, Update) and event.sender == self.sender

src/textual/geometry.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def contains_point(self, point: tuple[int, int]) -> bool:
5151

5252
def translate(self, x: int, y: int) -> Region:
5353
_x, _y, width, height = self
54-
return Region(self.x + _x, self.y + _y, width, height)
54+
return Region(_x + x, _y + y, width, height)
5555

5656
def __contains__(self, other: Any) -> bool:
5757
try:

src/textual/view.py

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ def render(self) -> RenderableType:
9393
# yield from segments
9494

9595
def get_widget_at(
96-
self, x: int, y: int, offset_x: int = 0, offset_y: int = 0, deep: bool = False
96+
self, x: int, y: int, deep: bool = False
9797
) -> Tuple[Widget, Region]:
9898

9999
for layout, (layout_region, render) in self.layout.map.items():
@@ -102,14 +102,13 @@ def get_widget_at(
102102
widget = layout.renderable
103103
if deep and isinstance(layout.renderable, View):
104104

105-
if isinstance(layout.renderable, View):
106-
view = layout.renderable
107-
translate_x = region.x
108-
translate_y = region.y
109-
widget, region = view.get_widget_at(
110-
x - region.x, y - region.y, deep=True
111-
)
112-
region = region.translate(translate_x, translate_y)
105+
view = layout.renderable
106+
translate_x = region.x
107+
translate_y = region.y
108+
widget, region = view.get_widget_at(
109+
x - region.x, y - region.y, deep=True
110+
)
111+
region = region.translate(translate_x, translate_y)
113112

114113
if isinstance(widget, WidgetBase):
115114
return widget, region
@@ -182,6 +181,7 @@ async def on_resize(self, event: events.Resize) -> None:
182181
async def _on_mouse_move(self, event: events.MouseMove) -> None:
183182
try:
184183
widget, region = self.get_widget_at(event.x, event.y, deep=True)
184+
log.debug("MOVE =%r %r", widget, region)
185185
log.debug("mouse over %r %r", widget, region)
186186
except NoWidget:
187187
await self.app.set_mouse_over(None)
@@ -206,24 +206,20 @@ async def forward_event(self, event: events.Event) -> None:
206206
if isinstance(event, (events.Enter, events.Leave)):
207207
await self.post_message(event)
208208

209-
elif isinstance(event, (events.MouseDown)):
210-
try:
211-
widget, _region = self.get_widget_at(event.x, event.y, deep=True)
212-
except NoWidget:
213-
await self.app.set_focus(None)
214-
else:
215-
await self.app.set_focus(widget)
216-
217209
elif isinstance(event, events.MouseMove):
218210
await self._on_mouse_move(event)
219211

220212
elif isinstance(event, events.MouseEvent):
213+
log.debug("MOUSE %r", event)
221214
try:
222-
widget, region = self.get_widget_at(event.x, event.y)
215+
widget, region = self.get_widget_at(event.x, event.y, deep=True)
223216
except NoWidget:
224-
pass
217+
if isinstance(event, events.MouseDown):
218+
await self.app.set_focus(None)
225219
else:
226-
await widget.forward_event(event)
220+
if isinstance(event, events.MouseDown):
221+
await self.app.set_focus(widget)
222+
await widget.forward_event(event.offset(-region.x, -region.y))
227223

228224
elif isinstance(event, (events.MouseScrollDown, events.MouseScrollUp)):
229225
widget, _region = self.get_widget_at(event.x, event.y)

src/textual/widget.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from rich.panel import Panel
1919
import rich.repr
2020
from rich.segment import Segment
21+
from rich.style import Style
2122

2223
from . import events
2324
from ._context import active_app
@@ -227,6 +228,9 @@ def line_cache(self) -> LineCache:
227228
# def __rich__(self) -> LineCache:
228229
# return self.line_cache
229230

231+
def get_style_at(self, x: int, y: int) -> Style:
232+
return self.line_cache.get_style_at(x, y)
233+
230234
def render(self) -> RenderableType:
231235
raise NotImplementedError
232236
# return self.line_cache
@@ -251,6 +255,16 @@ def render_update(self, x: int, y: int) -> Iterable[Segment]:
251255
width, height = self.size
252256
yield from self.line_cache.render(x, y, width, height)
253257

258+
async def on_mouse_move(self, event: events.MouseMove) -> None:
259+
log.debug("%r", self.get_style_at(event.x, event.y))
260+
261+
async def on_mouse_up(self, event: events.MouseUp) -> None:
262+
log.debug("CLICKED %r", event)
263+
style = self.get_style_at(event.x, event.y)
264+
log.debug(style.meta)
265+
if "@click" in style.meta:
266+
await self.app.action(style.meta["@click"])
267+
254268

255269
class StaticWidget(Widget):
256270
def __init__(self, renderable: RenderableType, name: str | None = None) -> None:

src/textual/widgets/scroll_view.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
from __future__ import annotations
22

33
from rich.layout import Layout
4+
from rich.text import Text
45

56
from .. import events
67
from ..scrollbar import ScrollBar
8+
79
from ..page import Page
810
from ..view import LayoutView
911
from ..widget import StaticWidget
@@ -23,8 +25,9 @@ def __init__(self, name: str | None = None) -> None:
2325
super().__init__(layout=layout, name=name)
2426

2527
async def on_mount(self, event: events.Mount) -> None:
28+
text = Text.from_markup("Hello, [@click='bell']World[/]!")
2629
await self.mount_all(
27-
content=StaticWidget("Hello"),
30+
content=StaticWidget(text),
2831
vertical_scrollbar=StaticWidget(self._vertical_scrollbar),
2932
horizontal_scrollbar=StaticWidget(self._horizontal_Scrollbar),
3033
)

0 commit comments

Comments
 (0)