diff --git a/cheuph/render/tree_display.py b/cheuph/render/tree_display.py index 304ef56..cd38528 100644 --- a/cheuph/render/tree_display.py +++ b/cheuph/render/tree_display.py @@ -7,11 +7,144 @@ from .tree_list import TreeList __all__ = ["TreeDisplay"] class TreeDisplay: + """ + This class renders elements from an ElementSupply to a list of lines, which + can then be drawn onto a curses window. It maintains two "pointers" to + specific messages: An anchor, which is used for scrolling, and a cursor, + which highlights elements and can be used for interaction. + + ANCHOR + + The anchor is a message at a fixed vertical screen position. This position + is called the anchor offset and it specifies the position of the first line + of the anchor message relative to the top of the screen. + + A position of 0 would mean that the anchor is displayed at the top of the + screen, where a position of (height - 1) would mean that only the first + line of the anchor is displayed at the bottom of the screen (the rest is + offscreen). + + If no anchor is set, any attempt to render will result in a blank screen + (as if the room was empty) since the anchor is the point from which the + TreeDisplay starts to render the tree(s). + + CURSOR + + The cursor is a highlighted message that is meant to be used for user + interaction. + + The element that the cursor points to is passed highlighed=True in its + render() method, whereas all other elements are passed highlighted=False. + The cursor can also point to None (i. e. no message), in which case no + element is highlighted. + + At the moment, the TreeDisplay contains no cursor movement code, so the + user of the TreeDisplay has to implement their own by setting the cursor to + the respective element id themselves. You might also want to set the anchor + to the cursor and maybe center it on screen when the cursor is moved. + + RENDERING + + Rendering consists of these steps: + + 0. Initialize the TreeDisplay with the correct screen width and height and + keep the width and height up to date when the window resolution changes. + 1. Update the internal TreeList through one of various functions + 2. Cut out the display lines (the text that is later visible on screen) + 3. Draw the display lines to the curses window + + Step 2 uses the contents of the TreeList from step 1, but DOESN'T happen + automatically when the TreeList changes. It is also guaranteed to not + modify the display lines if it fails in some way. + + The display lines are supposed to contain the same text that is currently + visible on the screen, and can be used to look up mouse clicks (see the + "id" attribute in the "RENDERING - technical details" section below). + They're a simple format for translating between elements and onscreen text. + + RENDERING - technical details + + The process of rendering results in a TreeList and a list of lines (called + display lines) representing the text that should be displayed on the + screen. + + The TreeList's (vertical) offset of messages corresponds to the line on the + screen where the message will be drawn. This means that the anchor + message's offset in the TreeList will be the anchor offset referred to in + the ANCHOR section above. + + Like the Elements and RenderedElements, the TreeDisplay and TreeList also + use AttributedStrings for rendered content, especially in the display + lines. In addition to the attributes added by the Element during rendering, + the TreeDisplay also adds the following attributes to the display lines: + + 1. "id" - the id of the element visible on this line + 2. "parent" - the parent id (or None) of the element visible on this line + 3. "cursor" - True, only added if the cursor points to the element visible + on this line + + When an Element is rendered (producing a RenderedElement), its render() + function is called with: + 1. the element's depth/level (top-level elements have a depth of 0) + 1. the current screen width + 2. whether the element is highlighted (by the cursor) + 3. whether the element is folded + + The RenderedElement contains one or more lines (zero lines may break the + TreeDisplay, not sure yet) of AttributedText, which may contain formatting + information (such as text color or style). This means that an Element can + decide its own height. + + These lines should generally stay within the width passed to render(), but + may exceed it in certain exceptional situations (e. g. too close to the + right side of the screen). Because of this, the TreeDisplay supports + horizontal scrolling. + + SCROLLING + + The most basic form of scrolling is to just increase or decrease the anchor + offset. Depending on the application, this approach is enough. In some + cases, more complex behaviour is required. + + For example, if you're displaying a section from a live tree structure like + a chat room (see https://euphoria.io), there is no static top-most or + bottom-most message to anchor to (unless you want to download the whole + room's log). Also, new messages may appear at any time in (almost) any + place. + + With the above, basic scrolling model, offscreen conversations would often + slide around the visible messages, leading to frustration of the user. In + cases like this, an alternative scrolling model can be employed: + + First, change the anchor offset as required and render the messages. Then, + select the middle-most message as the new anchor and adapt the anchor + offset such that the message stays in the same position. + + FOLDING + + Finally, the TreeDisplay supports folding and unfolding elements, making + their subtrees invisible. + + Folding happens on elements. When an element is folded, it is still + displayed but its (direct and indirect) child messages are no longer + displayed. + """ + def __init__(self, supply: ElementSupply, width: int, height: int, ) -> None: + """ + supply - the ElementSupply that this TreeDisplay should use + width - the width of the target window (in characters) + height - the width of the target window (in lines) + + To use the TreeDisplay, you might also want to: + - set the anchor + - make sure the anchor is visible + """ + self._width = width self._height = height @@ -50,8 +183,8 @@ class TreeDisplay: # TODO Add the above into the TreeDisplay model. if self._anchor_id is None: - return # TODO draw empty screen - + self._rendered = None + return if self._root_id is None: ancestor_id = self._supply.get_furthest_ancestor_id( @@ -68,6 +201,13 @@ class TreeDisplay: self._fill_screen_upwards() self._fill_screen_downwards() + self._pad.set_lines(self._rendered.to_lines()) + # The vertical offset (anchor offset) is already being dealt with in + # the TreeView, since it is more useful to apply it there (each line's + # offset/index is also the anchor offset it would have if it was the + # anchor. + self._pad.stamp(self._horizontal_offset, 0, self._width, self._height) + def _render_tree(self, tree: Element, depth: int = 0 @@ -88,7 +228,7 @@ class TreeDisplay: def _fill_screen_upwards(self) -> None: if self._rendered is None: - return # TODO + return # TODO think of sensible thing to do here while True: if self._rendered.upper_offset <= 0: @@ -109,7 +249,7 @@ class TreeDisplay: """ if self._rendered is None: - return # TODO + return # TODO think of sensible thing to do here while True: if self._rendered.lower_offset >= self._height - 1: @@ -125,6 +265,7 @@ class TreeDisplay: self._rendered.add_below(self._render_tree(below_tree)) def draw_to(self, window: Any) -> None: + # TODO color styles of the text itself pass # Terminology: