aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/containers.py
diff options
context:
space:
mode:
authornkozlovskiy <nmk@ydb.tech>2023-09-29 12:24:06 +0300
committernkozlovskiy <nmk@ydb.tech>2023-09-29 12:41:34 +0300
commite0e3e1717e3d33762ce61950504f9637a6e669ed (patch)
treebca3ff6939b10ed60c3d5c12439963a1146b9711 /contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/containers.py
parent38f2c5852db84c7b4d83adfcb009eb61541d1ccd (diff)
downloadydb-e0e3e1717e3d33762ce61950504f9637a6e669ed.tar.gz
add ydb deps
Diffstat (limited to 'contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/containers.py')
-rw-r--r--contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/containers.py1665
1 files changed, 1665 insertions, 0 deletions
diff --git a/contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/containers.py b/contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/containers.py
new file mode 100644
index 0000000000..0bdafe18e0
--- /dev/null
+++ b/contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/containers.py
@@ -0,0 +1,1665 @@
+"""
+Container for the layout.
+(Containers can contain other containers or user interface controls.)
+"""
+from __future__ import unicode_literals
+
+from abc import ABCMeta, abstractmethod
+from six import with_metaclass
+from six.moves import range
+
+from .controls import UIControl, TokenListControl, UIContent
+from .dimension import LayoutDimension, sum_layout_dimensions, max_layout_dimensions
+from .margins import Margin
+from .screen import Point, WritePosition, _CHAR_CACHE
+from .utils import token_list_to_text, explode_tokens
+from prompt_toolkit.cache import SimpleCache
+from prompt_toolkit.filters import to_cli_filter, ViInsertMode, EmacsInsertMode
+from prompt_toolkit.mouse_events import MouseEvent, MouseEventType
+from prompt_toolkit.reactive import Integer
+from prompt_toolkit.token import Token
+from prompt_toolkit.utils import take_using_weights, get_cwidth
+
+__all__ = (
+ 'Container',
+ 'HSplit',
+ 'VSplit',
+ 'FloatContainer',
+ 'Float',
+ 'Window',
+ 'WindowRenderInfo',
+ 'ConditionalContainer',
+ 'ScrollOffsets',
+ 'ColorColumn',
+)
+
+Transparent = Token.Transparent
+
+
+class Container(with_metaclass(ABCMeta, object)):
+ """
+ Base class for user interface layout.
+ """
+ @abstractmethod
+ def reset(self):
+ """
+ Reset the state of this container and all the children.
+ (E.g. reset scroll offsets, etc...)
+ """
+
+ @abstractmethod
+ def preferred_width(self, cli, max_available_width):
+ """
+ Return a :class:`~prompt_toolkit.layout.dimension.LayoutDimension` that
+ represents the desired width for this container.
+
+ :param cli: :class:`~prompt_toolkit.interface.CommandLineInterface`.
+ """
+
+ @abstractmethod
+ def preferred_height(self, cli, width, max_available_height):
+ """
+ Return a :class:`~prompt_toolkit.layout.dimension.LayoutDimension` that
+ represents the desired height for this container.
+
+ :param cli: :class:`~prompt_toolkit.interface.CommandLineInterface`.
+ """
+
+ @abstractmethod
+ def write_to_screen(self, cli, screen, mouse_handlers, write_position):
+ """
+ Write the actual content to the screen.
+
+ :param cli: :class:`~prompt_toolkit.interface.CommandLineInterface`.
+ :param screen: :class:`~prompt_toolkit.layout.screen.Screen`
+ :param mouse_handlers: :class:`~prompt_toolkit.layout.mouse_handlers.MouseHandlers`.
+ """
+
+ @abstractmethod
+ def walk(self, cli):
+ """
+ Walk through all the layout nodes (and their children) and yield them.
+ """
+
+
+def _window_too_small():
+ " Create a `Window` that displays the 'Window too small' text. "
+ return Window(TokenListControl.static(
+ [(Token.WindowTooSmall, ' Window too small... ')]))
+
+
+class HSplit(Container):
+ """
+ Several layouts, one stacked above/under the other.
+
+ :param children: List of child :class:`.Container` objects.
+ :param window_too_small: A :class:`.Container` object that is displayed if
+ there is not enough space for all the children. By default, this is a
+ "Window too small" message.
+ :param get_dimensions: (`None` or a callable that takes a
+ `CommandLineInterface` and returns a list of `LayoutDimension`
+ instances.) By default the dimensions are taken from the children and
+ divided by the available space. However, when `get_dimensions` is specified,
+ this is taken instead.
+ :param report_dimensions_callback: When rendering, this function is called
+ with the `CommandLineInterface` and the list of used dimensions. (As a
+ list of integers.)
+ """
+ def __init__(self, children, window_too_small=None,
+ get_dimensions=None, report_dimensions_callback=None):
+ assert all(isinstance(c, Container) for c in children)
+ assert window_too_small is None or isinstance(window_too_small, Container)
+ assert get_dimensions is None or callable(get_dimensions)
+ assert report_dimensions_callback is None or callable(report_dimensions_callback)
+
+ self.children = children
+ self.window_too_small = window_too_small or _window_too_small()
+ self.get_dimensions = get_dimensions
+ self.report_dimensions_callback = report_dimensions_callback
+
+ def preferred_width(self, cli, max_available_width):
+ if self.children:
+ dimensions = [c.preferred_width(cli, max_available_width) for c in self.children]
+ return max_layout_dimensions(dimensions)
+ else:
+ return LayoutDimension(0)
+
+ def preferred_height(self, cli, width, max_available_height):
+ dimensions = [c.preferred_height(cli, width, max_available_height) for c in self.children]
+ return sum_layout_dimensions(dimensions)
+
+ def reset(self):
+ for c in self.children:
+ c.reset()
+
+ def write_to_screen(self, cli, screen, mouse_handlers, write_position):
+ """
+ Render the prompt to a `Screen` instance.
+
+ :param screen: The :class:`~prompt_toolkit.layout.screen.Screen` class
+ to which the output has to be written.
+ """
+ sizes = self._divide_heigths(cli, write_position)
+
+ if self.report_dimensions_callback:
+ self.report_dimensions_callback(cli, sizes)
+
+ if sizes is None:
+ self.window_too_small.write_to_screen(
+ cli, screen, mouse_handlers, write_position)
+ else:
+ # Draw child panes.
+ ypos = write_position.ypos
+ xpos = write_position.xpos
+ width = write_position.width
+
+ for s, c in zip(sizes, self.children):
+ c.write_to_screen(cli, screen, mouse_handlers, WritePosition(xpos, ypos, width, s))
+ ypos += s
+
+ def _divide_heigths(self, cli, write_position):
+ """
+ Return the heights for all rows.
+ Or None when there is not enough space.
+ """
+ if not self.children:
+ return []
+
+ # Calculate heights.
+ given_dimensions = self.get_dimensions(cli) if self.get_dimensions else None
+
+ def get_dimension_for_child(c, index):
+ if given_dimensions and given_dimensions[index] is not None:
+ return given_dimensions[index]
+ else:
+ return c.preferred_height(cli, write_position.width, write_position.extended_height)
+
+ dimensions = [get_dimension_for_child(c, index) for index, c in enumerate(self.children)]
+
+ # Sum dimensions
+ sum_dimensions = sum_layout_dimensions(dimensions)
+
+ # If there is not enough space for both.
+ # Don't do anything.
+ if sum_dimensions.min > write_position.extended_height:
+ return
+
+ # Find optimal sizes. (Start with minimal size, increase until we cover
+ # the whole height.)
+ sizes = [d.min for d in dimensions]
+
+ child_generator = take_using_weights(
+ items=list(range(len(dimensions))),
+ weights=[d.weight for d in dimensions])
+
+ i = next(child_generator)
+
+ while sum(sizes) < min(write_position.extended_height, sum_dimensions.preferred):
+ # Increase until we meet at least the 'preferred' size.
+ if sizes[i] < dimensions[i].preferred:
+ sizes[i] += 1
+ i = next(child_generator)
+
+ if not any([cli.is_returning, cli.is_exiting, cli.is_aborting]):
+ while sum(sizes) < min(write_position.height, sum_dimensions.max):
+ # Increase until we use all the available space. (or until "max")
+ if sizes[i] < dimensions[i].max:
+ sizes[i] += 1
+ i = next(child_generator)
+
+ return sizes
+
+ def walk(self, cli):
+ """ Walk through children. """
+ yield self
+ for c in self.children:
+ for i in c.walk(cli):
+ yield i
+
+
+class VSplit(Container):
+ """
+ Several layouts, one stacked left/right of the other.
+
+ :param children: List of child :class:`.Container` objects.
+ :param window_too_small: A :class:`.Container` object that is displayed if
+ there is not enough space for all the children. By default, this is a
+ "Window too small" message.
+ :param get_dimensions: (`None` or a callable that takes a
+ `CommandLineInterface` and returns a list of `LayoutDimension`
+ instances.) By default the dimensions are taken from the children and
+ divided by the available space. However, when `get_dimensions` is specified,
+ this is taken instead.
+ :param report_dimensions_callback: When rendering, this function is called
+ with the `CommandLineInterface` and the list of used dimensions. (As a
+ list of integers.)
+ """
+ def __init__(self, children, window_too_small=None,
+ get_dimensions=None, report_dimensions_callback=None):
+ assert all(isinstance(c, Container) for c in children)
+ assert window_too_small is None or isinstance(window_too_small, Container)
+ assert get_dimensions is None or callable(get_dimensions)
+ assert report_dimensions_callback is None or callable(report_dimensions_callback)
+
+ self.children = children
+ self.window_too_small = window_too_small or _window_too_small()
+ self.get_dimensions = get_dimensions
+ self.report_dimensions_callback = report_dimensions_callback
+
+ def preferred_width(self, cli, max_available_width):
+ dimensions = [c.preferred_width(cli, max_available_width) for c in self.children]
+ return sum_layout_dimensions(dimensions)
+
+ def preferred_height(self, cli, width, max_available_height):
+ sizes = self._divide_widths(cli, width)
+ if sizes is None:
+ return LayoutDimension()
+ else:
+ dimensions = [c.preferred_height(cli, s, max_available_height)
+ for s, c in zip(sizes, self.children)]
+ return max_layout_dimensions(dimensions)
+
+ def reset(self):
+ for c in self.children:
+ c.reset()
+
+ def _divide_widths(self, cli, width):
+ """
+ Return the widths for all columns.
+ Or None when there is not enough space.
+ """
+ if not self.children:
+ return []
+
+ # Calculate widths.
+ given_dimensions = self.get_dimensions(cli) if self.get_dimensions else None
+
+ def get_dimension_for_child(c, index):
+ if given_dimensions and given_dimensions[index] is not None:
+ return given_dimensions[index]
+ else:
+ return c.preferred_width(cli, width)
+
+ dimensions = [get_dimension_for_child(c, index) for index, c in enumerate(self.children)]
+
+ # Sum dimensions
+ sum_dimensions = sum_layout_dimensions(dimensions)
+
+ # If there is not enough space for both.
+ # Don't do anything.
+ if sum_dimensions.min > width:
+ return
+
+ # Find optimal sizes. (Start with minimal size, increase until we cover
+ # the whole height.)
+ sizes = [d.min for d in dimensions]
+
+ child_generator = take_using_weights(
+ items=list(range(len(dimensions))),
+ weights=[d.weight for d in dimensions])
+
+ i = next(child_generator)
+
+ while sum(sizes) < min(width, sum_dimensions.preferred):
+ # Increase until we meet at least the 'preferred' size.
+ if sizes[i] < dimensions[i].preferred:
+ sizes[i] += 1
+ i = next(child_generator)
+
+ while sum(sizes) < min(width, sum_dimensions.max):
+ # Increase until we use all the available space.
+ if sizes[i] < dimensions[i].max:
+ sizes[i] += 1
+ i = next(child_generator)
+
+ return sizes
+
+ def write_to_screen(self, cli, screen, mouse_handlers, write_position):
+ """
+ Render the prompt to a `Screen` instance.
+
+ :param screen: The :class:`~prompt_toolkit.layout.screen.Screen` class
+ to which the output has to be written.
+ """
+ if not self.children:
+ return
+
+ sizes = self._divide_widths(cli, write_position.width)
+
+ if self.report_dimensions_callback:
+ self.report_dimensions_callback(cli, sizes)
+
+ # If there is not enough space.
+ if sizes is None:
+ self.window_too_small.write_to_screen(
+ cli, screen, mouse_handlers, write_position)
+ return
+
+ # Calculate heights, take the largest possible, but not larger than write_position.extended_height.
+ heights = [child.preferred_height(cli, width, write_position.extended_height).preferred
+ for width, child in zip(sizes, self.children)]
+ height = max(write_position.height, min(write_position.extended_height, max(heights)))
+
+ # Draw child panes.
+ ypos = write_position.ypos
+ xpos = write_position.xpos
+
+ for s, c in zip(sizes, self.children):
+ c.write_to_screen(cli, screen, mouse_handlers, WritePosition(xpos, ypos, s, height))
+ xpos += s
+
+ def walk(self, cli):
+ """ Walk through children. """
+ yield self
+ for c in self.children:
+ for i in c.walk(cli):
+ yield i
+
+
+class FloatContainer(Container):
+ """
+ Container which can contain another container for the background, as well
+ as a list of floating containers on top of it.
+
+ Example Usage::
+
+ FloatContainer(content=Window(...),
+ floats=[
+ Float(xcursor=True,
+ ycursor=True,
+ layout=CompletionMenu(...))
+ ])
+ """
+ def __init__(self, content, floats):
+ assert isinstance(content, Container)
+ assert all(isinstance(f, Float) for f in floats)
+
+ self.content = content
+ self.floats = floats
+
+ def reset(self):
+ self.content.reset()
+
+ for f in self.floats:
+ f.content.reset()
+
+ def preferred_width(self, cli, write_position):
+ return self.content.preferred_width(cli, write_position)
+
+ def preferred_height(self, cli, width, max_available_height):
+ """
+ Return the preferred height of the float container.
+ (We don't care about the height of the floats, they should always fit
+ into the dimensions provided by the container.)
+ """
+ return self.content.preferred_height(cli, width, max_available_height)
+
+ def write_to_screen(self, cli, screen, mouse_handlers, write_position):
+ self.content.write_to_screen(cli, screen, mouse_handlers, write_position)
+
+ for fl in self.floats:
+ # When a menu_position was given, use this instead of the cursor
+ # position. (These cursor positions are absolute, translate again
+ # relative to the write_position.)
+ # Note: This should be inside the for-loop, because one float could
+ # set the cursor position to be used for the next one.
+ cursor_position = screen.menu_position or screen.cursor_position
+ cursor_position = Point(x=cursor_position.x - write_position.xpos,
+ y=cursor_position.y - write_position.ypos)
+
+ fl_width = fl.get_width(cli)
+ fl_height = fl.get_height(cli)
+
+ # Left & width given.
+ if fl.left is not None and fl_width is not None:
+ xpos = fl.left
+ width = fl_width
+ # Left & right given -> calculate width.
+ elif fl.left is not None and fl.right is not None:
+ xpos = fl.left
+ width = write_position.width - fl.left - fl.right
+ # Width & right given -> calculate left.
+ elif fl_width is not None and fl.right is not None:
+ xpos = write_position.width - fl.right - fl_width
+ width = fl_width
+ elif fl.xcursor:
+ width = fl_width
+ if width is None:
+ width = fl.content.preferred_width(cli, write_position.width).preferred
+ width = min(write_position.width, width)
+
+ xpos = cursor_position.x
+ if xpos + width > write_position.width:
+ xpos = max(0, write_position.width - width)
+ # Only width given -> center horizontally.
+ elif fl_width:
+ xpos = int((write_position.width - fl_width) / 2)
+ width = fl_width
+ # Otherwise, take preferred width from float content.
+ else:
+ width = fl.content.preferred_width(cli, write_position.width).preferred
+
+ if fl.left is not None:
+ xpos = fl.left
+ elif fl.right is not None:
+ xpos = max(0, write_position.width - width - fl.right)
+ else: # Center horizontally.
+ xpos = max(0, int((write_position.width - width) / 2))
+
+ # Trim.
+ width = min(width, write_position.width - xpos)
+
+ # Top & height given.
+ if fl.top is not None and fl_height is not None:
+ ypos = fl.top
+ height = fl_height
+ # Top & bottom given -> calculate height.
+ elif fl.top is not None and fl.bottom is not None:
+ ypos = fl.top
+ height = write_position.height - fl.top - fl.bottom
+ # Height & bottom given -> calculate top.
+ elif fl_height is not None and fl.bottom is not None:
+ ypos = write_position.height - fl_height - fl.bottom
+ height = fl_height
+ # Near cursor
+ elif fl.ycursor:
+ ypos = cursor_position.y + 1
+
+ height = fl_height
+ if height is None:
+ height = fl.content.preferred_height(
+ cli, width, write_position.extended_height).preferred
+
+ # Reduce height if not enough space. (We can use the
+ # extended_height when the content requires it.)
+ if height > write_position.extended_height - ypos:
+ if write_position.extended_height - ypos + 1 >= ypos:
+ # When the space below the cursor is more than
+ # the space above, just reduce the height.
+ height = write_position.extended_height - ypos
+ else:
+ # Otherwise, fit the float above the cursor.
+ height = min(height, cursor_position.y)
+ ypos = cursor_position.y - height
+
+ # Only height given -> center vertically.
+ elif fl_width:
+ ypos = int((write_position.height - fl_height) / 2)
+ height = fl_height
+ # Otherwise, take preferred height from content.
+ else:
+ height = fl.content.preferred_height(
+ cli, width, write_position.extended_height).preferred
+
+ if fl.top is not None:
+ ypos = fl.top
+ elif fl.bottom is not None:
+ ypos = max(0, write_position.height - height - fl.bottom)
+ else: # Center vertically.
+ ypos = max(0, int((write_position.height - height) / 2))
+
+ # Trim.
+ height = min(height, write_position.height - ypos)
+
+ # Write float.
+ # (xpos and ypos can be negative: a float can be partially visible.)
+ if height > 0 and width > 0:
+ wp = WritePosition(xpos=xpos + write_position.xpos,
+ ypos=ypos + write_position.ypos,
+ width=width, height=height)
+
+ if not fl.hide_when_covering_content or self._area_is_empty(screen, wp):
+ fl.content.write_to_screen(cli, screen, mouse_handlers, wp)
+
+ def _area_is_empty(self, screen, write_position):
+ """
+ Return True when the area below the write position is still empty.
+ (For floats that should not hide content underneath.)
+ """
+ wp = write_position
+ Transparent = Token.Transparent
+
+ for y in range(wp.ypos, wp.ypos + wp.height):
+ if y in screen.data_buffer:
+ row = screen.data_buffer[y]
+
+ for x in range(wp.xpos, wp.xpos + wp.width):
+ c = row[x]
+ if c.char != ' ' or c.token != Transparent:
+ return False
+
+ return True
+
+ def walk(self, cli):
+ """ Walk through children. """
+ yield self
+
+ for i in self.content.walk(cli):
+ yield i
+
+ for f in self.floats:
+ for i in f.content.walk(cli):
+ yield i
+
+
+class Float(object):
+ """
+ Float for use in a :class:`.FloatContainer`.
+
+ :param content: :class:`.Container` instance.
+ :param hide_when_covering_content: Hide the float when it covers content underneath.
+ """
+ def __init__(self, top=None, right=None, bottom=None, left=None,
+ width=None, height=None, get_width=None, get_height=None,
+ xcursor=False, ycursor=False, content=None,
+ hide_when_covering_content=False):
+ assert isinstance(content, Container)
+ assert width is None or get_width is None
+ assert height is None or get_height is None
+
+ self.left = left
+ self.right = right
+ self.top = top
+ self.bottom = bottom
+
+ self._width = width
+ self._height = height
+
+ self._get_width = get_width
+ self._get_height = get_height
+
+ self.xcursor = xcursor
+ self.ycursor = ycursor
+
+ self.content = content
+ self.hide_when_covering_content = hide_when_covering_content
+
+ def get_width(self, cli):
+ if self._width:
+ return self._width
+ if self._get_width:
+ return self._get_width(cli)
+
+ def get_height(self, cli):
+ if self._height:
+ return self._height
+ if self._get_height:
+ return self._get_height(cli)
+
+ def __repr__(self):
+ return 'Float(content=%r)' % self.content
+
+
+class WindowRenderInfo(object):
+ """
+ Render information, for the last render time of this control.
+ It stores mapping information between the input buffers (in case of a
+ :class:`~prompt_toolkit.layout.controls.BufferControl`) and the actual
+ render position on the output screen.
+
+ (Could be used for implementation of the Vi 'H' and 'L' key bindings as
+ well as implementing mouse support.)
+
+ :param ui_content: The original :class:`.UIContent` instance that contains
+ the whole input, without clipping. (ui_content)
+ :param horizontal_scroll: The horizontal scroll of the :class:`.Window` instance.
+ :param vertical_scroll: The vertical scroll of the :class:`.Window` instance.
+ :param window_width: The width of the window that displays the content,
+ without the margins.
+ :param window_height: The height of the window that displays the content.
+ :param configured_scroll_offsets: The scroll offsets as configured for the
+ :class:`Window` instance.
+ :param visible_line_to_row_col: Mapping that maps the row numbers on the
+ displayed screen (starting from zero for the first visible line) to
+ (row, col) tuples pointing to the row and column of the :class:`.UIContent`.
+ :param rowcol_to_yx: Mapping that maps (row, column) tuples representing
+ coordinates of the :class:`UIContent` to (y, x) absolute coordinates at
+ the rendered screen.
+ """
+ def __init__(self, ui_content, horizontal_scroll, vertical_scroll,
+ window_width, window_height,
+ configured_scroll_offsets,
+ visible_line_to_row_col, rowcol_to_yx,
+ x_offset, y_offset, wrap_lines):
+ assert isinstance(ui_content, UIContent)
+ assert isinstance(horizontal_scroll, int)
+ assert isinstance(vertical_scroll, int)
+ assert isinstance(window_width, int)
+ assert isinstance(window_height, int)
+ assert isinstance(configured_scroll_offsets, ScrollOffsets)
+ assert isinstance(visible_line_to_row_col, dict)
+ assert isinstance(rowcol_to_yx, dict)
+ assert isinstance(x_offset, int)
+ assert isinstance(y_offset, int)
+ assert isinstance(wrap_lines, bool)
+
+ self.ui_content = ui_content
+ self.vertical_scroll = vertical_scroll
+ self.window_width = window_width # Width without margins.
+ self.window_height = window_height
+
+ self.configured_scroll_offsets = configured_scroll_offsets
+ self.visible_line_to_row_col = visible_line_to_row_col
+ self.wrap_lines = wrap_lines
+
+ self._rowcol_to_yx = rowcol_to_yx # row/col from input to absolute y/x
+ # screen coordinates.
+ self._x_offset = x_offset
+ self._y_offset = y_offset
+
+ @property
+ def visible_line_to_input_line(self):
+ return dict(
+ (visible_line, rowcol[0])
+ for visible_line, rowcol in self.visible_line_to_row_col.items())
+
+ @property
+ def cursor_position(self):
+ """
+ Return the cursor position coordinates, relative to the left/top corner
+ of the rendered screen.
+ """
+ cpos = self.ui_content.cursor_position
+ y, x = self._rowcol_to_yx[cpos.y, cpos.x]
+ return Point(x=x - self._x_offset, y=y - self._y_offset)
+
+ @property
+ def applied_scroll_offsets(self):
+ """
+ Return a :class:`.ScrollOffsets` instance that indicates the actual
+ offset. This can be less than or equal to what's configured. E.g, when
+ the cursor is completely at the top, the top offset will be zero rather
+ than what's configured.
+ """
+ if self.displayed_lines[0] == 0:
+ top = 0
+ else:
+ # Get row where the cursor is displayed.
+ y = self.input_line_to_visible_line[self.ui_content.cursor_position.y]
+ top = min(y, self.configured_scroll_offsets.top)
+
+ return ScrollOffsets(
+ top=top,
+ bottom=min(self.ui_content.line_count - self.displayed_lines[-1] - 1,
+ self.configured_scroll_offsets.bottom),
+
+ # For left/right, it probably doesn't make sense to return something.
+ # (We would have to calculate the widths of all the lines and keep
+ # double width characters in mind.)
+ left=0, right=0)
+
+ @property
+ def displayed_lines(self):
+ """
+ List of all the visible rows. (Line numbers of the input buffer.)
+ The last line may not be entirely visible.
+ """
+ return sorted(row for row, col in self.visible_line_to_row_col.values())
+
+ @property
+ def input_line_to_visible_line(self):
+ """
+ Return the dictionary mapping the line numbers of the input buffer to
+ the lines of the screen. When a line spans several rows at the screen,
+ the first row appears in the dictionary.
+ """
+ result = {}
+ for k, v in self.visible_line_to_input_line.items():
+ if v in result:
+ result[v] = min(result[v], k)
+ else:
+ result[v] = k
+ return result
+
+ def first_visible_line(self, after_scroll_offset=False):
+ """
+ Return the line number (0 based) of the input document that corresponds
+ with the first visible line.
+ """
+ if after_scroll_offset:
+ return self.displayed_lines[self.applied_scroll_offsets.top]
+ else:
+ return self.displayed_lines[0]
+
+ def last_visible_line(self, before_scroll_offset=False):
+ """
+ Like `first_visible_line`, but for the last visible line.
+ """
+ if before_scroll_offset:
+ return self.displayed_lines[-1 - self.applied_scroll_offsets.bottom]
+ else:
+ return self.displayed_lines[-1]
+
+ def center_visible_line(self, before_scroll_offset=False,
+ after_scroll_offset=False):
+ """
+ Like `first_visible_line`, but for the center visible line.
+ """
+ return (self.first_visible_line(after_scroll_offset) +
+ (self.last_visible_line(before_scroll_offset) -
+ self.first_visible_line(after_scroll_offset)) // 2
+ )
+
+ @property
+ def content_height(self):
+ """
+ The full height of the user control.
+ """
+ return self.ui_content.line_count
+
+ @property
+ def full_height_visible(self):
+ """
+ True when the full height is visible (There is no vertical scroll.)
+ """
+ return self.vertical_scroll == 0 and self.last_visible_line() == self.content_height
+
+ @property
+ def top_visible(self):
+ """
+ True when the top of the buffer is visible.
+ """
+ return self.vertical_scroll == 0
+
+ @property
+ def bottom_visible(self):
+ """
+ True when the bottom of the buffer is visible.
+ """
+ return self.last_visible_line() == self.content_height - 1
+
+ @property
+ def vertical_scroll_percentage(self):
+ """
+ Vertical scroll as a percentage. (0 means: the top is visible,
+ 100 means: the bottom is visible.)
+ """
+ if self.bottom_visible:
+ return 100
+ else:
+ return (100 * self.vertical_scroll // self.content_height)
+
+ def get_height_for_line(self, lineno):
+ """
+ Return the height of the given line.
+ (The height that it would take, if this line became visible.)
+ """
+ if self.wrap_lines:
+ return self.ui_content.get_height_for_line(lineno, self.window_width)
+ else:
+ return 1
+
+
+class ScrollOffsets(object):
+ """
+ Scroll offsets for the :class:`.Window` class.
+
+ Note that left/right offsets only make sense if line wrapping is disabled.
+ """
+ def __init__(self, top=0, bottom=0, left=0, right=0):
+ assert isinstance(top, Integer)
+ assert isinstance(bottom, Integer)
+ assert isinstance(left, Integer)
+ assert isinstance(right, Integer)
+
+ self._top = top
+ self._bottom = bottom
+ self._left = left
+ self._right = right
+
+ @property
+ def top(self):
+ return int(self._top)
+
+ @property
+ def bottom(self):
+ return int(self._bottom)
+
+ @property
+ def left(self):
+ return int(self._left)
+
+ @property
+ def right(self):
+ return int(self._right)
+
+ def __repr__(self):
+ return 'ScrollOffsets(top=%r, bottom=%r, left=%r, right=%r)' % (
+ self.top, self.bottom, self.left, self.right)
+
+
+class ColorColumn(object):
+ def __init__(self, position, token=Token.ColorColumn):
+ self.position = position
+ self.token = token
+
+
+_in_insert_mode = ViInsertMode() | EmacsInsertMode()
+
+
+class Window(Container):
+ """
+ Container that holds a control.
+
+ :param content: :class:`~prompt_toolkit.layout.controls.UIControl` instance.
+ :param width: :class:`~prompt_toolkit.layout.dimension.LayoutDimension` instance.
+ :param height: :class:`~prompt_toolkit.layout.dimension.LayoutDimension` instance.
+ :param get_width: callable which takes a `CommandLineInterface` and returns a `LayoutDimension`.
+ :param get_height: callable which takes a `CommandLineInterface` and returns a `LayoutDimension`.
+ :param dont_extend_width: When `True`, don't take up more width then the
+ preferred width reported by the control.
+ :param dont_extend_height: When `True`, don't take up more width then the
+ preferred height reported by the control.
+ :param left_margins: A list of :class:`~prompt_toolkit.layout.margins.Margin`
+ instance to be displayed on the left. For instance:
+ :class:`~prompt_toolkit.layout.margins.NumberredMargin` can be one of
+ them in order to show line numbers.
+ :param right_margins: Like `left_margins`, but on the other side.
+ :param scroll_offsets: :class:`.ScrollOffsets` instance, representing the
+ preferred amount of lines/columns to be always visible before/after the
+ cursor. When both top and bottom are a very high number, the cursor
+ will be centered vertically most of the time.
+ :param allow_scroll_beyond_bottom: A `bool` or
+ :class:`~prompt_toolkit.filters.CLIFilter` instance. When True, allow
+ scrolling so far, that the top part of the content is not visible
+ anymore, while there is still empty space available at the bottom of
+ the window. In the Vi editor for instance, this is possible. You will
+ see tildes while the top part of the body is hidden.
+ :param wrap_lines: A `bool` or :class:`~prompt_toolkit.filters.CLIFilter`
+ instance. When True, don't scroll horizontally, but wrap lines instead.
+ :param get_vertical_scroll: Callable that takes this window
+ instance as input and returns a preferred vertical scroll.
+ (When this is `None`, the scroll is only determined by the last and
+ current cursor position.)
+ :param get_horizontal_scroll: Callable that takes this window
+ instance as input and returns a preferred vertical scroll.
+ :param always_hide_cursor: A `bool` or
+ :class:`~prompt_toolkit.filters.CLIFilter` instance. When True, never
+ display the cursor, even when the user control specifies a cursor
+ position.
+ :param cursorline: A `bool` or :class:`~prompt_toolkit.filters.CLIFilter`
+ instance. When True, display a cursorline.
+ :param cursorcolumn: A `bool` or :class:`~prompt_toolkit.filters.CLIFilter`
+ instance. When True, display a cursorcolumn.
+ :param get_colorcolumns: A callable that takes a `CommandLineInterface` and
+ returns a a list of :class:`.ColorColumn` instances that describe the
+ columns to be highlighted.
+ :param cursorline_token: The token to be used for highlighting the current line,
+ if `cursorline` is True.
+ :param cursorcolumn_token: The token to be used for highlighting the current line,
+ if `cursorcolumn` is True.
+ """
+ def __init__(self, content, width=None, height=None, get_width=None,
+ get_height=None, dont_extend_width=False, dont_extend_height=False,
+ left_margins=None, right_margins=None, scroll_offsets=None,
+ allow_scroll_beyond_bottom=False, wrap_lines=False,
+ get_vertical_scroll=None, get_horizontal_scroll=None, always_hide_cursor=False,
+ cursorline=False, cursorcolumn=False, get_colorcolumns=None,
+ cursorline_token=Token.CursorLine, cursorcolumn_token=Token.CursorColumn):
+ assert isinstance(content, UIControl)
+ assert width is None or isinstance(width, LayoutDimension)
+ assert height is None or isinstance(height, LayoutDimension)
+ assert get_width is None or callable(get_width)
+ assert get_height is None or callable(get_height)
+ assert width is None or get_width is None
+ assert height is None or get_height is None
+ assert scroll_offsets is None or isinstance(scroll_offsets, ScrollOffsets)
+ assert left_margins is None or all(isinstance(m, Margin) for m in left_margins)
+ assert right_margins is None or all(isinstance(m, Margin) for m in right_margins)
+ assert get_vertical_scroll is None or callable(get_vertical_scroll)
+ assert get_horizontal_scroll is None or callable(get_horizontal_scroll)
+ assert get_colorcolumns is None or callable(get_colorcolumns)
+
+ self.allow_scroll_beyond_bottom = to_cli_filter(allow_scroll_beyond_bottom)
+ self.always_hide_cursor = to_cli_filter(always_hide_cursor)
+ self.wrap_lines = to_cli_filter(wrap_lines)
+ self.cursorline = to_cli_filter(cursorline)
+ self.cursorcolumn = to_cli_filter(cursorcolumn)
+
+ self.content = content
+ self.dont_extend_width = dont_extend_width
+ self.dont_extend_height = dont_extend_height
+ self.left_margins = left_margins or []
+ self.right_margins = right_margins or []
+ self.scroll_offsets = scroll_offsets or ScrollOffsets()
+ self.get_vertical_scroll = get_vertical_scroll
+ self.get_horizontal_scroll = get_horizontal_scroll
+ self._width = get_width or (lambda cli: width)
+ self._height = get_height or (lambda cli: height)
+ self.get_colorcolumns = get_colorcolumns or (lambda cli: [])
+ self.cursorline_token = cursorline_token
+ self.cursorcolumn_token = cursorcolumn_token
+
+ # Cache for the screens generated by the margin.
+ self._ui_content_cache = SimpleCache(maxsize=8)
+ self._margin_width_cache = SimpleCache(maxsize=1)
+
+ self.reset()
+
+ def __repr__(self):
+ return 'Window(content=%r)' % self.content
+
+ def reset(self):
+ self.content.reset()
+
+ #: Scrolling position of the main content.
+ self.vertical_scroll = 0
+ self.horizontal_scroll = 0
+
+ # Vertical scroll 2: this is the vertical offset that a line is
+ # scrolled if a single line (the one that contains the cursor) consumes
+ # all of the vertical space.
+ self.vertical_scroll_2 = 0
+
+ #: Keep render information (mappings between buffer input and render
+ #: output.)
+ self.render_info = None
+
+ def _get_margin_width(self, cli, margin):
+ """
+ Return the width for this margin.
+ (Calculate only once per render time.)
+ """
+ # Margin.get_width, needs to have a UIContent instance.
+ def get_ui_content():
+ return self._get_ui_content(cli, width=0, height=0)
+
+ def get_width():
+ return margin.get_width(cli, get_ui_content)
+
+ key = (margin, cli.render_counter)
+ return self._margin_width_cache.get(key, get_width)
+
+ def preferred_width(self, cli, max_available_width):
+ # Calculate the width of the margin.
+ total_margin_width = sum(self._get_margin_width(cli, m) for m in
+ self.left_margins + self.right_margins)
+
+ # Window of the content. (Can be `None`.)
+ preferred_width = self.content.preferred_width(
+ cli, max_available_width - total_margin_width)
+
+ if preferred_width is not None:
+ # Include width of the margins.
+ preferred_width += total_margin_width
+
+ # Merge.
+ return self._merge_dimensions(
+ dimension=self._width(cli),
+ preferred=preferred_width,
+ dont_extend=self.dont_extend_width)
+
+ def preferred_height(self, cli, width, max_available_height):
+ total_margin_width = sum(self._get_margin_width(cli, m) for m in
+ self.left_margins + self.right_margins)
+ wrap_lines = self.wrap_lines(cli)
+
+ return self._merge_dimensions(
+ dimension=self._height(cli),
+ preferred=self.content.preferred_height(
+ cli, width - total_margin_width, max_available_height, wrap_lines),
+ dont_extend=self.dont_extend_height)
+
+ @staticmethod
+ def _merge_dimensions(dimension, preferred=None, dont_extend=False):
+ """
+ Take the LayoutDimension from this `Window` class and the received
+ preferred size from the `UIControl` and return a `LayoutDimension` to
+ report to the parent container.
+ """
+ dimension = dimension or LayoutDimension()
+
+ # When a preferred dimension was explicitly given to the Window,
+ # ignore the UIControl.
+ if dimension.preferred_specified:
+ preferred = dimension.preferred
+
+ # When a 'preferred' dimension is given by the UIControl, make sure
+ # that it stays within the bounds of the Window.
+ if preferred is not None:
+ if dimension.max:
+ preferred = min(preferred, dimension.max)
+
+ if dimension.min:
+ preferred = max(preferred, dimension.min)
+
+ # When a `dont_extend` flag has been given, use the preferred dimension
+ # also as the max dimension.
+ if dont_extend and preferred is not None:
+ max_ = min(dimension.max, preferred)
+ else:
+ max_ = dimension.max
+
+ return LayoutDimension(
+ min=dimension.min, max=max_,
+ preferred=preferred, weight=dimension.weight)
+
+ def _get_ui_content(self, cli, width, height):
+ """
+ Create a `UIContent` instance.
+ """
+ def get_content():
+ return self.content.create_content(cli, width=width, height=height)
+
+ key = (cli.render_counter, width, height)
+ return self._ui_content_cache.get(key, get_content)
+
+ def _get_digraph_char(self, cli):
+ " Return `False`, or the Digraph symbol to be used. "
+ if cli.quoted_insert:
+ return '^'
+ if cli.vi_state.waiting_for_digraph:
+ if cli.vi_state.digraph_symbol1:
+ return cli.vi_state.digraph_symbol1
+ return '?'
+ return False
+
+ def write_to_screen(self, cli, screen, mouse_handlers, write_position):
+ """
+ Write window to screen. This renders the user control, the margins and
+ copies everything over to the absolute position at the given screen.
+ """
+ # Calculate margin sizes.
+ left_margin_widths = [self._get_margin_width(cli, m) for m in self.left_margins]
+ right_margin_widths = [self._get_margin_width(cli, m) for m in self.right_margins]
+ total_margin_width = sum(left_margin_widths + right_margin_widths)
+
+ # Render UserControl.
+ ui_content = self.content.create_content(
+ cli, write_position.width - total_margin_width, write_position.height)
+ assert isinstance(ui_content, UIContent)
+
+ # Scroll content.
+ wrap_lines = self.wrap_lines(cli)
+ scroll_func = self._scroll_when_linewrapping if wrap_lines else self._scroll_without_linewrapping
+
+ scroll_func(
+ ui_content, write_position.width - total_margin_width, write_position.height, cli)
+
+ # Write body
+ visible_line_to_row_col, rowcol_to_yx = self._copy_body(
+ cli, ui_content, screen, write_position,
+ sum(left_margin_widths), write_position.width - total_margin_width,
+ self.vertical_scroll, self.horizontal_scroll,
+ has_focus=self.content.has_focus(cli),
+ wrap_lines=wrap_lines, highlight_lines=True,
+ vertical_scroll_2=self.vertical_scroll_2,
+ always_hide_cursor=self.always_hide_cursor(cli))
+
+ # Remember render info. (Set before generating the margins. They need this.)
+ x_offset=write_position.xpos + sum(left_margin_widths)
+ y_offset=write_position.ypos
+
+ self.render_info = WindowRenderInfo(
+ ui_content=ui_content,
+ horizontal_scroll=self.horizontal_scroll,
+ vertical_scroll=self.vertical_scroll,
+ window_width=write_position.width - total_margin_width,
+ window_height=write_position.height,
+ configured_scroll_offsets=self.scroll_offsets,
+ visible_line_to_row_col=visible_line_to_row_col,
+ rowcol_to_yx=rowcol_to_yx,
+ x_offset=x_offset,
+ y_offset=y_offset,
+ wrap_lines=wrap_lines)
+
+ # Set mouse handlers.
+ def mouse_handler(cli, mouse_event):
+ """ Wrapper around the mouse_handler of the `UIControl` that turns
+ screen coordinates into line coordinates. """
+ # Find row/col position first.
+ yx_to_rowcol = dict((v, k) for k, v in rowcol_to_yx.items())
+ y = mouse_event.position.y
+ x = mouse_event.position.x
+
+ # If clicked below the content area, look for a position in the
+ # last line instead.
+ max_y = write_position.ypos + len(visible_line_to_row_col) - 1
+ y = min(max_y, y)
+
+ while x >= 0:
+ try:
+ row, col = yx_to_rowcol[y, x]
+ except KeyError:
+ # Try again. (When clicking on the right side of double
+ # width characters, or on the right side of the input.)
+ x -= 1
+ else:
+ # Found position, call handler of UIControl.
+ result = self.content.mouse_handler(
+ cli, MouseEvent(position=Point(x=col, y=row),
+ event_type=mouse_event.event_type))
+ break
+ else:
+ # nobreak.
+ # (No x/y coordinate found for the content. This happens in
+ # case of a FillControl, that only specifies a background, but
+ # doesn't have a content. Report (0,0) instead.)
+ result = self.content.mouse_handler(
+ cli, MouseEvent(position=Point(x=0, y=0),
+ event_type=mouse_event.event_type))
+
+ # If it returns NotImplemented, handle it here.
+ if result == NotImplemented:
+ return self._mouse_handler(cli, mouse_event)
+
+ return result
+
+ mouse_handlers.set_mouse_handler_for_range(
+ x_min=write_position.xpos + sum(left_margin_widths),
+ x_max=write_position.xpos + write_position.width - total_margin_width,
+ y_min=write_position.ypos,
+ y_max=write_position.ypos + write_position.height,
+ handler=mouse_handler)
+
+ # Render and copy margins.
+ move_x = 0
+
+ def render_margin(m, width):
+ " Render margin. Return `Screen`. "
+ # Retrieve margin tokens.
+ tokens = m.create_margin(cli, self.render_info, width, write_position.height)
+
+ # Turn it into a UIContent object.
+ # already rendered those tokens using this size.)
+ return TokenListControl.static(tokens).create_content(
+ cli, width + 1, write_position.height)
+
+ for m, width in zip(self.left_margins, left_margin_widths):
+ # Create screen for margin.
+ margin_screen = render_margin(m, width)
+
+ # Copy and shift X.
+ self._copy_margin(cli, margin_screen, screen, write_position, move_x, width)
+ move_x += width
+
+ move_x = write_position.width - sum(right_margin_widths)
+
+ for m, width in zip(self.right_margins, right_margin_widths):
+ # Create screen for margin.
+ margin_screen = render_margin(m, width)
+
+ # Copy and shift X.
+ self._copy_margin(cli, margin_screen, screen, write_position, move_x, width)
+ move_x += width
+
+ def _copy_body(self, cli, ui_content, new_screen, write_position, move_x,
+ width, vertical_scroll=0, horizontal_scroll=0,
+ has_focus=False, wrap_lines=False, highlight_lines=False,
+ vertical_scroll_2=0, always_hide_cursor=False):
+ """
+ Copy the UIContent into the output screen.
+ """
+ xpos = write_position.xpos + move_x
+ ypos = write_position.ypos
+ line_count = ui_content.line_count
+ new_buffer = new_screen.data_buffer
+ empty_char = _CHAR_CACHE['', Token]
+ ZeroWidthEscape = Token.ZeroWidthEscape
+
+ # Map visible line number to (row, col) of input.
+ # 'col' will always be zero if line wrapping is off.
+ visible_line_to_row_col = {}
+ rowcol_to_yx = {} # Maps (row, col) from the input to (y, x) screen coordinates.
+
+ # Fill background with default_char first.
+ default_char = ui_content.default_char
+
+ if default_char:
+ for y in range(ypos, ypos + write_position.height):
+ new_buffer_row = new_buffer[y]
+ for x in range(xpos, xpos + width):
+ new_buffer_row[x] = default_char
+
+ # Copy content.
+ def copy():
+ y = - vertical_scroll_2
+ lineno = vertical_scroll
+
+ while y < write_position.height and lineno < line_count:
+ # Take the next line and copy it in the real screen.
+ line = ui_content.get_line(lineno)
+
+ col = 0
+ x = -horizontal_scroll
+
+ visible_line_to_row_col[y] = (lineno, horizontal_scroll)
+ new_buffer_row = new_buffer[y + ypos]
+
+ for token, text in line:
+ # Remember raw VT escape sequences. (E.g. FinalTerm's
+ # escape sequences.)
+ if token == ZeroWidthEscape:
+ new_screen.zero_width_escapes[y + ypos][x + xpos] += text
+ continue
+
+ for c in text:
+ char = _CHAR_CACHE[c, token]
+ char_width = char.width
+
+ # Wrap when the line width is exceeded.
+ if wrap_lines and x + char_width > width:
+ visible_line_to_row_col[y + 1] = (
+ lineno, visible_line_to_row_col[y][1] + x)
+ y += 1
+ x = -horizontal_scroll # This would be equal to zero.
+ # (horizontal_scroll=0 when wrap_lines.)
+ new_buffer_row = new_buffer[y + ypos]
+
+ if y >= write_position.height:
+ return y # Break out of all for loops.
+
+ # Set character in screen and shift 'x'.
+ if x >= 0 and y >= 0 and x < write_position.width:
+ new_buffer_row[x + xpos] = char
+
+ # When we print a multi width character, make sure
+ # to erase the neighbous positions in the screen.
+ # (The empty string if different from everything,
+ # so next redraw this cell will repaint anyway.)
+ if char_width > 1:
+ for i in range(1, char_width):
+ new_buffer_row[x + xpos + i] = empty_char
+
+ # If this is a zero width characters, then it's
+ # probably part of a decomposed unicode character.
+ # See: https://en.wikipedia.org/wiki/Unicode_equivalence
+ # Merge it in the previous cell.
+ elif char_width == 0 and x - 1 >= 0:
+ prev_char = new_buffer_row[x + xpos - 1]
+ char2 = _CHAR_CACHE[prev_char.char + c, prev_char.token]
+ new_buffer_row[x + xpos - 1] = char2
+
+ # Keep track of write position for each character.
+ rowcol_to_yx[lineno, col] = (y + ypos, x + xpos)
+
+ col += 1
+ x += char_width
+
+ lineno += 1
+ y += 1
+ return y
+
+ y = copy()
+
+ def cursor_pos_to_screen_pos(row, col):
+ " Translate row/col from UIContent to real Screen coordinates. "
+ try:
+ y, x = rowcol_to_yx[row, col]
+ except KeyError:
+ # Normally this should never happen. (It is a bug, if it happens.)
+ # But to be sure, return (0, 0)
+ return Point(y=0, x=0)
+
+ # raise ValueError(
+ # 'Invalid position. row=%r col=%r, vertical_scroll=%r, '
+ # 'horizontal_scroll=%r, height=%r' %
+ # (row, col, vertical_scroll, horizontal_scroll, write_position.height))
+ else:
+ return Point(y=y, x=x)
+
+ # Set cursor and menu positions.
+ if ui_content.cursor_position:
+ screen_cursor_position = cursor_pos_to_screen_pos(
+ ui_content.cursor_position.y, ui_content.cursor_position.x)
+
+ if has_focus:
+ new_screen.cursor_position = screen_cursor_position
+
+ if always_hide_cursor:
+ new_screen.show_cursor = False
+ else:
+ new_screen.show_cursor = ui_content.show_cursor
+
+ self._highlight_digraph(cli, new_screen)
+
+ if highlight_lines:
+ self._highlight_cursorlines(
+ cli, new_screen, screen_cursor_position, xpos, ypos, width,
+ write_position.height)
+
+ # Draw input characters from the input processor queue.
+ if has_focus and ui_content.cursor_position:
+ self._show_input_processor_key_buffer(cli, new_screen)
+
+ # Set menu position.
+ if not new_screen.menu_position and ui_content.menu_position:
+ new_screen.menu_position = cursor_pos_to_screen_pos(
+ ui_content.menu_position.y, ui_content.menu_position.x)
+
+ # Update output screne height.
+ new_screen.height = max(new_screen.height, ypos + write_position.height)
+
+ return visible_line_to_row_col, rowcol_to_yx
+
+ def _highlight_digraph(self, cli, new_screen):
+ """
+ When we are in Vi digraph mode, put a question mark underneath the
+ cursor.
+ """
+ digraph_char = self._get_digraph_char(cli)
+ if digraph_char:
+ cpos = new_screen.cursor_position
+ new_screen.data_buffer[cpos.y][cpos.x] = \
+ _CHAR_CACHE[digraph_char, Token.Digraph]
+
+ def _show_input_processor_key_buffer(self, cli, new_screen):
+ """
+ When the user is typing a key binding that consists of several keys,
+ display the last pressed key if the user is in insert mode and the key
+ is meaningful to be displayed.
+ E.g. Some people want to bind 'jj' to escape in Vi insert mode. But the
+ first 'j' needs to be displayed in order to get some feedback.
+ """
+ key_buffer = cli.input_processor.key_buffer
+
+ if key_buffer and _in_insert_mode(cli) and not cli.is_done:
+ # The textual data for the given key. (Can be a VT100 escape
+ # sequence.)
+ data = key_buffer[-1].data
+
+ # Display only if this is a 1 cell width character.
+ if get_cwidth(data) == 1:
+ cpos = new_screen.cursor_position
+ new_screen.data_buffer[cpos.y][cpos.x] = \
+ _CHAR_CACHE[data, Token.PartialKeyBinding]
+
+ def _highlight_cursorlines(self, cli, new_screen, cpos, x, y, width, height):
+ """
+ Highlight cursor row/column.
+ """
+ cursor_line_token = (':', ) + self.cursorline_token
+ cursor_column_token = (':', ) + self.cursorcolumn_token
+
+ data_buffer = new_screen.data_buffer
+
+ # Highlight cursor line.
+ if self.cursorline(cli):
+ row = data_buffer[cpos.y]
+ for x in range(x, x + width):
+ original_char = row[x]
+ row[x] = _CHAR_CACHE[
+ original_char.char, original_char.token + cursor_line_token]
+
+ # Highlight cursor column.
+ if self.cursorcolumn(cli):
+ for y2 in range(y, y + height):
+ row = data_buffer[y2]
+ original_char = row[cpos.x]
+ row[cpos.x] = _CHAR_CACHE[
+ original_char.char, original_char.token + cursor_column_token]
+
+ # Highlight color columns
+ for cc in self.get_colorcolumns(cli):
+ assert isinstance(cc, ColorColumn)
+ color_column_token = (':', ) + cc.token
+ column = cc.position
+
+ for y2 in range(y, y + height):
+ row = data_buffer[y2]
+ original_char = row[column]
+ row[column] = _CHAR_CACHE[
+ original_char.char, original_char.token + color_column_token]
+
+ def _copy_margin(self, cli, lazy_screen, new_screen, write_position, move_x, width):
+ """
+ Copy characters from the margin screen to the real screen.
+ """
+ xpos = write_position.xpos + move_x
+ ypos = write_position.ypos
+
+ margin_write_position = WritePosition(xpos, ypos, width, write_position.height)
+ self._copy_body(cli, lazy_screen, new_screen, margin_write_position, 0, width)
+
+ def _scroll_when_linewrapping(self, ui_content, width, height, cli):
+ """
+ Scroll to make sure the cursor position is visible and that we maintain
+ the requested scroll offset.
+
+ Set `self.horizontal_scroll/vertical_scroll`.
+ """
+ scroll_offsets_bottom = self.scroll_offsets.bottom
+ scroll_offsets_top = self.scroll_offsets.top
+
+ # We don't have horizontal scrolling.
+ self.horizontal_scroll = 0
+
+ # If the current line consumes more than the whole window height,
+ # then we have to scroll vertically inside this line. (We don't take
+ # the scroll offsets into account for this.)
+ # Also, ignore the scroll offsets in this case. Just set the vertical
+ # scroll to this line.
+ if ui_content.get_height_for_line(ui_content.cursor_position.y, width) > height - scroll_offsets_top:
+ # Calculate the height of the text before the cursor, with the line
+ # containing the cursor included, and the character belowe the
+ # cursor included as well.
+ line = explode_tokens(ui_content.get_line(ui_content.cursor_position.y))
+ text_before_cursor = token_list_to_text(line[:ui_content.cursor_position.x + 1])
+ text_before_height = UIContent.get_height_for_text(text_before_cursor, width)
+
+ # Adjust scroll offset.
+ self.vertical_scroll = ui_content.cursor_position.y
+ self.vertical_scroll_2 = min(text_before_height - 1, self.vertical_scroll_2)
+ self.vertical_scroll_2 = max(0, text_before_height - height, self.vertical_scroll_2)
+ return
+ else:
+ self.vertical_scroll_2 = 0
+
+ # Current line doesn't consume the whole height. Take scroll offsets into account.
+ def get_min_vertical_scroll():
+ # Make sure that the cursor line is not below the bottom.
+ # (Calculate how many lines can be shown between the cursor and the .)
+ used_height = 0
+ prev_lineno = ui_content.cursor_position.y
+
+ for lineno in range(ui_content.cursor_position.y, -1, -1):
+ used_height += ui_content.get_height_for_line(lineno, width)
+
+ if used_height > height - scroll_offsets_bottom:
+ return prev_lineno
+ else:
+ prev_lineno = lineno
+ return 0
+
+ def get_max_vertical_scroll():
+ # Make sure that the cursor line is not above the top.
+ prev_lineno = ui_content.cursor_position.y
+ used_height = 0
+
+ for lineno in range(ui_content.cursor_position.y - 1, -1, -1):
+ used_height += ui_content.get_height_for_line(lineno, width)
+
+ if used_height > scroll_offsets_top:
+ return prev_lineno
+ else:
+ prev_lineno = lineno
+ return prev_lineno
+
+ def get_topmost_visible():
+ """
+ Calculate the upper most line that can be visible, while the bottom
+ is still visible. We should not allow scroll more than this if
+ `allow_scroll_beyond_bottom` is false.
+ """
+ prev_lineno = ui_content.line_count - 1
+ used_height = 0
+ for lineno in range(ui_content.line_count - 1, -1, -1):
+ used_height += ui_content.get_height_for_line(lineno, width)
+ if used_height > height:
+ return prev_lineno
+ else:
+ prev_lineno = lineno
+ return prev_lineno
+
+ # Scroll vertically. (Make sure that the whole line which contains the
+ # cursor is visible.
+ topmost_visible = get_topmost_visible()
+
+ # Note: the `min(topmost_visible, ...)` is to make sure that we
+ # don't require scrolling up because of the bottom scroll offset,
+ # when we are at the end of the document.
+ self.vertical_scroll = max(self.vertical_scroll, min(topmost_visible, get_min_vertical_scroll()))
+ self.vertical_scroll = min(self.vertical_scroll, get_max_vertical_scroll())
+
+ # Disallow scrolling beyond bottom?
+ if not self.allow_scroll_beyond_bottom(cli):
+ self.vertical_scroll = min(self.vertical_scroll, topmost_visible)
+
+ def _scroll_without_linewrapping(self, ui_content, width, height, cli):
+ """
+ Scroll to make sure the cursor position is visible and that we maintain
+ the requested scroll offset.
+
+ Set `self.horizontal_scroll/vertical_scroll`.
+ """
+ cursor_position = ui_content.cursor_position or Point(0, 0)
+
+ # Without line wrapping, we will never have to scroll vertically inside
+ # a single line.
+ self.vertical_scroll_2 = 0
+
+ if ui_content.line_count == 0:
+ self.vertical_scroll = 0
+ self.horizontal_scroll = 0
+ return
+ else:
+ current_line_text = token_list_to_text(ui_content.get_line(cursor_position.y))
+
+ def do_scroll(current_scroll, scroll_offset_start, scroll_offset_end,
+ cursor_pos, window_size, content_size):
+ " Scrolling algorithm. Used for both horizontal and vertical scrolling. "
+ # Calculate the scroll offset to apply.
+ # This can obviously never be more than have the screen size. Also, when the
+ # cursor appears at the top or bottom, we don't apply the offset.
+ scroll_offset_start = int(min(scroll_offset_start, window_size / 2, cursor_pos))
+ scroll_offset_end = int(min(scroll_offset_end, window_size / 2,
+ content_size - 1 - cursor_pos))
+
+ # Prevent negative scroll offsets.
+ if current_scroll < 0:
+ current_scroll = 0
+
+ # Scroll back if we scrolled to much and there's still space to show more of the document.
+ if (not self.allow_scroll_beyond_bottom(cli) and
+ current_scroll > content_size - window_size):
+ current_scroll = max(0, content_size - window_size)
+
+ # Scroll up if cursor is before visible part.
+ if current_scroll > cursor_pos - scroll_offset_start:
+ current_scroll = max(0, cursor_pos - scroll_offset_start)
+
+ # Scroll down if cursor is after visible part.
+ if current_scroll < (cursor_pos + 1) - window_size + scroll_offset_end:
+ current_scroll = (cursor_pos + 1) - window_size + scroll_offset_end
+
+ return current_scroll
+
+ # When a preferred scroll is given, take that first into account.
+ if self.get_vertical_scroll:
+ self.vertical_scroll = self.get_vertical_scroll(self)
+ assert isinstance(self.vertical_scroll, int)
+ if self.get_horizontal_scroll:
+ self.horizontal_scroll = self.get_horizontal_scroll(self)
+ assert isinstance(self.horizontal_scroll, int)
+
+ # Update horizontal/vertical scroll to make sure that the cursor
+ # remains visible.
+ offsets = self.scroll_offsets
+
+ self.vertical_scroll = do_scroll(
+ current_scroll=self.vertical_scroll,
+ scroll_offset_start=offsets.top,
+ scroll_offset_end=offsets.bottom,
+ cursor_pos=ui_content.cursor_position.y,
+ window_size=height,
+ content_size=ui_content.line_count)
+
+ self.horizontal_scroll = do_scroll(
+ current_scroll=self.horizontal_scroll,
+ scroll_offset_start=offsets.left,
+ scroll_offset_end=offsets.right,
+ cursor_pos=get_cwidth(current_line_text[:ui_content.cursor_position.x]),
+ window_size=width,
+ # We can only analyse the current line. Calculating the width off
+ # all the lines is too expensive.
+ content_size=max(get_cwidth(current_line_text), self.horizontal_scroll + width))
+
+ def _mouse_handler(self, cli, mouse_event):
+ """
+ Mouse handler. Called when the UI control doesn't handle this
+ particular event.
+ """
+ if mouse_event.event_type == MouseEventType.SCROLL_DOWN:
+ self._scroll_down(cli)
+ elif mouse_event.event_type == MouseEventType.SCROLL_UP:
+ self._scroll_up(cli)
+
+ def _scroll_down(self, cli):
+ " Scroll window down. "
+ info = self.render_info
+
+ if self.vertical_scroll < info.content_height - info.window_height:
+ if info.cursor_position.y <= info.configured_scroll_offsets.top:
+ self.content.move_cursor_down(cli)
+
+ self.vertical_scroll += 1
+
+ def _scroll_up(self, cli):
+ " Scroll window up. "
+ info = self.render_info
+
+ if info.vertical_scroll > 0:
+ # TODO: not entirely correct yet in case of line wrapping and long lines.
+ if info.cursor_position.y >= info.window_height - 1 - info.configured_scroll_offsets.bottom:
+ self.content.move_cursor_up(cli)
+
+ self.vertical_scroll -= 1
+
+ def walk(self, cli):
+ # Only yield self. A window doesn't have children.
+ yield self
+
+
+class ConditionalContainer(Container):
+ """
+ Wrapper around any other container that can change the visibility. The
+ received `filter` determines whether the given container should be
+ displayed or not.
+
+ :param content: :class:`.Container` instance.
+ :param filter: :class:`~prompt_toolkit.filters.CLIFilter` instance.
+ """
+ def __init__(self, content, filter):
+ assert isinstance(content, Container)
+
+ self.content = content
+ self.filter = to_cli_filter(filter)
+
+ def __repr__(self):
+ return 'ConditionalContainer(%r, filter=%r)' % (self.content, self.filter)
+
+ def reset(self):
+ self.content.reset()
+
+ def preferred_width(self, cli, max_available_width):
+ if self.filter(cli):
+ return self.content.preferred_width(cli, max_available_width)
+ else:
+ return LayoutDimension.exact(0)
+
+ def preferred_height(self, cli, width, max_available_height):
+ if self.filter(cli):
+ return self.content.preferred_height(cli, width, max_available_height)
+ else:
+ return LayoutDimension.exact(0)
+
+ def write_to_screen(self, cli, screen, mouse_handlers, write_position):
+ if self.filter(cli):
+ return self.content.write_to_screen(cli, screen, mouse_handlers, write_position)
+
+ def walk(self, cli):
+ return self.content.walk(cli)
+
+
+# Deprecated alias for 'Container'.
+Layout = Container