mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
change name to textual
This commit is contained in:
8
poetry.lock
generated
8
poetry.lock
generated
@@ -35,10 +35,10 @@ python-versions = "^3.6"
|
||||
develop = false
|
||||
|
||||
[package.dependencies]
|
||||
typing-extensions = {version = "^3.7.4", markers = "python_version < \"3.8\""}
|
||||
pygments = "^2.6.0"
|
||||
colorama = "^0.4.0"
|
||||
commonmark = "^0.9.0"
|
||||
pygments = "^2.6.0"
|
||||
typing-extensions = {version = "^3.7.4", markers = "python_version < \"3.8\""}
|
||||
|
||||
[package.extras]
|
||||
jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"]
|
||||
@@ -47,7 +47,7 @@ jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"]
|
||||
type = "git"
|
||||
url = "git@github.com:willmcgugan/rich"
|
||||
reference = "tui"
|
||||
resolved_reference = "34630b02b60c1c1cce4b072410cd93f6a6385039"
|
||||
resolved_reference = "803651999e50ac2df1a60f1c0dd85729d7b556ce"
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
@@ -60,7 +60,7 @@ python-versions = "*"
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.7"
|
||||
content-hash = "b5400274e471bd51f46cb352194316d479f45e3affa0d9aba926a7bbf2e0c551"
|
||||
content-hash = "8557cc8fc8af15c01c0f20b519b680fbb32f35233a0065137d288dfdf67af3f1"
|
||||
|
||||
[metadata.files]
|
||||
colorama = [
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
[tool.poetry]
|
||||
name = "rich.tui"
|
||||
name = "textual"
|
||||
version = "0.1.0"
|
||||
description = "Build Text User Interfaces with Rich"
|
||||
description = "Rich TUI"
|
||||
authors = ["Will McGugan <willmcgugan@gmail.com>"]
|
||||
license = "MIT"
|
||||
packages = [
|
||||
{ include = "rich" }
|
||||
]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.7"
|
||||
rich = "^10.2.2"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
rich = {git = "git@github.com:willmcgugan/rich", rev = "tui"}
|
||||
|
||||
@@ -3,5 +3,8 @@
|
||||
{
|
||||
"path": "."
|
||||
}
|
||||
]
|
||||
],
|
||||
"settings": {
|
||||
"python.pythonPath": "/Users/willmcgugan/Venvs/textual/bin/python"
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
from time import time
|
||||
from typing import ClassVar
|
||||
from enum import auto, Enum
|
||||
|
||||
from .case import camel_to_snake
|
||||
|
||||
|
||||
class ActionType(Enum):
|
||||
CUSTOM = auto()
|
||||
QUIT = auto()
|
||||
|
||||
|
||||
class Action:
|
||||
type: ClassVar[ActionType]
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.time = time()
|
||||
|
||||
def __init_subclass__(cls, type: ActionType) -> None:
|
||||
super().__init_subclass__()
|
||||
cls.type = type
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
if not hasattr(self, "_name"):
|
||||
_name = camel_to_snake(self.__class__.__name__)
|
||||
if _name.endswith("_event"):
|
||||
_name = _name[:-6]
|
||||
self._name = _name
|
||||
return self._name
|
||||
|
||||
|
||||
class QuitAction(Action, type=ActionType.QUIT):
|
||||
pass
|
||||
@@ -7,7 +7,7 @@ import weakref
|
||||
from rich.repr import rich_repr, RichReprResult
|
||||
|
||||
from . import events
|
||||
from .types import MessageTarget
|
||||
from ._types import MessageTarget
|
||||
|
||||
|
||||
TimerCallback = Callable[[], Awaitable[None]]
|
||||
@@ -26,4 +26,4 @@ class EventTarget(Protocol):
|
||||
...
|
||||
|
||||
|
||||
MessageHandler = Callable[["Message"], Awaitable]
|
||||
MessageHandler = Callable[["Message"], Awaitable]
|
||||
@@ -2,16 +2,16 @@ import asyncio
|
||||
|
||||
import logging
|
||||
import signal
|
||||
from typing import Any, Dict, Set
|
||||
from typing import Any, ClassVar, Dict, Set, List
|
||||
|
||||
from rich.control import Control
|
||||
from rich.repr import rich_repr, RichReprResult
|
||||
from rich.screen import Screen
|
||||
from rich import get_console
|
||||
from rich.console import Console
|
||||
|
||||
from . import events
|
||||
from ._context import active_app
|
||||
from .. import get_console
|
||||
from ..console import Console
|
||||
from .driver import Driver, CursesDriver
|
||||
from .message_pump import MessagePump
|
||||
from .view import View, LayoutView
|
||||
@@ -26,6 +26,8 @@ LayoutDefinition = Dict[str, Any]
|
||||
class App(MessagePump):
|
||||
view: View
|
||||
|
||||
KEYS: ClassVar[Dict[str, str]] = {}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
console: Console = None,
|
||||
@@ -88,8 +90,28 @@ class App(MessagePump):
|
||||
Screen(Control.home(), self.view, Control.home(), application_mode=True)
|
||||
)
|
||||
|
||||
async def on_startup(self, event: events.Startup) -> None:
|
||||
pass
|
||||
async def action(self, action: str) -> None:
|
||||
if "." in action:
|
||||
destination, action_name, *tokens = action.split(".")
|
||||
else:
|
||||
destination = "app"
|
||||
action_name = action
|
||||
tokens = []
|
||||
|
||||
if destination == "app":
|
||||
method_name = f"action_{action_name}"
|
||||
method = getattr(self, method_name, None)
|
||||
if method is not None:
|
||||
await method(tokens)
|
||||
|
||||
async def on_key(self, event: events.Key) -> None:
|
||||
key_action = self.KEYS.get(event.key, None)
|
||||
if key_action is not None:
|
||||
log.debug("action %r", key_action)
|
||||
await self.action(key_action)
|
||||
|
||||
# if event.key == "q":
|
||||
# await self.close_messages()
|
||||
|
||||
async def on_shutdown_request(self, event: events.ShutdownRequest) -> None:
|
||||
await self.close_messages()
|
||||
@@ -102,6 +124,12 @@ class App(MessagePump):
|
||||
async def on_mouse_move(self, event: events.MouseMove) -> None:
|
||||
await self.view.post_message(event)
|
||||
|
||||
async def on_mouse_clicked(self, event: events.MouseClicked) -> None:
|
||||
await self.view.post_message(event)
|
||||
|
||||
async def action_quit(self, tokens: List[str]) -> None:
|
||||
await self.close_messages()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import asyncio
|
||||
@@ -118,14 +146,10 @@ if __name__ == "__main__":
|
||||
)
|
||||
|
||||
class MyApp(App):
|
||||
async def on_key(self, event: events.Key) -> None:
|
||||
log.debug("on_key %r", event)
|
||||
if event.key == "q":
|
||||
await self.close_messages()
|
||||
|
||||
KEYS = {"q": "quit"}
|
||||
|
||||
async def on_startup(self, event: events.Startup) -> None:
|
||||
await self.view.mount(Header(self.title), slot="header")
|
||||
await self.view.mount(Placeholder(), slot="body")
|
||||
self.refresh()
|
||||
await self.view.mount_all(header=Header(self.title), body=Placeholder())
|
||||
|
||||
MyApp.run()
|
||||
@@ -11,7 +11,7 @@ from threading import Event, Thread
|
||||
from typing import Optional, Tuple, TYPE_CHECKING
|
||||
|
||||
from . import events
|
||||
from .types import MessageTarget
|
||||
from ._types import MessageTarget
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..console import Console
|
||||
@@ -4,10 +4,10 @@ from enum import auto, Enum
|
||||
from time import monotonic
|
||||
from typing import ClassVar, Optional, Set, TYPE_CHECKING
|
||||
|
||||
from ..repr import rich_repr, RichReprResult
|
||||
from rich.repr import rich_repr, RichReprResult
|
||||
|
||||
from .message import Message
|
||||
from .types import Callback, MessageTarget
|
||||
from ._types import Callback, MessageTarget
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -28,7 +28,7 @@ class EventType(Enum):
|
||||
SHUTDOWN_REQUEST = auto()
|
||||
SHUTDOWN = auto()
|
||||
EXIT = auto()
|
||||
REFRESH = auto()
|
||||
UPDATED = auto()
|
||||
TIMER = auto()
|
||||
FOCUS = auto()
|
||||
BLUR = auto()
|
||||
@@ -54,6 +54,7 @@ class Event(Message):
|
||||
def __init_subclass__(
|
||||
cls, type: EventType, priority: int = 0, bubble: bool = False
|
||||
) -> None:
|
||||
cls.type = type
|
||||
super().__init_subclass__(priority=priority, bubble=bubble)
|
||||
|
||||
def __enter__(self) -> "Event":
|
||||
@@ -81,6 +82,10 @@ class Created(Event, type=EventType.CREATED):
|
||||
pass
|
||||
|
||||
|
||||
class Updated(Event, type=EventType.UPDATED):
|
||||
"""Indicates the sender was updated and needs a refresh."""
|
||||
|
||||
|
||||
class Idle(Event, type=EventType.IDLE):
|
||||
"""Sent when there are no more items in the message queue."""
|
||||
|
||||
@@ -111,10 +116,6 @@ class Shutdown(Event, type=EventType.SHUTDOWN):
|
||||
pass
|
||||
|
||||
|
||||
class Refresh(Event, type=EventType.REFRESH):
|
||||
pass
|
||||
|
||||
|
||||
@rich_repr
|
||||
class Key(Event, type=EventType.KEY, bubble=True):
|
||||
code: int = 0
|
||||
@@ -145,7 +146,7 @@ class MouseMove(Event, type=EventType.MOUSE_MOVE):
|
||||
|
||||
|
||||
@rich_repr
|
||||
class MousePressed(Event, type=EventType.MOUSE_PRESSED):
|
||||
class MouseBase(Event, type=EventType.MOUSE_PRESSED):
|
||||
def __init__(
|
||||
self,
|
||||
sender: MessageTarget,
|
||||
@@ -173,15 +174,19 @@ class MousePressed(Event, type=EventType.MOUSE_PRESSED):
|
||||
yield "shift", self.shift, False
|
||||
|
||||
|
||||
class MouseReleased(MousePressed, type=EventType.MOUSE_RELEASED):
|
||||
class MousePressed(MouseBase, type=EventType.MOUSE_MOVE):
|
||||
pass
|
||||
|
||||
|
||||
class MouseClicked(MousePressed, type=EventType.MOUSE_CLICKED):
|
||||
class MouseReleased(MouseBase, type=EventType.MOUSE_RELEASED):
|
||||
pass
|
||||
|
||||
|
||||
class MouseDoubleClicked(MousePressed, type=EventType.MOUSE_DOUBLE_CLICKED):
|
||||
class MouseClicked(MouseBase, type=EventType.MOUSE_CLICKED):
|
||||
pass
|
||||
|
||||
|
||||
class MouseDoubleClicked(MouseBase, type=EventType.MOUSE_DOUBLE_CLICKED):
|
||||
pass
|
||||
|
||||
|
||||
@@ -225,4 +230,4 @@ class Focus(Event, type=EventType.FOCUS):
|
||||
|
||||
|
||||
class Blur(Event, type=EventType.BLUR):
|
||||
pass
|
||||
pass
|
||||
@@ -3,7 +3,7 @@ from typing import ClassVar
|
||||
|
||||
from .case import camel_to_snake
|
||||
|
||||
from .types import MessageTarget
|
||||
from ._types import MessageTarget
|
||||
|
||||
|
||||
class Message:
|
||||
@@ -31,4 +31,4 @@ class Message:
|
||||
suppress (bool, optional): True if the default action should be suppressed,
|
||||
or False if the default actions should be performed. Defaults to True.
|
||||
"""
|
||||
self.suppress = suppress
|
||||
self.suppress = suppress
|
||||
@@ -1,15 +1,13 @@
|
||||
from functools import total_ordering
|
||||
from typing import AsyncIterable, Optional, NamedTuple, Set, Type, Tuple, TYPE_CHECKING
|
||||
from typing import Optional, NamedTuple, Set, Type, TYPE_CHECKING
|
||||
import asyncio
|
||||
from asyncio import PriorityQueue
|
||||
|
||||
from functools import total_ordering
|
||||
import logging
|
||||
|
||||
from . import events
|
||||
from .message import Message
|
||||
from ._timer import Timer, TimerCallback
|
||||
from .types import MessageHandler
|
||||
from ._types import MessageHandler
|
||||
|
||||
log = logging.getLogger("rich")
|
||||
|
||||
@@ -18,23 +16,29 @@ class MessageQueueItem(NamedTuple):
|
||||
priority: int
|
||||
message: Message
|
||||
|
||||
def __lt__(self, other: "MessageQueueItem") -> bool:
|
||||
return self.priority < other.priority
|
||||
def __lt__(self, other: object) -> bool:
|
||||
other_priority = other.priority if isinstance(other, MessageQueueItem) else 0
|
||||
return self.priority < other_priority
|
||||
|
||||
def __le__(self, other: "MessageQueueItem") -> bool:
|
||||
return self.priority <= other.priority
|
||||
def __le__(self, other: object) -> bool:
|
||||
other_priority = other.priority if isinstance(other, MessageQueueItem) else 0
|
||||
return self.priority <= other_priority
|
||||
|
||||
def __gt__(self, other: "MessageQueueItem") -> bool:
|
||||
return self.priority > other.priority
|
||||
def __gt__(self, other: object) -> bool:
|
||||
other_priority = other.priority if isinstance(other, MessageQueueItem) else 0
|
||||
return self.priority > other_priority
|
||||
|
||||
def __ge__(self, other: "MessageQueueItem") -> bool:
|
||||
return self.priority >= other.priority
|
||||
def __ge__(self, other: object) -> bool:
|
||||
other_priority = other.priority if isinstance(other, MessageQueueItem) else 0
|
||||
return self.priority >= other_priority
|
||||
|
||||
def __eq__(self, other: "MessageQueueItem") -> bool:
|
||||
return self.priority == other.priority
|
||||
def __eq__(self, other: object) -> bool:
|
||||
other_priority = other.priority if isinstance(other, MessageQueueItem) else 0
|
||||
return self.priority == other_priority
|
||||
|
||||
def __ne__(self, other: "MessageQueueItem") -> bool:
|
||||
return self.priority != other.priority
|
||||
def __ne__(self, other: object) -> bool:
|
||||
other_priority = other.priority if isinstance(other, MessageQueueItem) else 0
|
||||
return self.priority != other_priority
|
||||
|
||||
|
||||
class MessagePumpClosed(Exception):
|
||||
@@ -51,14 +55,14 @@ class MessagePump:
|
||||
self._parent = parent
|
||||
self._closing: bool = False
|
||||
self._closed: bool = False
|
||||
self._disabled_messages: Set[Message] = set()
|
||||
self._disabled_messages: Set[Type[Message]] = set()
|
||||
|
||||
def check_message_enabled(self, message: Message) -> bool:
|
||||
return type(message) not in self._disabled_messages
|
||||
|
||||
def disable_messages(self, *messages: Type[Message]) -> None:
|
||||
"""Disable message types from being proccessed."""
|
||||
self._disabled_messages.intersection_update(messages)
|
||||
self._disabled_messages.update(messages)
|
||||
|
||||
def enable_messages(self, *messages: Type[Message]) -> None:
|
||||
"""Enable processing of messages types."""
|
||||
@@ -117,7 +121,6 @@ class MessagePump:
|
||||
except Exception:
|
||||
log.exception("error getting message")
|
||||
break
|
||||
log.debug("%r -> %r", message, self)
|
||||
await self.dispatch_message(message, priority)
|
||||
if self._message_queue.empty():
|
||||
await self.dispatch_message(events.Idle(self))
|
||||
@@ -137,7 +140,10 @@ class MessagePump:
|
||||
if dispatch_function is not None:
|
||||
await dispatch_function(event)
|
||||
if event.bubble and self._parent:
|
||||
await self._parent.post_message(event, priority)
|
||||
if event.sender == self._parent:
|
||||
log.debug("bubbled event abandoned; %r", event)
|
||||
else:
|
||||
await self._parent.post_message(event, priority)
|
||||
|
||||
async def on_message(self, message: Message) -> None:
|
||||
pass
|
||||
38
src/textual/state.py
Normal file
38
src/textual/state.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from typing import Generic, Type, TypeVar
|
||||
|
||||
|
||||
ParentType = TypeVar("ParentType")
|
||||
ValueType = TypeVar("ValueType")
|
||||
|
||||
|
||||
class Reactive(Generic[ValueType]):
|
||||
def __init__(self, default: ValueType) -> None:
|
||||
self._default = default
|
||||
|
||||
def __set_name__(self, owner: object, name: str) -> None:
|
||||
self.internal_name = f"_{name}"
|
||||
setattr(owner, self.internal_name, self._default)
|
||||
|
||||
def __get__(self, obj: object, obj_type: Type[object]) -> ValueType:
|
||||
print("__get__", obj, obj_type)
|
||||
|
||||
return getattr(obj, self.internal_name)
|
||||
|
||||
def __set__(self, obj: object, value: ValueType) -> None:
|
||||
print("__set__", obj, value)
|
||||
setattr(obj, self.internal_name, value)
|
||||
|
||||
|
||||
class Example:
|
||||
def __init__(self, foo: int = 3) -> None:
|
||||
self.foo = foo
|
||||
|
||||
color: Reactive[str] = Reactive("blue")
|
||||
|
||||
|
||||
example = Example()
|
||||
|
||||
print(example.color)
|
||||
example.color = "red"
|
||||
print(example.color)
|
||||
print(example.foo)
|
||||
@@ -9,6 +9,7 @@ from rich.repr import rich_repr, RichReprResult
|
||||
|
||||
from . import events
|
||||
from ._context import active_app
|
||||
from .message import Message
|
||||
from .message_pump import MessagePump
|
||||
from .widget import Widget
|
||||
from .widgets.header import Header
|
||||
@@ -50,6 +51,10 @@ class View(ABC, MessagePump):
|
||||
async def mount(self, widget: Widget, *, slot: str = "main") -> None:
|
||||
...
|
||||
|
||||
async def mount_all(self, **widgets: Widget) -> None:
|
||||
for slot, widget in widgets.items():
|
||||
await self.mount(widget, slot=slot)
|
||||
|
||||
|
||||
class LayoutView(View):
|
||||
layout: Layout
|
||||
@@ -76,6 +81,7 @@ class LayoutView(View):
|
||||
)
|
||||
self.layout = layout
|
||||
self.mouse_over: Optional[MessagePump] = None
|
||||
self.focused: Optional[MessagePump] = None
|
||||
super().__init__()
|
||||
|
||||
def __rich_repr__(self) -> RichReprResult:
|
||||
@@ -84,10 +90,10 @@ class LayoutView(View):
|
||||
def __rich__(self) -> RenderableType:
|
||||
return self.layout
|
||||
|
||||
def get_widget_at(self, x: int, y: int) -> Tuple[MessagePump, Region]:
|
||||
def get_widget_at(self, x: int, y: int) -> Tuple[Widget, Region]:
|
||||
for layout, (region, render) in self.layout.map.items():
|
||||
if region.contains(x, y):
|
||||
if isinstance(layout.renderable, MessagePump):
|
||||
if isinstance(layout.renderable, Widget):
|
||||
return layout.renderable, region
|
||||
else:
|
||||
break
|
||||
@@ -101,6 +107,21 @@ class LayoutView(View):
|
||||
await self.app.add(widget)
|
||||
await widget.post_message(events.Mount(sender=self))
|
||||
|
||||
async def set_focus(self, widget: Optional[Widget]) -> None:
|
||||
if widget == self.focused:
|
||||
return
|
||||
if widget is None:
|
||||
if self.focused is not None:
|
||||
focused = self.focused
|
||||
self.focused = None
|
||||
await focused.post_message(events.Blur(self))
|
||||
elif widget.can_focus:
|
||||
if self.focused is not None:
|
||||
await self.focused.post_message(events.Blur(self))
|
||||
if widget is not None and self.focused != widget:
|
||||
self.focused = widget
|
||||
await widget.post_message(events.Focus(self))
|
||||
|
||||
async def on_startup(self, event: events.Startup) -> None:
|
||||
await self.mount(Header(self.title), slot="header")
|
||||
|
||||
@@ -129,3 +150,11 @@ class LayoutView(View):
|
||||
await widget.post_message(
|
||||
events.MouseMove(self, event.x - region.x, event.y - region.y)
|
||||
)
|
||||
|
||||
async def on_mouse_clicked(self, event: events.MouseClicked) -> None:
|
||||
try:
|
||||
widget, _region = self.get_widget_at(event.x, event.y)
|
||||
except NoWidget:
|
||||
await self.set_focus(None)
|
||||
else:
|
||||
await self.set_focus(widget)
|
||||
@@ -11,6 +11,7 @@ from rich.repr import rich_repr, RichReprResult
|
||||
|
||||
from . import events
|
||||
from ._context import active_app
|
||||
from .message import Message
|
||||
from .message_pump import MessagePump
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -36,13 +37,18 @@ class Widget(MessagePump):
|
||||
Widget._count += 1
|
||||
self.size = WidgetDimensions(0, 0)
|
||||
self.size_changed = False
|
||||
self.mouse_over = False
|
||||
self._has_focus = False
|
||||
self._mouse_over = False
|
||||
super().__init__()
|
||||
if not self.mouse_events:
|
||||
self.disable_messages(events.MouseMove)
|
||||
if not self.idle_events:
|
||||
self.disable_messages(events.Idle)
|
||||
|
||||
def __init_subclass__(cls, can_focus: bool = True) -> None:
|
||||
super().__init_subclass__()
|
||||
cls.can_focus = can_focus
|
||||
|
||||
@property
|
||||
def app(self) -> "App":
|
||||
return active_app.get()
|
||||
@@ -51,6 +57,14 @@ class Widget(MessagePump):
|
||||
def console(self) -> Console:
|
||||
return active_app.get().console
|
||||
|
||||
@property
|
||||
def has_focus(self) -> bool:
|
||||
return self._has_focus
|
||||
|
||||
@property
|
||||
def mouse_over(self) -> bool:
|
||||
return self._mouse_over
|
||||
|
||||
async def refresh(self) -> None:
|
||||
self.app.refresh()
|
||||
|
||||
@@ -64,7 +78,7 @@ class Widget(MessagePump):
|
||||
Align.center(Pretty(self), vertical="middle"),
|
||||
title=self.__class__.__name__,
|
||||
border_style="green" if self.mouse_over else "blue",
|
||||
box=box.HEAVY if self.mouse_over else box.ROUNDED,
|
||||
box=box.HEAVY if self.has_focus else box.ROUNDED,
|
||||
)
|
||||
|
||||
def __rich_console__(
|
||||
@@ -75,9 +89,24 @@ class Widget(MessagePump):
|
||||
self.size = new_size
|
||||
yield renderable
|
||||
|
||||
async def post_message(
|
||||
self, message: Message, priority: Optional[int] = None
|
||||
) -> bool:
|
||||
if not self.check_message_enabled(message):
|
||||
return True
|
||||
log.debug("%r -> %r", message, self)
|
||||
return await super().post_message(message, priority)
|
||||
|
||||
async def on_event(self, event: events.Event, priority: int) -> None:
|
||||
if isinstance(event, (events.MouseEnter, events.MouseLeave)):
|
||||
self.mouse_over = isinstance(event, events.MouseEnter)
|
||||
log.debug("%r", self.mouse_over)
|
||||
self._mouse_over = isinstance(event, events.MouseEnter)
|
||||
await self.refresh()
|
||||
await super().on_event(event, priority)
|
||||
|
||||
async def on_focus(self, event: events.Focus) -> None:
|
||||
self._has_focus = True
|
||||
await self.refresh()
|
||||
|
||||
async def on_blur(self, event: events.Focus) -> None:
|
||||
self._has_focus = False
|
||||
await self.refresh()
|
||||
0
src/textual/widgets/__init__.py
Normal file
0
src/textual/widgets/__init__.py
Normal file
@@ -3,7 +3,8 @@ from ..widget import Widget
|
||||
from rich.repr import RichReprResult
|
||||
|
||||
|
||||
class Placeholder(Widget):
|
||||
class Placeholder(Widget, can_focus=True):
|
||||
def __rich_repr__(self) -> RichReprResult:
|
||||
yield "name", self.name
|
||||
yield "mouse_over", self.mouse_over
|
||||
yield "has_focus", self.has_focus
|
||||
yield "mouse_over", self.mouse_over
|
||||
Reference in New Issue
Block a user