mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
fix for timer issue
This commit is contained in:
@@ -1,15 +1,17 @@
|
|||||||
/* CSS file for basic.py */
|
/* CSS file for basic.py */
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
* {
|
* {
|
||||||
transition: color 500ms linear, background 500ms linear;
|
transition: color 500ms linear, background 500ms linear;
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
App > Screen {
|
App > Screen {
|
||||||
layout: dock;
|
layout: dock;
|
||||||
docks: side=left/1;
|
docks: side=left/1;
|
||||||
background: $surface;
|
background: $background;
|
||||||
color: $text-surface;
|
color: $text-background;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar {
|
#sidebar {
|
||||||
@@ -27,7 +29,7 @@ App > Screen {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#sidebar .title {
|
#sidebar .title {
|
||||||
height: 1;
|
height: 3;
|
||||||
background: $primary-darken-2;
|
background: $primary-darken-2;
|
||||||
color: $text-primary-darken-2;
|
color: $text-primary-darken-2;
|
||||||
border-right: outer $primary-darken-3;
|
border-right: outer $primary-darken-3;
|
||||||
@@ -54,21 +56,24 @@ App > Screen {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#content {
|
#content {
|
||||||
color: $text-surface;
|
color: $text-background;
|
||||||
background: $surface;
|
background: $background;
|
||||||
layout: vertical;
|
layout: vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
Tweet {
|
Tweet {
|
||||||
height: 10;
|
height: 16;
|
||||||
max-width: 80;
|
max-width: 80;
|
||||||
margin: 1 2;
|
margin: 1 3;
|
||||||
background: $background;
|
background: $surface;
|
||||||
color: $text-background;
|
color: $text-surface;
|
||||||
layout: vertical
|
layout: vertical;
|
||||||
|
border: outer $accent2;
|
||||||
|
padding: 1;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
TweetHeader {
|
TweetHeader {
|
||||||
height:1
|
height:1
|
||||||
background: $accent1
|
background: $accent1
|
||||||
@@ -76,8 +81,8 @@ TweetHeader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TweetBody {
|
TweetBody {
|
||||||
background: $background
|
background: $surface
|
||||||
color: $text-background-fade-1
|
color: $text-surface-fade-1
|
||||||
height:6;
|
height:6;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,7 +93,7 @@ TweetBody {
|
|||||||
height: 3
|
height: 3
|
||||||
border: tall $text-background;
|
border: tall $text-background;
|
||||||
|
|
||||||
margin: 0 1 1 1;
|
margin: 1 1 1 1;
|
||||||
|
|
||||||
transition: background 200ms in_out_cubic, color 300ms in_out_cubic;
|
transition: background 200ms in_out_cubic, color 300ms in_out_cubic;
|
||||||
|
|
||||||
@@ -97,11 +102,11 @@ TweetBody {
|
|||||||
.button:hover {
|
.button:hover {
|
||||||
background: $accent1-darken-1;
|
background: $accent1-darken-1;
|
||||||
color: $text-accent1-darken-1;
|
color: $text-accent1-darken-1;
|
||||||
width:20;
|
width: 20;
|
||||||
height: 3
|
height: 3
|
||||||
border: tall $text-background;
|
border: tall $text-background;
|
||||||
|
|
||||||
margin: 0 1 1 1;
|
margin: 1 1 1 1;
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -112,3 +117,57 @@ TweetBody {
|
|||||||
height: 1;
|
height: 1;
|
||||||
border-top: hkey $accent2-darken-2;
|
border-top: hkey $accent2-darken-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#sidebar .content {
|
||||||
|
layout: vertical
|
||||||
|
}
|
||||||
|
|
||||||
|
OptionItem {
|
||||||
|
height: 3;
|
||||||
|
background: $primary;
|
||||||
|
transition: background 100ms linear;
|
||||||
|
border-right: outer $primary-darken-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
OptionItem:hover {
|
||||||
|
height: 3;
|
||||||
|
background: $accent1-darken-2;
|
||||||
|
/* border-top: hkey $accent2-darken-3;
|
||||||
|
border-bottom: hkey $accent2-darken-3; */
|
||||||
|
text-style: bold;
|
||||||
|
border-right: outer $accent1-darken-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
Error {
|
||||||
|
max-width: 78;
|
||||||
|
height:3;
|
||||||
|
background: $error;
|
||||||
|
color: $text-error;
|
||||||
|
border-top: hkey $error-darken-3;
|
||||||
|
border-bottom: hkey $error-darken-3;
|
||||||
|
margin: 1 3;
|
||||||
|
text-style: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
Warning {
|
||||||
|
max-width: 80;
|
||||||
|
height:3;
|
||||||
|
background: $warning;
|
||||||
|
color: $text-warning-fade-1;
|
||||||
|
border-top: hkey $warning-darken-3;
|
||||||
|
border-bottom: hkey $warning-darken-3;
|
||||||
|
margin: 1 2;
|
||||||
|
text-style: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
Success {
|
||||||
|
max-width: 80;
|
||||||
|
height:3;
|
||||||
|
background: $success-lighten-2;
|
||||||
|
color: $text-success-lighten-2-fade-1;
|
||||||
|
border-top: hkey $success-darken-3;
|
||||||
|
border-bottom: hkey $success-darken-3;
|
||||||
|
margin: 1 2;
|
||||||
|
text-style: bold;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from rich.align import Align
|
||||||
from rich.console import RenderableType
|
from rich.console import RenderableType
|
||||||
from rich.text import Text
|
from rich.text import Text
|
||||||
|
|
||||||
@@ -24,6 +25,26 @@ class Tweet(Widget):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class OptionItem(Widget):
|
||||||
|
def render(self) -> Text:
|
||||||
|
return Align.center(Text("Option", justify="center"), vertical="middle")
|
||||||
|
|
||||||
|
|
||||||
|
class Error(Widget):
|
||||||
|
def render(self) -> Text:
|
||||||
|
return Text("This is an error message", justify="center")
|
||||||
|
|
||||||
|
|
||||||
|
class Warning(Widget):
|
||||||
|
def render(self) -> Text:
|
||||||
|
return Text("This is a warning message", justify="center")
|
||||||
|
|
||||||
|
|
||||||
|
class Success(Widget):
|
||||||
|
def render(self) -> Text:
|
||||||
|
return Text("This is a success message", justify="center")
|
||||||
|
|
||||||
|
|
||||||
class BasicApp(App):
|
class BasicApp(App):
|
||||||
"""A basic app demonstrating CSS"""
|
"""A basic app demonstrating CSS"""
|
||||||
|
|
||||||
@@ -36,14 +57,20 @@ class BasicApp(App):
|
|||||||
self.mount(
|
self.mount(
|
||||||
header=Widget(),
|
header=Widget(),
|
||||||
content=Widget(
|
content=Widget(
|
||||||
Tweet(TweetHeader(), TweetBody(), Widget(classes={"button"})),
|
Tweet(TweetBody(), Widget(classes={"button"})),
|
||||||
Tweet(TweetHeader(), TweetBody()),
|
Error(),
|
||||||
Tweet(TweetHeader(), TweetBody()),
|
Tweet(TweetBody()),
|
||||||
|
Warning(),
|
||||||
|
Tweet(TweetBody()),
|
||||||
|
Success(),
|
||||||
),
|
),
|
||||||
footer=Widget(),
|
footer=Widget(),
|
||||||
sidebar=Widget(
|
sidebar=Widget(
|
||||||
Widget(classes={"title"}),
|
Widget(classes={"title"}),
|
||||||
Widget(classes={"user"}),
|
Widget(classes={"user"}),
|
||||||
|
OptionItem(),
|
||||||
|
OptionItem(),
|
||||||
|
OptionItem(),
|
||||||
Widget(classes={"content"}),
|
Widget(classes={"content"}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
import asyncio
|
import asyncio
|
||||||
import sys
|
import sys
|
||||||
from time import time
|
from time import monotonic
|
||||||
from typing import Any, Callable, TypeVar
|
from typing import Any, Callable, TypeVar
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
@@ -117,7 +117,7 @@ class Animator:
|
|||||||
"""An object to manage updates to a given attribute over a period of time."""
|
"""An object to manage updates to a given attribute over a period of time."""
|
||||||
|
|
||||||
def __init__(self, target: MessageTarget, frames_per_second: int = 60) -> None:
|
def __init__(self, target: MessageTarget, frames_per_second: int = 60) -> None:
|
||||||
self._animations: dict[tuple[object, str], Animation] = {}
|
self._animations: dict[tuple[object, str, object], Animation] = {}
|
||||||
self.target = target
|
self.target = target
|
||||||
self._timer = Timer(
|
self._timer = Timer(
|
||||||
target,
|
target,
|
||||||
@@ -130,7 +130,7 @@ class Animator:
|
|||||||
|
|
||||||
def get_time(self) -> float:
|
def get_time(self) -> float:
|
||||||
"""Get the current wall clock time."""
|
"""Get the current wall clock time."""
|
||||||
return time()
|
return monotonic()
|
||||||
|
|
||||||
async def start(self) -> None:
|
async def start(self) -> None:
|
||||||
"""Start the animator task."""
|
"""Start the animator task."""
|
||||||
@@ -180,9 +180,10 @@ class Animator:
|
|||||||
final_value = value
|
final_value = value
|
||||||
start_time = self.get_time()
|
start_time = self.get_time()
|
||||||
|
|
||||||
animation_key = (id(obj), attribute)
|
animation_key = (id(obj), attribute, final_value)
|
||||||
|
|
||||||
if animation_key in self._animations:
|
if animation_key in self._animations:
|
||||||
self._animations[animation_key](start_time)
|
return
|
||||||
|
|
||||||
easing_function = EASING[easing] if isinstance(easing, str) else easing
|
easing_function = EASING[easing] if isinstance(easing, str) else easing
|
||||||
|
|
||||||
@@ -219,10 +220,11 @@ class Animator:
|
|||||||
easing=easing_function,
|
easing=easing_function,
|
||||||
)
|
)
|
||||||
assert animation is not None, "animation expected to be non-None"
|
assert animation is not None, "animation expected to be non-None"
|
||||||
|
|
||||||
self._animations[animation_key] = animation
|
self._animations[animation_key] = animation
|
||||||
self._timer.resume()
|
self._timer.resume()
|
||||||
|
|
||||||
def __call__(self) -> None:
|
def __call__(self, time: float) -> None:
|
||||||
if not self._animations:
|
if not self._animations:
|
||||||
self._timer.pause()
|
self._timer.pause()
|
||||||
else:
|
else:
|
||||||
@@ -236,4 +238,4 @@ class Animator:
|
|||||||
|
|
||||||
def on_animation_frame(self) -> None:
|
def on_animation_frame(self) -> None:
|
||||||
# TODO: We should be able to do animation without refreshing everything
|
# TODO: We should be able to do animation without refreshing everything
|
||||||
self.target.screen.refresh(layout=True)
|
self.target.screen.refresh_layout()
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ from .geometry import Region, Offset, Size
|
|||||||
|
|
||||||
|
|
||||||
from ._loop import loop_last
|
from ._loop import loop_last
|
||||||
|
from ._profile import timer
|
||||||
from ._segment_tools import line_crop
|
from ._segment_tools import line_crop
|
||||||
from ._types import Lines
|
from ._types import Lines
|
||||||
from .widget import Widget
|
from .widget import Widget
|
||||||
@@ -459,7 +460,6 @@ class Compositor:
|
|||||||
Returns:
|
Returns:
|
||||||
SegmentLines: A renderable
|
SegmentLines: A renderable
|
||||||
"""
|
"""
|
||||||
|
|
||||||
width, height = self.size
|
width, height = self.size
|
||||||
screen_region = Region(0, 0, width, height)
|
screen_region = Region(0, 0, width, height)
|
||||||
|
|
||||||
@@ -495,6 +495,8 @@ class Compositor:
|
|||||||
cut_segments = [line]
|
cut_segments = [line]
|
||||||
else:
|
else:
|
||||||
# More than one cut, which means we need to divide the line
|
# More than one cut, which means we need to divide the line
|
||||||
|
if not final_cuts:
|
||||||
|
continue
|
||||||
render_x = render_region.x
|
render_x = render_region.x
|
||||||
relative_cuts = [cut - render_x for cut in final_cuts]
|
relative_cuts = [cut - render_x for cut in final_cuts]
|
||||||
_, *cut_segments = divide(line, relative_cuts)
|
_, *cut_segments = divide(line, relative_cuts)
|
||||||
|
|||||||
@@ -8,15 +8,17 @@ from asyncio import (
|
|||||||
sleep,
|
sleep,
|
||||||
Task,
|
Task,
|
||||||
)
|
)
|
||||||
|
from functools import partial
|
||||||
from time import monotonic
|
from time import monotonic
|
||||||
from typing import Awaitable, Callable, Union
|
from typing import Awaitable, Callable, Union
|
||||||
|
|
||||||
from rich.repr import Result, rich_repr
|
from rich.repr import Result, rich_repr
|
||||||
|
|
||||||
from . import events
|
from . import events
|
||||||
|
from ._callback import invoke
|
||||||
from ._types import MessageTarget
|
from ._types import MessageTarget
|
||||||
|
|
||||||
TimerCallback = Union[Callable[[], Awaitable[None]], Callable[[], None]]
|
TimerCallback = Union[Callable[[float], Awaitable[None]], Callable[[float], None]]
|
||||||
|
|
||||||
|
|
||||||
class EventTargetGone(Exception):
|
class EventTargetGone(Exception):
|
||||||
@@ -36,9 +38,21 @@ class Timer:
|
|||||||
name: str | None = None,
|
name: str | None = None,
|
||||||
callback: TimerCallback | None = None,
|
callback: TimerCallback | None = None,
|
||||||
repeat: int | None = None,
|
repeat: int | None = None,
|
||||||
skip: bool = False,
|
skip: bool = True,
|
||||||
pause: bool = False,
|
pause: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""A class to send timer-based events.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event_target (MessageTarget): The object which will receive the timer events.
|
||||||
|
interval (float): The time between timer events.
|
||||||
|
sender (MessageTarget): The sender of the event.s
|
||||||
|
name (str | None, optional): A name to assign the event (for debugging). Defaults to None.
|
||||||
|
callback (TimerCallback | None, optional): A optional callback to invoke when the event is handled. Defaults to None.
|
||||||
|
repeat (int | None, optional): The number of times to repeat the timer, or None for no repeat. Defaults to None.
|
||||||
|
skip (bool, optional): Enable skipping of scheduled events that couldn't be sent in time. Defaults to True.
|
||||||
|
pause (bool, optional): Start the timer paused. Defaults to False.
|
||||||
|
"""
|
||||||
self._target_repr = repr(event_target)
|
self._target_repr = repr(event_target)
|
||||||
self._target = weakref.ref(event_target)
|
self._target = weakref.ref(event_target)
|
||||||
self._interval = interval
|
self._interval = interval
|
||||||
@@ -102,11 +116,19 @@ class Timer:
|
|||||||
if wait_time:
|
if wait_time:
|
||||||
await sleep(wait_time)
|
await sleep(wait_time)
|
||||||
event = events.Timer(
|
event = events.Timer(
|
||||||
self.sender, timer=self, count=count, callback=self._callback
|
self.sender,
|
||||||
|
timer=self,
|
||||||
|
time=next_timer,
|
||||||
|
count=count,
|
||||||
|
callback=self._callback,
|
||||||
)
|
)
|
||||||
count += 1
|
count += 1
|
||||||
try:
|
try:
|
||||||
await self.target.post_message(event)
|
if self._callback is not None:
|
||||||
|
await invoke(self._callback, next_timer)
|
||||||
|
else:
|
||||||
|
await self.target.post_priority_message(event)
|
||||||
|
|
||||||
except EventTargetGone:
|
except EventTargetGone:
|
||||||
break
|
break
|
||||||
await self._active.wait()
|
await self._active.wait()
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ class MessageTarget(Protocol):
|
|||||||
async def post_message(self, message: "Message") -> bool:
|
async def post_message(self, message: "Message") -> bool:
|
||||||
...
|
...
|
||||||
|
|
||||||
|
async def post_priority_message(self, message: "Message") -> bool:
|
||||||
|
...
|
||||||
|
|
||||||
def post_message_no_wait(self, message: "Message") -> bool:
|
def post_message_no_wait(self, message: "Message") -> bool:
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ from .file_monitor import FileMonitor
|
|||||||
from .geometry import Offset, Region, Size
|
from .geometry import Offset, Region, Size
|
||||||
from .layouts.dock import Dock
|
from .layouts.dock import Dock
|
||||||
from .message_pump import MessagePump
|
from .message_pump import MessagePump
|
||||||
|
from ._profile import timer
|
||||||
from .reactive import Reactive
|
from .reactive import Reactive
|
||||||
from .screen import Screen
|
from .screen import Screen
|
||||||
from .widget import Widget
|
from .widget import Widget
|
||||||
@@ -113,16 +114,17 @@ class App(DOMNode):
|
|||||||
self._refresh_required = False
|
self._refresh_required = False
|
||||||
|
|
||||||
self.design = ColorSystem(
|
self.design = ColorSystem(
|
||||||
primary="#1b72b1", # blueish
|
primary="#406e8e", # blueish
|
||||||
secondary="#471EC2", # purplesis
|
secondary="#6d9f71", # purplesis
|
||||||
warning="#ffa629", # orange
|
warning="#ffa62b", # orange
|
||||||
error="#db1a4a", # error
|
error="#ba3c5b", # error
|
||||||
success="#38d645", # green
|
success="#6d9f71", # green
|
||||||
accent1="#1b72b1",
|
accent1="#ffa62b",
|
||||||
accent2="#ffa629",
|
accent2="#5a4599",
|
||||||
)
|
)
|
||||||
|
|
||||||
self.stylesheet = Stylesheet(variables=self.get_css_variables())
|
self.stylesheet = Stylesheet(variables=self.get_css_variables())
|
||||||
|
self._require_styles_update = False
|
||||||
|
|
||||||
self.css_file = css_file
|
self.css_file = css_file
|
||||||
self.css_monitor = (
|
self.css_monitor = (
|
||||||
@@ -321,7 +323,8 @@ class App(DOMNode):
|
|||||||
Should be called whenever CSS classes / pseudo classes change.
|
Should be called whenever CSS classes / pseudo classes change.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.post_message_no_wait(messages.StylesUpdated(self))
|
self._require_styles_update = True
|
||||||
|
self.check_idle()
|
||||||
|
|
||||||
def mount(self, *anon_widgets: Widget, **widgets: Widget) -> None:
|
def mount(self, *anon_widgets: Widget, **widgets: Widget) -> None:
|
||||||
self.register(self.screen, *anon_widgets, **widgets)
|
self.register(self.screen, *anon_widgets, **widgets)
|
||||||
@@ -471,6 +474,11 @@ class App(DOMNode):
|
|||||||
if self.log_file is not None:
|
if self.log_file is not None:
|
||||||
self.log_file.close()
|
self.log_file.close()
|
||||||
|
|
||||||
|
async def on_idle(self) -> None:
|
||||||
|
if self._require_styles_update:
|
||||||
|
await self.post_message(messages.StylesUpdated(self))
|
||||||
|
self._require_styles_update = False
|
||||||
|
|
||||||
def _register_child(self, parent: DOMNode, child: DOMNode) -> bool:
|
def _register_child(self, parent: DOMNode, child: DOMNode) -> bool:
|
||||||
if child not in self.registry:
|
if child not in self.registry:
|
||||||
parent.children._append(child)
|
parent.children._append(child)
|
||||||
@@ -713,7 +721,7 @@ class App(DOMNode):
|
|||||||
await self.action(
|
await self.action(
|
||||||
action, default_namespace=default_namespace, modifiers=modifiers
|
action, default_namespace=default_namespace, modifiers=modifiers
|
||||||
)
|
)
|
||||||
elif isinstance(action, Callable):
|
elif callable(action):
|
||||||
await action()
|
await action()
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -300,7 +300,7 @@ class Stylesheet:
|
|||||||
if is_animatable(key) and new_render_value != old_render_value:
|
if is_animatable(key) and new_render_value != old_render_value:
|
||||||
transition = new_styles.get_transition(key)
|
transition = new_styles.get_transition(key)
|
||||||
if transition is not None:
|
if transition is not None:
|
||||||
duration, easing, delay = transition
|
duration, easing, _delay = transition
|
||||||
node.app.animator.animate(
|
node.app.animator.animate(
|
||||||
node.styles.base,
|
node.styles.base,
|
||||||
key,
|
key,
|
||||||
|
|||||||
@@ -364,11 +364,13 @@ class Timer(Event, verbosity=3, bubble=False):
|
|||||||
self,
|
self,
|
||||||
sender: MessageTarget,
|
sender: MessageTarget,
|
||||||
timer: "TimerClass",
|
timer: "TimerClass",
|
||||||
|
time: float,
|
||||||
count: int = 0,
|
count: int = 0,
|
||||||
callback: TimerCallback | None = None,
|
callback: TimerCallback | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(sender)
|
super().__init__(sender)
|
||||||
self.timer = timer
|
self.timer = timer
|
||||||
|
self.time = time
|
||||||
self.count = count
|
self.count = count
|
||||||
self.callback = callback
|
self.callback = callback
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from asyncio import CancelledError
|
from asyncio import CancelledError
|
||||||
from asyncio import Queue, QueueEmpty, Task
|
from asyncio import PriorityQueue, QueueEmpty, Task
|
||||||
from functools import partial
|
from functools import partial, total_ordering
|
||||||
from typing import TYPE_CHECKING, Awaitable, Iterable, Callable
|
from typing import TYPE_CHECKING, Awaitable, Iterable, Callable, NamedTuple
|
||||||
from weakref import WeakSet
|
from weakref import WeakSet
|
||||||
|
|
||||||
from . import events
|
from . import events
|
||||||
@@ -32,9 +32,26 @@ class MessagePumpClosed(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@total_ordering
|
||||||
|
class MessagePriority:
|
||||||
|
__slots__ = ["message", "priority"]
|
||||||
|
|
||||||
|
def __init__(self, message: Message | None = None, priority: int = 0):
|
||||||
|
self.message = message
|
||||||
|
self.priority = priority
|
||||||
|
|
||||||
|
def __eq__(self, other: object) -> bool:
|
||||||
|
assert isinstance(other, MessagePriority)
|
||||||
|
return self.priority == other.priority
|
||||||
|
|
||||||
|
def __gt__(self, other: object) -> bool:
|
||||||
|
assert isinstance(other, MessagePriority)
|
||||||
|
return self.priority > other.priority
|
||||||
|
|
||||||
|
|
||||||
class MessagePump:
|
class MessagePump:
|
||||||
def __init__(self, parent: MessagePump | None = None) -> None:
|
def __init__(self, parent: MessagePump | None = None) -> None:
|
||||||
self._message_queue: Queue[Message | None] = Queue()
|
self._message_queue: PriorityQueue[MessagePriority] = PriorityQueue()
|
||||||
self._parent = parent
|
self._parent = parent
|
||||||
self._running: bool = False
|
self._running: bool = False
|
||||||
self._closing: bool = False
|
self._closing: bool = False
|
||||||
@@ -96,7 +113,7 @@ class MessagePump:
|
|||||||
return self._pending_message
|
return self._pending_message
|
||||||
finally:
|
finally:
|
||||||
self._pending_message = None
|
self._pending_message = None
|
||||||
message = await self._message_queue.get()
|
message = (await self._message_queue.get()).message
|
||||||
if message is None:
|
if message is None:
|
||||||
self._closed = True
|
self._closed = True
|
||||||
raise MessagePumpClosed("The message pump is now closed")
|
raise MessagePumpClosed("The message pump is now closed")
|
||||||
@@ -111,7 +128,7 @@ class MessagePump:
|
|||||||
"""
|
"""
|
||||||
if self._pending_message is None:
|
if self._pending_message is None:
|
||||||
try:
|
try:
|
||||||
self._pending_message = self._message_queue.get_nowait()
|
self._pending_message = self._message_queue.get_nowait().message
|
||||||
except QueueEmpty:
|
except QueueEmpty:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -155,7 +172,7 @@ class MessagePump:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def close_messages_no_wait(self) -> None:
|
def close_messages_no_wait(self) -> None:
|
||||||
self._message_queue.put_nowait(None)
|
self._message_queue.put_nowait(MessagePriority(None))
|
||||||
|
|
||||||
async def close_messages(self) -> None:
|
async def close_messages(self) -> None:
|
||||||
"""Close message queue, and optionally wait for queue to finish processing."""
|
"""Close message queue, and optionally wait for queue to finish processing."""
|
||||||
@@ -164,7 +181,7 @@ class MessagePump:
|
|||||||
|
|
||||||
self._closing = True
|
self._closing = True
|
||||||
|
|
||||||
await self._message_queue.put(None)
|
await self._message_queue.put(MessagePriority(None))
|
||||||
|
|
||||||
for task in self._child_tasks:
|
for task in self._child_tasks:
|
||||||
task.cancel()
|
task.cancel()
|
||||||
@@ -284,7 +301,25 @@ class MessagePump:
|
|||||||
return False
|
return False
|
||||||
if not self.check_message_enabled(message):
|
if not self.check_message_enabled(message):
|
||||||
return True
|
return True
|
||||||
await self._message_queue.put(message)
|
await self._message_queue.put(MessagePriority(message))
|
||||||
|
return True
|
||||||
|
|
||||||
|
# TODO: This may not be needed, or may only be needed by the timer
|
||||||
|
# Consider removing or making private
|
||||||
|
async def post_priority_message(self, message: Message) -> bool:
|
||||||
|
"""Post a "priority" messages which will be processes prior to regular messages.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message (Message): A message.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the messages was processed.
|
||||||
|
"""
|
||||||
|
if self._closing or self._closed:
|
||||||
|
return False
|
||||||
|
if not self.check_message_enabled(message):
|
||||||
|
return True
|
||||||
|
await self._message_queue.put(MessagePriority(message, -1))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def post_message_no_wait(self, message: Message) -> bool:
|
def post_message_no_wait(self, message: Message) -> bool:
|
||||||
@@ -292,7 +327,7 @@ class MessagePump:
|
|||||||
return False
|
return False
|
||||||
if not self.check_message_enabled(message):
|
if not self.check_message_enabled(message):
|
||||||
return True
|
return True
|
||||||
self._message_queue.put_nowait(message)
|
self._message_queue.put_nowait(MessagePriority(message))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def post_message_from_child(self, message: Message) -> bool:
|
async def post_message_from_child(self, message: Message) -> bool:
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ class Screen(Widget):
|
|||||||
def on_idle(self, event: events.Idle) -> None:
|
def on_idle(self, event: events.Idle) -> None:
|
||||||
# Check for any widgets marked as 'dirty' (needs a repaint)
|
# Check for any widgets marked as 'dirty' (needs a repaint)
|
||||||
if self._dirty_widgets:
|
if self._dirty_widgets:
|
||||||
|
self.log(dirty=len(self._dirty_widgets))
|
||||||
for widget in self._dirty_widgets:
|
for widget in self._dirty_widgets:
|
||||||
# Repaint widgets
|
# Repaint widgets
|
||||||
# TODO: Combine these in to a single update.
|
# TODO: Combine these in to a single update.
|
||||||
@@ -100,7 +101,7 @@ class Screen(Widget):
|
|||||||
# Reset dirty list
|
# Reset dirty list
|
||||||
self._dirty_widgets.clear()
|
self._dirty_widgets.clear()
|
||||||
|
|
||||||
async def refresh_layout(self) -> None:
|
def refresh_layout(self) -> None:
|
||||||
"""Refresh the layout (can change size and positions of widgets)."""
|
"""Refresh the layout (can change size and positions of widgets)."""
|
||||||
if not self.size:
|
if not self.size:
|
||||||
return
|
return
|
||||||
@@ -144,11 +145,11 @@ class Screen(Widget):
|
|||||||
|
|
||||||
async def handle_layout(self, message: messages.Layout) -> None:
|
async def handle_layout(self, message: messages.Layout) -> None:
|
||||||
message.stop()
|
message.stop()
|
||||||
await self.refresh_layout()
|
self.refresh_layout()
|
||||||
|
|
||||||
async def on_resize(self, event: events.Resize) -> None:
|
async def on_resize(self, event: events.Resize) -> None:
|
||||||
self.size_updated(event.size, event.virtual_size, event.container_size)
|
self.size_updated(event.size, event.virtual_size, event.container_size)
|
||||||
await self.refresh_layout()
|
self.refresh_layout()
|
||||||
event.stop()
|
event.stop()
|
||||||
|
|
||||||
async def _on_mouse_move(self, event: events.MouseMove) -> None:
|
async def _on_mouse_move(self, event: events.MouseMove) -> None:
|
||||||
|
|||||||
@@ -165,9 +165,9 @@ def test_lab_to_rgb(r, g, b, L_, a_, b_):
|
|||||||
def test_rgb_lab_rgb_roundtrip():
|
def test_rgb_lab_rgb_roundtrip():
|
||||||
"""Test RGB -> CIE-L*ab -> RGB color conversion roundtripping."""
|
"""Test RGB -> CIE-L*ab -> RGB color conversion roundtripping."""
|
||||||
|
|
||||||
for r in range(0, 256, 4):
|
for r in range(0, 256, 32):
|
||||||
for g in range(0, 256, 4):
|
for g in range(0, 256, 32):
|
||||||
for b in range(0, 256, 4):
|
for b in range(0, 256, 32):
|
||||||
c_ = lab_to_rgb(rgb_to_lab(Color(r, g, b)))
|
c_ = lab_to_rgb(rgb_to_lab(Color(r, g, b)))
|
||||||
assert c_.r == pytest.approx(r, abs=1)
|
assert c_.r == pytest.approx(r, abs=1)
|
||||||
assert c_.g == pytest.approx(g, abs=1)
|
assert c_.g == pytest.approx(g, abs=1)
|
||||||
|
|||||||
Reference in New Issue
Block a user