Skip to content

Commit 15417d4

Browse files
authored
Merge pull request #1379 from Textualize/scroll-to-top
fix scroll to top
2 parents d11e3f2 + d264ebb commit 15417d4

File tree

10 files changed

+85
-62
lines changed

10 files changed

+85
-62
lines changed

CHANGELOG.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](http://keepachangelog.com/)
66
and this project adheres to [Semantic Versioning](http://semver.org/).
77

8-
## Unreleased
8+
## [0.7.0] - 2022-12-17
99

1010
### Added
1111

@@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1717
- When looking for bindings that have priority, they are now looked from `App` downwards. https://github.com/Textualize/textual/issues/1343
1818
- `BINDINGS` on an `App`-derived class have priority by default. https://github.com/Textualize/textual/issues/1343
1919
- `BINDINGS` on a `Screen`-derived class have priority by default. https://github.com/Textualize/textual/issues/1343
20+
- Added a message parameter to Widget.exit
2021

2122
### Fixed
2223

@@ -265,7 +266,8 @@ https://textual.textualize.io/blog/2022/11/08/version-040/#version-040
265266
- New handler system for messages that doesn't require inheritance
266267
- Improved traceback handling
267268

268-
[0.6.0]: https://github.com/Textualize/textual/compare/v0.3.0...v0.6.0
269+
[0.7.0]: https://github.com/Textualize/textual/compare/v0.6.0...v0.7.0
270+
[0.6.0]: https://github.com/Textualize/textual/compare/v0.5.0...v0.6.0
269271
[0.5.0]: https://github.com/Textualize/textual/compare/v0.4.0...v0.5.0
270272
[0.4.0]: https://github.com/Textualize/textual/compare/v0.3.0...v0.4.0
271273
[0.3.0]: https://github.com/Textualize/textual/compare/v0.2.1...v0.3.0

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "textual"
3-
version = "0.6.0"
3+
version = "0.7.0"
44
homepage = "https://github.com/Textualize/textual"
55
description = "Modern Text User Interface framework"
66
authors = ["Will McGugan <[email protected]>"]

src/textual/app.py

Lines changed: 35 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,7 @@ def __init__(
350350
self.devtools = DevtoolsClient()
351351

352352
self._return_value: ReturnType | None = None
353+
self._exit = False
353354

354355
self.css_monitor = (
355356
FileMonitor(self.css_path, self._on_css_change)
@@ -416,14 +417,20 @@ def screen_stack(self) -> list[Screen]:
416417
"""list[Screen]: A *copy* of the screen stack."""
417418
return self._screen_stack.copy()
418419

419-
def exit(self, result: ReturnType | None = None) -> None:
420+
def exit(
421+
self, result: ReturnType | None = None, message: RenderableType | None = None
422+
) -> None:
420423
"""Exit the app, and return the supplied result.
421424
422425
Args:
423426
result (ReturnType | None, optional): Return value. Defaults to None.
427+
message (RenderableType | None): Optional message to display on exit.
424428
"""
429+
self._exit = True
425430
self._return_value = result
426431
self.post_message_no_wait(messages.ExitApp(sender=self))
432+
if message:
433+
self._exit_renderables.append(message)
427434

428435
@property
429436
def focused(self) -> Widget | None:
@@ -1082,9 +1089,9 @@ def _get_screen(self, screen: Screen | str) -> tuple[Screen, AwaitMount]:
10821089
_screen = self.get_screen(screen)
10831090
if not _screen.is_running:
10841091
widgets = self._register(self, _screen)
1085-
return (_screen, AwaitMount(widgets))
1092+
return (_screen, AwaitMount(_screen, widgets))
10861093
else:
1087-
return (_screen, AwaitMount([]))
1094+
return (_screen, AwaitMount(_screen, []))
10881095

10891096
def _replace_screen(self, screen: Screen) -> Screen:
10901097
"""Handle the replaced screen.
@@ -1130,7 +1137,7 @@ def switch_screen(self, screen: Screen | str) -> AwaitMount:
11301137
self.screen.post_message_no_wait(events.ScreenResume(self))
11311138
self.log.system(f"{self.screen} is current (SWITCHED)")
11321139
return await_mount
1133-
return AwaitMount([])
1140+
return AwaitMount(self.screen, [])
11341141

11351142
def install_screen(self, screen: Screen, name: str | None = None) -> AwaitMount:
11361143
"""Install a screen.
@@ -1408,28 +1415,29 @@ async def invoke_ready_callback() -> None:
14081415
)
14091416
driver = self._driver = driver_class(self.console, self, size=terminal_size)
14101417

1411-
driver.start_application_mode()
1412-
try:
1413-
if headless:
1414-
await run_process_messages()
1415-
else:
1416-
if self.devtools is not None:
1417-
devtools = self.devtools
1418-
assert devtools is not None
1419-
from .devtools.redirect_output import StdoutRedirector
1420-
1421-
redirector = StdoutRedirector(devtools)
1422-
with redirect_stderr(redirector):
1423-
with redirect_stdout(redirector): # type: ignore
1424-
await run_process_messages()
1418+
if not self._exit:
1419+
driver.start_application_mode()
1420+
try:
1421+
if headless:
1422+
await run_process_messages()
14251423
else:
1426-
null_file = _NullFile()
1427-
with redirect_stderr(null_file):
1428-
with redirect_stdout(null_file):
1429-
await run_process_messages()
1424+
if self.devtools is not None:
1425+
devtools = self.devtools
1426+
assert devtools is not None
1427+
from .devtools.redirect_output import StdoutRedirector
1428+
1429+
redirector = StdoutRedirector(devtools)
1430+
with redirect_stderr(redirector):
1431+
with redirect_stdout(redirector): # type: ignore
1432+
await run_process_messages()
1433+
else:
1434+
null_file = _NullFile()
1435+
with redirect_stderr(null_file):
1436+
with redirect_stdout(null_file):
1437+
await run_process_messages()
14301438

1431-
finally:
1432-
driver.stop_application_mode()
1439+
finally:
1440+
driver.stop_application_mode()
14331441
except Exception as error:
14341442
self._handle_exception(error)
14351443

@@ -1565,7 +1573,6 @@ def _register(
15651573
if widget.children:
15661574
self._register(widget, *widget.children)
15671575
apply_stylesheet(widget)
1568-
15691576
return list(widgets)
15701577

15711578
def _unregister(self, widget: Widget) -> None:
@@ -1735,13 +1742,12 @@ async def check_bindings(self, key: str, priority: bool = False) -> bool:
17351742
"""Handle a key press.
17361743
17371744
Args:
1738-
key (str): A key
1745+
key (str): A key.
17391746
priority (bool): If `True` check from `App` down, otherwise from focused up.
17401747
17411748
Returns:
17421749
bool: True if the key was handled by a binding, otherwise False
17431750
"""
1744-
17451751
for namespace, bindings in (
17461752
reversed(self._binding_chain) if priority else self._binding_chain
17471753
):
@@ -2046,7 +2052,8 @@ async def _prune_node(self, root: Widget) -> None:
20462052
self._unregister(root)
20472053

20482054
async def action_check_bindings(self, key: str) -> None:
2049-
await self.check_bindings(key)
2055+
if not await self.check_bindings(key, priority=True):
2056+
await self.check_bindings(key, priority=False)
20502057

20512058
async def action_quit(self) -> None:
20522059
"""Quit the app as soon as possible."""

src/textual/binding.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,11 @@ def make_bindings(bindings: Iterable[BindingType]) -> Iterable[Binding]:
7777
description=binding.description,
7878
show=binding.show,
7979
key_display=binding.key_display,
80-
priority=default_priority
81-
if binding.priority is None
82-
else binding.priority,
80+
priority=(
81+
default_priority
82+
if binding.priority is None
83+
else binding.priority
84+
),
8385
)
8486

8587
self.keys: MutableMapping[str, Binding] = (

src/textual/css/constants.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,20 +36,21 @@
3636
VALID_TEXT_ALIGN: Final = {"start", "end", "left", "right", "center", "justify"}
3737
VALID_SCROLLBAR_GUTTER: Final = {"auto", "stable"}
3838
VALID_STYLE_FLAGS: Final = {
39-
"none",
40-
"not",
41-
"bold",
39+
"b",
4240
"blink",
41+
"bold",
42+
"dim",
43+
"i",
4344
"italic",
44-
"underline",
45+
"none",
46+
"not",
47+
"o",
4548
"overline",
49+
"reverse",
4650
"strike",
47-
"b",
48-
"i",
4951
"u",
52+
"underline",
5053
"uu",
51-
"o",
52-
"reverse",
5354
}
5455

5556
NULL_SPACING: Final = Spacing.all(0)

src/textual/dom.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -517,8 +517,8 @@ def text_style(self) -> Style:
517517
@property
518518
def rich_style(self) -> Style:
519519
"""Get a Rich Style object for this DOMNode."""
520-
background = WHITE
521-
color = BLACK
520+
background = Color(0, 0, 0, 0)
521+
color = Color(255, 255, 255, 0)
522522
style = Style()
523523
for node in reversed(self.ancestors_with_self):
524524
styles = node.styles
@@ -530,7 +530,8 @@ def rich_style(self) -> Style:
530530
if styles.has_rule("auto_color") and styles.auto_color:
531531
color = background.get_contrast_text(color.a)
532532
style += Style.from_color(
533-
(background + color).rich_color, background.rich_color
533+
(background + color).rich_color if (background.a or color.a) else None,
534+
background.rich_color if background.a else None,
534535
)
535536
return style
536537

src/textual/geometry.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,7 @@ def get_scroll_to_visible(
320320
Offset: An offset required to add to region to move it inside window_region.
321321
"""
322322

323-
if region in window_region:
323+
if region in window_region and not top:
324324
# Region is already inside the window, so no need to move it.
325325
return NULL_OFFSET
326326

@@ -341,19 +341,19 @@ def get_scroll_to_visible(
341341
key=abs,
342342
)
343343

344-
if not (
344+
if top:
345+
delta_y = top_ - window_top
346+
347+
elif not (
345348
(window_bottom > top_ >= window_top)
346349
and (window_bottom > bottom >= window_top)
347350
):
348351
# The window needs to scroll on the Y axis to bring region in to view
349-
if top:
350-
delta_y = top_ - window_top
351-
else:
352-
delta_y = min(
353-
top_ - window_top,
354-
top_ - (window_bottom - region.height),
355-
key=abs,
356-
)
352+
delta_y = min(
353+
top_ - window_top,
354+
top_ - (window_bottom - region.height),
355+
key=abs,
356+
)
357357
return Offset(delta_x, delta_y)
358358

359359
def __bool__(self) -> bool:

src/textual/screen.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,7 @@ def set_focus(self, widget: Widget | None, scroll_visible: bool = True) -> None:
297297
self.focused.post_message_no_wait(events.Blur(self))
298298
self.focused.emit_no_wait(events.DescendantBlur(self))
299299
self.focused = None
300+
self.log.debug("focus was removed")
300301
elif widget.can_focus:
301302
if self.focused != widget:
302303
if self.focused is not None:
@@ -310,6 +311,7 @@ def set_focus(self, widget: Widget | None, scroll_visible: bool = True) -> None:
310311
self.screen.scroll_to_widget(widget)
311312
widget.post_message_no_wait(events.Focus(self))
312313
widget.emit_no_wait(events.DescendantFocus(self))
314+
self.log.debug(widget, "was focused")
313315

314316
async def _on_idle(self, event: events.Idle) -> None:
315317
# Check for any widgets marked as 'dirty' (needs a repaint)

src/textual/widget.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,8 @@ class AwaitMount:
8585
8686
"""
8787

88-
def __init__(self, widgets: Sequence[Widget]) -> None:
88+
def __init__(self, parent: Widget, widgets: Sequence[Widget]) -> None:
89+
self._parent = parent
8990
self._widgets = widgets
9091

9192
def __await__(self) -> Generator[None, None, None]:
@@ -97,6 +98,7 @@ async def await_mount() -> None:
9798
]
9899
if aws:
99100
await wait(aws)
101+
self._parent.refresh(layout=True)
100102

101103
return await_mount().__await__()
102104

@@ -595,12 +597,12 @@ def mount(
595597
else:
596598
parent = self
597599

598-
return AwaitMount(
599-
self.app._register(
600-
parent, *widgets, before=insert_before, after=insert_after
601-
)
600+
mounted = self.app._register(
601+
parent, *widgets, before=insert_before, after=insert_after
602602
)
603603

604+
return AwaitMount(self, mounted)
605+
604606
def move_child(
605607
self,
606608
child: int | Widget,
@@ -1805,7 +1807,7 @@ def scroll_to_region(
18051807
if spacing is not None:
18061808
window = window.shrink(spacing)
18071809

1808-
if window in region:
1810+
if window in region and not top:
18091811
return Offset()
18101812

18111813
delta_x, delta_y = Region.get_scroll_to_visible(window, region, top=top)

src/textual/widgets/_tree.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,7 @@ def get_label_width(self, node: TreeNode[TreeDataType]) -> int:
451451

452452
def clear(self) -> None:
453453
"""Clear all nodes under root."""
454+
self._line_cache.clear()
454455
self._tree_lines_cached = None
455456
self._current_id = 0
456457
root_label = self.root._label
@@ -614,6 +615,11 @@ def _tree_lines(self) -> list[_TreeLine]:
614615
assert self._tree_lines_cached is not None
615616
return self._tree_lines_cached
616617

618+
def _on_idle(self) -> None:
619+
"""Check tree needs a rebuild on idle."""
620+
# Property calls build if required
621+
self._tree_lines
622+
617623
def _build(self) -> None:
618624
"""Builds the tree by traversing nodes, and creating tree lines."""
619625

@@ -805,7 +811,7 @@ async def _on_click(self, event: events.Click) -> None:
805811
cursor_line = meta["line"]
806812
if meta.get("toggle", False):
807813
node = self.get_node_at_line(cursor_line)
808-
if node is not None and self.auto_expand:
814+
if node is not None:
809815
self._toggle_node(node)
810816

811817
else:

0 commit comments

Comments
 (0)