containers.py 66 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665
  1. """
  2. Container for the layout.
  3. (Containers can contain other containers or user interface controls.)
  4. """
  5. from __future__ import unicode_literals
  6. from abc import ABCMeta, abstractmethod
  7. from six import with_metaclass
  8. from six.moves import range
  9. from .controls import UIControl, TokenListControl, UIContent
  10. from .dimension import LayoutDimension, sum_layout_dimensions, max_layout_dimensions
  11. from .margins import Margin
  12. from .screen import Point, WritePosition, _CHAR_CACHE
  13. from .utils import token_list_to_text, explode_tokens
  14. from prompt_toolkit.cache import SimpleCache
  15. from prompt_toolkit.filters import to_cli_filter, ViInsertMode, EmacsInsertMode
  16. from prompt_toolkit.mouse_events import MouseEvent, MouseEventType
  17. from prompt_toolkit.reactive import Integer
  18. from prompt_toolkit.token import Token
  19. from prompt_toolkit.utils import take_using_weights, get_cwidth
  20. __all__ = (
  21. 'Container',
  22. 'HSplit',
  23. 'VSplit',
  24. 'FloatContainer',
  25. 'Float',
  26. 'Window',
  27. 'WindowRenderInfo',
  28. 'ConditionalContainer',
  29. 'ScrollOffsets',
  30. 'ColorColumn',
  31. )
  32. Transparent = Token.Transparent
  33. class Container(with_metaclass(ABCMeta, object)):
  34. """
  35. Base class for user interface layout.
  36. """
  37. @abstractmethod
  38. def reset(self):
  39. """
  40. Reset the state of this container and all the children.
  41. (E.g. reset scroll offsets, etc...)
  42. """
  43. @abstractmethod
  44. def preferred_width(self, cli, max_available_width):
  45. """
  46. Return a :class:`~prompt_toolkit.layout.dimension.LayoutDimension` that
  47. represents the desired width for this container.
  48. :param cli: :class:`~prompt_toolkit.interface.CommandLineInterface`.
  49. """
  50. @abstractmethod
  51. def preferred_height(self, cli, width, max_available_height):
  52. """
  53. Return a :class:`~prompt_toolkit.layout.dimension.LayoutDimension` that
  54. represents the desired height for this container.
  55. :param cli: :class:`~prompt_toolkit.interface.CommandLineInterface`.
  56. """
  57. @abstractmethod
  58. def write_to_screen(self, cli, screen, mouse_handlers, write_position):
  59. """
  60. Write the actual content to the screen.
  61. :param cli: :class:`~prompt_toolkit.interface.CommandLineInterface`.
  62. :param screen: :class:`~prompt_toolkit.layout.screen.Screen`
  63. :param mouse_handlers: :class:`~prompt_toolkit.layout.mouse_handlers.MouseHandlers`.
  64. """
  65. @abstractmethod
  66. def walk(self, cli):
  67. """
  68. Walk through all the layout nodes (and their children) and yield them.
  69. """
  70. def _window_too_small():
  71. " Create a `Window` that displays the 'Window too small' text. "
  72. return Window(TokenListControl.static(
  73. [(Token.WindowTooSmall, ' Window too small... ')]))
  74. class HSplit(Container):
  75. """
  76. Several layouts, one stacked above/under the other.
  77. :param children: List of child :class:`.Container` objects.
  78. :param window_too_small: A :class:`.Container` object that is displayed if
  79. there is not enough space for all the children. By default, this is a
  80. "Window too small" message.
  81. :param get_dimensions: (`None` or a callable that takes a
  82. `CommandLineInterface` and returns a list of `LayoutDimension`
  83. instances.) By default the dimensions are taken from the children and
  84. divided by the available space. However, when `get_dimensions` is specified,
  85. this is taken instead.
  86. :param report_dimensions_callback: When rendering, this function is called
  87. with the `CommandLineInterface` and the list of used dimensions. (As a
  88. list of integers.)
  89. """
  90. def __init__(self, children, window_too_small=None,
  91. get_dimensions=None, report_dimensions_callback=None):
  92. assert all(isinstance(c, Container) for c in children)
  93. assert window_too_small is None or isinstance(window_too_small, Container)
  94. assert get_dimensions is None or callable(get_dimensions)
  95. assert report_dimensions_callback is None or callable(report_dimensions_callback)
  96. self.children = children
  97. self.window_too_small = window_too_small or _window_too_small()
  98. self.get_dimensions = get_dimensions
  99. self.report_dimensions_callback = report_dimensions_callback
  100. def preferred_width(self, cli, max_available_width):
  101. if self.children:
  102. dimensions = [c.preferred_width(cli, max_available_width) for c in self.children]
  103. return max_layout_dimensions(dimensions)
  104. else:
  105. return LayoutDimension(0)
  106. def preferred_height(self, cli, width, max_available_height):
  107. dimensions = [c.preferred_height(cli, width, max_available_height) for c in self.children]
  108. return sum_layout_dimensions(dimensions)
  109. def reset(self):
  110. for c in self.children:
  111. c.reset()
  112. def write_to_screen(self, cli, screen, mouse_handlers, write_position):
  113. """
  114. Render the prompt to a `Screen` instance.
  115. :param screen: The :class:`~prompt_toolkit.layout.screen.Screen` class
  116. to which the output has to be written.
  117. """
  118. sizes = self._divide_heigths(cli, write_position)
  119. if self.report_dimensions_callback:
  120. self.report_dimensions_callback(cli, sizes)
  121. if sizes is None:
  122. self.window_too_small.write_to_screen(
  123. cli, screen, mouse_handlers, write_position)
  124. else:
  125. # Draw child panes.
  126. ypos = write_position.ypos
  127. xpos = write_position.xpos
  128. width = write_position.width
  129. for s, c in zip(sizes, self.children):
  130. c.write_to_screen(cli, screen, mouse_handlers, WritePosition(xpos, ypos, width, s))
  131. ypos += s
  132. def _divide_heigths(self, cli, write_position):
  133. """
  134. Return the heights for all rows.
  135. Or None when there is not enough space.
  136. """
  137. if not self.children:
  138. return []
  139. # Calculate heights.
  140. given_dimensions = self.get_dimensions(cli) if self.get_dimensions else None
  141. def get_dimension_for_child(c, index):
  142. if given_dimensions and given_dimensions[index] is not None:
  143. return given_dimensions[index]
  144. else:
  145. return c.preferred_height(cli, write_position.width, write_position.extended_height)
  146. dimensions = [get_dimension_for_child(c, index) for index, c in enumerate(self.children)]
  147. # Sum dimensions
  148. sum_dimensions = sum_layout_dimensions(dimensions)
  149. # If there is not enough space for both.
  150. # Don't do anything.
  151. if sum_dimensions.min > write_position.extended_height:
  152. return
  153. # Find optimal sizes. (Start with minimal size, increase until we cover
  154. # the whole height.)
  155. sizes = [d.min for d in dimensions]
  156. child_generator = take_using_weights(
  157. items=list(range(len(dimensions))),
  158. weights=[d.weight for d in dimensions])
  159. i = next(child_generator)
  160. while sum(sizes) < min(write_position.extended_height, sum_dimensions.preferred):
  161. # Increase until we meet at least the 'preferred' size.
  162. if sizes[i] < dimensions[i].preferred:
  163. sizes[i] += 1
  164. i = next(child_generator)
  165. if not any([cli.is_returning, cli.is_exiting, cli.is_aborting]):
  166. while sum(sizes) < min(write_position.height, sum_dimensions.max):
  167. # Increase until we use all the available space. (or until "max")
  168. if sizes[i] < dimensions[i].max:
  169. sizes[i] += 1
  170. i = next(child_generator)
  171. return sizes
  172. def walk(self, cli):
  173. """ Walk through children. """
  174. yield self
  175. for c in self.children:
  176. for i in c.walk(cli):
  177. yield i
  178. class VSplit(Container):
  179. """
  180. Several layouts, one stacked left/right of the other.
  181. :param children: List of child :class:`.Container` objects.
  182. :param window_too_small: A :class:`.Container` object that is displayed if
  183. there is not enough space for all the children. By default, this is a
  184. "Window too small" message.
  185. :param get_dimensions: (`None` or a callable that takes a
  186. `CommandLineInterface` and returns a list of `LayoutDimension`
  187. instances.) By default the dimensions are taken from the children and
  188. divided by the available space. However, when `get_dimensions` is specified,
  189. this is taken instead.
  190. :param report_dimensions_callback: When rendering, this function is called
  191. with the `CommandLineInterface` and the list of used dimensions. (As a
  192. list of integers.)
  193. """
  194. def __init__(self, children, window_too_small=None,
  195. get_dimensions=None, report_dimensions_callback=None):
  196. assert all(isinstance(c, Container) for c in children)
  197. assert window_too_small is None or isinstance(window_too_small, Container)
  198. assert get_dimensions is None or callable(get_dimensions)
  199. assert report_dimensions_callback is None or callable(report_dimensions_callback)
  200. self.children = children
  201. self.window_too_small = window_too_small or _window_too_small()
  202. self.get_dimensions = get_dimensions
  203. self.report_dimensions_callback = report_dimensions_callback
  204. def preferred_width(self, cli, max_available_width):
  205. dimensions = [c.preferred_width(cli, max_available_width) for c in self.children]
  206. return sum_layout_dimensions(dimensions)
  207. def preferred_height(self, cli, width, max_available_height):
  208. sizes = self._divide_widths(cli, width)
  209. if sizes is None:
  210. return LayoutDimension()
  211. else:
  212. dimensions = [c.preferred_height(cli, s, max_available_height)
  213. for s, c in zip(sizes, self.children)]
  214. return max_layout_dimensions(dimensions)
  215. def reset(self):
  216. for c in self.children:
  217. c.reset()
  218. def _divide_widths(self, cli, width):
  219. """
  220. Return the widths for all columns.
  221. Or None when there is not enough space.
  222. """
  223. if not self.children:
  224. return []
  225. # Calculate widths.
  226. given_dimensions = self.get_dimensions(cli) if self.get_dimensions else None
  227. def get_dimension_for_child(c, index):
  228. if given_dimensions and given_dimensions[index] is not None:
  229. return given_dimensions[index]
  230. else:
  231. return c.preferred_width(cli, width)
  232. dimensions = [get_dimension_for_child(c, index) for index, c in enumerate(self.children)]
  233. # Sum dimensions
  234. sum_dimensions = sum_layout_dimensions(dimensions)
  235. # If there is not enough space for both.
  236. # Don't do anything.
  237. if sum_dimensions.min > width:
  238. return
  239. # Find optimal sizes. (Start with minimal size, increase until we cover
  240. # the whole height.)
  241. sizes = [d.min for d in dimensions]
  242. child_generator = take_using_weights(
  243. items=list(range(len(dimensions))),
  244. weights=[d.weight for d in dimensions])
  245. i = next(child_generator)
  246. while sum(sizes) < min(width, sum_dimensions.preferred):
  247. # Increase until we meet at least the 'preferred' size.
  248. if sizes[i] < dimensions[i].preferred:
  249. sizes[i] += 1
  250. i = next(child_generator)
  251. while sum(sizes) < min(width, sum_dimensions.max):
  252. # Increase until we use all the available space.
  253. if sizes[i] < dimensions[i].max:
  254. sizes[i] += 1
  255. i = next(child_generator)
  256. return sizes
  257. def write_to_screen(self, cli, screen, mouse_handlers, write_position):
  258. """
  259. Render the prompt to a `Screen` instance.
  260. :param screen: The :class:`~prompt_toolkit.layout.screen.Screen` class
  261. to which the output has to be written.
  262. """
  263. if not self.children:
  264. return
  265. sizes = self._divide_widths(cli, write_position.width)
  266. if self.report_dimensions_callback:
  267. self.report_dimensions_callback(cli, sizes)
  268. # If there is not enough space.
  269. if sizes is None:
  270. self.window_too_small.write_to_screen(
  271. cli, screen, mouse_handlers, write_position)
  272. return
  273. # Calculate heights, take the largest possible, but not larger than write_position.extended_height.
  274. heights = [child.preferred_height(cli, width, write_position.extended_height).preferred
  275. for width, child in zip(sizes, self.children)]
  276. height = max(write_position.height, min(write_position.extended_height, max(heights)))
  277. # Draw child panes.
  278. ypos = write_position.ypos
  279. xpos = write_position.xpos
  280. for s, c in zip(sizes, self.children):
  281. c.write_to_screen(cli, screen, mouse_handlers, WritePosition(xpos, ypos, s, height))
  282. xpos += s
  283. def walk(self, cli):
  284. """ Walk through children. """
  285. yield self
  286. for c in self.children:
  287. for i in c.walk(cli):
  288. yield i
  289. class FloatContainer(Container):
  290. """
  291. Container which can contain another container for the background, as well
  292. as a list of floating containers on top of it.
  293. Example Usage::
  294. FloatContainer(content=Window(...),
  295. floats=[
  296. Float(xcursor=True,
  297. ycursor=True,
  298. layout=CompletionMenu(...))
  299. ])
  300. """
  301. def __init__(self, content, floats):
  302. assert isinstance(content, Container)
  303. assert all(isinstance(f, Float) for f in floats)
  304. self.content = content
  305. self.floats = floats
  306. def reset(self):
  307. self.content.reset()
  308. for f in self.floats:
  309. f.content.reset()
  310. def preferred_width(self, cli, write_position):
  311. return self.content.preferred_width(cli, write_position)
  312. def preferred_height(self, cli, width, max_available_height):
  313. """
  314. Return the preferred height of the float container.
  315. (We don't care about the height of the floats, they should always fit
  316. into the dimensions provided by the container.)
  317. """
  318. return self.content.preferred_height(cli, width, max_available_height)
  319. def write_to_screen(self, cli, screen, mouse_handlers, write_position):
  320. self.content.write_to_screen(cli, screen, mouse_handlers, write_position)
  321. for fl in self.floats:
  322. # When a menu_position was given, use this instead of the cursor
  323. # position. (These cursor positions are absolute, translate again
  324. # relative to the write_position.)
  325. # Note: This should be inside the for-loop, because one float could
  326. # set the cursor position to be used for the next one.
  327. cursor_position = screen.menu_position or screen.cursor_position
  328. cursor_position = Point(x=cursor_position.x - write_position.xpos,
  329. y=cursor_position.y - write_position.ypos)
  330. fl_width = fl.get_width(cli)
  331. fl_height = fl.get_height(cli)
  332. # Left & width given.
  333. if fl.left is not None and fl_width is not None:
  334. xpos = fl.left
  335. width = fl_width
  336. # Left & right given -> calculate width.
  337. elif fl.left is not None and fl.right is not None:
  338. xpos = fl.left
  339. width = write_position.width - fl.left - fl.right
  340. # Width & right given -> calculate left.
  341. elif fl_width is not None and fl.right is not None:
  342. xpos = write_position.width - fl.right - fl_width
  343. width = fl_width
  344. elif fl.xcursor:
  345. width = fl_width
  346. if width is None:
  347. width = fl.content.preferred_width(cli, write_position.width).preferred
  348. width = min(write_position.width, width)
  349. xpos = cursor_position.x
  350. if xpos + width > write_position.width:
  351. xpos = max(0, write_position.width - width)
  352. # Only width given -> center horizontally.
  353. elif fl_width:
  354. xpos = int((write_position.width - fl_width) / 2)
  355. width = fl_width
  356. # Otherwise, take preferred width from float content.
  357. else:
  358. width = fl.content.preferred_width(cli, write_position.width).preferred
  359. if fl.left is not None:
  360. xpos = fl.left
  361. elif fl.right is not None:
  362. xpos = max(0, write_position.width - width - fl.right)
  363. else: # Center horizontally.
  364. xpos = max(0, int((write_position.width - width) / 2))
  365. # Trim.
  366. width = min(width, write_position.width - xpos)
  367. # Top & height given.
  368. if fl.top is not None and fl_height is not None:
  369. ypos = fl.top
  370. height = fl_height
  371. # Top & bottom given -> calculate height.
  372. elif fl.top is not None and fl.bottom is not None:
  373. ypos = fl.top
  374. height = write_position.height - fl.top - fl.bottom
  375. # Height & bottom given -> calculate top.
  376. elif fl_height is not None and fl.bottom is not None:
  377. ypos = write_position.height - fl_height - fl.bottom
  378. height = fl_height
  379. # Near cursor
  380. elif fl.ycursor:
  381. ypos = cursor_position.y + 1
  382. height = fl_height
  383. if height is None:
  384. height = fl.content.preferred_height(
  385. cli, width, write_position.extended_height).preferred
  386. # Reduce height if not enough space. (We can use the
  387. # extended_height when the content requires it.)
  388. if height > write_position.extended_height - ypos:
  389. if write_position.extended_height - ypos + 1 >= ypos:
  390. # When the space below the cursor is more than
  391. # the space above, just reduce the height.
  392. height = write_position.extended_height - ypos
  393. else:
  394. # Otherwise, fit the float above the cursor.
  395. height = min(height, cursor_position.y)
  396. ypos = cursor_position.y - height
  397. # Only height given -> center vertically.
  398. elif fl_width:
  399. ypos = int((write_position.height - fl_height) / 2)
  400. height = fl_height
  401. # Otherwise, take preferred height from content.
  402. else:
  403. height = fl.content.preferred_height(
  404. cli, width, write_position.extended_height).preferred
  405. if fl.top is not None:
  406. ypos = fl.top
  407. elif fl.bottom is not None:
  408. ypos = max(0, write_position.height - height - fl.bottom)
  409. else: # Center vertically.
  410. ypos = max(0, int((write_position.height - height) / 2))
  411. # Trim.
  412. height = min(height, write_position.height - ypos)
  413. # Write float.
  414. # (xpos and ypos can be negative: a float can be partially visible.)
  415. if height > 0 and width > 0:
  416. wp = WritePosition(xpos=xpos + write_position.xpos,
  417. ypos=ypos + write_position.ypos,
  418. width=width, height=height)
  419. if not fl.hide_when_covering_content or self._area_is_empty(screen, wp):
  420. fl.content.write_to_screen(cli, screen, mouse_handlers, wp)
  421. def _area_is_empty(self, screen, write_position):
  422. """
  423. Return True when the area below the write position is still empty.
  424. (For floats that should not hide content underneath.)
  425. """
  426. wp = write_position
  427. Transparent = Token.Transparent
  428. for y in range(wp.ypos, wp.ypos + wp.height):
  429. if y in screen.data_buffer:
  430. row = screen.data_buffer[y]
  431. for x in range(wp.xpos, wp.xpos + wp.width):
  432. c = row[x]
  433. if c.char != ' ' or c.token != Transparent:
  434. return False
  435. return True
  436. def walk(self, cli):
  437. """ Walk through children. """
  438. yield self
  439. for i in self.content.walk(cli):
  440. yield i
  441. for f in self.floats:
  442. for i in f.content.walk(cli):
  443. yield i
  444. class Float(object):
  445. """
  446. Float for use in a :class:`.FloatContainer`.
  447. :param content: :class:`.Container` instance.
  448. :param hide_when_covering_content: Hide the float when it covers content underneath.
  449. """
  450. def __init__(self, top=None, right=None, bottom=None, left=None,
  451. width=None, height=None, get_width=None, get_height=None,
  452. xcursor=False, ycursor=False, content=None,
  453. hide_when_covering_content=False):
  454. assert isinstance(content, Container)
  455. assert width is None or get_width is None
  456. assert height is None or get_height is None
  457. self.left = left
  458. self.right = right
  459. self.top = top
  460. self.bottom = bottom
  461. self._width = width
  462. self._height = height
  463. self._get_width = get_width
  464. self._get_height = get_height
  465. self.xcursor = xcursor
  466. self.ycursor = ycursor
  467. self.content = content
  468. self.hide_when_covering_content = hide_when_covering_content
  469. def get_width(self, cli):
  470. if self._width:
  471. return self._width
  472. if self._get_width:
  473. return self._get_width(cli)
  474. def get_height(self, cli):
  475. if self._height:
  476. return self._height
  477. if self._get_height:
  478. return self._get_height(cli)
  479. def __repr__(self):
  480. return 'Float(content=%r)' % self.content
  481. class WindowRenderInfo(object):
  482. """
  483. Render information, for the last render time of this control.
  484. It stores mapping information between the input buffers (in case of a
  485. :class:`~prompt_toolkit.layout.controls.BufferControl`) and the actual
  486. render position on the output screen.
  487. (Could be used for implementation of the Vi 'H' and 'L' key bindings as
  488. well as implementing mouse support.)
  489. :param ui_content: The original :class:`.UIContent` instance that contains
  490. the whole input, without clipping. (ui_content)
  491. :param horizontal_scroll: The horizontal scroll of the :class:`.Window` instance.
  492. :param vertical_scroll: The vertical scroll of the :class:`.Window` instance.
  493. :param window_width: The width of the window that displays the content,
  494. without the margins.
  495. :param window_height: The height of the window that displays the content.
  496. :param configured_scroll_offsets: The scroll offsets as configured for the
  497. :class:`Window` instance.
  498. :param visible_line_to_row_col: Mapping that maps the row numbers on the
  499. displayed screen (starting from zero for the first visible line) to
  500. (row, col) tuples pointing to the row and column of the :class:`.UIContent`.
  501. :param rowcol_to_yx: Mapping that maps (row, column) tuples representing
  502. coordinates of the :class:`UIContent` to (y, x) absolute coordinates at
  503. the rendered screen.
  504. """
  505. def __init__(self, ui_content, horizontal_scroll, vertical_scroll,
  506. window_width, window_height,
  507. configured_scroll_offsets,
  508. visible_line_to_row_col, rowcol_to_yx,
  509. x_offset, y_offset, wrap_lines):
  510. assert isinstance(ui_content, UIContent)
  511. assert isinstance(horizontal_scroll, int)
  512. assert isinstance(vertical_scroll, int)
  513. assert isinstance(window_width, int)
  514. assert isinstance(window_height, int)
  515. assert isinstance(configured_scroll_offsets, ScrollOffsets)
  516. assert isinstance(visible_line_to_row_col, dict)
  517. assert isinstance(rowcol_to_yx, dict)
  518. assert isinstance(x_offset, int)
  519. assert isinstance(y_offset, int)
  520. assert isinstance(wrap_lines, bool)
  521. self.ui_content = ui_content
  522. self.vertical_scroll = vertical_scroll
  523. self.window_width = window_width # Width without margins.
  524. self.window_height = window_height
  525. self.configured_scroll_offsets = configured_scroll_offsets
  526. self.visible_line_to_row_col = visible_line_to_row_col
  527. self.wrap_lines = wrap_lines
  528. self._rowcol_to_yx = rowcol_to_yx # row/col from input to absolute y/x
  529. # screen coordinates.
  530. self._x_offset = x_offset
  531. self._y_offset = y_offset
  532. @property
  533. def visible_line_to_input_line(self):
  534. return dict(
  535. (visible_line, rowcol[0])
  536. for visible_line, rowcol in self.visible_line_to_row_col.items())
  537. @property
  538. def cursor_position(self):
  539. """
  540. Return the cursor position coordinates, relative to the left/top corner
  541. of the rendered screen.
  542. """
  543. cpos = self.ui_content.cursor_position
  544. y, x = self._rowcol_to_yx[cpos.y, cpos.x]
  545. return Point(x=x - self._x_offset, y=y - self._y_offset)
  546. @property
  547. def applied_scroll_offsets(self):
  548. """
  549. Return a :class:`.ScrollOffsets` instance that indicates the actual
  550. offset. This can be less than or equal to what's configured. E.g, when
  551. the cursor is completely at the top, the top offset will be zero rather
  552. than what's configured.
  553. """
  554. if self.displayed_lines[0] == 0:
  555. top = 0
  556. else:
  557. # Get row where the cursor is displayed.
  558. y = self.input_line_to_visible_line[self.ui_content.cursor_position.y]
  559. top = min(y, self.configured_scroll_offsets.top)
  560. return ScrollOffsets(
  561. top=top,
  562. bottom=min(self.ui_content.line_count - self.displayed_lines[-1] - 1,
  563. self.configured_scroll_offsets.bottom),
  564. # For left/right, it probably doesn't make sense to return something.
  565. # (We would have to calculate the widths of all the lines and keep
  566. # double width characters in mind.)
  567. left=0, right=0)
  568. @property
  569. def displayed_lines(self):
  570. """
  571. List of all the visible rows. (Line numbers of the input buffer.)
  572. The last line may not be entirely visible.
  573. """
  574. return sorted(row for row, col in self.visible_line_to_row_col.values())
  575. @property
  576. def input_line_to_visible_line(self):
  577. """
  578. Return the dictionary mapping the line numbers of the input buffer to
  579. the lines of the screen. When a line spans several rows at the screen,
  580. the first row appears in the dictionary.
  581. """
  582. result = {}
  583. for k, v in self.visible_line_to_input_line.items():
  584. if v in result:
  585. result[v] = min(result[v], k)
  586. else:
  587. result[v] = k
  588. return result
  589. def first_visible_line(self, after_scroll_offset=False):
  590. """
  591. Return the line number (0 based) of the input document that corresponds
  592. with the first visible line.
  593. """
  594. if after_scroll_offset:
  595. return self.displayed_lines[self.applied_scroll_offsets.top]
  596. else:
  597. return self.displayed_lines[0]
  598. def last_visible_line(self, before_scroll_offset=False):
  599. """
  600. Like `first_visible_line`, but for the last visible line.
  601. """
  602. if before_scroll_offset:
  603. return self.displayed_lines[-1 - self.applied_scroll_offsets.bottom]
  604. else:
  605. return self.displayed_lines[-1]
  606. def center_visible_line(self, before_scroll_offset=False,
  607. after_scroll_offset=False):
  608. """
  609. Like `first_visible_line`, but for the center visible line.
  610. """
  611. return (self.first_visible_line(after_scroll_offset) +
  612. (self.last_visible_line(before_scroll_offset) -
  613. self.first_visible_line(after_scroll_offset)) // 2
  614. )
  615. @property
  616. def content_height(self):
  617. """
  618. The full height of the user control.
  619. """
  620. return self.ui_content.line_count
  621. @property
  622. def full_height_visible(self):
  623. """
  624. True when the full height is visible (There is no vertical scroll.)
  625. """
  626. return self.vertical_scroll == 0 and self.last_visible_line() == self.content_height
  627. @property
  628. def top_visible(self):
  629. """
  630. True when the top of the buffer is visible.
  631. """
  632. return self.vertical_scroll == 0
  633. @property
  634. def bottom_visible(self):
  635. """
  636. True when the bottom of the buffer is visible.
  637. """
  638. return self.last_visible_line() == self.content_height - 1
  639. @property
  640. def vertical_scroll_percentage(self):
  641. """
  642. Vertical scroll as a percentage. (0 means: the top is visible,
  643. 100 means: the bottom is visible.)
  644. """
  645. if self.bottom_visible:
  646. return 100
  647. else:
  648. return (100 * self.vertical_scroll // self.content_height)
  649. def get_height_for_line(self, lineno):
  650. """
  651. Return the height of the given line.
  652. (The height that it would take, if this line became visible.)
  653. """
  654. if self.wrap_lines:
  655. return self.ui_content.get_height_for_line(lineno, self.window_width)
  656. else:
  657. return 1
  658. class ScrollOffsets(object):
  659. """
  660. Scroll offsets for the :class:`.Window` class.
  661. Note that left/right offsets only make sense if line wrapping is disabled.
  662. """
  663. def __init__(self, top=0, bottom=0, left=0, right=0):
  664. assert isinstance(top, Integer)
  665. assert isinstance(bottom, Integer)
  666. assert isinstance(left, Integer)
  667. assert isinstance(right, Integer)
  668. self._top = top
  669. self._bottom = bottom
  670. self._left = left
  671. self._right = right
  672. @property
  673. def top(self):
  674. return int(self._top)
  675. @property
  676. def bottom(self):
  677. return int(self._bottom)
  678. @property
  679. def left(self):
  680. return int(self._left)
  681. @property
  682. def right(self):
  683. return int(self._right)
  684. def __repr__(self):
  685. return 'ScrollOffsets(top=%r, bottom=%r, left=%r, right=%r)' % (
  686. self.top, self.bottom, self.left, self.right)
  687. class ColorColumn(object):
  688. def __init__(self, position, token=Token.ColorColumn):
  689. self.position = position
  690. self.token = token
  691. _in_insert_mode = ViInsertMode() | EmacsInsertMode()
  692. class Window(Container):
  693. """
  694. Container that holds a control.
  695. :param content: :class:`~prompt_toolkit.layout.controls.UIControl` instance.
  696. :param width: :class:`~prompt_toolkit.layout.dimension.LayoutDimension` instance.
  697. :param height: :class:`~prompt_toolkit.layout.dimension.LayoutDimension` instance.
  698. :param get_width: callable which takes a `CommandLineInterface` and returns a `LayoutDimension`.
  699. :param get_height: callable which takes a `CommandLineInterface` and returns a `LayoutDimension`.
  700. :param dont_extend_width: When `True`, don't take up more width then the
  701. preferred width reported by the control.
  702. :param dont_extend_height: When `True`, don't take up more width then the
  703. preferred height reported by the control.
  704. :param left_margins: A list of :class:`~prompt_toolkit.layout.margins.Margin`
  705. instance to be displayed on the left. For instance:
  706. :class:`~prompt_toolkit.layout.margins.NumberredMargin` can be one of
  707. them in order to show line numbers.
  708. :param right_margins: Like `left_margins`, but on the other side.
  709. :param scroll_offsets: :class:`.ScrollOffsets` instance, representing the
  710. preferred amount of lines/columns to be always visible before/after the
  711. cursor. When both top and bottom are a very high number, the cursor
  712. will be centered vertically most of the time.
  713. :param allow_scroll_beyond_bottom: A `bool` or
  714. :class:`~prompt_toolkit.filters.CLIFilter` instance. When True, allow
  715. scrolling so far, that the top part of the content is not visible
  716. anymore, while there is still empty space available at the bottom of
  717. the window. In the Vi editor for instance, this is possible. You will
  718. see tildes while the top part of the body is hidden.
  719. :param wrap_lines: A `bool` or :class:`~prompt_toolkit.filters.CLIFilter`
  720. instance. When True, don't scroll horizontally, but wrap lines instead.
  721. :param get_vertical_scroll: Callable that takes this window
  722. instance as input and returns a preferred vertical scroll.
  723. (When this is `None`, the scroll is only determined by the last and
  724. current cursor position.)
  725. :param get_horizontal_scroll: Callable that takes this window
  726. instance as input and returns a preferred vertical scroll.
  727. :param always_hide_cursor: A `bool` or
  728. :class:`~prompt_toolkit.filters.CLIFilter` instance. When True, never
  729. display the cursor, even when the user control specifies a cursor
  730. position.
  731. :param cursorline: A `bool` or :class:`~prompt_toolkit.filters.CLIFilter`
  732. instance. When True, display a cursorline.
  733. :param cursorcolumn: A `bool` or :class:`~prompt_toolkit.filters.CLIFilter`
  734. instance. When True, display a cursorcolumn.
  735. :param get_colorcolumns: A callable that takes a `CommandLineInterface` and
  736. returns a a list of :class:`.ColorColumn` instances that describe the
  737. columns to be highlighted.
  738. :param cursorline_token: The token to be used for highlighting the current line,
  739. if `cursorline` is True.
  740. :param cursorcolumn_token: The token to be used for highlighting the current line,
  741. if `cursorcolumn` is True.
  742. """
  743. def __init__(self, content, width=None, height=None, get_width=None,
  744. get_height=None, dont_extend_width=False, dont_extend_height=False,
  745. left_margins=None, right_margins=None, scroll_offsets=None,
  746. allow_scroll_beyond_bottom=False, wrap_lines=False,
  747. get_vertical_scroll=None, get_horizontal_scroll=None, always_hide_cursor=False,
  748. cursorline=False, cursorcolumn=False, get_colorcolumns=None,
  749. cursorline_token=Token.CursorLine, cursorcolumn_token=Token.CursorColumn):
  750. assert isinstance(content, UIControl)
  751. assert width is None or isinstance(width, LayoutDimension)
  752. assert height is None or isinstance(height, LayoutDimension)
  753. assert get_width is None or callable(get_width)
  754. assert get_height is None or callable(get_height)
  755. assert width is None or get_width is None
  756. assert height is None or get_height is None
  757. assert scroll_offsets is None or isinstance(scroll_offsets, ScrollOffsets)
  758. assert left_margins is None or all(isinstance(m, Margin) for m in left_margins)
  759. assert right_margins is None or all(isinstance(m, Margin) for m in right_margins)
  760. assert get_vertical_scroll is None or callable(get_vertical_scroll)
  761. assert get_horizontal_scroll is None or callable(get_horizontal_scroll)
  762. assert get_colorcolumns is None or callable(get_colorcolumns)
  763. self.allow_scroll_beyond_bottom = to_cli_filter(allow_scroll_beyond_bottom)
  764. self.always_hide_cursor = to_cli_filter(always_hide_cursor)
  765. self.wrap_lines = to_cli_filter(wrap_lines)
  766. self.cursorline = to_cli_filter(cursorline)
  767. self.cursorcolumn = to_cli_filter(cursorcolumn)
  768. self.content = content
  769. self.dont_extend_width = dont_extend_width
  770. self.dont_extend_height = dont_extend_height
  771. self.left_margins = left_margins or []
  772. self.right_margins = right_margins or []
  773. self.scroll_offsets = scroll_offsets or ScrollOffsets()
  774. self.get_vertical_scroll = get_vertical_scroll
  775. self.get_horizontal_scroll = get_horizontal_scroll
  776. self._width = get_width or (lambda cli: width)
  777. self._height = get_height or (lambda cli: height)
  778. self.get_colorcolumns = get_colorcolumns or (lambda cli: [])
  779. self.cursorline_token = cursorline_token
  780. self.cursorcolumn_token = cursorcolumn_token
  781. # Cache for the screens generated by the margin.
  782. self._ui_content_cache = SimpleCache(maxsize=8)
  783. self._margin_width_cache = SimpleCache(maxsize=1)
  784. self.reset()
  785. def __repr__(self):
  786. return 'Window(content=%r)' % self.content
  787. def reset(self):
  788. self.content.reset()
  789. #: Scrolling position of the main content.
  790. self.vertical_scroll = 0
  791. self.horizontal_scroll = 0
  792. # Vertical scroll 2: this is the vertical offset that a line is
  793. # scrolled if a single line (the one that contains the cursor) consumes
  794. # all of the vertical space.
  795. self.vertical_scroll_2 = 0
  796. #: Keep render information (mappings between buffer input and render
  797. #: output.)
  798. self.render_info = None
  799. def _get_margin_width(self, cli, margin):
  800. """
  801. Return the width for this margin.
  802. (Calculate only once per render time.)
  803. """
  804. # Margin.get_width, needs to have a UIContent instance.
  805. def get_ui_content():
  806. return self._get_ui_content(cli, width=0, height=0)
  807. def get_width():
  808. return margin.get_width(cli, get_ui_content)
  809. key = (margin, cli.render_counter)
  810. return self._margin_width_cache.get(key, get_width)
  811. def preferred_width(self, cli, max_available_width):
  812. # Calculate the width of the margin.
  813. total_margin_width = sum(self._get_margin_width(cli, m) for m in
  814. self.left_margins + self.right_margins)
  815. # Window of the content. (Can be `None`.)
  816. preferred_width = self.content.preferred_width(
  817. cli, max_available_width - total_margin_width)
  818. if preferred_width is not None:
  819. # Include width of the margins.
  820. preferred_width += total_margin_width
  821. # Merge.
  822. return self._merge_dimensions(
  823. dimension=self._width(cli),
  824. preferred=preferred_width,
  825. dont_extend=self.dont_extend_width)
  826. def preferred_height(self, cli, width, max_available_height):
  827. total_margin_width = sum(self._get_margin_width(cli, m) for m in
  828. self.left_margins + self.right_margins)
  829. wrap_lines = self.wrap_lines(cli)
  830. return self._merge_dimensions(
  831. dimension=self._height(cli),
  832. preferred=self.content.preferred_height(
  833. cli, width - total_margin_width, max_available_height, wrap_lines),
  834. dont_extend=self.dont_extend_height)
  835. @staticmethod
  836. def _merge_dimensions(dimension, preferred=None, dont_extend=False):
  837. """
  838. Take the LayoutDimension from this `Window` class and the received
  839. preferred size from the `UIControl` and return a `LayoutDimension` to
  840. report to the parent container.
  841. """
  842. dimension = dimension or LayoutDimension()
  843. # When a preferred dimension was explicitly given to the Window,
  844. # ignore the UIControl.
  845. if dimension.preferred_specified:
  846. preferred = dimension.preferred
  847. # When a 'preferred' dimension is given by the UIControl, make sure
  848. # that it stays within the bounds of the Window.
  849. if preferred is not None:
  850. if dimension.max:
  851. preferred = min(preferred, dimension.max)
  852. if dimension.min:
  853. preferred = max(preferred, dimension.min)
  854. # When a `dont_extend` flag has been given, use the preferred dimension
  855. # also as the max dimension.
  856. if dont_extend and preferred is not None:
  857. max_ = min(dimension.max, preferred)
  858. else:
  859. max_ = dimension.max
  860. return LayoutDimension(
  861. min=dimension.min, max=max_,
  862. preferred=preferred, weight=dimension.weight)
  863. def _get_ui_content(self, cli, width, height):
  864. """
  865. Create a `UIContent` instance.
  866. """
  867. def get_content():
  868. return self.content.create_content(cli, width=width, height=height)
  869. key = (cli.render_counter, width, height)
  870. return self._ui_content_cache.get(key, get_content)
  871. def _get_digraph_char(self, cli):
  872. " Return `False`, or the Digraph symbol to be used. "
  873. if cli.quoted_insert:
  874. return '^'
  875. if cli.vi_state.waiting_for_digraph:
  876. if cli.vi_state.digraph_symbol1:
  877. return cli.vi_state.digraph_symbol1
  878. return '?'
  879. return False
  880. def write_to_screen(self, cli, screen, mouse_handlers, write_position):
  881. """
  882. Write window to screen. This renders the user control, the margins and
  883. copies everything over to the absolute position at the given screen.
  884. """
  885. # Calculate margin sizes.
  886. left_margin_widths = [self._get_margin_width(cli, m) for m in self.left_margins]
  887. right_margin_widths = [self._get_margin_width(cli, m) for m in self.right_margins]
  888. total_margin_width = sum(left_margin_widths + right_margin_widths)
  889. # Render UserControl.
  890. ui_content = self.content.create_content(
  891. cli, write_position.width - total_margin_width, write_position.height)
  892. assert isinstance(ui_content, UIContent)
  893. # Scroll content.
  894. wrap_lines = self.wrap_lines(cli)
  895. scroll_func = self._scroll_when_linewrapping if wrap_lines else self._scroll_without_linewrapping
  896. scroll_func(
  897. ui_content, write_position.width - total_margin_width, write_position.height, cli)
  898. # Write body
  899. visible_line_to_row_col, rowcol_to_yx = self._copy_body(
  900. cli, ui_content, screen, write_position,
  901. sum(left_margin_widths), write_position.width - total_margin_width,
  902. self.vertical_scroll, self.horizontal_scroll,
  903. has_focus=self.content.has_focus(cli),
  904. wrap_lines=wrap_lines, highlight_lines=True,
  905. vertical_scroll_2=self.vertical_scroll_2,
  906. always_hide_cursor=self.always_hide_cursor(cli))
  907. # Remember render info. (Set before generating the margins. They need this.)
  908. x_offset=write_position.xpos + sum(left_margin_widths)
  909. y_offset=write_position.ypos
  910. self.render_info = WindowRenderInfo(
  911. ui_content=ui_content,
  912. horizontal_scroll=self.horizontal_scroll,
  913. vertical_scroll=self.vertical_scroll,
  914. window_width=write_position.width - total_margin_width,
  915. window_height=write_position.height,
  916. configured_scroll_offsets=self.scroll_offsets,
  917. visible_line_to_row_col=visible_line_to_row_col,
  918. rowcol_to_yx=rowcol_to_yx,
  919. x_offset=x_offset,
  920. y_offset=y_offset,
  921. wrap_lines=wrap_lines)
  922. # Set mouse handlers.
  923. def mouse_handler(cli, mouse_event):
  924. """ Wrapper around the mouse_handler of the `UIControl` that turns
  925. screen coordinates into line coordinates. """
  926. # Find row/col position first.
  927. yx_to_rowcol = dict((v, k) for k, v in rowcol_to_yx.items())
  928. y = mouse_event.position.y
  929. x = mouse_event.position.x
  930. # If clicked below the content area, look for a position in the
  931. # last line instead.
  932. max_y = write_position.ypos + len(visible_line_to_row_col) - 1
  933. y = min(max_y, y)
  934. while x >= 0:
  935. try:
  936. row, col = yx_to_rowcol[y, x]
  937. except KeyError:
  938. # Try again. (When clicking on the right side of double
  939. # width characters, or on the right side of the input.)
  940. x -= 1
  941. else:
  942. # Found position, call handler of UIControl.
  943. result = self.content.mouse_handler(
  944. cli, MouseEvent(position=Point(x=col, y=row),
  945. event_type=mouse_event.event_type))
  946. break
  947. else:
  948. # nobreak.
  949. # (No x/y coordinate found for the content. This happens in
  950. # case of a FillControl, that only specifies a background, but
  951. # doesn't have a content. Report (0,0) instead.)
  952. result = self.content.mouse_handler(
  953. cli, MouseEvent(position=Point(x=0, y=0),
  954. event_type=mouse_event.event_type))
  955. # If it returns NotImplemented, handle it here.
  956. if result == NotImplemented:
  957. return self._mouse_handler(cli, mouse_event)
  958. return result
  959. mouse_handlers.set_mouse_handler_for_range(
  960. x_min=write_position.xpos + sum(left_margin_widths),
  961. x_max=write_position.xpos + write_position.width - total_margin_width,
  962. y_min=write_position.ypos,
  963. y_max=write_position.ypos + write_position.height,
  964. handler=mouse_handler)
  965. # Render and copy margins.
  966. move_x = 0
  967. def render_margin(m, width):
  968. " Render margin. Return `Screen`. "
  969. # Retrieve margin tokens.
  970. tokens = m.create_margin(cli, self.render_info, width, write_position.height)
  971. # Turn it into a UIContent object.
  972. # already rendered those tokens using this size.)
  973. return TokenListControl.static(tokens).create_content(
  974. cli, width + 1, write_position.height)
  975. for m, width in zip(self.left_margins, left_margin_widths):
  976. # Create screen for margin.
  977. margin_screen = render_margin(m, width)
  978. # Copy and shift X.
  979. self._copy_margin(cli, margin_screen, screen, write_position, move_x, width)
  980. move_x += width
  981. move_x = write_position.width - sum(right_margin_widths)
  982. for m, width in zip(self.right_margins, right_margin_widths):
  983. # Create screen for margin.
  984. margin_screen = render_margin(m, width)
  985. # Copy and shift X.
  986. self._copy_margin(cli, margin_screen, screen, write_position, move_x, width)
  987. move_x += width
  988. def _copy_body(self, cli, ui_content, new_screen, write_position, move_x,
  989. width, vertical_scroll=0, horizontal_scroll=0,
  990. has_focus=False, wrap_lines=False, highlight_lines=False,
  991. vertical_scroll_2=0, always_hide_cursor=False):
  992. """
  993. Copy the UIContent into the output screen.
  994. """
  995. xpos = write_position.xpos + move_x
  996. ypos = write_position.ypos
  997. line_count = ui_content.line_count
  998. new_buffer = new_screen.data_buffer
  999. empty_char = _CHAR_CACHE['', Token]
  1000. ZeroWidthEscape = Token.ZeroWidthEscape
  1001. # Map visible line number to (row, col) of input.
  1002. # 'col' will always be zero if line wrapping is off.
  1003. visible_line_to_row_col = {}
  1004. rowcol_to_yx = {} # Maps (row, col) from the input to (y, x) screen coordinates.
  1005. # Fill background with default_char first.
  1006. default_char = ui_content.default_char
  1007. if default_char:
  1008. for y in range(ypos, ypos + write_position.height):
  1009. new_buffer_row = new_buffer[y]
  1010. for x in range(xpos, xpos + width):
  1011. new_buffer_row[x] = default_char
  1012. # Copy content.
  1013. def copy():
  1014. y = - vertical_scroll_2
  1015. lineno = vertical_scroll
  1016. while y < write_position.height and lineno < line_count:
  1017. # Take the next line and copy it in the real screen.
  1018. line = ui_content.get_line(lineno)
  1019. col = 0
  1020. x = -horizontal_scroll
  1021. visible_line_to_row_col[y] = (lineno, horizontal_scroll)
  1022. new_buffer_row = new_buffer[y + ypos]
  1023. for token, text in line:
  1024. # Remember raw VT escape sequences. (E.g. FinalTerm's
  1025. # escape sequences.)
  1026. if token == ZeroWidthEscape:
  1027. new_screen.zero_width_escapes[y + ypos][x + xpos] += text
  1028. continue
  1029. for c in text:
  1030. char = _CHAR_CACHE[c, token]
  1031. char_width = char.width
  1032. # Wrap when the line width is exceeded.
  1033. if wrap_lines and x + char_width > width:
  1034. visible_line_to_row_col[y + 1] = (
  1035. lineno, visible_line_to_row_col[y][1] + x)
  1036. y += 1
  1037. x = -horizontal_scroll # This would be equal to zero.
  1038. # (horizontal_scroll=0 when wrap_lines.)
  1039. new_buffer_row = new_buffer[y + ypos]
  1040. if y >= write_position.height:
  1041. return y # Break out of all for loops.
  1042. # Set character in screen and shift 'x'.
  1043. if x >= 0 and y >= 0 and x < write_position.width:
  1044. new_buffer_row[x + xpos] = char
  1045. # When we print a multi width character, make sure
  1046. # to erase the neighbous positions in the screen.
  1047. # (The empty string if different from everything,
  1048. # so next redraw this cell will repaint anyway.)
  1049. if char_width > 1:
  1050. for i in range(1, char_width):
  1051. new_buffer_row[x + xpos + i] = empty_char
  1052. # If this is a zero width characters, then it's
  1053. # probably part of a decomposed unicode character.
  1054. # See: https://en.wikipedia.org/wiki/Unicode_equivalence
  1055. # Merge it in the previous cell.
  1056. elif char_width == 0 and x - 1 >= 0:
  1057. prev_char = new_buffer_row[x + xpos - 1]
  1058. char2 = _CHAR_CACHE[prev_char.char + c, prev_char.token]
  1059. new_buffer_row[x + xpos - 1] = char2
  1060. # Keep track of write position for each character.
  1061. rowcol_to_yx[lineno, col] = (y + ypos, x + xpos)
  1062. col += 1
  1063. x += char_width
  1064. lineno += 1
  1065. y += 1
  1066. return y
  1067. y = copy()
  1068. def cursor_pos_to_screen_pos(row, col):
  1069. " Translate row/col from UIContent to real Screen coordinates. "
  1070. try:
  1071. y, x = rowcol_to_yx[row, col]
  1072. except KeyError:
  1073. # Normally this should never happen. (It is a bug, if it happens.)
  1074. # But to be sure, return (0, 0)
  1075. return Point(y=0, x=0)
  1076. # raise ValueError(
  1077. # 'Invalid position. row=%r col=%r, vertical_scroll=%r, '
  1078. # 'horizontal_scroll=%r, height=%r' %
  1079. # (row, col, vertical_scroll, horizontal_scroll, write_position.height))
  1080. else:
  1081. return Point(y=y, x=x)
  1082. # Set cursor and menu positions.
  1083. if ui_content.cursor_position:
  1084. screen_cursor_position = cursor_pos_to_screen_pos(
  1085. ui_content.cursor_position.y, ui_content.cursor_position.x)
  1086. if has_focus:
  1087. new_screen.cursor_position = screen_cursor_position
  1088. if always_hide_cursor:
  1089. new_screen.show_cursor = False
  1090. else:
  1091. new_screen.show_cursor = ui_content.show_cursor
  1092. self._highlight_digraph(cli, new_screen)
  1093. if highlight_lines:
  1094. self._highlight_cursorlines(
  1095. cli, new_screen, screen_cursor_position, xpos, ypos, width,
  1096. write_position.height)
  1097. # Draw input characters from the input processor queue.
  1098. if has_focus and ui_content.cursor_position:
  1099. self._show_input_processor_key_buffer(cli, new_screen)
  1100. # Set menu position.
  1101. if not new_screen.menu_position and ui_content.menu_position:
  1102. new_screen.menu_position = cursor_pos_to_screen_pos(
  1103. ui_content.menu_position.y, ui_content.menu_position.x)
  1104. # Update output screne height.
  1105. new_screen.height = max(new_screen.height, ypos + write_position.height)
  1106. return visible_line_to_row_col, rowcol_to_yx
  1107. def _highlight_digraph(self, cli, new_screen):
  1108. """
  1109. When we are in Vi digraph mode, put a question mark underneath the
  1110. cursor.
  1111. """
  1112. digraph_char = self._get_digraph_char(cli)
  1113. if digraph_char:
  1114. cpos = new_screen.cursor_position
  1115. new_screen.data_buffer[cpos.y][cpos.x] = \
  1116. _CHAR_CACHE[digraph_char, Token.Digraph]
  1117. def _show_input_processor_key_buffer(self, cli, new_screen):
  1118. """
  1119. When the user is typing a key binding that consists of several keys,
  1120. display the last pressed key if the user is in insert mode and the key
  1121. is meaningful to be displayed.
  1122. E.g. Some people want to bind 'jj' to escape in Vi insert mode. But the
  1123. first 'j' needs to be displayed in order to get some feedback.
  1124. """
  1125. key_buffer = cli.input_processor.key_buffer
  1126. if key_buffer and _in_insert_mode(cli) and not cli.is_done:
  1127. # The textual data for the given key. (Can be a VT100 escape
  1128. # sequence.)
  1129. data = key_buffer[-1].data
  1130. # Display only if this is a 1 cell width character.
  1131. if get_cwidth(data) == 1:
  1132. cpos = new_screen.cursor_position
  1133. new_screen.data_buffer[cpos.y][cpos.x] = \
  1134. _CHAR_CACHE[data, Token.PartialKeyBinding]
  1135. def _highlight_cursorlines(self, cli, new_screen, cpos, x, y, width, height):
  1136. """
  1137. Highlight cursor row/column.
  1138. """
  1139. cursor_line_token = (':', ) + self.cursorline_token
  1140. cursor_column_token = (':', ) + self.cursorcolumn_token
  1141. data_buffer = new_screen.data_buffer
  1142. # Highlight cursor line.
  1143. if self.cursorline(cli):
  1144. row = data_buffer[cpos.y]
  1145. for x in range(x, x + width):
  1146. original_char = row[x]
  1147. row[x] = _CHAR_CACHE[
  1148. original_char.char, original_char.token + cursor_line_token]
  1149. # Highlight cursor column.
  1150. if self.cursorcolumn(cli):
  1151. for y2 in range(y, y + height):
  1152. row = data_buffer[y2]
  1153. original_char = row[cpos.x]
  1154. row[cpos.x] = _CHAR_CACHE[
  1155. original_char.char, original_char.token + cursor_column_token]
  1156. # Highlight color columns
  1157. for cc in self.get_colorcolumns(cli):
  1158. assert isinstance(cc, ColorColumn)
  1159. color_column_token = (':', ) + cc.token
  1160. column = cc.position
  1161. for y2 in range(y, y + height):
  1162. row = data_buffer[y2]
  1163. original_char = row[column]
  1164. row[column] = _CHAR_CACHE[
  1165. original_char.char, original_char.token + color_column_token]
  1166. def _copy_margin(self, cli, lazy_screen, new_screen, write_position, move_x, width):
  1167. """
  1168. Copy characters from the margin screen to the real screen.
  1169. """
  1170. xpos = write_position.xpos + move_x
  1171. ypos = write_position.ypos
  1172. margin_write_position = WritePosition(xpos, ypos, width, write_position.height)
  1173. self._copy_body(cli, lazy_screen, new_screen, margin_write_position, 0, width)
  1174. def _scroll_when_linewrapping(self, ui_content, width, height, cli):
  1175. """
  1176. Scroll to make sure the cursor position is visible and that we maintain
  1177. the requested scroll offset.
  1178. Set `self.horizontal_scroll/vertical_scroll`.
  1179. """
  1180. scroll_offsets_bottom = self.scroll_offsets.bottom
  1181. scroll_offsets_top = self.scroll_offsets.top
  1182. # We don't have horizontal scrolling.
  1183. self.horizontal_scroll = 0
  1184. # If the current line consumes more than the whole window height,
  1185. # then we have to scroll vertically inside this line. (We don't take
  1186. # the scroll offsets into account for this.)
  1187. # Also, ignore the scroll offsets in this case. Just set the vertical
  1188. # scroll to this line.
  1189. if ui_content.get_height_for_line(ui_content.cursor_position.y, width) > height - scroll_offsets_top:
  1190. # Calculate the height of the text before the cursor, with the line
  1191. # containing the cursor included, and the character belowe the
  1192. # cursor included as well.
  1193. line = explode_tokens(ui_content.get_line(ui_content.cursor_position.y))
  1194. text_before_cursor = token_list_to_text(line[:ui_content.cursor_position.x + 1])
  1195. text_before_height = UIContent.get_height_for_text(text_before_cursor, width)
  1196. # Adjust scroll offset.
  1197. self.vertical_scroll = ui_content.cursor_position.y
  1198. self.vertical_scroll_2 = min(text_before_height - 1, self.vertical_scroll_2)
  1199. self.vertical_scroll_2 = max(0, text_before_height - height, self.vertical_scroll_2)
  1200. return
  1201. else:
  1202. self.vertical_scroll_2 = 0
  1203. # Current line doesn't consume the whole height. Take scroll offsets into account.
  1204. def get_min_vertical_scroll():
  1205. # Make sure that the cursor line is not below the bottom.
  1206. # (Calculate how many lines can be shown between the cursor and the .)
  1207. used_height = 0
  1208. prev_lineno = ui_content.cursor_position.y
  1209. for lineno in range(ui_content.cursor_position.y, -1, -1):
  1210. used_height += ui_content.get_height_for_line(lineno, width)
  1211. if used_height > height - scroll_offsets_bottom:
  1212. return prev_lineno
  1213. else:
  1214. prev_lineno = lineno
  1215. return 0
  1216. def get_max_vertical_scroll():
  1217. # Make sure that the cursor line is not above the top.
  1218. prev_lineno = ui_content.cursor_position.y
  1219. used_height = 0
  1220. for lineno in range(ui_content.cursor_position.y - 1, -1, -1):
  1221. used_height += ui_content.get_height_for_line(lineno, width)
  1222. if used_height > scroll_offsets_top:
  1223. return prev_lineno
  1224. else:
  1225. prev_lineno = lineno
  1226. return prev_lineno
  1227. def get_topmost_visible():
  1228. """
  1229. Calculate the upper most line that can be visible, while the bottom
  1230. is still visible. We should not allow scroll more than this if
  1231. `allow_scroll_beyond_bottom` is false.
  1232. """
  1233. prev_lineno = ui_content.line_count - 1
  1234. used_height = 0
  1235. for lineno in range(ui_content.line_count - 1, -1, -1):
  1236. used_height += ui_content.get_height_for_line(lineno, width)
  1237. if used_height > height:
  1238. return prev_lineno
  1239. else:
  1240. prev_lineno = lineno
  1241. return prev_lineno
  1242. # Scroll vertically. (Make sure that the whole line which contains the
  1243. # cursor is visible.
  1244. topmost_visible = get_topmost_visible()
  1245. # Note: the `min(topmost_visible, ...)` is to make sure that we
  1246. # don't require scrolling up because of the bottom scroll offset,
  1247. # when we are at the end of the document.
  1248. self.vertical_scroll = max(self.vertical_scroll, min(topmost_visible, get_min_vertical_scroll()))
  1249. self.vertical_scroll = min(self.vertical_scroll, get_max_vertical_scroll())
  1250. # Disallow scrolling beyond bottom?
  1251. if not self.allow_scroll_beyond_bottom(cli):
  1252. self.vertical_scroll = min(self.vertical_scroll, topmost_visible)
  1253. def _scroll_without_linewrapping(self, ui_content, width, height, cli):
  1254. """
  1255. Scroll to make sure the cursor position is visible and that we maintain
  1256. the requested scroll offset.
  1257. Set `self.horizontal_scroll/vertical_scroll`.
  1258. """
  1259. cursor_position = ui_content.cursor_position or Point(0, 0)
  1260. # Without line wrapping, we will never have to scroll vertically inside
  1261. # a single line.
  1262. self.vertical_scroll_2 = 0
  1263. if ui_content.line_count == 0:
  1264. self.vertical_scroll = 0
  1265. self.horizontal_scroll = 0
  1266. return
  1267. else:
  1268. current_line_text = token_list_to_text(ui_content.get_line(cursor_position.y))
  1269. def do_scroll(current_scroll, scroll_offset_start, scroll_offset_end,
  1270. cursor_pos, window_size, content_size):
  1271. " Scrolling algorithm. Used for both horizontal and vertical scrolling. "
  1272. # Calculate the scroll offset to apply.
  1273. # This can obviously never be more than have the screen size. Also, when the
  1274. # cursor appears at the top or bottom, we don't apply the offset.
  1275. scroll_offset_start = int(min(scroll_offset_start, window_size / 2, cursor_pos))
  1276. scroll_offset_end = int(min(scroll_offset_end, window_size / 2,
  1277. content_size - 1 - cursor_pos))
  1278. # Prevent negative scroll offsets.
  1279. if current_scroll < 0:
  1280. current_scroll = 0
  1281. # Scroll back if we scrolled to much and there's still space to show more of the document.
  1282. if (not self.allow_scroll_beyond_bottom(cli) and
  1283. current_scroll > content_size - window_size):
  1284. current_scroll = max(0, content_size - window_size)
  1285. # Scroll up if cursor is before visible part.
  1286. if current_scroll > cursor_pos - scroll_offset_start:
  1287. current_scroll = max(0, cursor_pos - scroll_offset_start)
  1288. # Scroll down if cursor is after visible part.
  1289. if current_scroll < (cursor_pos + 1) - window_size + scroll_offset_end:
  1290. current_scroll = (cursor_pos + 1) - window_size + scroll_offset_end
  1291. return current_scroll
  1292. # When a preferred scroll is given, take that first into account.
  1293. if self.get_vertical_scroll:
  1294. self.vertical_scroll = self.get_vertical_scroll(self)
  1295. assert isinstance(self.vertical_scroll, int)
  1296. if self.get_horizontal_scroll:
  1297. self.horizontal_scroll = self.get_horizontal_scroll(self)
  1298. assert isinstance(self.horizontal_scroll, int)
  1299. # Update horizontal/vertical scroll to make sure that the cursor
  1300. # remains visible.
  1301. offsets = self.scroll_offsets
  1302. self.vertical_scroll = do_scroll(
  1303. current_scroll=self.vertical_scroll,
  1304. scroll_offset_start=offsets.top,
  1305. scroll_offset_end=offsets.bottom,
  1306. cursor_pos=ui_content.cursor_position.y,
  1307. window_size=height,
  1308. content_size=ui_content.line_count)
  1309. self.horizontal_scroll = do_scroll(
  1310. current_scroll=self.horizontal_scroll,
  1311. scroll_offset_start=offsets.left,
  1312. scroll_offset_end=offsets.right,
  1313. cursor_pos=get_cwidth(current_line_text[:ui_content.cursor_position.x]),
  1314. window_size=width,
  1315. # We can only analyse the current line. Calculating the width off
  1316. # all the lines is too expensive.
  1317. content_size=max(get_cwidth(current_line_text), self.horizontal_scroll + width))
  1318. def _mouse_handler(self, cli, mouse_event):
  1319. """
  1320. Mouse handler. Called when the UI control doesn't handle this
  1321. particular event.
  1322. """
  1323. if mouse_event.event_type == MouseEventType.SCROLL_DOWN:
  1324. self._scroll_down(cli)
  1325. elif mouse_event.event_type == MouseEventType.SCROLL_UP:
  1326. self._scroll_up(cli)
  1327. def _scroll_down(self, cli):
  1328. " Scroll window down. "
  1329. info = self.render_info
  1330. if self.vertical_scroll < info.content_height - info.window_height:
  1331. if info.cursor_position.y <= info.configured_scroll_offsets.top:
  1332. self.content.move_cursor_down(cli)
  1333. self.vertical_scroll += 1
  1334. def _scroll_up(self, cli):
  1335. " Scroll window up. "
  1336. info = self.render_info
  1337. if info.vertical_scroll > 0:
  1338. # TODO: not entirely correct yet in case of line wrapping and long lines.
  1339. if info.cursor_position.y >= info.window_height - 1 - info.configured_scroll_offsets.bottom:
  1340. self.content.move_cursor_up(cli)
  1341. self.vertical_scroll -= 1
  1342. def walk(self, cli):
  1343. # Only yield self. A window doesn't have children.
  1344. yield self
  1345. class ConditionalContainer(Container):
  1346. """
  1347. Wrapper around any other container that can change the visibility. The
  1348. received `filter` determines whether the given container should be
  1349. displayed or not.
  1350. :param content: :class:`.Container` instance.
  1351. :param filter: :class:`~prompt_toolkit.filters.CLIFilter` instance.
  1352. """
  1353. def __init__(self, content, filter):
  1354. assert isinstance(content, Container)
  1355. self.content = content
  1356. self.filter = to_cli_filter(filter)
  1357. def __repr__(self):
  1358. return 'ConditionalContainer(%r, filter=%r)' % (self.content, self.filter)
  1359. def reset(self):
  1360. self.content.reset()
  1361. def preferred_width(self, cli, max_available_width):
  1362. if self.filter(cli):
  1363. return self.content.preferred_width(cli, max_available_width)
  1364. else:
  1365. return LayoutDimension.exact(0)
  1366. def preferred_height(self, cli, width, max_available_height):
  1367. if self.filter(cli):
  1368. return self.content.preferred_height(cli, width, max_available_height)
  1369. else:
  1370. return LayoutDimension.exact(0)
  1371. def write_to_screen(self, cli, screen, mouse_handlers, write_position):
  1372. if self.filter(cli):
  1373. return self.content.write_to_screen(cli, screen, mouse_handlers, write_position)
  1374. def walk(self, cli):
  1375. return self.content.walk(cli)
  1376. # Deprecated alias for 'Container'.
  1377. Layout = Container