diff --git a/cheuph/attributed_lines.py b/cheuph/attributed_lines.py index acd8e94..bf55fa9 100644 --- a/cheuph/attributed_lines.py +++ b/cheuph/attributed_lines.py @@ -2,7 +2,7 @@ # TODO retrieve attributes of closest existing line (by y coordinate) import collections -from typing import Deque, Iterator, List, Optional, Tuple +from typing import Any, Deque, Iterator, List, Optional, Set, Tuple from .markup import AT, AttributedText, Attributes @@ -213,3 +213,12 @@ class AttributedLines: lines = self.render_lines(width, height, horizontal_offset=horizontal_offset) return AT("\n").join(lines) + + def all_values(self, attribute: str) -> Set[Any]: + values = set() + + for attributes, _ in self._lines: + if attribute in attributes: + values.add(attributes.get(attribute)) + + return values diff --git a/cheuph/cursor_rendering.py b/cheuph/cursor_rendering.py index 57c3455..dbc14bf 100644 --- a/cheuph/cursor_rendering.py +++ b/cheuph/cursor_rendering.py @@ -1,3 +1,5 @@ +# TODO move meta spaces rendering to message + from abc import ABC, abstractmethod from typing import Generic, Optional, Tuple, TypeVar @@ -287,12 +289,15 @@ class CursorTreeRenderer(Generic[E]): 3. Extend upwards until the top of the screen, if necessary """ + delta = 0 + # Step 1 lines = self._render_cursor() - # No need to use the anchor offset since we know we're always at the - # bottom of the screen - lines.lower_offset = self._height - 1 - delta = self._height - 1 - self._absolute_anchor_offset + lines.lower_offset = self._absolute_anchor_offset + + if lines.lower_offset < self._height - 1: + delta = self._height - 1 - lines.lower_offset + lines.lower_offset = self._height - 1 # Step 2 lowest_root_id = self._supply.lowest_root_id() @@ -360,6 +365,14 @@ class CursorTreeRenderer(Generic[E]): return self._render_lines_from_anchor(working_id) + def _render(self) -> int: + lines, delta, hit_top = self._render_lines() + + self._lines = lines + self._hit_top = hit_top + + return delta + # Finally, another public function! :P def render(self, width: int, height: int) -> None: @@ -369,10 +382,75 @@ class CursorTreeRenderer(Generic[E]): self._width = width self._height = height - lines, delta, hit_top = self._render_lines() + self._render() - self._lines = lines - self._hit_top = hit_top + # Scrolling + + def _closest_to_middle(self) -> Tuple[Optional[Id], int]: + """ + Finds the element/cursor closest to the middle of the screen, and its + on-screen offset. + + Returns None instead of an Id if the cursor is the closest. + """ + + middle_index = self.get_absolute_offset(0.5, self._height) + lines = list(self.lines) + + # This should never happen; there should always be at least a cursor. + # I'm just being defensive here. + if len(lines) < 1: + return 0, middle_index + + if middle_index < self.lines.upper_offset: + raise Exception() + attrs, _ = lines[0] + index = self.lines.upper_offset + elif middle_index > self.lines.lower_offset: + raise Exception() + attrs, _ = lines[-1] + index = self.lines.lower_offset + else: + attrs, _ = lines[middle_index - self.lines.upper_offset] + index = middle_index + + mid = attrs.get("mid") + # We know that all lines, including the cursor, have an offset. + index -= attrs.get("offset") or 0 + + return mid, index + + def _find_cursor(self) -> Optional[int]: + for index, line in enumerate(self.lines): + attrs, _ = line + + if attrs.get("cursor"): + return index + + return None + + def _cursor_visible(self) -> bool: + return True in self.lines.all_values("cursor") + + def scroll(self, scroll_delta: int) -> None: + self._absolute_anchor_offset += scroll_delta + + delta = self._render() + if delta != 0: + self._absolute_anchor_offset += delta + scroll_delta + self._render() + + cursor_index = self._find_cursor() + if cursor_index is None: + closest, offset = self._closest_to_middle() + + self._anchor_id = closest + self._absolute_anchor_offset = offset + else: + self._anchor_id = None + self._absolute_anchor_offset = cursor_index + + # Moving the cursor class BasicCursorRenderer(CursorRenderer):