123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665 |
- """
- 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
|