Skip to content

Commit

Permalink
Refactor segment rendering
Browse files Browse the repository at this point in the history
This commit introduces the following changes to themes and segment
rendering:

- Spacer segments are now regular string/function type segments with
  "width": "auto" in the themes.
- The "rjust"/"ljust" properties have been replaced by the "width"
  option combined with a new "align" option.
- Renderer._render_segments() is now a generator which renders each
  segment separately, and assigns the rendered contents to
  "_rendered_hl" and "_rendered_raw" in the segment dict.
- Renderer.render() returns the segments by joining the "_rendered_hl"
  values for each segment.
- Spacer segment widths are calculated in the render() method, and
  assigned to "_space_left" and "_space_right" in the segment dict.
  These spaces are then applied in Renderer._render_segments().
- All space characters are converted to no-break spaces (U+00A0) in the
  "_rendered_hl" property.

Refs Lokaltog#113.
Refs #154.
  • Loading branch information
Lokaltog committed Feb 1, 2013
1 parent cb860ce commit bfdb7f8
Show file tree
Hide file tree
Showing 8 changed files with 108 additions and 96 deletions.
23 changes: 11 additions & 12 deletions docs/source/configuration.rst
Expand Up @@ -237,12 +237,6 @@ Themes
highlighting group is defined in the :ref:`highlight_group
option <config-themes-seg-highlight_group>`.

``filler``
If the statusline is rendered with a specific width, remaining
whitespace is distributed among filler segments. The
highlighting group is defined in the :ref:`highlight_group
option <config-themes-seg-highlight_group>`.

``module``
.. _config-themes-seg-module:

Expand Down Expand Up @@ -276,13 +270,18 @@ Themes
``args``
A dict of arguments to be passed to a ``function`` segment.

``ljust``
If set, the segment will be left justified to the width specified by
this option.
``align``
Aligns the segments contents to the left (``l``), center (``c``) or
right (``r``).

``width``
Enforces a specific width for this segment.

``rjust``
If set, the segment will be right justified to the width specified
by this option.
This segment will work as a spacer if the width is set to ``auto``.
Several spacers may be used, and the space will be distributed
equally among all the spacer segments. Spacers may have contents,
either returned by a function or a static string, and the contents
can be aligned with the ``align`` property.

``priority``
Optional segment priority. Segments with priority ``-1`` (the
Expand Down
6 changes: 4 additions & 2 deletions powerline/config_files/themes/vim/cmdwin.json
Expand Up @@ -7,8 +7,10 @@
"highlight_group": ["file_name"]
},
{
"type": "filler",
"highlight_group": ["background"]
"type": "string",
"highlight_group": ["background"],
"draw_divider": false,
"width": "auto"
}
]
}
Expand Down
15 changes: 10 additions & 5 deletions powerline/config_files/themes/vim/default.json
Expand Up @@ -41,8 +41,10 @@
"before": " "
},
{
"type": "filler",
"highlight_group": ["background"]
"type": "string",
"highlight_group": ["background"],
"draw_divider": false,
"width": "auto"
}
],
"right": [
Expand Down Expand Up @@ -70,7 +72,8 @@
"args": { "gradient": true },
"priority": 30,
"after": "%",
"rjust": 4
"width": 4,
"align": "r"
},
{
"type": "string",
Expand All @@ -80,14 +83,16 @@
{
"name": "line_current",
"draw_divider": false,
"rjust": 3
"width": 3,
"align": "r"
},
{
"name": "col_current",
"draw_divider": false,
"priority": 30,
"before": ":",
"ljust": 3
"width": 3,
"align": "l"
}
]
}
Expand Down
12 changes: 8 additions & 4 deletions powerline/config_files/themes/vim/help.json
Expand Up @@ -6,8 +6,10 @@
"draw_divider": false
},
{
"type": "filler",
"highlight_group": ["background"]
"type": "string",
"highlight_group": ["background"],
"draw_divider": false,
"width": "auto"
}
],
"right": [
Expand All @@ -16,7 +18,8 @@
"args": { "gradient": true },
"priority": 30,
"after": "%",
"rjust": 4
"width": 4,
"align": "r"
},
{
"type": "string",
Expand All @@ -26,7 +29,8 @@
{
"name": "line_current",
"draw_divider": false,
"rjust": 3
"width": 3,
"align": "r"
}
]
}
Expand Down
122 changes: 55 additions & 67 deletions powerline/renderer.py
Expand Up @@ -11,8 +11,6 @@ class Renderer(object):

