commit
1fd773f6ef
13 changed files with 215 additions and 66 deletions
|
|
@ -371,11 +371,11 @@ We can perform this conversion by adding `self.scroll_offset` to `event.offset`.
|
|||
- Once we have the board coordinate underneath the mouse we divide the x coordinate by 8 and divide the y coordinate by 4 to give us the coordinate of a square.
|
||||
|
||||
If the cursor square coordinate calculated in `on_mouse_move` changes, Textual will call `watch_cursor_square` with the previous coordinate and new coordinate of the square. This method works out the regions of the widget to update and essentially does the reverse of the steps we took to go from mouse coordinates to square coordinates.
|
||||
The `get_square_region` function calculates a [Region][textual.geometry.Region] object for each square and uses them as a positional argument in a call to [refresh][textual.widget.Widget.refresh]. Passing Regions to `refresh` tells Textual to update only the cells underneath those regions, and not the entire region.
|
||||
The `get_square_region` function calculates a [Region][textual.geometry.Region] object for each square and uses them as a positional argument in a call to [refresh][textual.widget.Widget.refresh]. Passing Region objects to `refresh` tells Textual to update only the cells underneath those regions, and not the entire widget.
|
||||
|
||||
!!! note
|
||||
|
||||
Textual is smart about performing updates. If you refresh multiple regions (even if they overlap), Textual will combine them in to as few non-overlapping regions as possible.
|
||||
Textual is smart about performing updates. If you refresh multiple regions, Textual will combine them into as few non-overlapping regions as possible.
|
||||
|
||||
The final step is to update the `render_line` method to use the cursor style when rendering the square underneath the mouse.
|
||||
|
||||
|
|
|
|||
14
poetry.lock
generated
14
poetry.lock
generated
|
|
@ -573,6 +573,14 @@ category = "main"
|
|||
optional = true
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "msgpack-types"
|
||||
version = "0.2.0"
|
||||
description = "Type stubs for msgpack"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7,<4.0"
|
||||
|
||||
[[package]]
|
||||
name = "multidict"
|
||||
version = "6.0.4"
|
||||
|
|
@ -1040,7 +1048,7 @@ dev = ["aiohttp", "click", "msgpack"]
|
|||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.7"
|
||||
content-hash = "efa7c78c6403931436fb415a21c935eaafd7859130a9da8ef7e5d79ddb34b14d"
|
||||
content-hash = "425fac5cc893af33128a6baf4fc9e296781d322e64eea5d9341d1265c6637d3c"
|
||||
|
||||
[metadata.files]
|
||||
aiohttp = [
|
||||
|
|
@ -1548,6 +1556,10 @@ msgpack = [
|
|||
{file = "msgpack-1.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:4d5834a2a48965a349da1c5a79760d94a1a0172fbb5ab6b5b33cbf8447e109ce"},
|
||||
{file = "msgpack-1.0.4.tar.gz", hash = "sha256:f5d869c18f030202eb412f08b28d2afeea553d6613aee89e200d7aca7ef01f5f"},
|
||||
]
|
||||
msgpack-types = [
|
||||
{file = "msgpack-types-0.2.0.tar.gz", hash = "sha256:b6b7ce9f52599f9dc3497006be8cf6bed7bd2c83fa48c4df43ac6958b97b0720"},
|
||||
{file = "msgpack_types-0.2.0-py3-none-any.whl", hash = "sha256:7e5bce9e3bba9fe08ed14005ad107aa44ea8d4b779ec28b8db880826d4c67303"},
|
||||
]
|
||||
multidict = [
|
||||
{file = "multidict-6.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8"},
|
||||
{file = "multidict-6.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171"},
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ Jinja2 = "<3.1.0"
|
|||
syrupy = "^3.0.0"
|
||||
mkdocs-rss-plugin = "^1.5.0"
|
||||
httpx = "^0.23.1"
|
||||
msgpack-types = "^0.2.0"
|
||||
|
||||
[tool.black]
|
||||
includes = "src"
|
||||
|
|
|
|||
|
|
@ -16,27 +16,91 @@ if TYPE_CHECKING:
|
|||
INNER = 1
|
||||
OUTER = 2
|
||||
|
||||
BORDER_CHARS: dict[EdgeType, tuple[str, str, str]] = {
|
||||
# Each string of the tuple represents a sub-tuple itself:
|
||||
# - 1st string represents (top1, top2, top3)
|
||||
# - 2nd string represents (mid1, mid2, mid3)
|
||||
# - 3rd string represents (bottom1, bottom2, bottom3)
|
||||
"": (" ", " ", " "),
|
||||
"ascii": ("+-+", "| |", "+-+"),
|
||||
"none": (" ", " ", " "),
|
||||
"hidden": (" ", " ", " "),
|
||||
"blank": (" ", " ", " "),
|
||||
"round": ("╭─╮", "│ │", "╰─╯"),
|
||||
"solid": ("┌─┐", "│ │", "└─┘"),
|
||||
"double": ("╔═╗", "║ ║", "╚═╝"),
|
||||
"dashed": ("┏╍┓", "╏ ╏", "┗╍┛"),
|
||||
"heavy": ("┏━┓", "┃ ┃", "┗━┛"),
|
||||
"inner": ("▗▄▖", "▐ ▌", "▝▀▘"),
|
||||
"outer": ("▛▀▜", "▌ ▐", "▙▄▟"),
|
||||
"hkey": ("▔▔▔", " ", "▁▁▁"),
|
||||
"vkey": ("▏ ▕", "▏ ▕", "▏ ▕"),
|
||||
"tall": ("▊▔▎", "▊ ▎", "▊▁▎"),
|
||||
"wide": ("▁▁▁", "▎ ▋", "▔▔▔"),
|
||||
BORDER_CHARS: dict[
|
||||
EdgeType, tuple[tuple[str, str, str], tuple[str, str, str], tuple[str, str, str]]
|
||||
] = {
|
||||
# Three tuples for the top, middle, and bottom rows.
|
||||
# The sub-tuples are the characters for the left, center, and right borders.
|
||||
"": (
|
||||
(" ", " ", " "),
|
||||
(" ", " ", " "),
|
||||
(" ", " ", " "),
|
||||
),
|
||||
"ascii": (
|
||||
("+", "-", "+"),
|
||||
("|", " ", "|"),
|
||||
("+", "-", "+"),
|
||||
),
|
||||
"none": (
|
||||
(" ", " ", " "),
|
||||
(" ", " ", " "),
|
||||
(" ", " ", " "),
|
||||
),
|
||||
"hidden": (
|
||||
(" ", " ", " "),
|
||||
(" ", " ", " "),
|
||||
(" ", " ", " "),
|
||||
),
|
||||
"blank": (
|
||||
(" ", " ", " "),
|
||||
(" ", " ", " "),
|
||||
(" ", " ", " "),
|
||||
),
|
||||
"round": (
|
||||
("╭", "─", "╮"),
|
||||
("│", " ", "│"),
|
||||
("╰", "─", "╯"),
|
||||
),
|
||||
"solid": (
|
||||
("┌", "─", "┐"),
|
||||
("│", " ", "│"),
|
||||
("└", "─", "┘"),
|
||||
),
|
||||
"double": (
|
||||
("╔", "═", "╗"),
|
||||
("║", " ", "║"),
|
||||
("╚", "═", "╝"),
|
||||
),
|
||||
"dashed": (
|
||||
("┏", "╍", "┓"),
|
||||
("╏", " ", "╏"),
|
||||
("┗", "╍", "┛"),
|
||||
),
|
||||
"heavy": (
|
||||
("┏", "━", "┓"),
|
||||
("┃", " ", "┃"),
|
||||
("┗", "━", "┛"),
|
||||
),
|
||||
"inner": (
|
||||
("▗", "▄", "▖"),
|
||||
("▐", " ", "▌"),
|
||||
("▝", "▀", "▘"),
|
||||
),
|
||||
"outer": (
|
||||
("▛", "▀", "▜"),
|
||||
("▌", " ", "▐"),
|
||||
("▙", "▄", "▟"),
|
||||
),
|
||||
"hkey": (
|
||||
("▔", "▔", "▔"),
|
||||
(" ", " ", " "),
|
||||
("▁", "▁", "▁"),
|
||||
),
|
||||
"vkey": (
|
||||
("▏", " ", "▕"),
|
||||
("▏", " ", "▕"),
|
||||
("▏", " ", "▕"),
|
||||
),
|
||||
"tall": (
|
||||
("▊", "▔", "▎"),
|
||||
("▊", " ", "▎"),
|
||||
("▊", "▁", "▎"),
|
||||
),
|
||||
"wide": (
|
||||
("▁", "▁", "▁"),
|
||||
("▎", " ", "▋"),
|
||||
("▔", "▔", "▔"),
|
||||
),
|
||||
}
|
||||
|
||||
# Some of the borders are on the widget background and some are on the background of the parent
|
||||
|
|
@ -44,22 +108,86 @@ BORDER_CHARS: dict[EdgeType, tuple[str, str, str]] = {
|
|||
BORDER_LOCATIONS: dict[
|
||||
EdgeType, tuple[tuple[int, int, int], tuple[int, int, int], tuple[int, int, int]]
|
||||
] = {
|
||||
"": ((0, 0, 0), (0, 0, 0), (0, 0, 0)),
|
||||
"ascii": ((0, 0, 0), (0, 0, 0), (0, 0, 0)),
|
||||
"none": ((0, 0, 0), (0, 0, 0), (0, 0, 0)),
|
||||
"hidden": ((0, 0, 0), (0, 0, 0), (0, 0, 0)),
|
||||
"blank": ((0, 0, 0), (0, 0, 0), (0, 0, 0)),
|
||||
"round": ((0, 0, 0), (0, 0, 0), (0, 0, 0)),
|
||||
"solid": ((0, 0, 0), (0, 0, 0), (0, 0, 0)),
|
||||
"double": ((0, 0, 0), (0, 0, 0), (0, 0, 0)),
|
||||
"dashed": ((0, 0, 0), (0, 0, 0), (0, 0, 0)),
|
||||
"heavy": ((0, 0, 0), (0, 0, 0), (0, 0, 0)),
|
||||
"inner": ((1, 1, 1), (1, 1, 1), (1, 1, 1)),
|
||||
"outer": ((0, 0, 0), (0, 0, 0), (0, 0, 0)),
|
||||
"hkey": ((0, 0, 0), (0, 0, 0), (0, 0, 0)),
|
||||
"vkey": ((0, 0, 0), (0, 0, 0), (0, 0, 0)),
|
||||
"tall": ((2, 0, 1), (2, 0, 1), (2, 0, 1)),
|
||||
"wide": ((1, 1, 1), (0, 1, 3), (1, 1, 1)),
|
||||
"": (
|
||||
(0, 0, 0),
|
||||
(0, 0, 0),
|
||||
(0, 0, 0),
|
||||
),
|
||||
"ascii": (
|
||||
(0, 0, 0),
|
||||
(0, 0, 0),
|
||||
(0, 0, 0),
|
||||
),
|
||||
"none": (
|
||||
(0, 0, 0),
|
||||
(0, 0, 0),
|
||||
(0, 0, 0),
|
||||
),
|
||||
"hidden": (
|
||||
(0, 0, 0),
|
||||
(0, 0, 0),
|
||||
(0, 0, 0),
|
||||
),
|
||||
"blank": (
|
||||
(0, 0, 0),
|
||||
(0, 0, 0),
|
||||
(0, 0, 0),
|
||||
),
|
||||
"round": (
|
||||
(0, 0, 0),
|
||||
(0, 0, 0),
|
||||
(0, 0, 0),
|
||||
),
|
||||
"solid": (
|
||||
(0, 0, 0),
|
||||
(0, 0, 0),
|
||||
(0, 0, 0),
|
||||
),
|
||||
"double": (
|
||||
(0, 0, 0),
|
||||
(0, 0, 0),
|
||||
(0, 0, 0),
|
||||
),
|
||||
"dashed": (
|
||||
(0, 0, 0),
|
||||
(0, 0, 0),
|
||||
(0, 0, 0),
|
||||
),
|
||||
"heavy": (
|
||||
(0, 0, 0),
|
||||
(0, 0, 0),
|
||||
(0, 0, 0),
|
||||
),
|
||||
"inner": (
|
||||
(1, 1, 1),
|
||||
(1, 1, 1),
|
||||
(1, 1, 1),
|
||||
),
|
||||
"outer": (
|
||||
(0, 0, 0),
|
||||
(0, 0, 0),
|
||||
(0, 0, 0),
|
||||
),
|
||||
"hkey": (
|
||||
(0, 0, 0),
|
||||
(0, 0, 0),
|
||||
(0, 0, 0),
|
||||
),
|
||||
"vkey": (
|
||||
(0, 0, 0),
|
||||
(0, 0, 0),
|
||||
(0, 0, 0),
|
||||
),
|
||||
"tall": (
|
||||
(2, 0, 1),
|
||||
(2, 0, 1),
|
||||
(2, 0, 1),
|
||||
),
|
||||
"wide": (
|
||||
(1, 1, 1),
|
||||
(0, 1, 3),
|
||||
(1, 1, 1),
|
||||
),
|
||||
}
|
||||
|
||||
INVISIBLE_EDGE_TYPES = cast("frozenset[EdgeType]", frozenset(("", "none", "hidden")))
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
from typing import Callable
|
||||
|
||||
__all__ = ["cell_len"]
|
||||
|
||||
|
||||
cell_len: Callable[[str], int]
|
||||
try:
|
||||
from rich.cells import cached_cell_len as cell_len
|
||||
except ImportError:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
COLOR_NAME_TO_RGB: dict[str, tuple[int, int, int] | tuple[int, int, int, float]] = {
|
||||
COLOR_NAME_TO_RGB: dict[str, tuple[int, int, int] | tuple[int, int, int, int]] = {
|
||||
# Let's start with a specific pseudo-color::
|
||||
"transparent": (0, 0, 0, 0),
|
||||
# Then, the 16 common ANSI colors:
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ from __future__ import annotations
|
|||
|
||||
|
||||
from time import sleep, perf_counter
|
||||
from asyncio import get_running_loop
|
||||
from asyncio import get_running_loop, Future
|
||||
from threading import Thread, Event
|
||||
|
||||
|
||||
|
|
@ -13,7 +13,7 @@ class Sleeper(Thread):
|
|||
self._exit = False
|
||||
self._sleep_time = 0.0
|
||||
self._event = Event()
|
||||
self.future = None
|
||||
self.future: Future | None = None
|
||||
self._loop = get_running_loop()
|
||||
super().__init__(daemon=True)
|
||||
|
||||
|
|
@ -25,6 +25,7 @@ class Sleeper(Thread):
|
|||
sleep(self._sleep_time)
|
||||
self._event.clear()
|
||||
# self.future.set_result(None)
|
||||
assert self.future is not None
|
||||
self._loop.call_soon_threadsafe(self.future.set_result, None)
|
||||
|
||||
async def sleep(self, sleep_time: float) -> None:
|
||||
|
|
@ -33,9 +34,6 @@ class Sleeper(Thread):
|
|||
self._event.set()
|
||||
await future
|
||||
|
||||
# await self._async_event.wait()
|
||||
# self._async_event.clear()
|
||||
|
||||
|
||||
async def check_sleeps() -> None:
|
||||
sleeper = Sleeper()
|
||||
|
|
@ -46,7 +44,6 @@ async def check_sleeps() -> None:
|
|||
|
||||
while perf_counter() - start < sleep_for:
|
||||
sleep(0)
|
||||
# await sleeper.sleep(sleep_for)
|
||||
elapsed = perf_counter() - start
|
||||
return elapsed
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ try:
|
|||
import ctypes
|
||||
from ctypes.wintypes import LARGE_INTEGER
|
||||
|
||||
kernel32 = ctypes.windll.kernel32
|
||||
kernel32 = ctypes.windll.kernel32 # type: ignore[attr-defined]
|
||||
except Exception:
|
||||
sleep = time_sleep
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -431,7 +431,9 @@ class Color(NamedTuple):
|
|||
suggested_color = None
|
||||
if not color_text.startswith(("#", "rgb", "hsl")):
|
||||
# Seems like we tried to use a color name: let's try to find one that is close enough:
|
||||
suggested_color = get_suggestion(color_text, COLOR_NAME_TO_RGB.keys())
|
||||
suggested_color = get_suggestion(
|
||||
color_text, list(COLOR_NAME_TO_RGB.keys())
|
||||
)
|
||||
if suggested_color:
|
||||
error_message += f"; did you mean '{suggested_color}'?"
|
||||
raise ColorParseError(error_message, suggested_color)
|
||||
|
|
@ -447,10 +449,10 @@ class Color(NamedTuple):
|
|||
) = color_match.groups()
|
||||
|
||||
if rgb_hex_triple is not None:
|
||||
r, g, b = rgb_hex_triple
|
||||
r, g, b = rgb_hex_triple # type: ignore[misc]
|
||||
color = cls(int(f"{r}{r}", 16), int(f"{g}{g}", 16), int(f"{b}{b}", 16))
|
||||
elif rgb_hex_quad is not None:
|
||||
r, g, b, a = rgb_hex_quad
|
||||
r, g, b, a = rgb_hex_quad # type: ignore[misc]
|
||||
color = cls(
|
||||
int(f"{r}{r}", 16),
|
||||
int(f"{g}{g}", 16),
|
||||
|
|
|
|||
|
|
@ -61,7 +61,8 @@ class Bullet:
|
|||
self, console: Console, options: ConsoleOptions
|
||||
) -> RenderResult:
|
||||
yield _markup_and_highlight(self.markup)
|
||||
yield from self.examples
|
||||
if self.examples is not None:
|
||||
yield from self.examples
|
||||
|
||||
|
||||
@rich.repr.auto
|
||||
|
|
@ -76,7 +77,9 @@ class HelpText:
|
|||
context around the issue. These are rendered below the summary. Defaults to None.
|
||||
"""
|
||||
|
||||
def __init__(self, summary: str, *, bullets: Iterable[Bullet] = None) -> None:
|
||||
def __init__(
|
||||
self, summary: str, *, bullets: Iterable[Bullet] | None = None
|
||||
) -> None:
|
||||
self.summary: str = summary
|
||||
self.bullets: Iterable[Bullet] | None = bullets or []
|
||||
|
||||
|
|
@ -87,6 +90,7 @@ class HelpText:
|
|||
self, console: Console, options: ConsoleOptions
|
||||
) -> RenderResult:
|
||||
tree = Tree(_markup_and_highlight(f"[b blue]{self.summary}"), guide_style="dim")
|
||||
for bullet in self.bullets:
|
||||
tree.add(bullet)
|
||||
if self.bullets is not None:
|
||||
for bullet in self.bullets:
|
||||
tree.add(bullet)
|
||||
yield tree
|
||||
|
|
|
|||
|
|
@ -136,6 +136,7 @@ class DevtoolsClient:
|
|||
change, it will update its own Console to ensure it renders at
|
||||
the correct width for server-side display.
|
||||
"""
|
||||
assert self.websocket is not None
|
||||
async for message in self.websocket:
|
||||
if message.type == aiohttp.WSMsgType.TEXT:
|
||||
message_json = json.loads(message.data)
|
||||
|
|
|
|||
|
|
@ -2,11 +2,10 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
import json
|
||||
import pickle
|
||||
from json import JSONDecodeError
|
||||
from typing import cast
|
||||
from typing import Any, cast
|
||||
|
||||
from aiohttp import WSMessage, WSMsgType
|
||||
from aiohttp.abc import Request
|
||||
|
|
@ -232,17 +231,16 @@ class ClientHandler:
|
|||
)
|
||||
try:
|
||||
await self.service.send_server_info(client_handler=self)
|
||||
async for message in self.websocket:
|
||||
message = cast(WSMessage, message)
|
||||
|
||||
if message.type in (WSMsgType.TEXT, WSMsgType.BINARY):
|
||||
async for websocket_message in self.websocket:
|
||||
if websocket_message.type in (WSMsgType.TEXT, WSMsgType.BINARY):
|
||||
message: dict[str, Any]
|
||||
try:
|
||||
if isinstance(message.data, bytes):
|
||||
message = msgpack.unpackb(message.data)
|
||||
if isinstance(websocket_message.data, bytes):
|
||||
message = msgpack.unpackb(websocket_message.data)
|
||||
else:
|
||||
message = json.loads(message.data)
|
||||
message = json.loads(websocket_message.data)
|
||||
except JSONDecodeError:
|
||||
self.service.console.print(escape(str(message.data)))
|
||||
self.service.console.print(escape(str(websocket_message.data)))
|
||||
continue
|
||||
|
||||
type = message.get("type")
|
||||
|
|
@ -253,7 +251,7 @@ class ClientHandler:
|
|||
and not self.service.shutdown_event.is_set()
|
||||
):
|
||||
await self.incoming_queue.put(message)
|
||||
elif message.type == WSMsgType.ERROR:
|
||||
elif websocket_message.type == WSMsgType.ERROR:
|
||||
self.service.console.print(
|
||||
DevConsoleNotice("Websocket error occurred", level="error")
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ def blend_colors(color1: Color, color2: Color, ratio: float) -> Color:
|
|||
Returns:
|
||||
A Color representing the blending of the two supplied colors.
|
||||
"""
|
||||
assert color1.triplet is not None
|
||||
assert color2.triplet is not None
|
||||
r1, g1, b1 = color1.triplet
|
||||
r2, g2, b2 = color2.triplet
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue