From 6edfc5825c44d59a50f6e59303a68cf357b18d91 Mon Sep 17 00:00:00 2001 From: Anton Khirnov Date: Tue, 18 May 2021 13:35:59 +0200 Subject: widgets/thread: improve thread node decoration rendering Implement configurable message summary indentation, fix appearance when the message summary spans more than one line. --- alot/widgets/thread.py | 106 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 86 insertions(+), 20 deletions(-) diff --git a/alot/widgets/thread.py b/alot/widgets/thread.py index 111c8569..c89f175d 100644 --- a/alot/widgets/thread.py +++ b/alot/widgets/thread.py @@ -7,6 +7,8 @@ Widgets specific to thread mode import logging import re import urwid +from urwid.canvas import SolidCanvas, TextCanvas +from urwid.util import apply_target_encoding try: import pygments.lexers, pygments.formatters, pygments.util @@ -758,15 +760,89 @@ class MessageWidget(urwid.WidgetWrap): return self._attach_wgt.focus.attachment return None -class ThreadNode(urwid.WidgetWrap): +class _ThreadNodeDecoration(urwid.Widget): + """ + Decoration indicating thread structure in rows corresponding to a single + message. This is a box widget that is rendered alongside the message summary + widget and is sized by their common container to have a matching height. + + The decoration consists of a list of blocks, where nth entry decorates the + structure at the nth depth level. Each block is rendered with the same + 'indent' width. + The last block is treated specially, since its first row contains the arrow + pointing to the message itself. + """ _ARROW = '➤' _HBAR = '─' _VBAR = '│' _TNODE = '├' _CORNER = '└' - _decor_text = None - _have_next_sibling = None + _sizing = frozenset(['box']) + + # depth of the correponding message in the thread + _depth = None + + # decorations for all the blocks before the last one + _deco_leading = None + # decoration for the first row of the last block + _deco_last_row0 = None + # decoration for the other rows of the last block + _deco_last = None + + def __init__(self, have_next_sibling): + self._depth = len(have_next_sibling) - 1 + + deco_leading = [] + for d in range(self._depth - 1): + deco_leading.append(self._VBAR if have_next_sibling[d + 1] else '') + self._deco_leading = deco_leading + + if self._depth > 0: + if have_next_sibling[self._depth]: + self._deco_last_row0 = self._TNODE + self._deco_last = self._VBAR + else: + self._deco_last_row0 = self._CORNER + self._deco_last = '' + + def render(self, size, focus = False): + cols, rows = size + indent = cols // self._depth + + if indent < 1: + return SolidCanvas(' ', cols, rows) + + # pad each leading block with spaces according to indent + blocks_leading = [] + for d in self._deco_leading: + pad = max(indent - len(d), 0) + blocks_leading.append(d + ' ' * pad) + + # build the arrow pointing to the message in the first row + # of the last block + pad_last0 = max(indent - len(self._deco_last_row0), 0) + block_last0 = self._deco_last_row0 + self._HBAR * (pad_last0 - 1) + self._ARROW * (pad_last0 > 0) + lines = [''.join(blocks_leading + [block_last0])] + + pad_last = max(indent - len(self._deco_last), 0) + block_last = self._deco_last + ' ' * pad_last + lines.extend([''.join(blocks_leading + [block_last])] * (rows - 1)) + + text, cs = list(zip(*(apply_target_encoding(l) for l in lines))) + + return TextCanvas(text, cs = cs) + +class ThreadNode(urwid.WidgetWrap): + """ + A node in the thread tree. + + It is a flow widget that consists of 2 columns: + - (optional) decoration indicating the thread structure + - message summary + """ + _decor = None + _msg_depth = None def __init__(self, msg, thread, pos, indent): parity = 'odd' if pos & 1 else 'even' @@ -778,9 +854,6 @@ class ThreadNode(urwid.WidgetWrap): if msg.depth == 0: wgt = msg_summary else: - self._decor_text = urwid.Text('') - wgt = urwid.Columns([('pack', self._decor_text), msg_summary]) - ancestor_chain = [msg] for p in msg.parents(): ancestor_chain.insert(0, p) @@ -792,25 +865,18 @@ class ThreadNode(urwid.WidgetWrap): else: have_sibling.append(m != ancestor_chain[d - 1].replies[-1]) - self._have_next_sibling = have_sibling + self._decor = _ThreadNodeDecoration(have_sibling) + wgt = urwid.Columns([(indent * msg.depth, self._decor), msg_summary], + box_columns = [0]) super().__init__(wgt) + self._msg_depth = msg.depth + self.set_indent(indent) def set_indent(self, indent): - if self._decor_text is None: + if self._decor is None: return - seg_text = [] - if indent > 0: - depth = len(self._have_next_sibling) - 1 - for d in range(depth): - if d == depth - 1: - corner = self._TNODE if self._have_next_sibling[d + 1] else self._CORNER - seg_text.append(corner + self._ARROW) - else: - bar = self._VBAR if self._have_next_sibling[d + 1] else ' ' - seg_text.append(bar + ' ') - - self._decor_text.set_text(''.join(seg_text)) + self._w.contents[0] = (self._decor, ('given', self._msg_depth * indent, True)) -- cgit v1.2.3