Skip to content

Commit

Permalink
Merge pull request #1379 from Textualize/scroll-to-top
Browse files Browse the repository at this point in the history
fix scroll to top
  • Loading branch information
willmcgugan authored Dec 17, 2022
2 parents d11e3f2 + d264ebb commit 15417d4
Show file tree
Hide file tree
Showing 10 changed files with 85 additions and 62 deletions.
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## Unreleased
## [0.7.0] - 2022-12-17

### Added

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

### Fixed

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

[0.6.0]: https://github.com/Textualize/textual/compare/v0.3.0...v0.6.0
[0.7.0]: https://github.com/Textualize/textual/compare/v0.6.0...v0.7.0
[0.6.0]: https://github.com/Textualize/textual/compare/v0.5.0...v0.6.0
[0.5.0]: https://github.com/Textualize/textual/compare/v0.4.0...v0.5.0
[0.4.0]: https://github.com/Textualize/textual/compare/v0.3.0...v0.4.0
[0.3.0]: https://github.com/Textualize/textual/compare/v0.2.1...v0.3.0
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "textual"
version = "0.6.0"
version = "0.7.0"
homepage = "https://github.com/Textualize/textual"
description = "Modern Text User Interface framework"
authors = ["Will McGugan <[email protected]>"]
Expand Down
63 changes: 35 additions & 28 deletions src/textual/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,7 @@ def __init__(
self.devtools = DevtoolsClient()

self._return_value: ReturnType | None = None
self._exit = False

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

def exit(self, result: ReturnType | None = None) -> None:
def exit(
self, result: ReturnType | None = None, message: RenderableType | None = None
) -> None:
"""Exit the app, and return the supplied result.
Args:
result (ReturnType | None, optional): Return value. Defaults to None.
message (RenderableType | None): Optional message to display on exit.
"""
self._exit = True
self._return_value = result
self.post_message_no_wait(messages.ExitApp(sender=self))
if message:
self._exit_renderables.append(message)

@property
def focused(self) -> Widget | None:
Expand Down Expand Up @@ -1082,9 +1089,9 @@ def _get_screen(self, screen: Screen | str) -> tuple[Screen, AwaitMount]:
_screen = self.get_screen(screen)
if not _screen.is_running:
widgets = self._register(self, _screen)
return (_screen, AwaitMount(widgets))
return (_screen, AwaitMount(_screen, widgets))
else:
return (_screen, AwaitMount([]))
return (_screen, AwaitMount(_screen, []))

def _replace_screen(self, screen: Screen) -> Screen:
"""Handle the replaced screen.
Expand Down Expand Up @@ -1130,7 +1137,7 @@ def switch_screen(self, screen: Screen | str) -> AwaitMount:
self.screen.post_message_no_wait(events.ScreenResume(self))
self.log.system(f"{self.screen} is current (SWITCHED)")
return await_mount
return AwaitMount([])
return AwaitMount(self.screen, [])

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

driver.start_application_mode()
try:
if headless:
await run_process_messages()
else:
if self.devtools is not None:
devtools = self.devtools
assert devtools is not None
from .devtools.redirect_output import StdoutRedirector

redirector = StdoutRedirector(devtools)
with redirect_stderr(redirector):
with redirect_stdout(redirector): # type: ignore
await run_process_messages()
if not self._exit:
driver.start_application_mode()
try:
if headless:
await run_process_messages()
else:
null_file = _NullFile()
with redirect_stderr(null_file):
with redirect_stdout(null_file):
await run_process_messages()
if self.devtools is not None:
devtools = self.devtools
assert devtools is not None
from .devtools.redirect_output import StdoutRedirector

redirector = StdoutRedirector(devtools)
with redirect_stderr(redirector):
with redirect_stdout(redirector): # type: ignore
await run_process_messages()
else:
null_file = _NullFile()
with redirect_stderr(null_file):
with redirect_stdout(null_file):
await run_process_messages()

finally:
driver.stop_application_mode()
finally:
driver.stop_application_mode()
except Exception as error:
self._handle_exception(error)

Expand Down Expand Up @@ -1565,7 +1573,6 @@ def _register(
if widget.children:
self._register(widget, *widget.children)
apply_stylesheet(widget)

return list(widgets)

def _unregister(self, widget: Widget) -> None:
Expand Down Expand Up @@ -1735,13 +1742,12 @@ async def check_bindings(self, key: str, priority: bool = False) -> bool:
"""Handle a key press.
Args:
key (str): A key
key (str): A key.
priority (bool): If `True` check from `App` down, otherwise from focused up.
Returns:
bool: True if the key was handled by a binding, otherwise False
"""

for namespace, bindings in (
reversed(self._binding_chain) if priority else self._binding_chain
):
Expand Down Expand Up @@ -2046,7 +2052,8 @@ async def _prune_node(self, root: Widget) -> None:
self._unregister(root)

async def action_check_bindings(self, key: str) -> None:
await self.check_bindings(key)
if not await self.check_bindings(key, priority=True):
await self.check_bindings(key, priority=False)

async def action_quit(self) -> None:
"""Quit the app as soon as possible."""
Expand Down
8 changes: 5 additions & 3 deletions src/textual/binding.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,11 @@ def make_bindings(bindings: Iterable[BindingType]) -> Iterable[Binding]:
description=binding.description,
show=binding.show,
key_display=binding.key_display,
priority=default_priority
if binding.priority is None
else binding.priority,
priority=(
default_priority
if binding.priority is None
else binding.priority
),
)

self.keys: MutableMapping[str, Binding] = (
Expand Down
17 changes: 9 additions & 8 deletions src/textual/css/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,21 @@
VALID_TEXT_ALIGN: Final = {"start", "end", "left", "right", "center", "justify"}
VALID_SCROLLBAR_GUTTER: Final = {"auto", "stable"}
VALID_STYLE_FLAGS: Final = {
"none",
"not",
"bold",
"b",
"blink",
"bold",
"dim",
"i",
"italic",
"underline",
"none",
"not",
"o",
"overline",
"reverse",
"strike",
"b",
"i",
"u",
"underline",
"uu",
"o",
"reverse",
}

NULL_SPACING: Final = Spacing.all(0)
7 changes: 4 additions & 3 deletions src/textual/dom.py
Original file line number Diff line number Diff line change
Expand Up @@ -517,8 +517,8 @@ def text_style(self) -> Style:
@property
def rich_style(self) -> Style:
"""Get a Rich Style object for this DOMNode."""
background = WHITE
color = BLACK
background = Color(0, 0, 0, 0)
color = Color(255, 255, 255, 0)
style = Style()
for node in reversed(self.ancestors_with_self):
styles = node.styles
Expand All @@ -530,7 +530,8 @@ def rich_style(self) -> Style:
if styles.has_rule("auto_color") and styles.auto_color:
color = background.get_contrast_text(color.a)
style += Style.from_color(
(background + color).rich_color, background.rich_color
(background + color).rich_color if (background.a or color.a) else None,
background.rich_color if background.a else None,
)
return style

Expand Down
20 changes: 10 additions & 10 deletions src/textual/geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ def get_scroll_to_visible(
Offset: An offset required to add to region to move it inside window_region.
"""

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

Expand All @@ -341,19 +341,19 @@ def get_scroll_to_visible(
key=abs,
)

if not (
if top:
delta_y = top_ - window_top

elif not (
(window_bottom > top_ >= window_top)
and (window_bottom > bottom >= window_top)
):
# The window needs to scroll on the Y axis to bring region in to view
if top:
delta_y = top_ - window_top
else:
delta_y = min(
top_ - window_top,
top_ - (window_bottom - region.height),
key=abs,
)
delta_y = min(
top_ - window_top,
top_ - (window_bottom - region.height),
key=abs,
)
return Offset(delta_x, delta_y)

def __bool__(self) -> bool:
Expand Down
2 changes: 2 additions & 0 deletions src/textual/screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ def set_focus(self, widget: Widget | None, scroll_visible: bool = True) -> None:
self.focused.post_message_no_wait(events.Blur(self))
self.focused.emit_no_wait(events.DescendantBlur(self))
self.focused = None
self.log.debug("focus was removed")
elif widget.can_focus:
if self.focused != widget:
if self.focused is not None:
Expand All @@ -310,6 +311,7 @@ def set_focus(self, widget: Widget | None, scroll_visible: bool = True) -> None:
self.screen.scroll_to_widget(widget)
widget.post_message_no_wait(events.Focus(self))
widget.emit_no_wait(events.DescendantFocus(self))
self.log.debug(widget, "was focused")

async def _on_idle(self, event: events.Idle) -> None:
# Check for any widgets marked as 'dirty' (needs a repaint)
Expand Down
14 changes: 8 additions & 6 deletions src/textual/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@ class AwaitMount:
"""

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

def __await__(self) -> Generator[None, None, None]:
Expand All @@ -97,6 +98,7 @@ async def await_mount() -> None:
]
if aws:
await wait(aws)
self._parent.refresh(layout=True)

return await_mount().__await__()

Expand Down Expand Up @@ -595,12 +597,12 @@ def mount(
else:
parent = self

return AwaitMount(
self.app._register(
parent, *widgets, before=insert_before, after=insert_after
)
mounted = self.app._register(
parent, *widgets, before=insert_before, after=insert_after
)

return AwaitMount(self, mounted)

def move_child(
self,
child: int | Widget,
Expand Down Expand Up @@ -1805,7 +1807,7 @@ def scroll_to_region(
if spacing is not None:
window = window.shrink(spacing)

if window in region:
if window in region and not top:
return Offset()

delta_x, delta_y = Region.get_scroll_to_visible(window, region, top=top)
Expand Down
8 changes: 7 additions & 1 deletion src/textual/widgets/_tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,7 @@ def get_label_width(self, node: TreeNode[TreeDataType]) -> int:

def clear(self) -> None:
"""Clear all nodes under root."""
self._line_cache.clear()
self._tree_lines_cached = None
self._current_id = 0
root_label = self.root._label
Expand Down Expand Up @@ -614,6 +615,11 @@ def _tree_lines(self) -> list[_TreeLine]:
assert self._tree_lines_cached is not None
return self._tree_lines_cached

def _on_idle(self) -> None:
"""Check tree needs a rebuild on idle."""
# Property calls build if required
self._tree_lines

def _build(self) -> None:
"""Builds the tree by traversing nodes, and creating tree lines."""

Expand Down Expand Up @@ -805,7 +811,7 @@ async def _on_click(self, event: events.Click) -> None:
cursor_line = meta["line"]
if meta.get("toggle", False):
node = self.get_node_at_line(cursor_line)
if node is not None and self.auto_expand:
if node is not None:
self._toggle_node(node)

else:
Expand Down

0 comments on commit 15417d4

Please sign in to comment.