Implement parts of the message tree widget
This commit is contained in:
parent
8eaacf1558
commit
1fe8a87d7f
5 changed files with 450 additions and 24 deletions
|
|
@ -1,3 +1,6 @@
|
||||||
|
# TODO retrieve attributes of any (x, y) coordinates
|
||||||
|
# TODO retrieve attributes of closest existing line (by y coordinate)
|
||||||
|
|
||||||
import collections
|
import collections
|
||||||
from typing import Deque, Iterator, List, Optional, Tuple
|
from typing import Deque, Iterator, List, Optional, Tuple
|
||||||
|
|
||||||
|
|
@ -8,6 +11,7 @@ __all__ = ["Line", "AttributedLines"]
|
||||||
|
|
||||||
Line = Tuple[Attributes, AttributedText]
|
Line = Tuple[Attributes, AttributedText]
|
||||||
|
|
||||||
|
|
||||||
class AttributedLines:
|
class AttributedLines:
|
||||||
"""
|
"""
|
||||||
AttributedLines is a list of lines of AttributedText that maintains a
|
AttributedLines is a list of lines of AttributedText that maintains a
|
||||||
|
|
@ -44,26 +48,30 @@ class AttributedLines:
|
||||||
|
|
||||||
# Modifying functions
|
# Modifying functions
|
||||||
|
|
||||||
def append_above(self, line: Line) -> None:
|
def append_above(self,
|
||||||
|
attributes: Attributes,
|
||||||
|
text: AttributedText) -> None:
|
||||||
"""
|
"""
|
||||||
Append a line above all already existing lines. The existing lines'
|
Append a line above all already existing lines. The existing lines'
|
||||||
offsets do not change.
|
offsets do not change.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self._lines.appendleft(line)
|
self._lines.appendleft((attributes, text))
|
||||||
self.upper_offset -= 1
|
self.upper_offset -= 1
|
||||||
|
|
||||||
def append_below(self, line: Line) -> None:
|
def append_below(self,
|
||||||
|
attributes: Attributes,
|
||||||
|
text: AttributedText) -> None:
|
||||||
"""
|
"""
|
||||||
Append a line below all already existing lines. The existing lines'
|
Append a line below all already existing lines. The existing lines'
|
||||||
offsets do not change.
|
offsets do not change.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self._lines.append(line)
|
self._lines.append((attributes, text))
|
||||||
# lower offset does not need to be modified since it's calculated based
|
# lower offset does not need to be modified since it's calculated based
|
||||||
# on the upper offset
|
# on the upper offset
|
||||||
|
|
||||||
def expand_above(self, lines: "AttributedLines") -> None:
|
def extend_above(self, lines: "AttributedLines") -> None:
|
||||||
"""
|
"""
|
||||||
Prepend an AttributedLines, ignoring its offsets and using the current
|
Prepend an AttributedLines, ignoring its offsets and using the current
|
||||||
AttributedLines's offsets instead.
|
AttributedLines's offsets instead.
|
||||||
|
|
@ -72,7 +80,7 @@ class AttributedLines:
|
||||||
self._lines.extendleft(lines._lines)
|
self._lines.extendleft(lines._lines)
|
||||||
self.upper_offset -= len(lines)
|
self.upper_offset -= len(lines)
|
||||||
|
|
||||||
def expand_below(self, lines: "AttributedLines") -> None:
|
def extend_below(self, lines: "AttributedLines") -> None:
|
||||||
"""
|
"""
|
||||||
Append an AttributedLines, ignoring its offsets and using the current
|
Append an AttributedLines, ignoring its offsets and using the current
|
||||||
AttributedLines's offsets instead.
|
AttributedLines's offsets instead.
|
||||||
|
|
@ -111,10 +119,10 @@ class AttributedLines:
|
||||||
between = self.between(start_offset, end_offset)
|
between = self.between(start_offset, end_offset)
|
||||||
|
|
||||||
while between.upper_offset > start_offset:
|
while between.upper_offset > start_offset:
|
||||||
between.append_above(({}, AT()))
|
between.append_above({}, AT())
|
||||||
|
|
||||||
while between.lower_offset < end_offset:
|
while between.lower_offset < end_offset:
|
||||||
between.append_below(({}, AT()))
|
between.append_below({}, AT())
|
||||||
|
|
||||||
return between
|
return between
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
# TODO send event on mouse click
|
||||||
|
|
||||||
from typing import Any, Optional, Tuple
|
from typing import Any, Optional, Tuple
|
||||||
|
|
||||||
import urwid
|
import urwid
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,8 @@
|
||||||
from typing import Hashable
|
from dataclasses import dataclass
|
||||||
|
from typing import Hashable, List
|
||||||
|
|
||||||
|
from .attributed_lines import AttributedLines
|
||||||
|
from .markup import AttributedText
|
||||||
|
|
||||||
__all__ = ["Id", "Message", "RenderedMessage"]
|
__all__ = ["Id", "Message", "RenderedMessage"]
|
||||||
|
|
||||||
|
|
@ -19,9 +23,10 @@ class Message:
|
||||||
truncation status.
|
truncation status.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
def render(self, width: int) -> RenderedMessage:
|
||||||
|
pass # TODO
|
||||||
|
|
||||||
|
@dataclass
|
||||||
class RenderedMessage:
|
class RenderedMessage:
|
||||||
"""
|
"""
|
||||||
A RenderedMessage is the result of rendering a Message. It contains lines
|
A RenderedMessage is the result of rendering a Message. It contains lines
|
||||||
|
|
@ -40,4 +45,6 @@ class RenderedMessage:
|
||||||
rather the result of rendering a Message.
|
rather the result of rendering a Message.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
message_id: Id
|
||||||
|
meta: AttributedText
|
||||||
|
lines: List[AttributedText]
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,7 @@
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from .message import Id, Message
|
||||||
|
|
||||||
__all__ = ["MessageSupply"]
|
__all__ = ["MessageSupply"]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -12,4 +16,24 @@ class MessageSupply:
|
||||||
similar.
|
similar.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
# TODO should throw exception if it can't find the message
|
||||||
|
def get(self, message_id: Id) -> Message:
|
||||||
|
pass # TODO
|
||||||
|
|
||||||
|
def children_ids(self, message_id: Id) -> List[Id]:
|
||||||
|
pass # TODO
|
||||||
|
|
||||||
|
def parent_id(self, message_id: Id) -> Optional[Id]:
|
||||||
|
pass # TODO
|
||||||
|
|
||||||
|
def oldest_ancestor_id(self, message_id: Id) -> Id:
|
||||||
|
pass # TODO
|
||||||
|
|
||||||
|
def previous_id(self, message_id: Id) -> Optional[Id]:
|
||||||
|
pass # TODO
|
||||||
|
|
||||||
|
def next_id(self, message_id: Id) -> Optional[Id]:
|
||||||
|
pass # TODO
|
||||||
|
|
||||||
|
def lowest_root_id(self) -> Optional[Id]:
|
||||||
|
pass # TODO
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
from typing import Optional, Set
|
from typing import Optional, Set
|
||||||
|
|
||||||
import urwid
|
import urwid
|
||||||
|
|
||||||
import yaboli
|
import yaboli
|
||||||
|
|
||||||
|
from .attributed_lines import AttributedLines
|
||||||
from .attributed_lines_widget import AttributedLinesWidget
|
from .attributed_lines_widget import AttributedLinesWidget
|
||||||
from .message import Id
|
from .markup import AT, AttributedText
|
||||||
|
from .message import Id, RenderedMessage
|
||||||
from .message_supply import MessageSupply
|
from .message_supply import MessageSupply
|
||||||
from .rendered_message_cache import RenderedMessageCache
|
from .rendered_message_cache import RenderedMessageCache
|
||||||
|
|
||||||
|
|
@ -34,35 +37,417 @@ class MessageTreeWidget(urwid.WidgetWrap):
|
||||||
finishes editing it.
|
finishes editing it.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ROOM_IS_EMPTY = "<no messages>"
|
ROOM_IS_EMPTY_MESSAGE = "<no messages>"
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
|
# TODO config
|
||||||
room: yaboli.Room,
|
room: yaboli.Room,
|
||||||
supply: MessageSupply,
|
supply: MessageSupply,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
|
# yaboli.Room, used only for the current nick
|
||||||
self.room = room
|
self.room = room
|
||||||
|
# A supply of Message-s
|
||||||
self.supply = supply
|
self.supply = supply
|
||||||
|
# A cache of RenderedMessage-s
|
||||||
self.rendered = RenderedMessageCache()
|
self.rendered = RenderedMessageCache()
|
||||||
self.lines = AttributedLinesWidget()
|
# The lines that were last rendered
|
||||||
self.placeholder = urwid.Filler(urwid.Text(self.ROOM_IS_EMPTY,
|
self.lines = AttributedLines()
|
||||||
|
#
|
||||||
|
# Widget tha displays self.lines
|
||||||
|
self.lines_widget = AttributedLinesWidget()
|
||||||
|
# A placeholder if there are no messages to display
|
||||||
|
self.placeholder = urwid.Filler(urwid.Text(self.ROOM_IS_EMPTY_MESSAGE,
|
||||||
align=urwid.CENTER))
|
align=urwid.CENTER))
|
||||||
|
|
||||||
|
# The id of the message that the cursor is displayed under.
|
||||||
|
self.cursor_id: Optional[Id] = None
|
||||||
# If the anchor is None, but the cursor isn't, the cursor is used as
|
# If the anchor is None, but the cursor isn't, the cursor is used as
|
||||||
# the anchor.
|
# the anchor.
|
||||||
self.cursor: Optional[Id] = None
|
self.anchor_id: Optional[Id] = None
|
||||||
self.anchor: Optional[Id] = None
|
# The anchor's line's offset on the screen, measured in percent of the
|
||||||
self.anchor_offset = 0
|
# total height. For more information, see the comment above
|
||||||
|
# _get_absolute_offset() and _get_relative_offset().
|
||||||
|
self.anchor_offset = 0.5
|
||||||
|
|
||||||
self.folds: Set[Id] = set()
|
# The last known width (use this to invalidate the cache when needed)
|
||||||
|
self.width = 80
|
||||||
|
# Columns per indentation level
|
||||||
|
self.indent_width = 2
|
||||||
|
# Columns at beginning of line that are reserved for date etc.
|
||||||
|
self.meta_width = 6 # "HH:MM "
|
||||||
|
# Columns at the end to mark overlapping lines
|
||||||
|
self.overlap_width = 1
|
||||||
|
|
||||||
|
# Which sub-threads are folded
|
||||||
|
#self.folds: Set[Id] = set() # TODO
|
||||||
|
|
||||||
super().__init__(self.placeholder)
|
super().__init__(self.placeholder)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def usable_width(self) -> int:
|
||||||
|
"""
|
||||||
|
The width that's available for everything, while staying inside the
|
||||||
|
bounds of the overlap indicators.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.width - self.overlap_width
|
||||||
|
|
||||||
|
@property
|
||||||
|
def content_width(self) -> int:
|
||||||
|
"""
|
||||||
|
The width that's left over for messages and their indentation
|
||||||
|
information, after meta_width etc. are removed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.usable_width - self.meta_width
|
||||||
|
|
||||||
|
# Offsets
|
||||||
|
|
||||||
|
"""
|
||||||
|
On offsets:
|
||||||
|
|
||||||
|
An offset of 0.0 describes the middle of the first line on screen, whereas
|
||||||
|
an offset of 1.0 describes the middle of the last line on screen.
|
||||||
|
|
||||||
|
An example:
|
||||||
|
|
||||||
|
line 0 - 0.0
|
||||||
|
line 1 - 0.25
|
||||||
|
line 2 - 0.5
|
||||||
|
line 3 - 0.75
|
||||||
|
line 4 - 1.0
|
||||||
|
|
||||||
|
Let l be a line's index (starts with 0), o the offset and n the number of
|
||||||
|
lines visible on the screen.
|
||||||
|
|
||||||
|
OFFSET -> LINE NUMBER
|
||||||
|
|
||||||
|
l = round(o * (n - 1))
|
||||||
|
|
||||||
|
LINE NUMBER -> OFFSET
|
||||||
|
|
||||||
|
o = l / (n - 1)
|
||||||
|
|
||||||
|
Be careful if only one line is visible on the screen! Setting o to 0.5 is
|
||||||
|
recommended in that case.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_absolute_offset(offset: float, height: int) -> int:
|
||||||
|
return round(offset * (height - 1))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_relative_offset(line: int, height: int) -> float:
|
||||||
|
if height <= 1:
|
||||||
|
return 0.5
|
||||||
|
|
||||||
|
return line / (height - 1)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def absolute_anchor_offset(self) -> int:
|
||||||
|
return self._get_absolute_offset(self.anchor_offset, self.height)
|
||||||
|
|
||||||
|
@absolute_anchor_offset.setter
|
||||||
|
def absolute_anchor_offset(self, offset: int) -> None:
|
||||||
|
self.anchor_offset = self._get_relative_offset(offset, self.height)
|
||||||
|
|
||||||
|
# Message cache operations and maintenance
|
||||||
|
|
||||||
def invalidate_message(self, message_id: Id) -> None:
|
def invalidate_message(self, message_id: Id) -> None:
|
||||||
pass
|
"""
|
||||||
|
Invalidate the RenderedMessage cached under message_id.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass # TODO
|
||||||
|
|
||||||
def invalidate_all_messages(self) -> None:
|
def invalidate_all_messages(self) -> None:
|
||||||
pass
|
"""
|
||||||
|
Invalidate all cached RenderedMessage-s.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass # TODO
|
||||||
|
|
||||||
|
# Rendering a single message
|
||||||
|
|
||||||
|
def _render_message(self, message_id: Id, width: int) -> RenderedMessage:
|
||||||
|
"""
|
||||||
|
Somehow obtain a RenderedMessage for the specified message_id.
|
||||||
|
|
||||||
|
If the cache does not contain this message yet, or if it contains an
|
||||||
|
invalid message (wrong width), the message is rendered and then added
|
||||||
|
to the cache. Otherwise, the cached message is returned.
|
||||||
|
"""
|
||||||
|
|
||||||
|
cached = self.cache.get(message_id)
|
||||||
|
|
||||||
|
if cached.width != width:
|
||||||
|
self.invalidate_message(message_id)
|
||||||
|
cached = None
|
||||||
|
|
||||||
|
if cached is not None:
|
||||||
|
return cached
|
||||||
|
|
||||||
|
message = self.supply.get(message_id)
|
||||||
|
# TODO give current Room to message so it knows the current nick(s)
|
||||||
|
# TODO give current meta format to message
|
||||||
|
rendered = message.render(width)
|
||||||
|
self.cache.set(message_id, rendered)
|
||||||
|
return rendered
|
||||||
|
|
||||||
|
def _render_message_lines(self,
|
||||||
|
message_id: Id,
|
||||||
|
indent: AttributedText = AT(),
|
||||||
|
) -> AttributedLines:
|
||||||
|
"""
|
||||||
|
Render the message with the specified id into AttributedLines.
|
||||||
|
|
||||||
|
The lines have the format:
|
||||||
|
|
||||||
|
<meta><indent><nick and content>
|
||||||
|
|
||||||
|
Each line has the following line-wide attributes:
|
||||||
|
|
||||||
|
- mid - the id of the message
|
||||||
|
- offset - the offset to the message's topmost line
|
||||||
|
"""
|
||||||
|
|
||||||
|
width = self.content_width - len(indent)
|
||||||
|
rendered = self._render_message(message_id, width)
|
||||||
|
|
||||||
|
meta = rendered.meta
|
||||||
|
meta_spaces = AT(" " * len(rendered.meta))
|
||||||
|
|
||||||
|
lines = AttributedLines()
|
||||||
|
|
||||||
|
mid = rendered.message_id
|
||||||
|
offset = 0
|
||||||
|
|
||||||
|
lines.append_below({"mid": mid, "offset": offset},
|
||||||
|
meta + indent + rendered.lines[0])
|
||||||
|
|
||||||
|
for line in rendered.lines[1:]:
|
||||||
|
offset += 1
|
||||||
|
lines.append_below({"mid": mid, "offset": offset},
|
||||||
|
meta_spaces + indent + line)
|
||||||
|
|
||||||
|
return lines
|
||||||
|
|
||||||
|
def _render_cursor(self, indent: AttributedText = AT()) -> AttributedLines:
|
||||||
|
pass # TODO
|
||||||
|
|
||||||
|
# Rendering the tree
|
||||||
|
|
||||||
|
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_subtree(self,
|
||||||
|
lines: AttributedLines,
|
||||||
|
root_id: Id,
|
||||||
|
indent: AttributedText = AT(),
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Render a (sub-)tree to the AttributedLines specified.
|
||||||
|
|
||||||
|
This function also sets the vertical offset to be 0 on the anchor's
|
||||||
|
first line, or the cursor's first (and only) line if the cursor is the
|
||||||
|
anchor.
|
||||||
|
|
||||||
|
- lines - the AttributedLines object to render to
|
||||||
|
- root_id - the id of the message to start rendering at
|
||||||
|
- indent - the indent string to prepend
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self.anchor_id == root_id:
|
||||||
|
lines.lower_offset = -1
|
||||||
|
|
||||||
|
# Render main message (root)
|
||||||
|
rendered = self._render_message_lines(root_id, indent)
|
||||||
|
lines.extend_below(rendered)
|
||||||
|
|
||||||
|
# Determine new indent
|
||||||
|
extra_indent = AT("┃ " if self.cursor_id == root_id else "│ ")
|
||||||
|
new_indent = indent + extra_indent
|
||||||
|
|
||||||
|
# Render children
|
||||||
|
for child_id in self.supply.children_ids(root_id):
|
||||||
|
self._render_subtree(lines, child_id, new_indent)
|
||||||
|
|
||||||
|
# Render cursor if necessary
|
||||||
|
if self.cursor_id == root_id:
|
||||||
|
# The cursor also acts as anchor if anchor is not specified
|
||||||
|
if self.anchor_id is None:
|
||||||
|
lines.lower_offset = -1
|
||||||
|
|
||||||
|
cursor_indent = indent + AT("┗━")
|
||||||
|
lines.extend_below(self._render_cursor(indent))
|
||||||
|
|
||||||
|
def _render_tree_containing(self, message_id: Id) -> AttributedLines:
|
||||||
|
"""
|
||||||
|
Similar to _render_tree(), but finds the root of the specified message
|
||||||
|
first.
|
||||||
|
"""
|
||||||
|
|
||||||
|
root_id = self.supply.oldest_ancestor_id(message_id)
|
||||||
|
# Puts the message with the specific id into the cache
|
||||||
|
return self._render_tree(root_id)
|
||||||
|
|
||||||
|
def _expand_upwards_until(self,
|
||||||
|
lines: AttributedLines,
|
||||||
|
ancestor_id: Id,
|
||||||
|
target_upper_offset: int,
|
||||||
|
) -> Id:
|
||||||
|
"""
|
||||||
|
Render trees and prepend them to the 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
|
||||||
|
rendered, returns the original anchor id.
|
||||||
|
|
||||||
|
Starts at the first older sibling of the ancestor_id (the first sibling
|
||||||
|
above the ancestor_id) and moves upwards sibling by sibling. Does not
|
||||||
|
render the ancestor_id itself.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# This loop doesn't use a condition but rather break-s, because I think
|
||||||
|
# it looks cleaner that way. I don't like mixing conditions and breaks
|
||||||
|
# too much, if the conditions are of the same importance.
|
||||||
|
last_rendered_id = ancestor_id
|
||||||
|
|
||||||
|
while True:
|
||||||
|
if lines.upper_offset <= target_upper_offset: break
|
||||||
|
|
||||||
|
next_id = self.supply.previous_id(last_rendered_id)
|
||||||
|
if next_id is None: break
|
||||||
|
|
||||||
|
lines.extend_above(self._render_tree(next_id))
|
||||||
|
last_rendered_id = next_id
|
||||||
|
|
||||||
|
return last_rendered_id
|
||||||
|
|
||||||
|
def _expand_downwards_until(self,
|
||||||
|
lines: AttributedLines,
|
||||||
|
ancestor_id: Id,
|
||||||
|
target_lower_offset: int,
|
||||||
|
) -> Id:
|
||||||
|
"""
|
||||||
|
Render trees and 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
|
||||||
|
rendered, returns the original anchor id.
|
||||||
|
|
||||||
|
Starts at the first younger sibling of the ancestor_id (the first
|
||||||
|
sibling below the ancestor_id) and moves downwards sibling by sibling.
|
||||||
|
Does not render the ancestor_id itself.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Almost the same as _expand_upwards_until(), but with small changes.
|
||||||
|
# Maybe these could one day be combined into one function.
|
||||||
|
#
|
||||||
|
# This loop doesn't use a condition but rather break-s, because I think
|
||||||
|
# it looks cleaner that way. I don't like mixing conditions and breaks
|
||||||
|
# too much, if the conditions are of the same importance.
|
||||||
|
|
||||||
|
last_rendered_id = ancestor_id
|
||||||
|
|
||||||
|
while True:
|
||||||
|
if lines.upper_offset >= target_lower_offset: break
|
||||||
|
|
||||||
|
next_id = self.supply.next_id(last_rendered_id)
|
||||||
|
if next_id is None: break
|
||||||
|
|
||||||
|
lines.extend_below(self._draw_tree(next_id))
|
||||||
|
last_rendered_id = next_id
|
||||||
|
|
||||||
|
return last_rendered_id
|
||||||
|
|
||||||
|
# Rendering the screen
|
||||||
|
|
||||||
|
def _render_screen(self) -> AttributedLines:
|
||||||
|
"""
|
||||||
|
Render an AttributedLines that fills the screen (as far as possible),
|
||||||
|
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
|
||||||
|
|
||||||
|
lines: AttributedLines
|
||||||
|
|
||||||
|
if self.cursor_id is None:
|
||||||
|
# If the cursor is None, that means that it should always be
|
||||||
|
# displayed at the very bottom of the room. It also means that
|
||||||
|
# _render_subtree() can't render the cursor, because it would be
|
||||||
|
# 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:
|
||||||
|
# In this case, the cursor is automatically rendered correctly by
|
||||||
|
# _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
|
||||||
|
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
|
||||||
|
|
||||||
def redraw(self) -> None:
|
def redraw(self) -> None:
|
||||||
pass
|
"""
|
||||||
|
Render new lines and draw them (to the internal widget and thus to the
|
||||||
|
screen on the next screen update).
|
||||||
|
"""
|
||||||
|
|
||||||
|
lines = self._render_screen()
|
||||||
|
self.lines_widget.set_lines(lines)
|
||||||
|
self._w = self.lines_widget
|
||||||
|
self._invalidate() # Just to make sure this really gets rendered
|
||||||
|
|
||||||
|
# Cursor movement
|
||||||
|
|
||||||
|
# Scrolling
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue