Accommodate scrolling
Change the rendering code to accommodate for scrolling, and clean it up.
This commit is contained in:
parent
1fe8a87d7f
commit
8979d80062
1 changed files with 233 additions and 101 deletions
|
|
@ -1,7 +1,6 @@
|
||||||
from typing import Optional, Set
|
from typing import Optional, Set, Tuple
|
||||||
|
|
||||||
import urwid
|
import urwid
|
||||||
|
|
||||||
import yaboli
|
import yaboli
|
||||||
|
|
||||||
from .attributed_lines import AttributedLines
|
from .attributed_lines import AttributedLines
|
||||||
|
|
@ -14,6 +13,18 @@ from .rendered_message_cache import RenderedMessageCache
|
||||||
__all__ = ["MessageTreeWidget"]
|
__all__ = ["MessageTreeWidget"]
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
(lines, delta, hit_top, hit_bottom)
|
||||||
|
|
||||||
|
- lines - the rendered AttributedLines
|
||||||
|
- delta - how the absolute_anchor_offset needed to be changed to comply with
|
||||||
|
the scrolling rules
|
||||||
|
- hit_top - whether the renderer arrived at the topmost message of the supply
|
||||||
|
- hit_bottom - whether the renderer arrived at the bottommost message of the
|
||||||
|
supply
|
||||||
|
"""
|
||||||
|
RenderResult = Tuple[AttributedLines, int, bool, bool]
|
||||||
|
|
||||||
class MessageTreeWidget(urwid.WidgetWrap):
|
class MessageTreeWidget(urwid.WidgetWrap):
|
||||||
"""
|
"""
|
||||||
This widget displays an ElementSupply, including user interface like a
|
This widget displays an ElementSupply, including user interface like a
|
||||||
|
|
@ -53,7 +64,6 @@ class MessageTreeWidget(urwid.WidgetWrap):
|
||||||
self.rendered = RenderedMessageCache()
|
self.rendered = RenderedMessageCache()
|
||||||
# The lines that were last rendered
|
# The lines that were last rendered
|
||||||
self.lines = AttributedLines()
|
self.lines = AttributedLines()
|
||||||
#
|
|
||||||
# Widget tha displays self.lines
|
# Widget tha displays self.lines
|
||||||
self.lines_widget = AttributedLinesWidget()
|
self.lines_widget = AttributedLinesWidget()
|
||||||
# A placeholder if there are no messages to display
|
# A placeholder if there are no messages to display
|
||||||
|
|
@ -159,14 +169,14 @@ class MessageTreeWidget(urwid.WidgetWrap):
|
||||||
Invalidate the RenderedMessage cached under message_id.
|
Invalidate the RenderedMessage cached under message_id.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass # TODO
|
self.cache.invalidate(message_id)
|
||||||
|
|
||||||
def invalidate_all_messages(self) -> None:
|
def invalidate_all_messages(self) -> None:
|
||||||
"""
|
"""
|
||||||
Invalidate all cached RenderedMessage-s.
|
Invalidate all cached RenderedMessage-s.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass # TODO
|
self.cache.invalidate_all()
|
||||||
|
|
||||||
# Rendering a single message
|
# Rendering a single message
|
||||||
|
|
||||||
|
|
@ -234,22 +244,17 @@ class MessageTreeWidget(urwid.WidgetWrap):
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
def _render_cursor(self, indent: AttributedText = AT()) -> AttributedLines:
|
def _render_cursor(self, indent: AttributedText = AT()) -> AttributedLines:
|
||||||
pass # TODO
|
# Quick and dirty cursor rendering
|
||||||
|
nick = self.room.session.nick
|
||||||
# Rendering the tree
|
text = indent + AT(f"[{nick}]")
|
||||||
|
|
||||||
def _render_tree(self, root_id: Id) -> AttributedLines:
|
|
||||||
"""
|
|
||||||
A wrapper around _render_subtree(), for ease of use.
|
|
||||||
|
|
||||||
Doesn't adjust the offset; the AttributedLines returned does NOT take
|
|
||||||
into account the attribute_offset.
|
|
||||||
"""
|
|
||||||
|
|
||||||
lines = AttributedLines()
|
lines = AttributedLines()
|
||||||
self.render_subtree(lines, root_id)
|
lines.append_below({"cursor": True}, text)
|
||||||
|
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
|
# Rendering the tree
|
||||||
|
|
||||||
def _render_subtree(self,
|
def _render_subtree(self,
|
||||||
lines: AttributedLines,
|
lines: AttributedLines,
|
||||||
root_id: Id,
|
root_id: Id,
|
||||||
|
|
@ -291,6 +296,18 @@ class MessageTreeWidget(urwid.WidgetWrap):
|
||||||
cursor_indent = indent + AT("┗━")
|
cursor_indent = indent + AT("┗━")
|
||||||
lines.extend_below(self._render_cursor(indent))
|
lines.extend_below(self._render_cursor(indent))
|
||||||
|
|
||||||
|
def _render_tree(self, root_id: Id) -> AttributedLines:
|
||||||
|
"""
|
||||||
|
A wrapper around _render_subtree(), for ease of use.
|
||||||
|
|
||||||
|
Doesn't adjust the offset; the AttributedLines returned does NOT take
|
||||||
|
into account the attribute_offset.
|
||||||
|
"""
|
||||||
|
|
||||||
|
lines = AttributedLines()
|
||||||
|
self.render_subtree(lines, root_id)
|
||||||
|
return lines
|
||||||
|
|
||||||
def _render_tree_containing(self, message_id: Id) -> AttributedLines:
|
def _render_tree_containing(self, message_id: Id) -> AttributedLines:
|
||||||
"""
|
"""
|
||||||
Similar to _render_tree(), but finds the root of the specified message
|
Similar to _render_tree(), but finds the root of the specified message
|
||||||
|
|
@ -305,17 +322,16 @@ class MessageTreeWidget(urwid.WidgetWrap):
|
||||||
lines: AttributedLines,
|
lines: AttributedLines,
|
||||||
ancestor_id: Id,
|
ancestor_id: Id,
|
||||||
target_upper_offset: int,
|
target_upper_offset: int,
|
||||||
) -> Id:
|
) -> Tuple[Id, bool]:
|
||||||
"""
|
"""
|
||||||
Render trees and prepend them to the AttributedLines until its
|
Render trees (including the cursor) and prepend them to the
|
||||||
upper_offset matches or exceeds the target_upper_offset.
|
AttributedLines until its upper_offset matches or exceeds the
|
||||||
|
target_upper_offset.
|
||||||
|
|
||||||
Returns the last tree id that was rendered. If no tree could be
|
Returns whether it has hit the top of the supply.
|
||||||
rendered, returns the original anchor id.
|
|
||||||
|
|
||||||
Starts at the first older sibling of the ancestor_id (the first sibling
|
Assumes that the ancestor_id's tree is already rendered. Moves upwards
|
||||||
above the ancestor_id) and moves upwards sibling by sibling. Does not
|
through the siblings of the ancestor_id.
|
||||||
render the ancestor_id itself.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# This loop doesn't use a condition but rather break-s, because I think
|
# This loop doesn't use a condition but rather break-s, because I think
|
||||||
|
|
@ -324,31 +340,33 @@ class MessageTreeWidget(urwid.WidgetWrap):
|
||||||
last_rendered_id = ancestor_id
|
last_rendered_id = ancestor_id
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
if lines.upper_offset <= target_upper_offset: break
|
# Doing this check first because of a possible edge case: Using the
|
||||||
|
# other order, if the first message fills the screen, the function
|
||||||
|
# would return False, even though we've hit the top.
|
||||||
next_id = self.supply.previous_id(last_rendered_id)
|
next_id = self.supply.previous_id(last_rendered_id)
|
||||||
if next_id is None: break
|
if next_id is None:
|
||||||
|
return last_rendered_id, True
|
||||||
|
|
||||||
|
if lines.upper_offset <= target_upper_offset:
|
||||||
|
return last_rendered_id, False
|
||||||
|
|
||||||
lines.extend_above(self._render_tree(next_id))
|
lines.extend_above(self._render_tree(next_id))
|
||||||
last_rendered_id = next_id
|
last_rendered_id = next_id
|
||||||
|
|
||||||
return last_rendered_id
|
|
||||||
|
|
||||||
def _expand_downwards_until(self,
|
def _expand_downwards_until(self,
|
||||||
lines: AttributedLines,
|
lines: AttributedLines,
|
||||||
ancestor_id: Id,
|
ancestor_id: Id,
|
||||||
target_lower_offset: int,
|
target_lower_offset: int,
|
||||||
) -> Id:
|
) -> Tuple[Id, bool]:
|
||||||
"""
|
"""
|
||||||
Render trees and append them to the AttributedLines until its
|
Render trees (including the cursor, even if it's at the bottom) and
|
||||||
lower_offset matches or exceeds the target_lower_offset.
|
append them to the AttributedLines until its lower_offset matches or
|
||||||
|
exceeds the target_lower_offset.
|
||||||
|
|
||||||
Returns the last tree id that was rendered. If no tree could be
|
Returns whether it has hit the bottom of the supply.
|
||||||
rendered, returns the original anchor id.
|
|
||||||
|
|
||||||
Starts at the first younger sibling of the ancestor_id (the first
|
Assumes that the ancestor_id's tree is already rendered. Moves
|
||||||
sibling below the ancestor_id) and moves downwards sibling by sibling.
|
downwards through the siblings of the ancestor_id.
|
||||||
Does not render the ancestor_id itself.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Almost the same as _expand_upwards_until(), but with small changes.
|
# Almost the same as _expand_upwards_until(), but with small changes.
|
||||||
|
|
@ -361,93 +379,207 @@ class MessageTreeWidget(urwid.WidgetWrap):
|
||||||
last_rendered_id = ancestor_id
|
last_rendered_id = ancestor_id
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
if lines.upper_offset >= target_lower_offset: break
|
# Doing this check first because of a possible edge case: Using the
|
||||||
|
# other order, if the last message fills the screen, the function
|
||||||
|
# would return False, even though we've hit the bottom.
|
||||||
next_id = self.supply.next_id(last_rendered_id)
|
next_id = self.supply.next_id(last_rendered_id)
|
||||||
if next_id is None: break
|
if next_id is None:
|
||||||
|
break
|
||||||
|
|
||||||
|
if lines.lower_offset >= target_lower_offset:
|
||||||
|
return last_rendered_id, False
|
||||||
|
|
||||||
lines.extend_below(self._draw_tree(next_id))
|
lines.extend_below(self._draw_tree(next_id))
|
||||||
last_rendered_id = next_id
|
last_rendered_id = next_id
|
||||||
|
|
||||||
return last_rendered_id
|
lines.extend_below(self._render_cursor())
|
||||||
|
return last_rendered_id, True
|
||||||
|
|
||||||
# Rendering the screen
|
# Rendering the screen
|
||||||
|
|
||||||
def _render_screen(self) -> AttributedLines:
|
"""
|
||||||
|
On scrolling:
|
||||||
|
|
||||||
|
These are some restrictions on how the screen can scroll and thus how the
|
||||||
|
anchor_offset is interpreted. They are listed from most to least important.
|
||||||
|
|
||||||
|
1. There must always be something (a message, the cursor or similar) on the
|
||||||
|
bottommost line.
|
||||||
|
|
||||||
|
2. There must always be something on the topmost line.
|
||||||
|
|
||||||
|
Good:
|
||||||
|
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
|
||||||
|
first message
|
||||||
|
| bla
|
||||||
|
| | blabla
|
||||||
|
| last message
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
and
|
||||||
|
|
||||||
|
------------------------
|
||||||
|
first message
|
||||||
|
| bla
|
||||||
|
| blabla
|
||||||
|
| | more bla
|
||||||
|
| | even more bla
|
||||||
|
| not the last message
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
Bad:
|
||||||
|
|
||||||
|
------------------------
|
||||||
|
first message
|
||||||
|
| bla
|
||||||
|
| | blabla
|
||||||
|
| last message
|
||||||
|
|
||||||
|
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
and
|
||||||
|
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
|
||||||
|
first message
|
||||||
|
| bla
|
||||||
|
| | blabla
|
||||||
|
| not the last message
|
||||||
|
------------------------
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _render_screen_from_cursor(self) -> RenderResult:
|
||||||
|
"""
|
||||||
|
Uses the following strategy:
|
||||||
|
1. Render the cursor
|
||||||
|
2. Render the lowest tree, if there is one
|
||||||
|
3. Extend upwards until the top of the screen, if necessary
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Step 2
|
||||||
|
hit_top: bool
|
||||||
|
lowest_root_id = self.supply.lowest_root_id()
|
||||||
|
if lowest_root_id is None:
|
||||||
|
hit_top = True
|
||||||
|
else:
|
||||||
|
lines.extend_above(self._render_tree(lowest_root_id))
|
||||||
|
|
||||||
|
# Step 3
|
||||||
|
_, hit_top = self._expand_upwards_until(lines, lowest_root_id, 0)
|
||||||
|
|
||||||
|
return lines, delta, hit_top, True # we're always at the bottom
|
||||||
|
|
||||||
|
def _render_screen_from_anchor(self, anchor_id: Id) -> RenderResult:
|
||||||
|
"""
|
||||||
|
Uses the following strategy:
|
||||||
|
1. Render the anchor's tree
|
||||||
|
2. Extend upwards until the top of the screen
|
||||||
|
3. Adjust the offset to match rule 2
|
||||||
|
4. Extend downwards until the bottom of the screen
|
||||||
|
5. Adjust the offset to match rule 1
|
||||||
|
6. Extend upwards again until the top of the screen
|
||||||
|
"""
|
||||||
|
|
||||||
|
delta = 0
|
||||||
|
|
||||||
|
# Step 1
|
||||||
|
ancestor_id = self.supply.oldest_ancestor_id(anchor_id)
|
||||||
|
lines = self._render_tree(ancestor_id)
|
||||||
|
lines.upper_offset += self.absolute_anchor_offset
|
||||||
|
|
||||||
|
# Step 2
|
||||||
|
upper_id, hit_top = self._expand_upwards_until(lines, ancestor_id, 0)
|
||||||
|
|
||||||
|
# Step 3
|
||||||
|
if lines.upper_offset > 0:
|
||||||
|
delta -= lines.upper_offset
|
||||||
|
lines.upper_offset = 0
|
||||||
|
|
||||||
|
# Step 4
|
||||||
|
_, hit_bottom = self._expand_downwards_until(lines, ancestor_id,
|
||||||
|
self.height - 1)
|
||||||
|
|
||||||
|
# Step 5
|
||||||
|
if lines.lower_offset < self.height - 1:
|
||||||
|
delta += self.height - 1 - lines.lower_offset
|
||||||
|
|
||||||
|
# Step 6
|
||||||
|
if not hit_top:
|
||||||
|
_, hit_top = self._expand_upwards_until(lines, upper_id, 0)
|
||||||
|
|
||||||
|
return lines, delta, hit_top, hit_bottom
|
||||||
|
|
||||||
|
def _render_screen(self) -> RenderResult:
|
||||||
"""
|
"""
|
||||||
Render an AttributedLines that fills the screen (as far as possible),
|
Render an AttributedLines that fills the screen (as far as possible),
|
||||||
taking into account the anchor offset.
|
taking into account the anchor offset.
|
||||||
|
|
||||||
This does NOT fix scrolling (i. e. by min()- or max()-ing the upper and
|
|
||||||
lower offsets). Instead, scrolling should be fixed when the anchor
|
|
||||||
offset is changed or the resolution changes.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# TODO maybe extend with an additional offset for scrolling
|
if self.cursor_id is None and self.anchor_id is None:
|
||||||
|
return self._render_screen_from_cursor()
|
||||||
|
|
||||||
lines: AttributedLines
|
working_id: Id
|
||||||
|
if self.anchor_id is None:
|
||||||
if self.cursor_id is None:
|
# self.cursor_id can't be None, otherwise the first if
|
||||||
# If the cursor is None, that means that it should always be
|
# condition would have been met and this part wouldn't have
|
||||||
# displayed at the very bottom of the room. It also means that
|
# been executed in the first place.
|
||||||
# _render_subtree() can't render the cursor, because it would be
|
working_id = self.cursor_id # type: ignore
|
||||||
# the root of a message tree.
|
|
||||||
|
|
||||||
if self.anchor_id is None:
|
|
||||||
# Start with the cursor
|
|
||||||
lines = self._render_cursor()
|
|
||||||
lines.upper_offset = self.absolute_anchor_offset
|
|
||||||
# Then expand upwards
|
|
||||||
lowest_root_id = self.supply.lowest_root_id()
|
|
||||||
if lowest_root_id is not None:
|
|
||||||
self._expand_upwards_until(lines, lowest_root_id, 0)
|
|
||||||
else:
|
|
||||||
# Start with the anchor as usual
|
|
||||||
lines = self._render_tree_containing(self.anchor_id)
|
|
||||||
lines.upper_offset += self.absolute_anchor_offset
|
|
||||||
# And expand until the screen is full
|
|
||||||
self._expand_upwards_until(lines, self.anchor_id, 0)
|
|
||||||
until_id = self._expand_downwards_until(lines, self.anchor_id,
|
|
||||||
self.height - 1)
|
|
||||||
# After that, draw the cursor below, if necessary
|
|
||||||
lowest_root_id = self.supply.lowest_root_id()
|
|
||||||
if until_id == lowest_root_id:
|
|
||||||
lines.extend_below(self._render_cursor())
|
|
||||||
else:
|
else:
|
||||||
# In this case, the cursor is automatically rendered correctly by
|
working_id = self.anchor_id
|
||||||
# _render_subtree(), so we actually don't have to do a lot.
|
|
||||||
#
|
|
||||||
# This case is the normal case, and the case I thought of first
|
|
||||||
# when I designed this part.
|
|
||||||
|
|
||||||
working_id: Id
|
return self._render_screen_from_anchor(working_id)
|
||||||
if self.anchor_id is None:
|
|
||||||
working_id = self.cursor_id
|
|
||||||
else:
|
|
||||||
working_id = self.anchor_id
|
|
||||||
|
|
||||||
ancestor_id = self.supply.oldest_ancestor_id(working_id)
|
|
||||||
lines = self._render_tree(ancestor_id)
|
|
||||||
lines.upper_offset += self.absolute_anchor_offset
|
|
||||||
self._expand_upwards_until(lines, ancestor_id, 0)
|
|
||||||
self._expand_downwards_until(lines, ancestor_id,
|
|
||||||
self.height - 1)
|
|
||||||
|
|
||||||
return lines
|
|
||||||
|
|
||||||
# Updating the internal widget
|
# Updating the internal widget
|
||||||
|
|
||||||
def redraw(self) -> None:
|
def _update_with_lines(self, lines: AttributedLines) -> None:
|
||||||
"""
|
"""
|
||||||
Render new lines and draw them (to the internal widget and thus to the
|
Update evrything that needs to be updated when a new set of lines comes
|
||||||
screen on the next screen update).
|
in.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
lines = self._render_screen()
|
self.lines = lines
|
||||||
self.lines_widget.set_lines(lines)
|
self.lines_widget.set_lines(self.lines)
|
||||||
|
|
||||||
self._w = self.lines_widget
|
self._w = self.lines_widget
|
||||||
self._invalidate() # Just to make sure this really gets rendered
|
self._invalidate() # Just to make sure this really gets rendered
|
||||||
|
|
||||||
# Cursor movement
|
def redraw(self, fix_anchor_offset: bool = False) -> Tuple[bool, bool]:
|
||||||
|
"""
|
||||||
|
Render new lines and draw them (to the internal widget and thus to the
|
||||||
|
screen on the next screen update).
|
||||||
|
|
||||||
|
Returns a tuple (hit_top, hit_bottom):
|
||||||
|
- hit_top - whether the renderer arrived at the topmost message of the
|
||||||
|
supply
|
||||||
|
- hit_bottom - whether the renderer arrived at the bottommost message
|
||||||
|
of the supply
|
||||||
|
"""
|
||||||
|
|
||||||
|
lines, delta, hit_top, hit_bottom = self._render_screen()
|
||||||
|
self._update_with_lines(lines)
|
||||||
|
|
||||||
|
if fix_anchor_offset and delta != 0:
|
||||||
|
self.absolute_anchor_offset += delta
|
||||||
|
|
||||||
|
return hit_top, hit_bottom
|
||||||
|
|
||||||
# Scrolling
|
# Scrolling
|
||||||
|
|
||||||
|
def scroll_by(self, delta: int) -> None:
|
||||||
|
self.absolute_anchor_offset += delta
|
||||||
|
|
||||||
|
# Cursor movement
|
||||||
|
|
||||||
|
# TODO
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue