Improve the Segment class and rendering

The Segment class has been given two new properties: priority and
filler. The render method has been given a width argument.

If the width argument is specified when rendering a segment and the
rendered segments are too wide, segments are dropped in order of
priority, with the lowest priority (highest number) being dropped first
and any segment with priority < 0 is kept, regardless of the specified
width.

If the width argument is specified along with one or more filler
segments, the filler segments will pad the remaining space with space
characters until the desired width has been reached.

The handling of segment attributes has also been improved by lazily
looking up the correct attributes in the segment tree when it's being
rendered.

Finally, the rendering code itself has been improved a bit with less
code duplication and much more readable code.
This commit is contained in:
Kim Silkebækken 2012-11-14 11:53:39 +01:00
parent 21a48e997a
commit d3bace24ec
2 changed files with 172 additions and 173 deletions

View File

@ -37,142 +37,106 @@ class Segment:
ATTR_ITALIC = 2 ATTR_ITALIC = 2
ATTR_UNDERLINE = 4 ATTR_UNDERLINE = 4
def __init__(self, content='', fg=None, bg=None, attr=None, side=None, padding=None, divide=None): def __init__(self, contents=None, fg=None, bg=None, attr=None, side=None, padding=None, draw_divider=None, priority=None, filler=None):
'''Create a new segment. '''Create a new Powerline segment.
No arguments are required when creating new segments, as
empty/colorless segments can be used e.g. as left/right dividers.
''' '''
self.content = content
self.parent = None self.parent = None
self.prev = None
self.next = None self.contents = contents or ''
self.fg = fg
self.bg = bg
self.attr = attr
self.side = side
self.padding = padding
self.draw_divider = draw_divider
self.priority = priority
self.filler = filler
if self.filler:
# Filler segments should never have any dividers
self.draw_divider = False
# Set the parent property for child segments
for segment in self.contents:
try:
segment.parent = self
except AttributeError:
# Not a Segment node
continue
def init_attributes(self):
'''Initialize the default attributes for this segment.
This method is intended to be run when all segments in the segment tree
have the correct parent segment set (i.e. after the root segment has
been instantiated).
'''
def lookup_attr(attr, default, obj=self):
'''Looks up attributes in the segment tree.
If the attribute isn't found anywhere, the default argument is used
for this segment.
'''
# Check if the current object has the attribute defined
obj_attr = getattr(obj, attr)
if obj_attr is None:
try:
# Check if the object's parent has the attribute defined
return lookup_attr(attr, default, obj.parent)
except AttributeError:
# Root node reached
return default
return obj_attr
# Set default attributes
self.fg = lookup_attr('fg', False)
self.bg = lookup_attr('bg', False)
self.attr = lookup_attr('attr', False)
self.side = lookup_attr('side', 'l')
self.padding = lookup_attr('padding', ' ')
self.draw_divider = lookup_attr('draw_divider', True)
self.priority = lookup_attr('priority', -1)
self.filler = lookup_attr('filler', False)
try: try:
if len(fg) == 2: if len(self.fg) == 2:
self._fg = fg self.fg = self.fg
except TypeError: except TypeError:
# Only the terminal color is defined, so we need to get the hex color # Only the terminal color is defined, so we need to get the hex color
from lib.colors import cterm_to_hex from lib.colors import cterm_to_hex
self._fg = [fg, cterm_to_hex(fg)] self.fg = [self.fg, cterm_to_hex(self.fg)]
try: try:
if len(bg) == 2: if len(self.bg) == 2:
self._bg = bg self.bg = self.bg
except TypeError: except TypeError:
# Only the terminal color is defined, so we need to get the hex color # Only the terminal color is defined, so we need to get the hex color
from lib.colors import cterm_to_hex from lib.colors import cterm_to_hex
self._bg = [bg, cterm_to_hex(bg)] self.bg = [self.bg, cterm_to_hex(self.bg)]
self._attr = attr def render(self, renderer, width=None):
self._side = side
self._padding = padding
self._divide = divide
@property
def fg(self):
'''Segment foreground color property.
Recursively searches for the property or the parent segments' property
until one is found. If this property is not defined anywhere in the
tree, the default foreground color is used.
'''
if self.parent and self._fg[0] is None:
return self.parent.fg
return self._fg if self._fg[0] is not None else False
@property
def bg(self):
'''Segment background color property.
Recursively searches for the property or the parent segments' property
until one is found. If this property is not defined anywhere in the
tree, the default background color is used.
'''
if self.parent and self._bg[0] is None:
return self.parent.bg
return self._bg if self._bg[0] is not None else False
@property
def attr(self):
'''Segment attribute property.
Recursively searches for the property or the parent segments' property
until one is found. If this property is not defined anywhere in the
tree, no attributes are applied.
'''
if self.parent and self._attr is None:
return self.parent.attr
return self._attr if self._attr is not None else False
@property
def side(self):
'''Segment side property.
Recursively searches for the property or the parent segments' property
until one is found. If this property is not defined anywhere in the
tree, the left side is used for all segments.
'''
if self.parent and self._side is None:
return self.parent.side
return self._side if self._side is not None else 'l'
@property
def padding(self):
'''Segment padding property.
Return which string is used to pad the segment before and after the
divider symbol.
Recursively searches for the property or the parent segments' property
until one is found. If this property is not defined anywhere in the
tree, a single space is used for padding.
'''
if self.parent and self._padding is None:
return self.parent.padding
return self._padding if self._padding is not None else ' '
@property
def divide(self):
'''Segment divider property.
Returns whether a divider symbol should be drawn before/after the
segment.
Recursively searches for the property or the parent segments' property
until one is found. If this property is not defined anywhere in the
tree, then dividers will be drawn around all segments.
'''
if self.parent and self._divide is None:
return self.parent.divide
return self._divide if self._divide is not None else True
@property
def divider(self):
'''Segment divider property.
Returns the divider symbol to be used, depending on which side this
segment is on.
'''
return self.dividers[self.side]
def render(self, renderer):
'''Render the segment and all child segments. '''Render the segment and all child segments.
This method flattens the segment and all its child segments into This method flattens the segment and all its child segments into
a one-dimensional array. It then loops through this array and compares a one-dimensional array. It then loops through this array and compares
the foreground/background colors and divider/padding properties and the foreground/background colors and divider/padding properties and
returns the rendered statusline as a string. returns the rendered statusline as a string.
When a width is provided, low-priority segments are dropped one at
a time until the line is shorter than the width, or only segments
with a negative priority are left. If one or more filler segments are
provided they will fill the remaining space until the desired width is
reached.
''' '''
def flatten(segment): def flatten(segment):
'''Flattens the segment tree into a one-dimensional array. '''Flatten the segment tree into a one-dimensional array.
''' '''
ret = [] ret = []
for child_segment in segment.content: for child_segment in segment.contents:
child_segment.parent = segment if isinstance(child_segment.contents, str):
if isinstance(child_segment.content, str):
# If the contents of the child segment is a string then # If the contents of the child segment is a string then
# this is a tree node # this is a tree node
child_segment.init_attributes()
ret.append(child_segment) ret.append(child_segment)
else: else:
# This is a segment group that should be flattened # This is a segment group that should be flattened
@ -180,57 +144,92 @@ class Segment:
return ret return ret
segments = flatten(self) segments = flatten(self)
output = ''
# Loop through the segment array and create the segments, colors and def render_segments(segments, render_raw=True, render_highlighted=True):
# dividers '''Render a one-dimensional segment array.
#
# TODO Make this prettier
for idx, segment in enumerate(segments):
# Ensure that we always have a previous/next segment, if we're at
# the beginning/end of the array an empty segment is used for the
# prev/next segment
segment.prev = segments[idx - 1] if idx > 0 else Segment()
segment.next = segments[idx + 1] if idx < len(segments) - 1 else Segment()
if segment.side == 'l': By default this function renders both raw (un-highlighted segments
output += renderer.hl(segment.fg, segment.bg, segment.attr) used for calculating final width) and highlighted segments.
output += segment.padding '''
output += segment.content rendered_raw = ''
output += renderer.hl(attr=False) rendered_highlighted = ''
if segment.content:
if segment.next.bg == segment.bg: for idx, segment in enumerate(segments):
if segment.next.content and segment.divide: prev = segments[idx - 1] if idx > 0 else Segment()
output += segment.padding next = segments[idx + 1] if idx < len(segments) - 1 else Segment()
if segment.next.side == segment.side:
# Only draw the soft divider if this segment is on the same side compare_segment = next if segment.side == 'l' else prev
# No need to draw the soft divider if there's e.g. a vim separation point in the next segment divider_type = 'soft' if compare_segment.bg == segment.bg else 'hard'
output += segment.divider['soft'] divider = self.dividers[segment.side][divider_type]
# Don't draw a hard divider if the next segment is on
# the opposite side, it screws up the coloring if segment.filler:
elif segment.next.side == segment.side: # Filler segments shouldn't be padded
output += segment.padding segment_format = '{contents}'
output += renderer.hl(segment.bg, segment.next.bg) elif segment.draw_divider and (divider_type == 'hard' or segment.side == compare_segment.side):
output += segment.divider['hard'] # Draw divider if specified, and if the next segment is on
else: # the opposite side only draw the divider if it's a hard
pad_pre = False # divider
if segment.content: if segment.side == 'l':
if segment.prev.bg == segment.bg: segment_format = '{segment_hl}{padding}{contents}{padding}{divider_hl}{divider}'
if segment.prev.content and segment.divide:
pad_pre = True
if segment.prev.side == segment.side:
# Only draw the soft divider if this segment is on the same side
# No need to draw the soft divider if there's e.g. a vim separation point in the previous segment
output += segment.divider['soft']
else: else:
pad_pre = True segment_format = '{divider_hl}{divider}{segment_hl}{padding}{contents}{padding}'
output += renderer.hl(segment.bg, segment.prev.bg) elif segment.contents:
output += segment.divider['hard'] # Soft divided segments
output += renderer.hl(segment.fg, segment.bg, segment.attr) segment_format = '{segment_hl}{padding}{contents}{padding}'
if pad_pre: else:
output += segment.padding # Unknown segment type, skip it
output += segment.content continue
output += renderer.hl(attr=False)
output += segment.padding
return output if render_raw is True and segment.filler is False:
# Filler segments must be empty when used e.g. in vim (the
# %=%< segment which disappears), so they will be skipped
# when calculating the width using the raw rendering
rendered_raw += segment_format.format(
padding=segment.padding,
divider=divider,
contents=segment.contents,
divider_hl='',
segment_hl='',
)
if render_highlighted is True:
rendered_highlighted += segment_format.format(
padding=segment.padding,
divider=divider,
contents=segment.contents,
divider_hl='' if divider_type == 'soft' else renderer.hl(segment.bg, compare_segment.bg),
segment_hl=renderer.hl(segment.fg, segment.bg, segment.attr),
)
return {
'highlighted': rendered_highlighted,
'raw': rendered_raw,
}
rendered = render_segments(segments)
if not width:
# No width specified, so we don't need to crop or pad anything
return rendered['highlighted']
import math
# Create an ordered list of segments that can be dropped
segments_priority = [segment for segment in sorted(segments, key=lambda segment: segment.priority, reverse=True) if segment.priority > 0]
while len(rendered['raw']) > width and len(segments_priority):
segments.remove(segments_priority[0])
segments_priority.pop(0)
rendered = render_segments(segments, render_highlighted=False)
# Distribute the remaining space on the filler segments
segments_fillers = [segment for segment in segments if segment.filler is True]
segments_fillers_contents = ' ' * math.floor((width - len(rendered['raw'])) / len(segments_fillers))
for segment in segments_fillers:
segment.contents = segments_fillers_contents
# Do a final render now that we have handled the cropping and padding
rendered = render_segments(segments, render_raw=False)
return rendered['highlighted']

View File

@ -7,22 +7,22 @@ from lib.renderers import VimSegmentRenderer
powerline = Segment([ powerline = Segment([
Segment('NORMAL', 22, 148, attr=Segment.ATTR_BOLD), Segment('NORMAL', 22, 148, attr=Segment.ATTR_BOLD),
Segment('⭠ develop', 247, 240), Segment('⭠ develop', 247, 240, priority=10),
Segment([ Segment([
Segment(' ~/projects/powerline/lib/'), Segment(' ~/projects/powerline/lib/', draw_divider=False),
Segment('core.py ', 231, attr=Segment.ATTR_BOLD), Segment('core.py ', 231, attr=Segment.ATTR_BOLD),
], 250, 240, divide=False, padding=''), ], 250, 240, padding=''),
Segment('%<%='), Segment('%=%<', filler=True),
Segment([ Segment([
Segment('unix'), Segment('unix', priority=50),
Segment('utf-8'), Segment('utf-8', priority=50),
Segment('python'), Segment('python', priority=50),
Segment(' 83%%', 247, 240), Segment('83%', 247, 240, priority=30),
Segment([ Segment([
Segment(' ', 239), Segment('', 239),
Segment('23', attr=Segment.ATTR_BOLD), Segment('23', attr=Segment.ATTR_BOLD, padding='', draw_divider=False),
Segment(':1 ', 244), Segment(':1', 244, priority=30, padding='', draw_divider=False),
], 235, 252, divide=False, padding=''), ], 235, 252),
], 245, side='r'), ], 245, side='r'),
], bg=236) ], bg=236)