TERM_24BIT_COLORS = False

PADDING_CHAR = u'\u00a0' # No-break space

def __init__(self, theme_config, local_themes, theme_kwargs, term_24bit_colors=False):
self.theme = Theme(theme_config=theme_config, **theme_kwargs)
self.local_themes = local_themes
Expand All @@ -34,13 +32,6 @@ def get_theme(self):
else:
return self.theme

@staticmethod
def _returned_value(rendered_highlighted, segments, output_raw):
if output_raw:
return rendered_highlighted, ''.join((segment['rendered_raw'] for segment in segments))
else:
return rendered_highlighted

def render(self, mode=None, width=None, theme=None, segments=None, side=None, output_raw=False):
'''Render all segments.
Expand All @@ -56,31 +47,37 @@ def render(self, mode=None, width=None, theme=None, segments=None, side=None, ou
# Handle excluded/included segments for the current mode
segments = [segment for segment in segments\
if mode not in segment['exclude_modes'] or (segment['include_modes'] and segment in segment['include_modes'])]
rendered_highlighted = self._render_segments(mode, theme, segments)

segments = [segment for segment in self._render_segments(mode, theme, segments)]

if not width:
# No width specified, so we don't need to crop or pad anything
return self._returned_value(rendered_highlighted, segments, output_raw)
return self._returned_value(u''.join([segment['_rendered_hl'] for segment in segments]) + self.hl(), segments, output_raw)

# 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 self._total_len(segments) > width and len(segments_priority):
while sum([segment['_len'] for segment in segments]) > width and len(segments_priority):
segments.remove(segments_priority[0])
segments_priority.pop(0)

# Do another render pass so we can calculate the correct amount of filler space
self._render_segments(mode, theme, segments, render_highlighted=False)

# Distribute the remaining space on the filler segments
segments_fillers = [segment for segment in segments if segment['type'] == 'filler']
if segments_fillers:
segments_fillers_len, segments_fillers_remainder = divmod((width - self._total_len(segments)), len(segments_fillers))
segments_fillers_contents = self.PADDING_CHAR * segments_fillers_len
for segment in segments_fillers:
segment['contents'] = segments_fillers_contents
# Add remainder whitespace to the first filler segment
segments_fillers[0]['contents'] += self.PADDING_CHAR * segments_fillers_remainder

return self._returned_value(self._render_segments(mode, theme, segments), segments, output_raw)
# Distribute the remaining space on spacer segments
segments_spacers = [segment for segment in segments if segment['width'] == 'auto']
if segments_spacers:
distribute_len, distribute_len_remainder = divmod(width - sum([segment['_len'] for segment in segments]), len(segments_spacers))
for segment in segments_spacers:
if segment['align'] == 'l':
segment['_space_right'] += distribute_len
elif segment['align'] == 'r':
segment['_space_left'] += distribute_len
elif segment['align'] == 'c':
space_side, space_side_remainder = divmod(distribute_len, 2)
segment['_space_left'] += space_side + space_side_remainder
segment['_space_right'] += space_side
segments_spacers[0]['_space_right'] += distribute_len_remainder

rendered_highlighted = u''.join([segment['_rendered_hl'] for segment in self._render_segments(mode, theme, segments)]) + self.hl()

return self._returned_value(rendered_highlighted, segments, output_raw)

def _render_segments(self, mode, theme, segments, render_highlighted=True):
'''Internal segment rendering method.
Expand All @@ -93,19 +90,20 @@ def _render_segments(self, mode, theme, segments, render_highlighted=True):
highlighting strings added), and only renders the highlighted
statusline if render_highlighted is True.
'''
rendered_highlighted = u''
segments_len = len(segments)
try:
mode = mode if mode in segments[0]['highlight'] else Colorscheme.DEFAULT_MODE_KEY
except IndexError:
return ''
pass

for index, segment in enumerate(segments):
segment['_rendered_raw'] = u''
segment['_rendered_hl'] = u''

prev_segment = segments[index - 1] if index > 0 else theme.EMPTY_SEGMENT
next_segment = segments[index + 1] if index < segments_len - 1 else theme.EMPTY_SEGMENT
compare_segment = next_segment if segment['side'] == 'left' else prev_segment
segment['rendered_raw'] = u''
outer_padding = self.PADDING_CHAR if (index == 0 and segment['side'] == 'left') or (index == segments_len - 1 and segment['side'] == 'right') else ''
outer_padding = ' ' if (index == 0 and segment['side'] == 'left') or (index == segments_len - 1 and segment['side'] == 'right') else ''
divider_type = 'soft' if compare_segment['highlight'][mode]['bg'] == segment['highlight'][mode]['bg'] else 'hard'

divider_raw = theme.get_divider(segment['side'], divider_type)
Expand All @@ -114,22 +112,18 @@ def _render_segments(self, mode, theme, segments, render_highlighted=True):
contents_highlighted = ''

# Pad segments first
if segment['type'] == 'filler':
pass
elif segment['draw_divider'] or divider_type == 'hard':
if segment['draw_divider'] or (divider_type == 'hard' and segment['width'] != 'auto'):
if segment['side'] == 'left':
contents_raw = outer_padding + contents_raw + self.PADDING_CHAR
divider_raw = divider_raw + self.PADDING_CHAR
contents_raw = outer_padding + (segment['_space_left'] * ' ') + contents_raw + (segment['_space_right'] * ' ') + ' '
divider_raw = divider_raw + ' '
else:
contents_raw = self.PADDING_CHAR + contents_raw + outer_padding
divider_raw = self.PADDING_CHAR + divider_raw
elif contents_raw:
contents_raw = ' ' + (segment['_space_left'] * ' ') + contents_raw + (segment['_space_right'] * ' ') + outer_padding
divider_raw = ' ' + divider_raw
else:
if segment['side'] == 'left':
contents_raw = outer_padding + contents_raw
contents_raw = outer_padding + (segment['_space_left'] * ' ') + contents_raw + (segment['_space_right'] * ' ')
else:
contents_raw = contents_raw + outer_padding
else:
raise ValueError('Unknown segment type')
contents_raw = (segment['_space_left'] * ' ') + contents_raw + (segment['_space_right'] * ' ') + outer_padding

# Apply highlighting to padded dividers and contents
if render_highlighted:
Expand All @@ -144,37 +138,31 @@ def _render_segments(self, mode, theme, segments, render_highlighted=True):
contents_highlighted = self.hl(self.escape(contents_raw), **segment['highlight'][mode])

# Append padded raw and highlighted segments to the rendered segment variables
if segment['type'] == 'filler':
rendered_highlighted += contents_highlighted if contents_raw else ''
elif segment['draw_divider'] or divider_type == 'hard':
# Draw divider if specified, or if it's a hard divider
# Note: Hard dividers are always drawn, regardless of
# the draw_divider option
if segment['draw_divider'] or (divider_type == 'hard' and segment['width'] != 'auto'):
if segment['side'] == 'left':
segment['rendered_raw'] += contents_raw + divider_raw
rendered_highlighted += contents_highlighted + divider_highlighted
segment['_rendered_raw'] += contents_raw + divider_raw
segment['_rendered_hl'] += contents_highlighted + divider_highlighted
else:
segment['rendered_raw'] += divider_raw + contents_raw
rendered_highlighted += divider_highlighted + contents_highlighted
elif contents_raw:
# Segments without divider
segment['_rendered_raw'] += divider_raw + contents_raw
segment['_rendered_hl'] += divider_highlighted + contents_highlighted
else:
if segment['side'] == 'left':
segment['rendered_raw'] += contents_raw
rendered_highlighted += contents_highlighted
segment['_rendered_raw'] += contents_raw
segment['_rendered_hl'] += contents_highlighted
else:
segment['rendered_raw'] += contents_raw
rendered_highlighted += contents_highlighted
rendered_highlighted += self.hl()
return rendered_highlighted
segment['_rendered_raw'] += contents_raw
segment['_rendered_hl'] += contents_highlighted
segment['_len'] = len(segment['_rendered_raw'])
# Replace rendered spaces with no-break spaces
segment['_rendered_hl'] = segment['_rendered_hl'].replace(' ', u'\u00a0')
yield segment

@staticmethod
def _total_len(segments):
'''Return total/rendered length of all segments.
This method uses the rendered_raw property of the segments and requires
that the segments have been rendered using the render() method first.
'''
return len(''.join([segment['rendered_raw'] for segment in segments]))
def _returned_value(rendered_highlighted, segments, output_raw):
if output_raw:
return rendered_highlighted, ''.join((segment['_rendered_raw'] for segment in segments))
else:
return rendered_highlighted

@staticmethod
def escape(string):
Expand Down
3 changes: 3 additions & 0 deletions powerline/renderers/vim.py
Expand Up @@ -38,6 +38,9 @@ def render(self, winnr, current):
else:
mode = 'nc'
theme, segments = self.window_cache.get(window_id, (None, None))
for segment in segments:
segment['_space_left'] = 0
segment['_space_right'] = 0
statusline = super(VimRenderer, self).render(mode, winwidth, theme, segments)
return statusline

Expand Down
9 changes: 7 additions & 2 deletions powerline/segment.py
Expand Up @@ -45,11 +45,16 @@ def get(self, segment, side):
'contents_func': contents_func,
'contents': contents,
'args': segment.get('args', {}),
'ljust': segment.get('ljust', False),
'rjust': segment.get('rjust', False),
'priority': segment.get('priority', -1),
'draw_divider': segment.get('draw_divider', True),
'side': side,
'exclude_modes': segment.get('exclude_modes', []),
'include_modes': segment.get('include_modes', []),
'width': segment.get('width'),
'align': segment.get('align', 'l'),
'_rendered_raw': u'',
'_rendered_hl': u'',
'_len': 0,
'_space_left': 0,
'_space_right': 0,
}
14 changes: 10 additions & 4 deletions powerline/theme.py
Expand Up @@ -65,15 +65,21 @@ def get_segments(self, side=None):
else:
segment['contents'] = contents
parsed_segments.append(segment)
elif segment['type'] == 'filler' or (segment['type'] == 'string' and segment['contents'] is not None):
elif segment['width'] == 'auto' or (segment['type'] == 'string' and segment['contents'] is not None):
parsed_segments.append(segment)
else:
continue
for segment in parsed_segments:
segment = self.add_highlight(segment)
segment['contents'] = (segment['before'] + unicode(segment['contents']) + segment['after'])\
.ljust(segment['ljust'])\
.rjust(segment['rjust'])
segment['contents'] = segment['before'] + unicode(segment['contents'] if segment['contents'] is not None else '') + segment['after']
# Align segment contents
if segment['width'] and segment['width'] != 'auto':
if segment['align'] == 'l':
segment['contents'] = segment['contents'].ljust(segment['width'])
elif segment['align'] == 'r':
segment['contents'] = segment['contents'].rjust(segment['width'])
elif segment['align'] == 'c':
segment['contents'] = segment['contents'].center(segment['width'])
# We need to yield a copy of the segment, or else mode-dependent
# segment contents can't be cached correctly e.g. when caching
# non-current window contents for vim statuslines
Expand Down

0 comments on commit bfdb7f8

Please sign in to comment.