Remove old files
This commit is contained in:
parent
b71541c847
commit
a9ddf27525
4 changed files with 0 additions and 804 deletions
|
|
@ -1,69 +0,0 @@
|
|||
import datetime
|
||||
from dataclasses import dataclass
|
||||
from typing import Hashable, List, Optional
|
||||
|
||||
from .attributed_lines import AttributedLines
|
||||
from .markup import AT, AttributedText
|
||||
|
||||
__all__ = ["Id", "Message", "RenderedMessage"]
|
||||
|
||||
|
||||
Id = Hashable
|
||||
|
||||
|
||||
@dataclass
|
||||
class Message:
|
||||
"""
|
||||
A Message represents a single euphoria message. It contains the information
|
||||
and functionality necessary to render itself to lines of AttributedText.
|
||||
|
||||
It does not contain information that usually changes, like a list of its
|
||||
child messages, or if it is currently folded.
|
||||
|
||||
A message's content is assumed to never change. Truncated messages are
|
||||
never untruncated and displayed in full. Thus, the Message can ignore
|
||||
truncation status.
|
||||
"""
|
||||
|
||||
id: Id
|
||||
parent_id: Optional[Id]
|
||||
time: datetime.datetime
|
||||
nick: str
|
||||
content: str
|
||||
|
||||
def render(self, width: int) -> "RenderedMessage":
|
||||
lines = self.content.split("\n")
|
||||
|
||||
meta = AT(self.time.strftime("%H:%M "))
|
||||
|
||||
nick = AT(f"[{self.nick}] ")
|
||||
nick_spaces = AT(" " * len(nick))
|
||||
|
||||
result = []
|
||||
result.append(nick + AT(lines[0]))
|
||||
result.extend(nick_spaces + AT(line) for line in lines[1:])
|
||||
|
||||
return RenderedMessage(self.id, meta, result)
|
||||
|
||||
@dataclass
|
||||
class RenderedMessage:
|
||||
"""
|
||||
A RenderedMessage is the result of rendering a Message. It contains lines
|
||||
of AttributedText, the target width to which the Message was rendered, its
|
||||
final dimensions and possibly some other useful information.
|
||||
|
||||
It only contains the rendered sender nick and message body, NOT the
|
||||
message's indentation.
|
||||
|
||||
A RenderedMessage is immutable. It can be used in a cache to prevent
|
||||
re-rendering each message every time it is needed (preventing word wrapping
|
||||
and other fancy calculations to be repeated on every re-render).
|
||||
|
||||
It is also useful for scrolling and cursor movement, since the height of
|
||||
the message displayed on screen is not inherent to the Message object, but
|
||||
rather the result of rendering a Message.
|
||||
"""
|
||||
|
||||
message_id: Id
|
||||
meta: AttributedText
|
||||
lines: List[AttributedText]
|
||||
|
|
@ -1,140 +0,0 @@
|
|||
import abc
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from .exceptions import MessageSupplyException
|
||||
from .message import Id, Message
|
||||
|
||||
__all__ = ["MessageSupply", "InMemoryMessageSupply"]
|
||||
|
||||
|
||||
class MessageSupply(abc.ABC):
|
||||
"""
|
||||
A MessageSupply holds all of a room's known messages. It can be queried in
|
||||
different ways. Messages can also be added to or removed from the MessageSupply
|
||||
as they are received by the client.
|
||||
|
||||
The MessageSupply may use a database or keep messages in memory. A
|
||||
MessageSupply may also wrap around another MessageSupply, to provide caching or
|
||||
similar.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get(self, message_id: Id) -> Message:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def children_ids(self, message_id: Id) -> List[Id]:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def sibling_ids(self, message_id: Id) -> List[Id]:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def parent_id(self, message_id: Id) -> Optional[Id]:
|
||||
pass
|
||||
|
||||
def oldest_ancestor_id(self, message_id: Id) -> Id:
|
||||
ancestor_id = message_id
|
||||
|
||||
while True:
|
||||
parent_id = self.parent_id(ancestor_id)
|
||||
if parent_id is None: break
|
||||
ancestor_id = parent_id
|
||||
|
||||
return ancestor_id
|
||||
|
||||
def previous_id(self, message_id: Id) -> Optional[Id]:
|
||||
sibling_ids = self.sibling_ids(message_id)
|
||||
|
||||
try:
|
||||
i = sibling_ids.index(message_id)
|
||||
if i <= 0:
|
||||
return None
|
||||
else:
|
||||
return sibling_ids[i - 1]
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def next_id(self, message_id: Id) -> Optional[Id]:
|
||||
sibling_ids = self.sibling_ids(message_id)
|
||||
|
||||
try:
|
||||
i = sibling_ids.index(message_id)
|
||||
if i >= len(sibling_ids) - 1:
|
||||
return None
|
||||
else:
|
||||
return sibling_ids[i + 1]
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
@abc.abstractmethod
|
||||
def lowest_root_id(self) -> Optional[Id]:
|
||||
pass
|
||||
|
||||
class InMemoryMessageSupply(MessageSupply):
|
||||
"""
|
||||
This message supply stores messages in memory. It orders the messages by
|
||||
their ids.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._messages: Dict[Id, Message] = {}
|
||||
self._children: Dict[Id, List[Message]] = {}
|
||||
|
||||
def add(self, message: Message) -> None:
|
||||
if message.id in self._messages:
|
||||
self.remove(message.id)
|
||||
|
||||
self._messages[message.id] = message
|
||||
|
||||
if message.parent_id is not None:
|
||||
children = self._children.get(message.parent_id, [])
|
||||
children.append(message)
|
||||
children.sort(key=lambda m: m.id)
|
||||
self._children[message.parent_id] = children
|
||||
|
||||
def remove(self, message_id: Id) -> None:
|
||||
message = self._messages.get(message_id)
|
||||
if message is None: return
|
||||
|
||||
self._messages.pop(message)
|
||||
|
||||
if message.parent_id is not None:
|
||||
children = self._children.get(message.id)
|
||||
if children is not None: # just to satisfy mypy
|
||||
children.remove(message)
|
||||
|
||||
if not children:
|
||||
self._children.pop(message.id)
|
||||
|
||||
def get(self, message_id: Id) -> Message:
|
||||
message = self._messages.get(message_id)
|
||||
|
||||
if message is None:
|
||||
raise MessageSupplyException(
|
||||
f"message with id {message_id!r} does not exist")
|
||||
|
||||
return message
|
||||
|
||||
def child_ids(self, message_id: Id) -> List[Id]:
|
||||
return [m.id for m in self._children.get(message_id, [])]
|
||||
|
||||
def parent_id(self, message_id: Id) -> Optional[Id]:
|
||||
message = self.get(message_id)
|
||||
return message.parent_id
|
||||
|
||||
def sibling_ids(self, message_id: Id) -> List[Id]:
|
||||
parent_id = self.parent_id(message_id)
|
||||
|
||||
if parent_id is None:
|
||||
roots = [m for m in self._messages.values() if m.parent_id is None]
|
||||
sibling_ids = list(sorted(root.id for root in roots))
|
||||
else:
|
||||
sibling_ids = self.children_ids(parent_id)
|
||||
|
||||
return sibling_ids
|
||||
|
||||
def lowest_root_id(self) -> Optional[Id]:
|
||||
roots = list(sorted(self._messages.keys()))
|
||||
return roots[-1] if roots else None
|
||||
|
|
@ -1,585 +0,0 @@
|
|||
from typing import Optional, Set, Tuple
|
||||
|
||||
import urwid
|
||||
import yaboli
|
||||
|
||||
from .attributed_lines import AttributedLines
|
||||
from .attributed_lines_widget import AttributedLinesWidget
|
||||
from .markup import AT, AttributedText
|
||||
from .message import Id, RenderedMessage
|
||||
from .message_supply import MessageSupply
|
||||
from .rendered_message_cache import RenderedMessageCache
|
||||
|
||||
__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):
|
||||
"""
|
||||
This widget displays an ElementSupply, including user interface like a
|
||||
cursor or folding markers. It usually is part of a RoomWidget. It also
|
||||
keeps a RenderedMessageCache (and maybe even other caches).
|
||||
|
||||
It receives key presses and mouse clicks from its parent widget. It
|
||||
receives redraw requests and cache invalidation notices from the
|
||||
RoomWidget.
|
||||
|
||||
It doesn't directly receive new messages. Rather, the RoomWidget adds them
|
||||
to the ElementSupply and then submits a cache invalidation notice and a
|
||||
redraw request.
|
||||
|
||||
It emits a "room top hit" event (unnamed as of yet). When the RoomWidget
|
||||
receives this event, it should retrieve more messages from the server.
|
||||
|
||||
It emits a "edit" event (unnamed as of yet) when the user attempts to edit
|
||||
a message. When the RoomWidget receives this event, it should open a text
|
||||
editor to compose a new message, and send that message once the user
|
||||
finishes editing it.
|
||||
"""
|
||||
|
||||
ROOM_IS_EMPTY_MESSAGE = "<no messages>"
|
||||
|
||||
def __init__(self,
|
||||
# TODO config
|
||||
room: yaboli.Room,
|
||||
supply: MessageSupply,
|
||||
) -> None:
|
||||
|
||||
# yaboli.Room, used only for the current nick
|
||||
self.room = room
|
||||
# A supply of Message-s
|
||||
self.supply = supply
|
||||
# A cache of RenderedMessage-s
|
||||
self.rendered = RenderedMessageCache()
|
||||
# The lines that were last rendered
|
||||
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))
|
||||
|
||||
# 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
|
||||
# the anchor.
|
||||
self.anchor_id: Optional[Id] = None
|
||||
# The anchor's line's offset on the screen, measured in percent of the
|
||||
# total height. For more information, see the comment above
|
||||
# _get_absolute_offset() and _get_relative_offset().
|
||||
self.anchor_offset = 0.5
|
||||
|
||||
# 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)
|
||||
|
||||
@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:
|
||||
"""
|
||||
Invalidate the RenderedMessage cached under message_id.
|
||||
"""
|
||||
|
||||
self.cache.invalidate(message_id)
|
||||
|
||||
def invalidate_all_messages(self) -> None:
|
||||
"""
|
||||
Invalidate all cached RenderedMessage-s.
|
||||
"""
|
||||
|
||||
self.cache.invalidate_all()
|
||||
|
||||
# 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:
|
||||
# Quick and dirty cursor rendering
|
||||
nick = self.room.session.nick
|
||||
text = indent + AT(f"[{nick}]")
|
||||
|
||||
lines = AttributedLines()
|
||||
lines.append_below({"cursor": True}, text)
|
||||
|
||||
return lines
|
||||
|
||||
# Rendering the tree
|
||||
|
||||
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(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:
|
||||
"""
|
||||
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,
|
||||
) -> Tuple[Id, bool]:
|
||||
"""
|
||||
Render trees (including the cursor) and prepend them to the
|
||||
AttributedLines until its upper_offset matches or exceeds the
|
||||
target_upper_offset.
|
||||
|
||||
Returns whether it has hit the top of the supply.
|
||||
|
||||
Assumes that the ancestor_id's tree is already rendered. Moves upwards
|
||||
through the siblings of the ancestor_id.
|
||||
"""
|
||||
|
||||
# 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:
|
||||
# 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)
|
||||
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))
|
||||
last_rendered_id = next_id
|
||||
|
||||
def _expand_downwards_until(self,
|
||||
lines: AttributedLines,
|
||||
ancestor_id: Id,
|
||||
target_lower_offset: int,
|
||||
) -> Tuple[Id, bool]:
|
||||
"""
|
||||
Render trees (including the cursor, even if it's at the bottom) and
|
||||
append them to the AttributedLines until its lower_offset matches or
|
||||
exceeds the target_lower_offset.
|
||||
|
||||
Returns whether it has hit the bottom of the supply.
|
||||
|
||||
Assumes that the ancestor_id's tree is already rendered. Moves
|
||||
downwards through the siblings of the ancestor_id.
|
||||
"""
|
||||
|
||||
# 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:
|
||||
# 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)
|
||||
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))
|
||||
last_rendered_id = next_id
|
||||
|
||||
lines.extend_below(self._render_cursor())
|
||||
return last_rendered_id, True
|
||||
|
||||
# Rendering the screen
|
||||
|
||||
"""
|
||||
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),
|
||||
taking into account the anchor offset.
|
||||
"""
|
||||
|
||||
if self.cursor_id is None and self.anchor_id is None:
|
||||
return self._render_screen_from_cursor()
|
||||
|
||||
working_id: Id
|
||||
if self.anchor_id is None:
|
||||
# self.cursor_id can't be None, otherwise the first if
|
||||
# condition would have been met and this part wouldn't have
|
||||
# been executed in the first place.
|
||||
working_id = self.cursor_id # type: ignore
|
||||
else:
|
||||
working_id = self.anchor_id
|
||||
|
||||
return self._render_screen_from_anchor(working_id)
|
||||
|
||||
# Updating the internal widget
|
||||
|
||||
def _update_with_lines(self, lines: AttributedLines) -> None:
|
||||
"""
|
||||
Update evrything that needs to be updated when a new set of lines comes
|
||||
in.
|
||||
"""
|
||||
|
||||
self.lines = lines
|
||||
self.lines_widget.set_lines(self.lines)
|
||||
|
||||
self._w = self.lines_widget
|
||||
self._invalidate() # Just to make sure this really gets rendered
|
||||
|
||||
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
|
||||
|
||||
def scroll_by(self, delta: int) -> None:
|
||||
self.absolute_anchor_offset += delta
|
||||
|
||||
# Cursor movement
|
||||
|
||||
# TODO
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
__all__ = ["RenderedMessageCache"]
|
||||
|
||||
|
||||
class RenderedMessageCache:
|
||||
"""
|
||||
This is a cache for RenderedMessage-s. Message-s should not need to be
|
||||
redrawn every frame (and every in-between calculation).
|
||||
"""
|
||||
|
||||
pass
|
||||
Loading…
Add table
Add a link
Reference in a new issue