diff options
author | nkozlovskiy <nmk@ydb.tech> | 2023-09-29 12:24:06 +0300 |
---|---|---|
committer | nkozlovskiy <nmk@ydb.tech> | 2023-09-29 12:41:34 +0300 |
commit | e0e3e1717e3d33762ce61950504f9637a6e669ed (patch) | |
tree | bca3ff6939b10ed60c3d5c12439963a1146b9711 /contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/containers.py | |
parent | 38f2c5852db84c7b4d83adfcb009eb61541d1ccd (diff) | |
download | ydb-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.py | 1665 |
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 |