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 highlighting group is defined in the :ref:`highlight_group
option <config-themes-seg-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`` ``module``
.. _config-themes-seg-module: .. _config-themes-seg-module:
@ -276,13 +270,18 @@ Themes
``args`` ``args``
A dict of arguments to be passed to a ``function`` segment. A dict of arguments to be passed to a ``function`` segment.
``ljust`` ``align``
If set, the segment will be left justified to the width specified by Aligns the segments contents to the left (``l``), center (``c``) or
this option. right (``r``).
``rjust`` ``width``
If set, the segment will be right justified to the width specified Enforces a specific width for this segment.
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`` ``priority``
Optional segment priority. Segments with priority ``-1`` (the Optional segment priority. Segments with priority ``-1`` (the

View File

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

View File

@ -41,8 +41,10 @@
"before": " " "before": " "
}, },
{ {
"type": "filler", "type": "string",
"highlight_group": ["background"] "highlight_group": ["background"],
"draw_divider": false,
"width": "auto"
} }
], ],
"right": [ "right": [
@ -70,7 +72,8 @@
"args": { "gradient": true }, "args": { "gradient": true },
"priority": 30, "priority": 30,
"after": "%", "after": "%",
"rjust": 4 "width": 4,
"align": "r"
}, },
{ {
"type": "string", "type": "string",
@ -80,14 +83,16 @@
{ {
"name": "line_current", "name": "line_current",
"draw_divider": false, "draw_divider": false,
"rjust": 3 "width": 3,
"align": "r"
}, },
{ {
"name": "col_current", "name": "col_current",
"draw_divider": false, "draw_divider": false,
"priority": 30, "priority": 30,
"before": ":", "before": ":",
"ljust": 3 "width": 3,
"align": "l"
} }
] ]
} }

View File

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

View File

@ -11,8 +11,6 @@ class Renderer(object):
TERM_24BIT_COLORS = False TERM_24BIT_COLORS = False
PADDING_CHAR = u'\u00a0' # No-break space
def __init__(self, theme_config, local_themes, theme_kwargs, term_24bit_colors=False): def __init__(self, theme_config, local_themes, theme_kwargs, term_24bit_colors=False):
self.theme = Theme(theme_config=theme_config, **theme_kwargs) self.theme = Theme(theme_config=theme_config, **theme_kwargs)
self.local_themes = local_themes self.local_themes = local_themes
@ -34,13 +32,6 @@ class Renderer(object):
else: else:
return self.theme 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): def render(self, mode=None, width=None, theme=None, segments=None, side=None, output_raw=False):
'''Render all segments. '''Render all segments.
@ -56,31 +47,37 @@ class Renderer(object):
# Handle excluded/included segments for the current mode # Handle excluded/included segments for the current mode
segments = [segment for segment in segments\ segments = [segment for segment in segments\
if mode not in segment['exclude_modes'] or (segment['include_modes'] and segment in segment['include_modes'])] 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: if not width:
# No width specified, so we don't need to crop or pad anything # 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 # 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] 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.remove(segments_priority[0])
segments_priority.pop(0) segments_priority.pop(0)
# Do another render pass so we can calculate the correct amount of filler space # Distribute the remaining space on spacer segments
self._render_segments(mode, theme, segments, render_highlighted=False) 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 rendered_highlighted = u''.join([segment['_rendered_hl'] for segment in self._render_segments(mode, theme, segments)]) + self.hl()
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) return self._returned_value(rendered_highlighted, segments, output_raw)
def _render_segments(self, mode, theme, segments, render_highlighted=True): def _render_segments(self, mode, theme, segments, render_highlighted=True):
'''Internal segment rendering method. '''Internal segment rendering method.
@ -93,19 +90,20 @@ class Renderer(object):
highlighting strings added), and only renders the highlighted highlighting strings added), and only renders the highlighted
statusline if render_highlighted is True. statusline if render_highlighted is True.
''' '''
rendered_highlighted = u''
segments_len = len(segments) segments_len = len(segments)
try: try:
mode = mode if mode in segments[0]['highlight'] else Colorscheme.DEFAULT_MODE_KEY mode = mode if mode in segments[0]['highlight'] else Colorscheme.DEFAULT_MODE_KEY
except IndexError: except IndexError:
return '' pass
for index, segment in enumerate(segments): 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 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 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 compare_segment = next_segment if segment['side'] == 'left' else prev_segment
segment['rendered_raw'] = u'' outer_padding = ' ' if (index == 0 and segment['side'] == 'left') or (index == segments_len - 1 and segment['side'] == 'right') else ''
outer_padding = self.PADDING_CHAR 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_type = 'soft' if compare_segment['highlight'][mode]['bg'] == segment['highlight'][mode]['bg'] else 'hard'
divider_raw = theme.get_divider(segment['side'], divider_type) divider_raw = theme.get_divider(segment['side'], divider_type)
@ -114,22 +112,18 @@ class Renderer(object):
contents_highlighted = '' contents_highlighted = ''
# Pad segments first # Pad segments first
if segment['type'] == 'filler': if segment['draw_divider'] or (divider_type == 'hard' and segment['width'] != 'auto'):
pass
elif segment['draw_divider'] or divider_type == 'hard':
if segment['side'] == 'left': if segment['side'] == 'left':
contents_raw = outer_padding + contents_raw + self.PADDING_CHAR contents_raw = outer_padding + (segment['_space_left'] * ' ') + contents_raw + (segment['_space_right'] * ' ') + ' '
divider_raw = divider_raw + self.PADDING_CHAR divider_raw = divider_raw + ' '
else:
contents_raw = ' ' + (segment['_space_left'] * ' ') + contents_raw + (segment['_space_right'] * ' ') + outer_padding
divider_raw = ' ' + divider_raw
else: else:
contents_raw = self.PADDING_CHAR + contents_raw + outer_padding
divider_raw = self.PADDING_CHAR + divider_raw
elif contents_raw:
if segment['side'] == 'left': if segment['side'] == 'left':
contents_raw = outer_padding + contents_raw contents_raw = outer_padding + (segment['_space_left'] * ' ') + contents_raw + (segment['_space_right'] * ' ')
else: else:
contents_raw = contents_raw + outer_padding contents_raw = (segment['_space_left'] * ' ') + contents_raw + (segment['_space_right'] * ' ') + outer_padding
else:
raise ValueError('Unknown segment type')
# Apply highlighting to padded dividers and contents # Apply highlighting to padded dividers and contents
if render_highlighted: if render_highlighted:
@ -144,37 +138,31 @@ class Renderer(object):
contents_highlighted = self.hl(self.escape(contents_raw), **segment['highlight'][mode]) contents_highlighted = self.hl(self.escape(contents_raw), **segment['highlight'][mode])
# Append padded raw and highlighted segments to the rendered segment variables # Append padded raw and highlighted segments to the rendered segment variables
if segment['type'] == 'filler': if segment['draw_divider'] or (divider_type == 'hard' and segment['width'] != 'auto'):
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['side'] == 'left': if segment['side'] == 'left':
segment['rendered_raw'] += contents_raw + divider_raw segment['_rendered_raw'] += contents_raw + divider_raw
rendered_highlighted += contents_highlighted + divider_highlighted segment['_rendered_hl'] += contents_highlighted + divider_highlighted
else:
segment['_rendered_raw'] += divider_raw + contents_raw
segment['_rendered_hl'] += divider_highlighted + contents_highlighted
else: else:
segment['rendered_raw'] += divider_raw + contents_raw
rendered_highlighted += divider_highlighted + contents_highlighted
elif contents_raw:
# Segments without divider
if segment['side'] == 'left': if segment['side'] == 'left':
segment['rendered_raw'] += contents_raw segment['_rendered_raw'] += contents_raw
rendered_highlighted += contents_highlighted segment['_rendered_hl'] += contents_highlighted
else: else:
segment['rendered_raw'] += contents_raw segment['_rendered_raw'] += contents_raw
rendered_highlighted += contents_highlighted segment['_rendered_hl'] += contents_highlighted
rendered_highlighted += self.hl() segment['_len'] = len(segment['_rendered_raw'])
return rendered_highlighted # Replace rendered spaces with no-break spaces
segment['_rendered_hl'] = segment['_rendered_hl'].replace(' ', u'\u00a0')
yield segment
@staticmethod @staticmethod
def _total_len(segments): def _returned_value(rendered_highlighted, segments, output_raw):
'''Return total/rendered length of all segments. if output_raw:
return rendered_highlighted, ''.join((segment['_rendered_raw'] for segment in segments))
This method uses the rendered_raw property of the segments and requires else:
that the segments have been rendered using the render() method first. return rendered_highlighted
'''
return len(''.join([segment['rendered_raw'] for segment in segments]))
@staticmethod @staticmethod
def escape(string): def escape(string):

View File

@ -38,6 +38,9 @@ class VimRenderer(Renderer):
else: else:
mode = 'nc' mode = 'nc'
theme, segments = self.window_cache.get(window_id, (None, None)) 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) statusline = super(VimRenderer, self).render(mode, winwidth, theme, segments)
return statusline return statusline

View File

@ -45,11 +45,16 @@ class Segment(object):
'contents_func': contents_func, 'contents_func': contents_func,
'contents': contents, 'contents': contents,
'args': segment.get('args', {}), 'args': segment.get('args', {}),
'ljust': segment.get('ljust', False),
'rjust': segment.get('rjust', False),
'priority': segment.get('priority', -1), 'priority': segment.get('priority', -1),
'draw_divider': segment.get('draw_divider', True), 'draw_divider': segment.get('draw_divider', True),
'side': side, 'side': side,
'exclude_modes': segment.get('exclude_modes', []), 'exclude_modes': segment.get('exclude_modes', []),
'include_modes': segment.get('include_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: else:
segment['contents'] = contents segment['contents'] = contents
parsed_segments.append(segment) 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) parsed_segments.append(segment)
else: else:
continue continue
for segment in parsed_segments: for segment in parsed_segments:
segment = self.add_highlight(segment) segment = self.add_highlight(segment)
segment['contents'] = (segment['before'] + unicode(segment['contents']) + segment['after'])\ segment['contents'] = segment['before'] + unicode(segment['contents'] if segment['contents'] is not None else '') + segment['after']
.ljust(segment['ljust'])\ # Align segment contents
.rjust(segment['rjust']) 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 # We need to yield a copy of the segment, or else mode-dependent
# segment contents can't be cached correctly e.g. when caching # segment contents can't be cached correctly e.g. when caching
# non-current window contents for vim statuslines # non-current window contents for vim statuslines