Refactor segment rendering

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 #113.
Refs #154.
This commit is contained in:
Kim Silkebækken 2013-02-01 16:06:43 +01:00
parent cb860ce5d0
commit bfdb7f8028
8 changed files with 107 additions and 95 deletions

View File

@ -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:
@ -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``).
``rjust``
If set, the segment will be right justified to the width specified
by this option.
``width``
Enforces a specific width for this segment.
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

View File

@ -7,8 +7,10 @@
"highlight_group": ["file_name"]
},
{
"type": "filler",
"highlight_group": ["background"]
"type": "string",
"highlight_group": ["background"],
"draw_divider": false,
"width": "auto"
}
]
}

View File

@ -41,8 +41,10 @@
"before": " "
},
{
"type": "filler",
"highlight_group": ["background"]
"type": "string",
"highlight_group": ["background"],
"draw_divider": false,
"width": "auto"
}
],
"right": [
@ -70,7 +72,8 @@
"args": { "gradient": true },
"priority": 30,
"after": "%",
"rjust": 4
"width": 4,
"align": "r"
},
{
"type": "string",
@ -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"
}
]
}

View File

@ -6,8 +6,10 @@
"draw_divider": false
},
{
"type": "filler",
"highlight_group": ["background"]
"type": "string",
"highlight_group": ["background"],
"draw_divider": false,
"width": "auto"
}
],
"right": [
@ -16,7 +18,8 @@
"args": { "gradient": true },
"priority": 30,
"after": "%",
"rjust": 4
"width": 4,
"align": "r"
},
{
"type": "string",
@ -26,7 +29,8 @@
{
"name": "line_current",
"draw_divider": false,
"rjust": 3
"width": 3,
"align": "r"
}
]
}

View File

@ -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
@ -34,13 +32,6 @@ class Renderer(object):
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.
@ -56,31 +47,37 @@ class Renderer(object):
# 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 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
# 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
rendered_highlighted = u''.join([segment['_rendered_hl'] for segment in self._render_segments(mode, theme, segments)]) + self.hl()
return self._returned_value(self._render_segments(mode, theme, segments), segments, output_raw)
return self._returned_value(rendered_highlighted, segments, output_raw)
def _render_segments(self, mode, theme, segments, render_highlighted=True):
'''Internal segment rendering method.
@ -93,19 +90,20 @@ class Renderer(object):
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)
@ -114,22 +112,18 @@ class Renderer(object):
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 = ' ' + (segment['_space_left'] * ' ') + contents_raw + (segment['_space_right'] * ' ') + outer_padding
divider_raw = ' ' + divider_raw
else:
contents_raw = self.PADDING_CHAR + contents_raw + outer_padding
divider_raw = self.PADDING_CHAR + divider_raw
elif contents_raw:
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:
@ -144,37 +138,31 @@ class Renderer(object):
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
segment['_rendered_hl'] += divider_highlighted + contents_highlighted
else:
segment['rendered_raw'] += divider_raw + contents_raw
rendered_highlighted += divider_highlighted + contents_highlighted
elif contents_raw:
# Segments without divider
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):

View File

@ -38,6 +38,9 @@ class VimRenderer(Renderer):
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

View File

@ -45,11 +45,16 @@ class Segment(object):
'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,
}

View File

@ -65,15 +65,21 @@ class Theme(object):
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