mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
fix for call_later and scroll_to_widget
This commit is contained in:
@@ -7,56 +7,61 @@ from textual.widgets import Button, Header, Footer, Static
|
|||||||
|
|
||||||
|
|
||||||
class TimeDisplay(Static):
|
class TimeDisplay(Static):
|
||||||
"""Displays the time."""
|
"""A widget to display elapsed time."""
|
||||||
|
|
||||||
time = Reactive(0.0)
|
start_time = Reactive(monotonic)
|
||||||
|
time = Reactive.init(0.0)
|
||||||
|
total = Reactive(0.0)
|
||||||
|
|
||||||
|
def on_mount(self) -> None:
|
||||||
|
"""Event handler called when widget is added to the app."""
|
||||||
|
self.update_timer = self.set_interval(1 / 60, self.update_time, pause=True)
|
||||||
|
|
||||||
|
def update_time(self) -> None:
|
||||||
|
"""Method to update time to current."""
|
||||||
|
self.time = self.total + (monotonic() - self.start_time)
|
||||||
|
|
||||||
def watch_time(self, time: float) -> None:
|
def watch_time(self, time: float) -> None:
|
||||||
"""Called when time_delta changes."""
|
"""Called when the time attribute changes."""
|
||||||
minutes, seconds = divmod(time, 60)
|
minutes, seconds = divmod(time, 60)
|
||||||
hours, minutes = divmod(minutes, 60)
|
hours, minutes = divmod(minutes, 60)
|
||||||
self.update(f"{hours:02,.0f}:{minutes:02.0f}:{seconds:05.2f}")
|
self.update(f"{hours:02,.0f}:{minutes:02.0f}:{seconds:05.2f}")
|
||||||
|
|
||||||
|
def start(self) -> None:
|
||||||
|
"""Method to start (or resume) time updating."""
|
||||||
|
self.start_time = monotonic()
|
||||||
|
self.update_timer.resume()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""Method to stop the time display updating."""
|
||||||
|
self.update_timer.pause()
|
||||||
|
self.total += monotonic() - self.start_time
|
||||||
|
self.time = self.total
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
"""Method to reset the time display to zero."""
|
||||||
|
self.total = 0
|
||||||
|
self.time = 0
|
||||||
|
|
||||||
|
|
||||||
class Stopwatch(Static):
|
class Stopwatch(Static):
|
||||||
"""The timer widget (display + buttons)."""
|
"""A stopwatch widget."""
|
||||||
|
|
||||||
start_time = Reactive(0.0)
|
|
||||||
total = Reactive(0.0)
|
|
||||||
started = Reactive(False)
|
|
||||||
|
|
||||||
def watch_started(self, started: bool) -> None:
|
|
||||||
"""Called when the 'started' attribute changes."""
|
|
||||||
if started:
|
|
||||||
self.start_time = monotonic()
|
|
||||||
self.update_timer.resume()
|
|
||||||
self.add_class("started")
|
|
||||||
self.query_one("#stop").focus()
|
|
||||||
else:
|
|
||||||
self.update_timer.pause()
|
|
||||||
self.total += monotonic() - self.start_time
|
|
||||||
self.remove_class("started")
|
|
||||||
self.query_one("#start").focus()
|
|
||||||
|
|
||||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||||
"""Called when a button is pressed."""
|
"""Event handler called when a button is pressed."""
|
||||||
self.started = event.button.id == "start"
|
button_id = event.button.id
|
||||||
if event.button.id == "reset":
|
time_display = self.query_one(TimeDisplay)
|
||||||
self.total = 0.0
|
if button_id == "start":
|
||||||
self.update_elapsed()
|
time_display.start()
|
||||||
|
self.add_class("started")
|
||||||
def on_mount(self) -> None:
|
elif button_id == "stop":
|
||||||
"""Called when widget is first added."""
|
time_display.stop()
|
||||||
self.update_timer = self.set_interval(1 / 30, self.update_elapsed, pause=True)
|
self.remove_class("started")
|
||||||
|
elif button_id == "reset":
|
||||||
def update_elapsed(self) -> None:
|
time_display.reset()
|
||||||
"""Updates elapsed time."""
|
|
||||||
self.query_one(TimeDisplay).time = (
|
|
||||||
self.total + monotonic() - self.start_time if self.started else self.total
|
|
||||||
)
|
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
"""Composes the timer widget."""
|
"""Create child widgets of a stopwatch."""
|
||||||
yield Button("Start", id="start", variant="success")
|
yield Button("Start", id="start", variant="success")
|
||||||
yield Button("Stop", id="stop", variant="error")
|
yield Button("Stop", id="stop", variant="error")
|
||||||
yield Button("Reset", id="reset")
|
yield Button("Reset", id="reset")
|
||||||
@@ -68,8 +73,8 @@ class StopwatchApp(App):
|
|||||||
|
|
||||||
def on_load(self) -> None:
|
def on_load(self) -> None:
|
||||||
"""Called when the app first loads."""
|
"""Called when the app first loads."""
|
||||||
self.bind("a", "add_timer", description="Add")
|
self.bind("a", "add_stopwatch", description="Add")
|
||||||
self.bind("r", "remove_timer", description="Remove")
|
self.bind("r", "remove_stopwatch", description="Remove")
|
||||||
self.bind("d", "toggle_dark", description="Dark mode")
|
self.bind("d", "toggle_dark", description="Dark mode")
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
@@ -78,13 +83,13 @@ class StopwatchApp(App):
|
|||||||
yield Footer()
|
yield Footer()
|
||||||
yield Container(Stopwatch(), Stopwatch(), Stopwatch(), id="timers")
|
yield Container(Stopwatch(), Stopwatch(), Stopwatch(), id="timers")
|
||||||
|
|
||||||
def action_add_timer(self) -> None:
|
def action_add_stopwatch(self) -> None:
|
||||||
"""An action to add a timer."""
|
"""An action to add a timer."""
|
||||||
new_timer = Stopwatch()
|
new_stopwatch = Stopwatch()
|
||||||
self.query_one("#timers").mount(new_timer)
|
self.query_one("#timers").mount(new_stopwatch)
|
||||||
new_timer.scroll_visible()
|
new_stopwatch.scroll_visible()
|
||||||
|
|
||||||
def action_remove_timer(self) -> None:
|
def action_remove_stopwatch(self) -> None:
|
||||||
"""Called to remove a timer."""
|
"""Called to remove a timer."""
|
||||||
timers = self.query("#timers Stopwatch")
|
timers = self.query("#timers Stopwatch")
|
||||||
if timers:
|
if timers:
|
||||||
|
|||||||
@@ -45,18 +45,19 @@ class TimeDisplay(Static):
|
|||||||
|
|
||||||
|
|
||||||
class Stopwatch(Static):
|
class Stopwatch(Static):
|
||||||
"""A Textual app to manage stopwatches."""
|
"""A stopwatch widget."""
|
||||||
|
|
||||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||||
"""Event handler called when a button is pressed."""
|
"""Event handler called when a button is pressed."""
|
||||||
|
button_id = event.button.id
|
||||||
time_display = self.query_one(TimeDisplay)
|
time_display = self.query_one(TimeDisplay)
|
||||||
if event.button.id == "start":
|
if button_id == "start":
|
||||||
time_display.start()
|
time_display.start()
|
||||||
self.add_class("started")
|
self.add_class("started")
|
||||||
elif event.button.id == "stop":
|
elif button_id == "stop":
|
||||||
time_display.stop()
|
time_display.stop()
|
||||||
self.remove_class("started")
|
self.remove_class("started")
|
||||||
elif event.button.id == "reset":
|
elif button_id == "reset":
|
||||||
time_display.reset()
|
time_display.reset()
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
@@ -68,6 +69,8 @@ class Stopwatch(Static):
|
|||||||
|
|
||||||
|
|
||||||
class StopwatchApp(App):
|
class StopwatchApp(App):
|
||||||
|
"""A Textual app to manage stopwatches."""
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
"""Create child widgets for the app."""
|
"""Create child widgets for the app."""
|
||||||
yield Header()
|
yield Header()
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ This will be a simple yet **fully featured** app — you could distribute th
|
|||||||
Here's what the finished app will look like:
|
Here's what the finished app will look like:
|
||||||
|
|
||||||
|
|
||||||
```{.textual path="docs/examples/introduction/stopwatch.py"}
|
```{.textual path="docs/examples/introduction/stopwatch.py" press="tab,enter,_,tab,enter,_,tab,_,enter,_,tab,enter,_,_"}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Try the code
|
### Try the code
|
||||||
@@ -75,7 +75,7 @@ Hit the ++d++ key to toggle dark mode.
|
|||||||
|
|
||||||
Hit ++ctrl+c++ to exit the app and return to the command prompt.
|
Hit ++ctrl+c++ to exit the app and return to the command prompt.
|
||||||
|
|
||||||
### Looking at the code
|
### A closer look at the App class
|
||||||
|
|
||||||
Let's examine stopwatch01.py in more detail.
|
Let's examine stopwatch01.py in more detail.
|
||||||
|
|
||||||
@@ -83,7 +83,6 @@ Let's examine stopwatch01.py in more detail.
|
|||||||
--8<-- "docs/examples/introduction/stopwatch01.py"
|
--8<-- "docs/examples/introduction/stopwatch01.py"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
The first line imports the Textual `App` class. The second line imports two builtin widgets: `Footer` which shows available keys and `Header` which shows a title and the current time.
|
The first line imports the Textual `App` class. The second line imports two builtin widgets: `Footer` which shows available keys and `Header` which shows a title and the current time.
|
||||||
|
|
||||||
Widgets are re-usable components responsible for managing a part of the screen. We will cover how to build such widgets in this introduction.
|
Widgets are re-usable components responsible for managing a part of the screen. We will cover how to build such widgets in this introduction.
|
||||||
@@ -114,7 +113,7 @@ There are three methods in our stopwatch app currently.
|
|||||||
|
|
||||||
The last lines in "stopwatch01.py" may be familiar to you. We create an instance of our app class, and call `run()` within a `__name__ == "__main__"` conditional block. This is so that we could import `app` if we want to. Or we could run it with `python stopwatch01.py`.
|
The last lines in "stopwatch01.py" may be familiar to you. We create an instance of our app class, and call `run()` within a `__name__ == "__main__"` conditional block. This is so that we could import `app` if we want to. Or we could run it with `python stopwatch01.py`.
|
||||||
|
|
||||||
## Creating a custom widget
|
## Designing a UI with widgets
|
||||||
|
|
||||||
The header and footer are builtin widgets. For our Stopwatch application we will need to build custom widgets.
|
The header and footer are builtin widgets. For our Stopwatch application we will need to build custom widgets.
|
||||||
|
|
||||||
@@ -161,7 +160,7 @@ The new line in `Stopwatch.compose()` yields a single `Container` object which w
|
|||||||
|
|
||||||
### The unstyled app
|
### The unstyled app
|
||||||
|
|
||||||
Let's see what happens when we run "stopwatch02.py":
|
Let's see what happens when we run "stopwatch02.py".
|
||||||
|
|
||||||
```{.textual path="docs/examples/introduction/stopwatch02.py" title="stopwatch02.py"}
|
```{.textual path="docs/examples/introduction/stopwatch02.py" title="stopwatch02.py"}
|
||||||
```
|
```
|
||||||
@@ -170,7 +169,7 @@ The elements of the stopwatch application are there. The buttons are clickable a
|
|||||||
|
|
||||||
## Writing Textual CSS
|
## Writing Textual CSS
|
||||||
|
|
||||||
Every widget has a `styles` object which contains information regarding how that widget will look. Setting any of the attributes on that styles object will change how Textual displays the widget.
|
Every widget has a `styles` object which contains information regarding how that widget will look. Setting any of the attributes on the styles object will update the screen.
|
||||||
|
|
||||||
Here's how you might set white text and a blue background for a widget:
|
Here's how you might set white text and a blue background for a widget:
|
||||||
|
|
||||||
@@ -331,62 +330,66 @@ Here we have created two reactive attributes: `start_time` and `time`. These att
|
|||||||
|
|
||||||
`Reactive` is an example of a Python _descriptor_, which allows you to dynamically create properties.
|
`Reactive` is an example of a Python _descriptor_, which allows you to dynamically create properties.
|
||||||
|
|
||||||
The first argument to `Reactive` may be a default value, or a callable that returns the default value. In the example, the default for `start_time` is `monotonic` which is a function that returns the time. When `TimeDisplay` is mounted the `start_time` attribute will automatically be assigned the value returned by `monotonic()`.
|
The first argument to `Reactive` may be a default value or a callable that returns the default value. In the example, the default for `start_time` is `monotonic` which is a function that returns the time. When `TimeDisplay` is mounted, the `start_time` attribute will be assigned the result of `monotonic()`.
|
||||||
|
|
||||||
The `time` attribute has a simple float as the default value, so `self.time` will be `0` on start.
|
The `time` attribute has a simple float as the default value, so `self.time` will be `0` on start.
|
||||||
|
|
||||||
To update the time automatically we will use the `set_interval` method which tells Textual to call a function at given intervals. The `on_mount` method does this to call `self.update_time` 60 times a second.
|
In the `on_mount` handler method, the call to `set_interval` creates a timer object which calls `self.update_time` sixty times a second. This `update_time` method calculates the time elapsed since the widget started and assigns it to `self.time`. Which brings us to one of Reactive's super-powers.
|
||||||
|
|
||||||
In `update_time` we calculate the time elapsed since the widget started and assign it to `self.time`. Which brings us to one of Reactive's super-powers.
|
|
||||||
|
|
||||||
If you implement a method that begins with `watch_` followed by the name of a reactive attribute (making it a _watch method_), that method will be called when the attribute is modified.
|
If you implement a method that begins with `watch_` followed by the name of a reactive attribute (making it a _watch method_), that method will be called when the attribute is modified.
|
||||||
|
|
||||||
Because `watch_time` watches the `time` attribute, when we update `self.time` 60 times a second we also implicitly call `watch_time` which converts the elapsed time in to a string and updates the widget with a call to `self.update`.
|
Because `watch_time` watches the `time` attribute, when we update `self.time` 60 times a second we also implicitly call `watch_time` which converts the elapsed time in to a string and updates the widget with a call to `self.update`.
|
||||||
|
|
||||||
The end result is that all the `Stopwatch` widgets show the time elapsed since the widget was created:
|
The end result is that the `Stopwatch` widgets show the time elapsed since the widget was created:
|
||||||
|
|
||||||
```{.textual path="docs/examples/introduction/stopwatch05.py" title="stopwatch05.py"}
|
```{.textual path="docs/examples/introduction/stopwatch05.py" title="stopwatch05.py"}
|
||||||
```
|
```
|
||||||
|
|
||||||
We've seen how we can update widgets with a timer. But we still need to wire buttons to the widget
|
We've seen how we can update widgets with a timer. But we still need to wire buttons to the widget
|
||||||
|
|
||||||
### Wiring the Stopwatch
|
### Wiring buttons
|
||||||
|
|
||||||
To make a useful stopwatch we will need to add a little more code to `TimeDisplay`, to be able to start, stop, and reset the timer.
|
We need to be able to start, stop, and reset each stopwatch independently. We can do this by adding a few more methods to the `TimeDisplay` class.
|
||||||
|
|
||||||
```python title="stopwatch06.py" hl_lines="14-44 50-60"
|
|
||||||
|
```python title="stopwatch06.py" hl_lines="14-44 50-61"
|
||||||
--8<-- "docs/examples/introduction/stopwatch06.py"
|
--8<-- "docs/examples/introduction/stopwatch06.py"
|
||||||
```
|
```
|
||||||
|
|
||||||
Here's a summary of the changes made to `TimeDisplay`.
|
Here's a summary of the changes made to `TimeDisplay`.
|
||||||
|
|
||||||
- We've added a `total` reactive attribute to store the total time elapsed between clicking Stop and Start.
|
- We've added a `total` reactive attribute to store the total time elapsed between clicking that start and stop buttons.
|
||||||
- The call to `set_interval` has grown a `pause=True` attribute which starts the timer in pause mode. This is because we don't want to update the timer until the user hits the Start button.
|
- The call to `set_interval` has grown a `pause=True` argument which starts the timer in pause mode (when a timer is paused it won't run until `resume()` is called). This is because we don't want the time to update until the user hits the start button.
|
||||||
- We've stored the result of `set_interval` which returns a timer object. We will use this later to _resume_ the timer when we start the Stopwatch.
|
- We've stored the result of `set_interval` which returns a Timer object. We will use this later to _resume_ the timer when we start the Stopwatch.
|
||||||
- We've added `start()`, `stop()`, and `reset()` methods.
|
- We've added `start()`, `stop()`, and `reset()` methods.
|
||||||
|
|
||||||
The `on_button_pressed` method on `Stopwatch` has grown some code to manage the time display when the user clicked a button. Let's look at that in detail:
|
The `on_button_pressed` method on `Stopwatch` has grown some code to manage the time display when the user clicks a button. Let's look at that in detail:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||||
"""Event handler called when a button is pressed."""
|
"""Event handler called when a button is pressed."""
|
||||||
|
button_id = event.button.id
|
||||||
time_display = self.query_one(TimeDisplay)
|
time_display = self.query_one(TimeDisplay)
|
||||||
if event.button.id == "start":
|
if button_id == "start":
|
||||||
time_display.start()
|
time_display.start()
|
||||||
self.add_class("started")
|
self.add_class("started")
|
||||||
elif event.button.id == "stop":
|
elif button_id == "stop":
|
||||||
time_display.stop()
|
time_display.stop()
|
||||||
self.remove_class("started")
|
self.remove_class("started")
|
||||||
elif event.button.id == "reset":
|
elif button_id == "reset":
|
||||||
time_display.reset()
|
time_display.reset()
|
||||||
```
|
```
|
||||||
|
|
||||||
This code supplies the missing features and makes our app really useful. If you run it now you can start and stop timers independently.
|
This code supplies the missing features and makes our app useful. We've made the following changes.
|
||||||
|
|
||||||
- The first line calls `query_one` to get a reference to the `TimeDisplay` widget. This method queries for a child widget. You may supply a Widget type or a CSS selector.
|
- The first line stores the button's id, which we will use to decide what to do in response.
|
||||||
- We call the `TimeDisplay` method that matches the button pressed.
|
- The second line calls `query_one` to get a reference to the `TimeDisplay` widget. This method queries for a child widget. You may supply a Widget type or a CSS selector.
|
||||||
- We add the "started" class when the Stopwatch is started, and remove it when it is stopped.
|
- We call the method on `TimeDisplay` that matches the pressed button.
|
||||||
|
- We add the "started" class when the Stopwatch is started, and remove it when it is stopped. This will update the Stopwatch visuals via CSS and show the buttons that match the state.
|
||||||
|
|
||||||
|
If you run stopwatch06.py you will be able to use the stopwatches independently.
|
||||||
|
|
||||||
```{.textual path="docs/examples/introduction/stopwatch06.py" title="stopwatch06.py" press="tab,enter,_,_,tab,enter,_,tab"}
|
```{.textual path="docs/examples/introduction/stopwatch06.py" title="stopwatch06.py" press="tab,enter,_,_,tab,enter,_,tab"}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The only remaining feature of the Stopwatch app let to implement is the ability to add and remove timers.
|
||||||
|
|||||||
@@ -463,8 +463,10 @@ class Compositor:
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
raise errors.NoWidget("Widget is not in layout")
|
raise errors.NoWidget("Widget is not in layout")
|
||||||
|
|
||||||
|
@timer("get_widget_at")
|
||||||
def get_widget_at(self, x: int, y: int) -> tuple[Widget, Region]:
|
def get_widget_at(self, x: int, y: int) -> tuple[Widget, Region]:
|
||||||
"""Get the widget under the given point or None."""
|
"""Get the widget under the given point or None."""
|
||||||
|
# TODO: Optimize with some line based lookup
|
||||||
contains = Region.contains
|
contains = Region.contains
|
||||||
for widget, cropped_region, region, *_ in self:
|
for widget, cropped_region, region, *_ in self:
|
||||||
if contains(cropped_region, x, y):
|
if contains(cropped_region, x, y):
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
self,
|
self,
|
||||||
driver_class: Type[Driver] | None = None,
|
driver_class: Type[Driver] | None = None,
|
||||||
log_path: str | PurePath = "",
|
log_path: str | PurePath = "",
|
||||||
log_verbosity: int = 1,
|
log_verbosity: int = 0,
|
||||||
log_color_system: Literal[
|
log_color_system: Literal[
|
||||||
"auto", "standard", "256", "truecolor", "windows"
|
"auto", "standard", "256", "truecolor", "windows"
|
||||||
] = "auto",
|
] = "auto",
|
||||||
@@ -991,7 +991,9 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
self.log(f"Couldn't connect to devtools ({self.devtools.url})")
|
self.log(f"Couldn't connect to devtools ({self.devtools.url})")
|
||||||
|
|
||||||
self.log("---")
|
self.log("---")
|
||||||
|
|
||||||
self.log(driver=self.driver_class)
|
self.log(driver=self.driver_class)
|
||||||
|
self.log(log_verbosity=self.log_verbosity)
|
||||||
self.log(loop=asyncio.get_running_loop())
|
self.log(loop=asyncio.get_running_loop())
|
||||||
self.log(features=self.features)
|
self.log(features=self.features)
|
||||||
|
|
||||||
@@ -1106,7 +1108,7 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
parent.children._append(child)
|
parent.children._append(child)
|
||||||
self._registry.add(child)
|
self._registry.add(child)
|
||||||
child._attach(parent)
|
child._attach(parent)
|
||||||
child.on_register(self)
|
child._post_register(self)
|
||||||
child.start_messages()
|
child.start_messages()
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ class DOMNode(MessagePump):
|
|||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
|
|
||||||
def on_register(self, app: App) -> None:
|
def _post_register(self, app: App) -> None:
|
||||||
"""Called when the widget is registered
|
"""Called when the widget is registered
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
@rich.repr.auto
|
@rich.repr.auto
|
||||||
class Event(Message):
|
class Event(Message, verbosity=2):
|
||||||
def __rich_repr__(self) -> rich.repr.Result:
|
def __rich_repr__(self) -> rich.repr.Result:
|
||||||
return
|
return
|
||||||
yield
|
yield
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ class Widget(DOMNode):
|
|||||||
return
|
return
|
||||||
yield
|
yield
|
||||||
|
|
||||||
def on_register(self, app: App) -> None:
|
def _post_register(self, app: App) -> None:
|
||||||
"""Called when the instance is registered.
|
"""Called when the instance is registered.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -699,6 +699,7 @@ class Widget(DOMNode):
|
|||||||
Returns:
|
Returns:
|
||||||
bool: True if the scroll position changed, otherwise False.
|
bool: True if the scroll position changed, otherwise False.
|
||||||
"""
|
"""
|
||||||
|
self.log(self, x, y, verbosity=0)
|
||||||
scrolled_x = scrolled_y = False
|
scrolled_x = scrolled_y = False
|
||||||
if animate:
|
if animate:
|
||||||
# TODO: configure animation speed
|
# TODO: configure animation speed
|
||||||
@@ -861,7 +862,6 @@ class Widget(DOMNode):
|
|||||||
Returns:
|
Returns:
|
||||||
Offset: The distance that was scrolled.
|
Offset: The distance that was scrolled.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
window = self.content_region.at_offset(self.scroll_offset)
|
window = self.content_region.at_offset(self.scroll_offset)
|
||||||
if spacing is not None:
|
if spacing is not None:
|
||||||
window = window.shrink(spacing)
|
window = window.shrink(spacing)
|
||||||
@@ -1035,8 +1035,9 @@ class Widget(DOMNode):
|
|||||||
|
|
||||||
self.scroll_x = self.validate_scroll_x(self.scroll_x)
|
self.scroll_x = self.validate_scroll_x(self.scroll_x)
|
||||||
self.scroll_y = self.validate_scroll_y(self.scroll_y)
|
self.scroll_y = self.validate_scroll_y(self.scroll_y)
|
||||||
self.refresh(layout=True)
|
# self.refresh(layout=True)
|
||||||
self.call_later(self.scroll_to, self.scroll_x, self.scroll_y)
|
self.scroll_to(self.scroll_x, self.scroll_y)
|
||||||
|
# self.call_later(self.scroll_to, self.scroll_x, self.scroll_y)
|
||||||
else:
|
else:
|
||||||
self.refresh()
|
self.refresh()
|
||||||
|
|
||||||
@@ -1105,9 +1106,6 @@ class Widget(DOMNode):
|
|||||||
offset_x, offset_y = self.screen.get_offset(self)
|
offset_x, offset_y = self.screen.get_offset(self)
|
||||||
return self.screen.get_style_at(x + offset_x, y + offset_y)
|
return self.screen.get_style_at(x + offset_x, y + offset_y)
|
||||||
|
|
||||||
def call_later(self, callback: Callable, *args, **kwargs) -> None:
|
|
||||||
self.app.call_later(callback, *args, **kwargs)
|
|
||||||
|
|
||||||
async def forward_event(self, event: events.Event) -> None:
|
async def forward_event(self, event: events.Event) -> None:
|
||||||
event.set_forwarded()
|
event.set_forwarded()
|
||||||
await self.post_message(event)
|
await self.post_message(event)
|
||||||
|
|||||||
Reference in New Issue
Block a user