Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/prompt_toolkit/layout/containers.py: 19%
968 statements
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-26 06:07 +0000
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-26 06:07 +0000
1"""
2Container for the layout.
3(Containers can contain other containers or user interface controls.)
4"""
5from __future__ import annotations
7from abc import ABCMeta, abstractmethod
8from enum import Enum
9from functools import partial
10from typing import (
11 TYPE_CHECKING,
12 Callable,
13 Dict,
14 List,
15 Optional,
16 Sequence,
17 Tuple,
18 Union,
19 cast,
20)
22from prompt_toolkit.application.current import get_app
23from prompt_toolkit.cache import SimpleCache
24from prompt_toolkit.data_structures import Point
25from prompt_toolkit.filters import (
26 FilterOrBool,
27 emacs_insert_mode,
28 to_filter,
29 vi_insert_mode,
30)
31from prompt_toolkit.formatted_text import (
32 AnyFormattedText,
33 StyleAndTextTuples,
34 to_formatted_text,
35)
36from prompt_toolkit.formatted_text.utils import (
37 fragment_list_to_text,
38 fragment_list_width,
39)
40from prompt_toolkit.key_binding import KeyBindingsBase
41from prompt_toolkit.mouse_events import MouseEvent, MouseEventType
42from prompt_toolkit.utils import get_cwidth, take_using_weights, to_int, to_str
44from .controls import (
45 DummyControl,
46 FormattedTextControl,
47 GetLinePrefixCallable,
48 UIContent,
49 UIControl,
50)
51from .dimension import (
52 AnyDimension,
53 Dimension,
54 max_layout_dimensions,
55 sum_layout_dimensions,
56 to_dimension,
57)
58from .margins import Margin
59from .mouse_handlers import MouseHandlers
60from .screen import _CHAR_CACHE, Screen, WritePosition
61from .utils import explode_text_fragments
63if TYPE_CHECKING:
64 from typing_extensions import Protocol, TypeGuard
66 from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone
69__all__ = [
70 "AnyContainer",
71 "Container",
72 "HorizontalAlign",
73 "VerticalAlign",
74 "HSplit",
75 "VSplit",
76 "FloatContainer",
77 "Float",
78 "WindowAlign",
79 "Window",
80 "WindowRenderInfo",
81 "ConditionalContainer",
82 "ScrollOffsets",
83 "ColorColumn",
84 "to_container",
85 "to_window",
86 "is_container",
87 "DynamicContainer",
88]
91class Container(metaclass=ABCMeta):
92 """
93 Base class for user interface layout.
94 """
96 @abstractmethod
97 def reset(self) -> None:
98 """
99 Reset the state of this container and all the children.
100 (E.g. reset scroll offsets, etc...)
101 """
103 @abstractmethod
104 def preferred_width(self, max_available_width: int) -> Dimension:
105 """
106 Return a :class:`~prompt_toolkit.layout.Dimension` that represents the
107 desired width for this container.
108 """
110 @abstractmethod
111 def preferred_height(self, width: int, max_available_height: int) -> Dimension:
112 """
113 Return a :class:`~prompt_toolkit.layout.Dimension` that represents the
114 desired height for this container.
115 """
117 @abstractmethod
118 def write_to_screen(
119 self,
120 screen: Screen,
121 mouse_handlers: MouseHandlers,
122 write_position: WritePosition,
123 parent_style: str,
124 erase_bg: bool,
125 z_index: int | None,
126 ) -> None:
127 """
128 Write the actual content to the screen.
130 :param screen: :class:`~prompt_toolkit.layout.screen.Screen`
131 :param mouse_handlers: :class:`~prompt_toolkit.layout.mouse_handlers.MouseHandlers`.
132 :param parent_style: Style string to pass to the :class:`.Window`
133 object. This will be applied to all content of the windows.
134 :class:`.VSplit` and :class:`.HSplit` can use it to pass their
135 style down to the windows that they contain.
136 :param z_index: Used for propagating z_index from parent to child.
137 """
139 def is_modal(self) -> bool:
140 """
141 When this container is modal, key bindings from parent containers are
142 not taken into account if a user control in this container is focused.
143 """
144 return False
146 def get_key_bindings(self) -> KeyBindingsBase | None:
147 """
148 Returns a :class:`.KeyBindings` object. These bindings become active when any
149 user control in this container has the focus, except if any containers
150 between this container and the focused user control is modal.
151 """
152 return None
154 @abstractmethod
155 def get_children(self) -> list[Container]:
156 """
157 Return the list of child :class:`.Container` objects.
158 """
159 return []
162if TYPE_CHECKING:
164 class MagicContainer(Protocol):
165 """
166 Any object that implements ``__pt_container__`` represents a container.
167 """
169 def __pt_container__(self) -> AnyContainer:
170 ...
173AnyContainer = Union[Container, "MagicContainer"]
176def _window_too_small() -> Window:
177 "Create a `Window` that displays the 'Window too small' text."
178 return Window(
179 FormattedTextControl(text=[("class:window-too-small", " Window too small... ")])
180 )
183class VerticalAlign(Enum):
184 "Alignment for `HSplit`."
185 TOP = "TOP"
186 CENTER = "CENTER"
187 BOTTOM = "BOTTOM"
188 JUSTIFY = "JUSTIFY"
191class HorizontalAlign(Enum):
192 "Alignment for `VSplit`."
193 LEFT = "LEFT"
194 CENTER = "CENTER"
195 RIGHT = "RIGHT"
196 JUSTIFY = "JUSTIFY"
199class _Split(Container):
200 """
201 The common parts of `VSplit` and `HSplit`.
202 """
204 def __init__(
205 self,
206 children: Sequence[AnyContainer],
207 window_too_small: Container | None = None,
208 padding: AnyDimension = Dimension.exact(0),
209 padding_char: str | None = None,
210 padding_style: str = "",
211 width: AnyDimension = None,
212 height: AnyDimension = None,
213 z_index: int | None = None,
214 modal: bool = False,
215 key_bindings: KeyBindingsBase | None = None,
216 style: str | Callable[[], str] = "",
217 ) -> None:
218 self.children = [to_container(c) for c in children]
219 self.window_too_small = window_too_small or _window_too_small()
220 self.padding = padding
221 self.padding_char = padding_char
222 self.padding_style = padding_style
224 self.width = width
225 self.height = height
226 self.z_index = z_index
228 self.modal = modal
229 self.key_bindings = key_bindings
230 self.style = style
232 def is_modal(self) -> bool:
233 return self.modal
235 def get_key_bindings(self) -> KeyBindingsBase | None:
236 return self.key_bindings
238 def get_children(self) -> list[Container]:
239 return self.children
242class HSplit(_Split):
243 """
244 Several layouts, one stacked above/under the other. ::
246 +--------------------+
247 | |
248 +--------------------+
249 | |
250 +--------------------+
252 By default, this doesn't display a horizontal line between the children,
253 but if this is something you need, then create a HSplit as follows::
255 HSplit(children=[ ... ], padding_char='-',
256 padding=1, padding_style='#ffff00')
258 :param children: List of child :class:`.Container` objects.
259 :param window_too_small: A :class:`.Container` object that is displayed if
260 there is not enough space for all the children. By default, this is a
261 "Window too small" message.
262 :param align: `VerticalAlign` value.
263 :param width: When given, use this width instead of looking at the children.
264 :param height: When given, use this height instead of looking at the children.
265 :param z_index: (int or None) When specified, this can be used to bring
266 element in front of floating elements. `None` means: inherit from parent.
267 :param style: A style string.
268 :param modal: ``True`` or ``False``.
269 :param key_bindings: ``None`` or a :class:`.KeyBindings` object.
271 :param padding: (`Dimension` or int), size to be used for the padding.
272 :param padding_char: Character to be used for filling in the padding.
273 :param padding_style: Style to applied to the padding.
274 """
276 def __init__(
277 self,
278 children: Sequence[AnyContainer],
279 window_too_small: Container | None = None,
280 align: VerticalAlign = VerticalAlign.JUSTIFY,
281 padding: AnyDimension = 0,
282 padding_char: str | None = None,
283 padding_style: str = "",
284 width: AnyDimension = None,
285 height: AnyDimension = None,
286 z_index: int | None = None,
287 modal: bool = False,
288 key_bindings: KeyBindingsBase | None = None,
289 style: str | Callable[[], str] = "",
290 ) -> None:
291 super().__init__(
292 children=children,
293 window_too_small=window_too_small,
294 padding=padding,
295 padding_char=padding_char,
296 padding_style=padding_style,
297 width=width,
298 height=height,
299 z_index=z_index,
300 modal=modal,
301 key_bindings=key_bindings,
302 style=style,
303 )
305 self.align = align
307 self._children_cache: SimpleCache[
308 tuple[Container, ...], list[Container]
309 ] = SimpleCache(maxsize=1)
310 self._remaining_space_window = Window() # Dummy window.
312 def preferred_width(self, max_available_width: int) -> Dimension:
313 if self.width is not None:
314 return to_dimension(self.width)
316 if self.children:
317 dimensions = [c.preferred_width(max_available_width) for c in self.children]
318 return max_layout_dimensions(dimensions)
319 else:
320 return Dimension()
322 def preferred_height(self, width: int, max_available_height: int) -> Dimension:
323 if self.height is not None:
324 return to_dimension(self.height)
326 dimensions = [
327 c.preferred_height(width, max_available_height) for c in self._all_children
328 ]
329 return sum_layout_dimensions(dimensions)
331 def reset(self) -> None:
332 for c in self.children:
333 c.reset()
335 @property
336 def _all_children(self) -> list[Container]:
337 """
338 List of child objects, including padding.
339 """
341 def get() -> list[Container]:
342 result: list[Container] = []
344 # Padding Top.
345 if self.align in (VerticalAlign.CENTER, VerticalAlign.BOTTOM):
346 result.append(Window(width=Dimension(preferred=0)))
348 # The children with padding.
349 for child in self.children:
350 result.append(child)
351 result.append(
352 Window(
353 height=self.padding,
354 char=self.padding_char,
355 style=self.padding_style,
356 )
357 )
358 if result:
359 result.pop()
361 # Padding right.
362 if self.align in (VerticalAlign.CENTER, VerticalAlign.TOP):
363 result.append(Window(width=Dimension(preferred=0)))
365 return result
367 return self._children_cache.get(tuple(self.children), get)
369 def write_to_screen(
370 self,
371 screen: Screen,
372 mouse_handlers: MouseHandlers,
373 write_position: WritePosition,
374 parent_style: str,
375 erase_bg: bool,
376 z_index: int | None,
377 ) -> None:
378 """
379 Render the prompt to a `Screen` instance.
381 :param screen: The :class:`~prompt_toolkit.layout.screen.Screen` class
382 to which the output has to be written.
383 """
384 sizes = self._divide_heights(write_position)
385 style = parent_style + " " + to_str(self.style)
386 z_index = z_index if self.z_index is None else self.z_index
388 if sizes is None:
389 self.window_too_small.write_to_screen(
390 screen, mouse_handlers, write_position, style, erase_bg, z_index
391 )
392 else:
393 #
394 ypos = write_position.ypos
395 xpos = write_position.xpos
396 width = write_position.width
398 # Draw child panes.
399 for s, c in zip(sizes, self._all_children):
400 c.write_to_screen(
401 screen,
402 mouse_handlers,
403 WritePosition(xpos, ypos, width, s),
404 style,
405 erase_bg,
406 z_index,
407 )
408 ypos += s
410 # Fill in the remaining space. This happens when a child control
411 # refuses to take more space and we don't have any padding. Adding a
412 # dummy child control for this (in `self._all_children`) is not
413 # desired, because in some situations, it would take more space, even
414 # when it's not required. This is required to apply the styling.
415 remaining_height = write_position.ypos + write_position.height - ypos
416 if remaining_height > 0:
417 self._remaining_space_window.write_to_screen(
418 screen,
419 mouse_handlers,
420 WritePosition(xpos, ypos, width, remaining_height),
421 style,
422 erase_bg,
423 z_index,
424 )
426 def _divide_heights(self, write_position: WritePosition) -> list[int] | None:
427 """
428 Return the heights for all rows.
429 Or None when there is not enough space.
430 """
431 if not self.children:
432 return []
434 width = write_position.width
435 height = write_position.height
437 # Calculate heights.
438 dimensions = [c.preferred_height(width, height) for c in self._all_children]
440 # Sum dimensions
441 sum_dimensions = sum_layout_dimensions(dimensions)
443 # If there is not enough space for both.
444 # Don't do anything.
445 if sum_dimensions.min > height:
446 return None
448 # Find optimal sizes. (Start with minimal size, increase until we cover
449 # the whole height.)
450 sizes = [d.min for d in dimensions]
452 child_generator = take_using_weights(
453 items=list(range(len(dimensions))), weights=[d.weight for d in dimensions]
454 )
456 i = next(child_generator)
458 # Increase until we meet at least the 'preferred' size.
459 preferred_stop = min(height, sum_dimensions.preferred)
460 preferred_dimensions = [d.preferred for d in dimensions]
462 while sum(sizes) < preferred_stop:
463 if sizes[i] < preferred_dimensions[i]:
464 sizes[i] += 1
465 i = next(child_generator)
467 # Increase until we use all the available space. (or until "max")
468 if not get_app().is_done:
469 max_stop = min(height, sum_dimensions.max)
470 max_dimensions = [d.max for d in dimensions]
472 while sum(sizes) < max_stop:
473 if sizes[i] < max_dimensions[i]:
474 sizes[i] += 1
475 i = next(child_generator)
477 return sizes
480class VSplit(_Split):
481 """
482 Several layouts, one stacked left/right of the other. ::
484 +---------+----------+
485 | | |
486 | | |
487 +---------+----------+
489 By default, this doesn't display a vertical line between the children, but
490 if this is something you need, then create a HSplit as follows::
492 VSplit(children=[ ... ], padding_char='|',
493 padding=1, padding_style='#ffff00')
495 :param children: List of child :class:`.Container` objects.
496 :param window_too_small: A :class:`.Container` object that is displayed if
497 there is not enough space for all the children. By default, this is a
498 "Window too small" message.
499 :param align: `HorizontalAlign` value.
500 :param width: When given, use this width instead of looking at the children.
501 :param height: When given, use this height instead of looking at the children.
502 :param z_index: (int or None) When specified, this can be used to bring
503 element in front of floating elements. `None` means: inherit from parent.
504 :param style: A style string.
505 :param modal: ``True`` or ``False``.
506 :param key_bindings: ``None`` or a :class:`.KeyBindings` object.
508 :param padding: (`Dimension` or int), size to be used for the padding.
509 :param padding_char: Character to be used for filling in the padding.
510 :param padding_style: Style to applied to the padding.
511 """
513 def __init__(
514 self,
515 children: Sequence[AnyContainer],
516 window_too_small: Container | None = None,
517 align: HorizontalAlign = HorizontalAlign.JUSTIFY,
518 padding: AnyDimension = 0,
519 padding_char: str | None = None,
520 padding_style: str = "",
521 width: AnyDimension = None,
522 height: AnyDimension = None,
523 z_index: int | None = None,
524 modal: bool = False,
525 key_bindings: KeyBindingsBase | None = None,
526 style: str | Callable[[], str] = "",
527 ) -> None:
528 super().__init__(
529 children=children,
530 window_too_small=window_too_small,
531 padding=padding,
532 padding_char=padding_char,
533 padding_style=padding_style,
534 width=width,
535 height=height,
536 z_index=z_index,
537 modal=modal,
538 key_bindings=key_bindings,
539 style=style,
540 )
542 self.align = align
544 self._children_cache: SimpleCache[
545 tuple[Container, ...], list[Container]
546 ] = SimpleCache(maxsize=1)
547 self._remaining_space_window = Window() # Dummy window.
549 def preferred_width(self, max_available_width: int) -> Dimension:
550 if self.width is not None:
551 return to_dimension(self.width)
553 dimensions = [
554 c.preferred_width(max_available_width) for c in self._all_children
555 ]
557 return sum_layout_dimensions(dimensions)
559 def preferred_height(self, width: int, max_available_height: int) -> Dimension:
560 if self.height is not None:
561 return to_dimension(self.height)
563 # At the point where we want to calculate the heights, the widths have
564 # already been decided. So we can trust `width` to be the actual
565 # `width` that's going to be used for the rendering. So,
566 # `divide_widths` is supposed to use all of the available width.
567 # Using only the `preferred` width caused a bug where the reported
568 # height was more than required. (we had a `BufferControl` which did
569 # wrap lines because of the smaller width returned by `_divide_widths`.
571 sizes = self._divide_widths(width)
572 children = self._all_children
574 if sizes is None:
575 return Dimension()
576 else:
577 dimensions = [
578 c.preferred_height(s, max_available_height)
579 for s, c in zip(sizes, children)
580 ]
581 return max_layout_dimensions(dimensions)
583 def reset(self) -> None:
584 for c in self.children:
585 c.reset()
587 @property
588 def _all_children(self) -> list[Container]:
589 """
590 List of child objects, including padding.
591 """
593 def get() -> list[Container]:
594 result: list[Container] = []
596 # Padding left.
597 if self.align in (HorizontalAlign.CENTER, HorizontalAlign.RIGHT):
598 result.append(Window(width=Dimension(preferred=0)))
600 # The children with padding.
601 for child in self.children:
602 result.append(child)
603 result.append(
604 Window(
605 width=self.padding,
606 char=self.padding_char,
607 style=self.padding_style,
608 )
609 )
610 if result:
611 result.pop()
613 # Padding right.
614 if self.align in (HorizontalAlign.CENTER, HorizontalAlign.LEFT):
615 result.append(Window(width=Dimension(preferred=0)))
617 return result
619 return self._children_cache.get(tuple(self.children), get)
621 def _divide_widths(self, width: int) -> list[int] | None:
622 """
623 Return the widths for all columns.
624 Or None when there is not enough space.
625 """
626 children = self._all_children
628 if not children:
629 return []
631 # Calculate widths.
632 dimensions = [c.preferred_width(width) for c in children]
633 preferred_dimensions = [d.preferred for d in dimensions]
635 # Sum dimensions
636 sum_dimensions = sum_layout_dimensions(dimensions)
638 # If there is not enough space for both.
639 # Don't do anything.
640 if sum_dimensions.min > width:
641 return None
643 # Find optimal sizes. (Start with minimal size, increase until we cover
644 # the whole width.)
645 sizes = [d.min for d in dimensions]
647 child_generator = take_using_weights(
648 items=list(range(len(dimensions))), weights=[d.weight for d in dimensions]
649 )
651 i = next(child_generator)
653 # Increase until we meet at least the 'preferred' size.
654 preferred_stop = min(width, sum_dimensions.preferred)
656 while sum(sizes) < preferred_stop:
657 if sizes[i] < preferred_dimensions[i]:
658 sizes[i] += 1
659 i = next(child_generator)
661 # Increase until we use all the available space.
662 max_dimensions = [d.max for d in dimensions]
663 max_stop = min(width, sum_dimensions.max)
665 while sum(sizes) < max_stop:
666 if sizes[i] < max_dimensions[i]:
667 sizes[i] += 1
668 i = next(child_generator)
670 return sizes
672 def write_to_screen(
673 self,
674 screen: Screen,
675 mouse_handlers: MouseHandlers,
676 write_position: WritePosition,
677 parent_style: str,
678 erase_bg: bool,
679 z_index: int | None,
680 ) -> None:
681 """
682 Render the prompt to a `Screen` instance.
684 :param screen: The :class:`~prompt_toolkit.layout.screen.Screen` class
685 to which the output has to be written.
686 """
687 if not self.children:
688 return
690 children = self._all_children
691 sizes = self._divide_widths(write_position.width)
692 style = parent_style + " " + to_str(self.style)
693 z_index = z_index if self.z_index is None else self.z_index
695 # If there is not enough space.
696 if sizes is None:
697 self.window_too_small.write_to_screen(
698 screen, mouse_handlers, write_position, style, erase_bg, z_index
699 )
700 return
702 # Calculate heights, take the largest possible, but not larger than
703 # write_position.height.
704 heights = [
705 child.preferred_height(width, write_position.height).preferred
706 for width, child in zip(sizes, children)
707 ]
708 height = max(write_position.height, min(write_position.height, max(heights)))
710 #
711 ypos = write_position.ypos
712 xpos = write_position.xpos
714 # Draw all child panes.
715 for s, c in zip(sizes, children):
716 c.write_to_screen(
717 screen,
718 mouse_handlers,
719 WritePosition(xpos, ypos, s, height),
720 style,
721 erase_bg,
722 z_index,
723 )
724 xpos += s
726 # Fill in the remaining space. This happens when a child control
727 # refuses to take more space and we don't have any padding. Adding a
728 # dummy child control for this (in `self._all_children`) is not
729 # desired, because in some situations, it would take more space, even
730 # when it's not required. This is required to apply the styling.
731 remaining_width = write_position.xpos + write_position.width - xpos
732 if remaining_width > 0:
733 self._remaining_space_window.write_to_screen(
734 screen,
735 mouse_handlers,
736 WritePosition(xpos, ypos, remaining_width, height),
737 style,
738 erase_bg,
739 z_index,
740 )
743class FloatContainer(Container):
744 """
745 Container which can contain another container for the background, as well
746 as a list of floating containers on top of it.
748 Example Usage::
750 FloatContainer(content=Window(...),
751 floats=[
752 Float(xcursor=True,
753 ycursor=True,
754 content=CompletionsMenu(...))
755 ])
757 :param z_index: (int or None) When specified, this can be used to bring
758 element in front of floating elements. `None` means: inherit from parent.
759 This is the z_index for the whole `Float` container as a whole.
760 """
762 def __init__(
763 self,
764 content: AnyContainer,
765 floats: list[Float],
766 modal: bool = False,
767 key_bindings: KeyBindingsBase | None = None,
768 style: str | Callable[[], str] = "",
769 z_index: int | None = None,
770 ) -> None:
771 self.content = to_container(content)
772 self.floats = floats
774 self.modal = modal
775 self.key_bindings = key_bindings
776 self.style = style
777 self.z_index = z_index
779 def reset(self) -> None:
780 self.content.reset()
782 for f in self.floats:
783 f.content.reset()
785 def preferred_width(self, max_available_width: int) -> Dimension:
786 return self.content.preferred_width(max_available_width)
788 def preferred_height(self, width: int, max_available_height: int) -> Dimension:
789 """
790 Return the preferred height of the float container.
791 (We don't care about the height of the floats, they should always fit
792 into the dimensions provided by the container.)
793 """
794 return self.content.preferred_height(width, max_available_height)
796 def write_to_screen(
797 self,
798 screen: Screen,
799 mouse_handlers: MouseHandlers,
800 write_position: WritePosition,
801 parent_style: str,
802 erase_bg: bool,
803 z_index: int | None,
804 ) -> None:
805 style = parent_style + " " + to_str(self.style)
806 z_index = z_index if self.z_index is None else self.z_index
808 self.content.write_to_screen(
809 screen, mouse_handlers, write_position, style, erase_bg, z_index
810 )
812 for number, fl in enumerate(self.floats):
813 # z_index of a Float is computed by summing the z_index of the
814 # container and the `Float`.
815 new_z_index = (z_index or 0) + fl.z_index
816 style = parent_style + " " + to_str(self.style)
818 # If the float that we have here, is positioned relative to the
819 # cursor position, but the Window that specifies the cursor
820 # position is not drawn yet, because it's a Float itself, we have
821 # to postpone this calculation. (This is a work-around, but good
822 # enough for now.)
823 postpone = fl.xcursor is not None or fl.ycursor is not None
825 if postpone:
826 new_z_index = (
827 number + 10**8
828 ) # Draw as late as possible, but keep the order.
829 screen.draw_with_z_index(
830 z_index=new_z_index,
831 draw_func=partial(
832 self._draw_float,
833 fl,
834 screen,
835 mouse_handlers,
836 write_position,
837 style,
838 erase_bg,
839 new_z_index,
840 ),
841 )
842 else:
843 self._draw_float(
844 fl,
845 screen,
846 mouse_handlers,
847 write_position,
848 style,
849 erase_bg,
850 new_z_index,
851 )
853 def _draw_float(
854 self,
855 fl: Float,
856 screen: Screen,
857 mouse_handlers: MouseHandlers,
858 write_position: WritePosition,
859 style: str,
860 erase_bg: bool,
861 z_index: int | None,
862 ) -> None:
863 "Draw a single Float."
864 # When a menu_position was given, use this instead of the cursor
865 # position. (These cursor positions are absolute, translate again
866 # relative to the write_position.)
867 # Note: This should be inside the for-loop, because one float could
868 # set the cursor position to be used for the next one.
869 cpos = screen.get_menu_position(
870 fl.attach_to_window or get_app().layout.current_window
871 )
872 cursor_position = Point(
873 x=cpos.x - write_position.xpos, y=cpos.y - write_position.ypos
874 )
876 fl_width = fl.get_width()
877 fl_height = fl.get_height()
878 width: int
879 height: int
880 xpos: int
881 ypos: int
883 # Left & width given.
884 if fl.left is not None and fl_width is not None:
885 xpos = fl.left
886 width = fl_width
887 # Left & right given -> calculate width.
888 elif fl.left is not None and fl.right is not None:
889 xpos = fl.left
890 width = write_position.width - fl.left - fl.right
891 # Width & right given -> calculate left.
892 elif fl_width is not None and fl.right is not None:
893 xpos = write_position.width - fl.right - fl_width
894 width = fl_width
895 # Near x position of cursor.
896 elif fl.xcursor:
897 if fl_width is None:
898 width = fl.content.preferred_width(write_position.width).preferred
899 width = min(write_position.width, width)
900 else:
901 width = fl_width
903 xpos = cursor_position.x
904 if xpos + width > write_position.width:
905 xpos = max(0, write_position.width - width)
906 # Only width given -> center horizontally.
907 elif fl_width:
908 xpos = int((write_position.width - fl_width) / 2)
909 width = fl_width
910 # Otherwise, take preferred width from float content.
911 else:
912 width = fl.content.preferred_width(write_position.width).preferred
914 if fl.left is not None:
915 xpos = fl.left
916 elif fl.right is not None:
917 xpos = max(0, write_position.width - width - fl.right)
918 else: # Center horizontally.
919 xpos = max(0, int((write_position.width - width) / 2))
921 # Trim.
922 width = min(width, write_position.width - xpos)
924 # Top & height given.
925 if fl.top is not None and fl_height is not None:
926 ypos = fl.top
927 height = fl_height
928 # Top & bottom given -> calculate height.
929 elif fl.top is not None and fl.bottom is not None:
930 ypos = fl.top
931 height = write_position.height - fl.top - fl.bottom
932 # Height & bottom given -> calculate top.
933 elif fl_height is not None and fl.bottom is not None:
934 ypos = write_position.height - fl_height - fl.bottom
935 height = fl_height
936 # Near cursor.
937 elif fl.ycursor:
938 ypos = cursor_position.y + (0 if fl.allow_cover_cursor else 1)
940 if fl_height is None:
941 height = fl.content.preferred_height(
942 width, write_position.height
943 ).preferred
944 else:
945 height = fl_height
947 # Reduce height if not enough space. (We can use the height
948 # when the content requires it.)
949 if height > write_position.height - ypos:
950 if write_position.height - ypos + 1 >= ypos:
951 # When the space below the cursor is more than
952 # the space above, just reduce the height.
953 height = write_position.height - ypos
954 else:
955 # Otherwise, fit the float above the cursor.
956 height = min(height, cursor_position.y)
957 ypos = cursor_position.y - height
959 # Only height given -> center vertically.
960 elif fl_height:
961 ypos = int((write_position.height - fl_height) / 2)
962 height = fl_height
963 # Otherwise, take preferred height from content.
964 else:
965 height = fl.content.preferred_height(width, write_position.height).preferred
967 if fl.top is not None:
968 ypos = fl.top
969 elif fl.bottom is not None:
970 ypos = max(0, write_position.height - height - fl.bottom)
971 else: # Center vertically.
972 ypos = max(0, int((write_position.height - height) / 2))
974 # Trim.
975 height = min(height, write_position.height - ypos)
977 # Write float.
978 # (xpos and ypos can be negative: a float can be partially visible.)
979 if height > 0 and width > 0:
980 wp = WritePosition(
981 xpos=xpos + write_position.xpos,
982 ypos=ypos + write_position.ypos,
983 width=width,
984 height=height,
985 )
987 if not fl.hide_when_covering_content or self._area_is_empty(screen, wp):
988 fl.content.write_to_screen(
989 screen,
990 mouse_handlers,
991 wp,
992 style,
993 erase_bg=not fl.transparent(),
994 z_index=z_index,
995 )
997 def _area_is_empty(self, screen: Screen, write_position: WritePosition) -> bool:
998 """
999 Return True when the area below the write position is still empty.
1000 (For floats that should not hide content underneath.)
1001 """
1002 wp = write_position
1004 for y in range(wp.ypos, wp.ypos + wp.height):
1005 if y in screen.data_buffer:
1006 row = screen.data_buffer[y]
1008 for x in range(wp.xpos, wp.xpos + wp.width):
1009 c = row[x]
1010 if c.char != " ":
1011 return False
1013 return True
1015 def is_modal(self) -> bool:
1016 return self.modal
1018 def get_key_bindings(self) -> KeyBindingsBase | None:
1019 return self.key_bindings
1021 def get_children(self) -> list[Container]:
1022 children = [self.content]
1023 children.extend(f.content for f in self.floats)
1024 return children
1027class Float:
1028 """
1029 Float for use in a :class:`.FloatContainer`.
1030 Except for the `content` parameter, all other options are optional.
1032 :param content: :class:`.Container` instance.
1034 :param width: :class:`.Dimension` or callable which returns a :class:`.Dimension`.
1035 :param height: :class:`.Dimension` or callable which returns a :class:`.Dimension`.
1037 :param left: Distance to the left edge of the :class:`.FloatContainer`.
1038 :param right: Distance to the right edge of the :class:`.FloatContainer`.
1039 :param top: Distance to the top of the :class:`.FloatContainer`.
1040 :param bottom: Distance to the bottom of the :class:`.FloatContainer`.
1042 :param attach_to_window: Attach to the cursor from this window, instead of
1043 the current window.
1044 :param hide_when_covering_content: Hide the float when it covers content underneath.
1045 :param allow_cover_cursor: When `False`, make sure to display the float
1046 below the cursor. Not on top of the indicated position.
1047 :param z_index: Z-index position. For a Float, this needs to be at least
1048 one. It is relative to the z_index of the parent container.
1049 :param transparent: :class:`.Filter` indicating whether this float needs to be
1050 drawn transparently.
1051 """
1053 def __init__(
1054 self,
1055 content: AnyContainer,
1056 top: int | None = None,
1057 right: int | None = None,
1058 bottom: int | None = None,
1059 left: int | None = None,
1060 width: int | Callable[[], int] | None = None,
1061 height: int | Callable[[], int] | None = None,
1062 xcursor: bool = False,
1063 ycursor: bool = False,
1064 attach_to_window: AnyContainer | None = None,
1065 hide_when_covering_content: bool = False,
1066 allow_cover_cursor: bool = False,
1067 z_index: int = 1,
1068 transparent: bool = False,
1069 ) -> None:
1070 assert z_index >= 1
1072 self.left = left
1073 self.right = right
1074 self.top = top
1075 self.bottom = bottom
1077 self.width = width
1078 self.height = height
1080 self.xcursor = xcursor
1081 self.ycursor = ycursor
1083 self.attach_to_window = (
1084 to_window(attach_to_window) if attach_to_window else None
1085 )
1087 self.content = to_container(content)
1088 self.hide_when_covering_content = hide_when_covering_content
1089 self.allow_cover_cursor = allow_cover_cursor
1090 self.z_index = z_index
1091 self.transparent = to_filter(transparent)
1093 def get_width(self) -> int | None:
1094 if callable(self.width):
1095 return self.width()
1096 return self.width
1098 def get_height(self) -> int | None:
1099 if callable(self.height):
1100 return self.height()
1101 return self.height
1103 def __repr__(self) -> str:
1104 return "Float(content=%r)" % self.content
1107class WindowRenderInfo:
1108 """
1109 Render information for the last render time of this control.
1110 It stores mapping information between the input buffers (in case of a
1111 :class:`~prompt_toolkit.layout.controls.BufferControl`) and the actual
1112 render position on the output screen.
1114 (Could be used for implementation of the Vi 'H' and 'L' key bindings as
1115 well as implementing mouse support.)
1117 :param ui_content: The original :class:`.UIContent` instance that contains
1118 the whole input, without clipping. (ui_content)
1119 :param horizontal_scroll: The horizontal scroll of the :class:`.Window` instance.
1120 :param vertical_scroll: The vertical scroll of the :class:`.Window` instance.
1121 :param window_width: The width of the window that displays the content,
1122 without the margins.
1123 :param window_height: The height of the window that displays the content.
1124 :param configured_scroll_offsets: The scroll offsets as configured for the
1125 :class:`Window` instance.
1126 :param visible_line_to_row_col: Mapping that maps the row numbers on the
1127 displayed screen (starting from zero for the first visible line) to
1128 (row, col) tuples pointing to the row and column of the :class:`.UIContent`.
1129 :param rowcol_to_yx: Mapping that maps (row, column) tuples representing
1130 coordinates of the :class:`UIContent` to (y, x) absolute coordinates at
1131 the rendered screen.
1132 """
1134 def __init__(
1135 self,
1136 window: Window,
1137 ui_content: UIContent,
1138 horizontal_scroll: int,
1139 vertical_scroll: int,
1140 window_width: int,
1141 window_height: int,
1142 configured_scroll_offsets: ScrollOffsets,
1143 visible_line_to_row_col: dict[int, tuple[int, int]],
1144 rowcol_to_yx: dict[tuple[int, int], tuple[int, int]],
1145 x_offset: int,
1146 y_offset: int,
1147 wrap_lines: bool,
1148 ) -> None:
1149 self.window = window
1150 self.ui_content = ui_content
1151 self.vertical_scroll = vertical_scroll
1152 self.window_width = window_width # Width without margins.
1153 self.window_height = window_height
1155 self.configured_scroll_offsets = configured_scroll_offsets
1156 self.visible_line_to_row_col = visible_line_to_row_col
1157 self.wrap_lines = wrap_lines
1159 self._rowcol_to_yx = rowcol_to_yx # row/col from input to absolute y/x
1160 # screen coordinates.
1161 self._x_offset = x_offset
1162 self._y_offset = y_offset
1164 @property
1165 def visible_line_to_input_line(self) -> dict[int, int]:
1166 return {
1167 visible_line: rowcol[0]
1168 for visible_line, rowcol in self.visible_line_to_row_col.items()
1169 }
1171 @property
1172 def cursor_position(self) -> Point:
1173 """
1174 Return the cursor position coordinates, relative to the left/top corner
1175 of the rendered screen.
1176 """
1177 cpos = self.ui_content.cursor_position
1178 try:
1179 y, x = self._rowcol_to_yx[cpos.y, cpos.x]
1180 except KeyError:
1181 # For `DummyControl` for instance, the content can be empty, and so
1182 # will `_rowcol_to_yx` be. Return 0/0 by default.
1183 return Point(x=0, y=0)
1184 else:
1185 return Point(x=x - self._x_offset, y=y - self._y_offset)
1187 @property
1188 def applied_scroll_offsets(self) -> ScrollOffsets:
1189 """
1190 Return a :class:`.ScrollOffsets` instance that indicates the actual
1191 offset. This can be less than or equal to what's configured. E.g, when
1192 the cursor is completely at the top, the top offset will be zero rather
1193 than what's configured.
1194 """
1195 if self.displayed_lines[0] == 0:
1196 top = 0
1197 else:
1198 # Get row where the cursor is displayed.
1199 y = self.input_line_to_visible_line[self.ui_content.cursor_position.y]
1200 top = min(y, self.configured_scroll_offsets.top)
1202 return ScrollOffsets(
1203 top=top,
1204 bottom=min(
1205 self.ui_content.line_count - self.displayed_lines[-1] - 1,
1206 self.configured_scroll_offsets.bottom,
1207 ),
1208 # For left/right, it probably doesn't make sense to return something.
1209 # (We would have to calculate the widths of all the lines and keep
1210 # double width characters in mind.)
1211 left=0,
1212 right=0,
1213 )
1215 @property
1216 def displayed_lines(self) -> list[int]:
1217 """
1218 List of all the visible rows. (Line numbers of the input buffer.)
1219 The last line may not be entirely visible.
1220 """
1221 return sorted(row for row, col in self.visible_line_to_row_col.values())
1223 @property
1224 def input_line_to_visible_line(self) -> dict[int, int]:
1225 """
1226 Return the dictionary mapping the line numbers of the input buffer to
1227 the lines of the screen. When a line spans several rows at the screen,
1228 the first row appears in the dictionary.
1229 """
1230 result: dict[int, int] = {}
1231 for k, v in self.visible_line_to_input_line.items():
1232 if v in result:
1233 result[v] = min(result[v], k)
1234 else:
1235 result[v] = k
1236 return result
1238 def first_visible_line(self, after_scroll_offset: bool = False) -> int:
1239 """
1240 Return the line number (0 based) of the input document that corresponds
1241 with the first visible line.
1242 """
1243 if after_scroll_offset:
1244 return self.displayed_lines[self.applied_scroll_offsets.top]
1245 else:
1246 return self.displayed_lines[0]
1248 def last_visible_line(self, before_scroll_offset: bool = False) -> int:
1249 """
1250 Like `first_visible_line`, but for the last visible line.
1251 """
1252 if before_scroll_offset:
1253 return self.displayed_lines[-1 - self.applied_scroll_offsets.bottom]
1254 else:
1255 return self.displayed_lines[-1]
1257 def center_visible_line(
1258 self, before_scroll_offset: bool = False, after_scroll_offset: bool = False
1259 ) -> int:
1260 """
1261 Like `first_visible_line`, but for the center visible line.
1262 """
1263 return (
1264 self.first_visible_line(after_scroll_offset)
1265 + (
1266 self.last_visible_line(before_scroll_offset)
1267 - self.first_visible_line(after_scroll_offset)
1268 )
1269 // 2
1270 )
1272 @property
1273 def content_height(self) -> int:
1274 """
1275 The full height of the user control.
1276 """
1277 return self.ui_content.line_count
1279 @property
1280 def full_height_visible(self) -> bool:
1281 """
1282 True when the full height is visible (There is no vertical scroll.)
1283 """
1284 return (
1285 self.vertical_scroll == 0
1286 and self.last_visible_line() == self.content_height
1287 )
1289 @property
1290 def top_visible(self) -> bool:
1291 """
1292 True when the top of the buffer is visible.
1293 """
1294 return self.vertical_scroll == 0
1296 @property
1297 def bottom_visible(self) -> bool:
1298 """
1299 True when the bottom of the buffer is visible.
1300 """
1301 return self.last_visible_line() == self.content_height - 1
1303 @property
1304 def vertical_scroll_percentage(self) -> int:
1305 """
1306 Vertical scroll as a percentage. (0 means: the top is visible,
1307 100 means: the bottom is visible.)
1308 """
1309 if self.bottom_visible:
1310 return 100
1311 else:
1312 return 100 * self.vertical_scroll // self.content_height
1314 def get_height_for_line(self, lineno: int) -> int:
1315 """
1316 Return the height of the given line.
1317 (The height that it would take, if this line became visible.)
1318 """
1319 if self.wrap_lines:
1320 return self.ui_content.get_height_for_line(
1321 lineno, self.window_width, self.window.get_line_prefix
1322 )
1323 else:
1324 return 1
1327class ScrollOffsets:
1328 """
1329 Scroll offsets for the :class:`.Window` class.
1331 Note that left/right offsets only make sense if line wrapping is disabled.
1332 """
1334 def __init__(
1335 self,
1336 top: int | Callable[[], int] = 0,
1337 bottom: int | Callable[[], int] = 0,
1338 left: int | Callable[[], int] = 0,
1339 right: int | Callable[[], int] = 0,
1340 ) -> None:
1341 self._top = top
1342 self._bottom = bottom
1343 self._left = left
1344 self._right = right
1346 @property
1347 def top(self) -> int:
1348 return to_int(self._top)
1350 @property
1351 def bottom(self) -> int:
1352 return to_int(self._bottom)
1354 @property
1355 def left(self) -> int:
1356 return to_int(self._left)
1358 @property
1359 def right(self) -> int:
1360 return to_int(self._right)
1362 def __repr__(self) -> str:
1363 return "ScrollOffsets(top={!r}, bottom={!r}, left={!r}, right={!r})".format(
1364 self._top,
1365 self._bottom,
1366 self._left,
1367 self._right,
1368 )
1371class ColorColumn:
1372 """
1373 Column for a :class:`.Window` to be colored.
1374 """
1376 def __init__(self, position: int, style: str = "class:color-column") -> None:
1377 self.position = position
1378 self.style = style
1381_in_insert_mode = vi_insert_mode | emacs_insert_mode
1384class WindowAlign(Enum):
1385 """
1386 Alignment of the Window content.
1388 Note that this is different from `HorizontalAlign` and `VerticalAlign`,
1389 which are used for the alignment of the child containers in respectively
1390 `VSplit` and `HSplit`.
1391 """
1393 LEFT = "LEFT"
1394 RIGHT = "RIGHT"
1395 CENTER = "CENTER"
1398class Window(Container):
1399 """
1400 Container that holds a control.
1402 :param content: :class:`.UIControl` instance.
1403 :param width: :class:`.Dimension` instance or callable.
1404 :param height: :class:`.Dimension` instance or callable.
1405 :param z_index: When specified, this can be used to bring element in front
1406 of floating elements.
1407 :param dont_extend_width: When `True`, don't take up more width then the
1408 preferred width reported by the control.
1409 :param dont_extend_height: When `True`, don't take up more width then the
1410 preferred height reported by the control.
1411 :param ignore_content_width: A `bool` or :class:`.Filter` instance. Ignore
1412 the :class:`.UIContent` width when calculating the dimensions.
1413 :param ignore_content_height: A `bool` or :class:`.Filter` instance. Ignore
1414 the :class:`.UIContent` height when calculating the dimensions.
1415 :param left_margins: A list of :class:`.Margin` instance to be displayed on
1416 the left. For instance: :class:`~prompt_toolkit.layout.NumberedMargin`
1417 can be one of them in order to show line numbers.
1418 :param right_margins: Like `left_margins`, but on the other side.
1419 :param scroll_offsets: :class:`.ScrollOffsets` instance, representing the
1420 preferred amount of lines/columns to be always visible before/after the
1421 cursor. When both top and bottom are a very high number, the cursor
1422 will be centered vertically most of the time.
1423 :param allow_scroll_beyond_bottom: A `bool` or
1424 :class:`.Filter` instance. When True, allow scrolling so far, that the
1425 top part of the content is not visible anymore, while there is still
1426 empty space available at the bottom of the window. In the Vi editor for
1427 instance, this is possible. You will see tildes while the top part of
1428 the body is hidden.
1429 :param wrap_lines: A `bool` or :class:`.Filter` instance. When True, don't
1430 scroll horizontally, but wrap lines instead.
1431 :param get_vertical_scroll: Callable that takes this window
1432 instance as input and returns a preferred vertical scroll.
1433 (When this is `None`, the scroll is only determined by the last and
1434 current cursor position.)
1435 :param get_horizontal_scroll: Callable that takes this window
1436 instance as input and returns a preferred vertical scroll.
1437 :param always_hide_cursor: A `bool` or
1438 :class:`.Filter` instance. When True, never display the cursor, even
1439 when the user control specifies a cursor position.
1440 :param cursorline: A `bool` or :class:`.Filter` instance. When True,
1441 display a cursorline.
1442 :param cursorcolumn: A `bool` or :class:`.Filter` instance. When True,
1443 display a cursorcolumn.
1444 :param colorcolumns: A list of :class:`.ColorColumn` instances that
1445 describe the columns to be highlighted, or a callable that returns such
1446 a list.
1447 :param align: :class:`.WindowAlign` value or callable that returns an
1448 :class:`.WindowAlign` value. alignment of content.
1449 :param style: A style string. Style to be applied to all the cells in this
1450 window. (This can be a callable that returns a string.)
1451 :param char: (string) Character to be used for filling the background. This
1452 can also be a callable that returns a character.
1453 :param get_line_prefix: None or a callable that returns formatted text to
1454 be inserted before a line. It takes a line number (int) and a
1455 wrap_count and returns formatted text. This can be used for
1456 implementation of line continuations, things like Vim "breakindent" and
1457 so on.
1458 """
1460 def __init__(
1461 self,
1462 content: UIControl | None = None,
1463 width: AnyDimension = None,
1464 height: AnyDimension = None,
1465 z_index: int | None = None,
1466 dont_extend_width: FilterOrBool = False,
1467 dont_extend_height: FilterOrBool = False,
1468 ignore_content_width: FilterOrBool = False,
1469 ignore_content_height: FilterOrBool = False,
1470 left_margins: Sequence[Margin] | None = None,
1471 right_margins: Sequence[Margin] | None = None,
1472 scroll_offsets: ScrollOffsets | None = None,
1473 allow_scroll_beyond_bottom: FilterOrBool = False,
1474 wrap_lines: FilterOrBool = False,
1475 get_vertical_scroll: Callable[[Window], int] | None = None,
1476 get_horizontal_scroll: Callable[[Window], int] | None = None,
1477 always_hide_cursor: FilterOrBool = False,
1478 cursorline: FilterOrBool = False,
1479 cursorcolumn: FilterOrBool = False,
1480 colorcolumns: (
1481 None | list[ColorColumn] | Callable[[], list[ColorColumn]]
1482 ) = None,
1483 align: WindowAlign | Callable[[], WindowAlign] = WindowAlign.LEFT,
1484 style: str | Callable[[], str] = "",
1485 char: None | str | Callable[[], str] = None,
1486 get_line_prefix: GetLinePrefixCallable | None = None,
1487 ) -> None:
1488 self.allow_scroll_beyond_bottom = to_filter(allow_scroll_beyond_bottom)
1489 self.always_hide_cursor = to_filter(always_hide_cursor)
1490 self.wrap_lines = to_filter(wrap_lines)
1491 self.cursorline = to_filter(cursorline)
1492 self.cursorcolumn = to_filter(cursorcolumn)
1494 self.content = content or DummyControl()
1495 self.dont_extend_width = to_filter(dont_extend_width)
1496 self.dont_extend_height = to_filter(dont_extend_height)
1497 self.ignore_content_width = to_filter(ignore_content_width)
1498 self.ignore_content_height = to_filter(ignore_content_height)
1499 self.left_margins = left_margins or []
1500 self.right_margins = right_margins or []
1501 self.scroll_offsets = scroll_offsets or ScrollOffsets()
1502 self.get_vertical_scroll = get_vertical_scroll
1503 self.get_horizontal_scroll = get_horizontal_scroll
1504 self.colorcolumns = colorcolumns or []
1505 self.align = align
1506 self.style = style
1507 self.char = char
1508 self.get_line_prefix = get_line_prefix
1510 self.width = width
1511 self.height = height
1512 self.z_index = z_index
1514 # Cache for the screens generated by the margin.
1515 self._ui_content_cache: SimpleCache[
1516 tuple[int, int, int], UIContent
1517 ] = SimpleCache(maxsize=8)
1518 self._margin_width_cache: SimpleCache[tuple[Margin, int], int] = SimpleCache(
1519 maxsize=1
1520 )
1522 self.reset()
1524 def __repr__(self) -> str:
1525 return "Window(content=%r)" % self.content
1527 def reset(self) -> None:
1528 self.content.reset()
1530 #: Scrolling position of the main content.
1531 self.vertical_scroll = 0
1532 self.horizontal_scroll = 0
1534 # Vertical scroll 2: this is the vertical offset that a line is
1535 # scrolled if a single line (the one that contains the cursor) consumes
1536 # all of the vertical space.
1537 self.vertical_scroll_2 = 0
1539 #: Keep render information (mappings between buffer input and render
1540 #: output.)
1541 self.render_info: WindowRenderInfo | None = None
1543 def _get_margin_width(self, margin: Margin) -> int:
1544 """
1545 Return the width for this margin.
1546 (Calculate only once per render time.)
1547 """
1549 # Margin.get_width, needs to have a UIContent instance.
1550 def get_ui_content() -> UIContent:
1551 return self._get_ui_content(width=0, height=0)
1553 def get_width() -> int:
1554 return margin.get_width(get_ui_content)
1556 key = (margin, get_app().render_counter)
1557 return self._margin_width_cache.get(key, get_width)
1559 def _get_total_margin_width(self) -> int:
1560 """
1561 Calculate and return the width of the margin (left + right).
1562 """
1563 return sum(self._get_margin_width(m) for m in self.left_margins) + sum(
1564 self._get_margin_width(m) for m in self.right_margins
1565 )
1567 def preferred_width(self, max_available_width: int) -> Dimension:
1568 """
1569 Calculate the preferred width for this window.
1570 """
1572 def preferred_content_width() -> int | None:
1573 """Content width: is only calculated if no exact width for the
1574 window was given."""
1575 if self.ignore_content_width():
1576 return None
1578 # Calculate the width of the margin.
1579 total_margin_width = self._get_total_margin_width()
1581 # Window of the content. (Can be `None`.)
1582 preferred_width = self.content.preferred_width(
1583 max_available_width - total_margin_width
1584 )
1586 if preferred_width is not None:
1587 # Include width of the margins.
1588 preferred_width += total_margin_width
1589 return preferred_width
1591 # Merge.
1592 return self._merge_dimensions(
1593 dimension=to_dimension(self.width),
1594 get_preferred=preferred_content_width,
1595 dont_extend=self.dont_extend_width(),
1596 )
1598 def preferred_height(self, width: int, max_available_height: int) -> Dimension:
1599 """
1600 Calculate the preferred height for this window.
1601 """
1603 def preferred_content_height() -> int | None:
1604 """Content height: is only calculated if no exact height for the
1605 window was given."""
1606 if self.ignore_content_height():
1607 return None
1609 total_margin_width = self._get_total_margin_width()
1610 wrap_lines = self.wrap_lines()
1612 return self.content.preferred_height(
1613 width - total_margin_width,
1614 max_available_height,
1615 wrap_lines,
1616 self.get_line_prefix,
1617 )
1619 return self._merge_dimensions(
1620 dimension=to_dimension(self.height),
1621 get_preferred=preferred_content_height,
1622 dont_extend=self.dont_extend_height(),
1623 )
1625 @staticmethod
1626 def _merge_dimensions(
1627 dimension: Dimension | None,
1628 get_preferred: Callable[[], int | None],
1629 dont_extend: bool = False,
1630 ) -> Dimension:
1631 """
1632 Take the Dimension from this `Window` class and the received preferred
1633 size from the `UIControl` and return a `Dimension` to report to the
1634 parent container.
1635 """
1636 dimension = dimension or Dimension()
1638 # When a preferred dimension was explicitly given to the Window,
1639 # ignore the UIControl.
1640 preferred: int | None
1642 if dimension.preferred_specified:
1643 preferred = dimension.preferred
1644 else:
1645 # Otherwise, calculate the preferred dimension from the UI control
1646 # content.
1647 preferred = get_preferred()
1649 # When a 'preferred' dimension is given by the UIControl, make sure
1650 # that it stays within the bounds of the Window.
1651 if preferred is not None:
1652 if dimension.max_specified:
1653 preferred = min(preferred, dimension.max)
1655 if dimension.min_specified:
1656 preferred = max(preferred, dimension.min)
1658 # When a `dont_extend` flag has been given, use the preferred dimension
1659 # also as the max dimension.
1660 max_: int | None
1661 min_: int | None
1663 if dont_extend and preferred is not None:
1664 max_ = min(dimension.max, preferred)
1665 else:
1666 max_ = dimension.max if dimension.max_specified else None
1668 min_ = dimension.min if dimension.min_specified else None
1670 return Dimension(
1671 min=min_, max=max_, preferred=preferred, weight=dimension.weight
1672 )
1674 def _get_ui_content(self, width: int, height: int) -> UIContent:
1675 """
1676 Create a `UIContent` instance.
1677 """
1679 def get_content() -> UIContent:
1680 return self.content.create_content(width=width, height=height)
1682 key = (get_app().render_counter, width, height)
1683 return self._ui_content_cache.get(key, get_content)
1685 def _get_digraph_char(self) -> str | None:
1686 "Return `False`, or the Digraph symbol to be used."
1687 app = get_app()
1688 if app.quoted_insert:
1689 return "^"
1690 if app.vi_state.waiting_for_digraph:
1691 if app.vi_state.digraph_symbol1:
1692 return app.vi_state.digraph_symbol1
1693 return "?"
1694 return None
1696 def write_to_screen(
1697 self,
1698 screen: Screen,
1699 mouse_handlers: MouseHandlers,
1700 write_position: WritePosition,
1701 parent_style: str,
1702 erase_bg: bool,
1703 z_index: int | None,
1704 ) -> None:
1705 """
1706 Write window to screen. This renders the user control, the margins and
1707 copies everything over to the absolute position at the given screen.
1708 """
1709 # If dont_extend_width/height was given. Then reduce width/height in
1710 # WritePosition if the parent wanted us to paint in a bigger area.
1711 # (This happens if this window is bundled with another window in a
1712 # HSplit/VSplit, but with different size requirements.)
1713 write_position = WritePosition(
1714 xpos=write_position.xpos,
1715 ypos=write_position.ypos,
1716 width=write_position.width,
1717 height=write_position.height,
1718 )
1720 if self.dont_extend_width():
1721 write_position.width = min(
1722 write_position.width,
1723 self.preferred_width(write_position.width).preferred,
1724 )
1726 if self.dont_extend_height():
1727 write_position.height = min(
1728 write_position.height,
1729 self.preferred_height(
1730 write_position.width, write_position.height
1731 ).preferred,
1732 )
1734 # Draw
1735 z_index = z_index if self.z_index is None else self.z_index
1737 draw_func = partial(
1738 self._write_to_screen_at_index,
1739 screen,
1740 mouse_handlers,
1741 write_position,
1742 parent_style,
1743 erase_bg,
1744 )
1746 if z_index is None or z_index <= 0:
1747 # When no z_index is given, draw right away.
1748 draw_func()
1749 else:
1750 # Otherwise, postpone.
1751 screen.draw_with_z_index(z_index=z_index, draw_func=draw_func)
1753 def _write_to_screen_at_index(
1754 self,
1755 screen: Screen,
1756 mouse_handlers: MouseHandlers,
1757 write_position: WritePosition,
1758 parent_style: str,
1759 erase_bg: bool,
1760 ) -> None:
1761 # Don't bother writing invisible windows.
1762 # (We save some time, but also avoid applying last-line styling.)
1763 if write_position.height <= 0 or write_position.width <= 0:
1764 return
1766 # Calculate margin sizes.
1767 left_margin_widths = [self._get_margin_width(m) for m in self.left_margins]
1768 right_margin_widths = [self._get_margin_width(m) for m in self.right_margins]
1769 total_margin_width = sum(left_margin_widths + right_margin_widths)
1771 # Render UserControl.
1772 ui_content = self.content.create_content(
1773 write_position.width - total_margin_width, write_position.height
1774 )
1775 assert isinstance(ui_content, UIContent)
1777 # Scroll content.
1778 wrap_lines = self.wrap_lines()
1779 self._scroll(
1780 ui_content, write_position.width - total_margin_width, write_position.height
1781 )
1783 # Erase background and fill with `char`.
1784 self._fill_bg(screen, write_position, erase_bg)
1786 # Resolve `align` attribute.
1787 align = self.align() if callable(self.align) else self.align
1789 # Write body
1790 visible_line_to_row_col, rowcol_to_yx = self._copy_body(
1791 ui_content,
1792 screen,
1793 write_position,
1794 sum(left_margin_widths),
1795 write_position.width - total_margin_width,
1796 self.vertical_scroll,
1797 self.horizontal_scroll,
1798 wrap_lines=wrap_lines,
1799 highlight_lines=True,
1800 vertical_scroll_2=self.vertical_scroll_2,
1801 always_hide_cursor=self.always_hide_cursor(),
1802 has_focus=get_app().layout.current_control == self.content,
1803 align=align,
1804 get_line_prefix=self.get_line_prefix,
1805 )
1807 # Remember render info. (Set before generating the margins. They need this.)
1808 x_offset = write_position.xpos + sum(left_margin_widths)
1809 y_offset = write_position.ypos
1811 render_info = WindowRenderInfo(
1812 window=self,
1813 ui_content=ui_content,
1814 horizontal_scroll=self.horizontal_scroll,
1815 vertical_scroll=self.vertical_scroll,
1816 window_width=write_position.width - total_margin_width,
1817 window_height=write_position.height,
1818 configured_scroll_offsets=self.scroll_offsets,
1819 visible_line_to_row_col=visible_line_to_row_col,
1820 rowcol_to_yx=rowcol_to_yx,
1821 x_offset=x_offset,
1822 y_offset=y_offset,
1823 wrap_lines=wrap_lines,
1824 )
1825 self.render_info = render_info
1827 # Set mouse handlers.
1828 def mouse_handler(mouse_event: MouseEvent) -> NotImplementedOrNone:
1829 """
1830 Wrapper around the mouse_handler of the `UIControl` that turns
1831 screen coordinates into line coordinates.
1832 Returns `NotImplemented` if no UI invalidation should be done.
1833 """
1834 # Don't handle mouse events outside of the current modal part of
1835 # the UI.
1836 if self not in get_app().layout.walk_through_modal_area():
1837 return NotImplemented
1839 # Find row/col position first.
1840 yx_to_rowcol = {v: k for k, v in rowcol_to_yx.items()}
1841 y = mouse_event.position.y
1842 x = mouse_event.position.x
1844 # If clicked below the content area, look for a position in the
1845 # last line instead.
1846 max_y = write_position.ypos + len(visible_line_to_row_col) - 1
1847 y = min(max_y, y)
1848 result: NotImplementedOrNone
1850 while x >= 0:
1851 try:
1852 row, col = yx_to_rowcol[y, x]
1853 except KeyError:
1854 # Try again. (When clicking on the right side of double
1855 # width characters, or on the right side of the input.)
1856 x -= 1
1857 else:
1858 # Found position, call handler of UIControl.
1859 result = self.content.mouse_handler(
1860 MouseEvent(
1861 position=Point(x=col, y=row),
1862 event_type=mouse_event.event_type,
1863 button=mouse_event.button,
1864 modifiers=mouse_event.modifiers,
1865 )
1866 )
1867 break
1868 else:
1869 # nobreak.
1870 # (No x/y coordinate found for the content. This happens in
1871 # case of a DummyControl, that does not have any content.
1872 # Report (0,0) instead.)
1873 result = self.content.mouse_handler(
1874 MouseEvent(
1875 position=Point(x=0, y=0),
1876 event_type=mouse_event.event_type,
1877 button=mouse_event.button,
1878 modifiers=mouse_event.modifiers,
1879 )
1880 )
1882 # If it returns NotImplemented, handle it here.
1883 if result == NotImplemented:
1884 result = self._mouse_handler(mouse_event)
1886 return result
1888 mouse_handlers.set_mouse_handler_for_range(
1889 x_min=write_position.xpos + sum(left_margin_widths),
1890 x_max=write_position.xpos + write_position.width - total_margin_width,
1891 y_min=write_position.ypos,
1892 y_max=write_position.ypos + write_position.height,
1893 handler=mouse_handler,
1894 )
1896 # Render and copy margins.
1897 move_x = 0
1899 def render_margin(m: Margin, width: int) -> UIContent:
1900 "Render margin. Return `Screen`."
1901 # Retrieve margin fragments.
1902 fragments = m.create_margin(render_info, width, write_position.height)
1904 # Turn it into a UIContent object.
1905 # already rendered those fragments using this size.)
1906 return FormattedTextControl(fragments).create_content(
1907 width + 1, write_position.height
1908 )
1910 for m, width in zip(self.left_margins, left_margin_widths):
1911 if width > 0: # (ConditionalMargin returns a zero width. -- Don't render.)
1912 # Create screen for margin.
1913 margin_content = render_margin(m, width)
1915 # Copy and shift X.
1916 self._copy_margin(margin_content, screen, write_position, move_x, width)
1917 move_x += width
1919 move_x = write_position.width - sum(right_margin_widths)
1921 for m, width in zip(self.right_margins, right_margin_widths):
1922 # Create screen for margin.
1923 margin_content = render_margin(m, width)
1925 # Copy and shift X.
1926 self._copy_margin(margin_content, screen, write_position, move_x, width)
1927 move_x += width
1929 # Apply 'self.style'
1930 self._apply_style(screen, write_position, parent_style)
1932 # Tell the screen that this user control has been painted at this
1933 # position.
1934 screen.visible_windows_to_write_positions[self] = write_position
1936 def _copy_body(
1937 self,
1938 ui_content: UIContent,
1939 new_screen: Screen,
1940 write_position: WritePosition,
1941 move_x: int,
1942 width: int,
1943 vertical_scroll: int = 0,
1944 horizontal_scroll: int = 0,
1945 wrap_lines: bool = False,
1946 highlight_lines: bool = False,
1947 vertical_scroll_2: int = 0,
1948 always_hide_cursor: bool = False,
1949 has_focus: bool = False,
1950 align: WindowAlign = WindowAlign.LEFT,
1951 get_line_prefix: Callable[[int, int], AnyFormattedText] | None = None,
1952 ) -> tuple[dict[int, tuple[int, int]], dict[tuple[int, int], tuple[int, int]]]:
1953 """
1954 Copy the UIContent into the output screen.
1955 Return (visible_line_to_row_col, rowcol_to_yx) tuple.
1957 :param get_line_prefix: None or a callable that takes a line number
1958 (int) and a wrap_count (int) and returns formatted text.
1959 """
1960 xpos = write_position.xpos + move_x
1961 ypos = write_position.ypos
1962 line_count = ui_content.line_count
1963 new_buffer = new_screen.data_buffer
1964 empty_char = _CHAR_CACHE["", ""]
1966 # Map visible line number to (row, col) of input.
1967 # 'col' will always be zero if line wrapping is off.
1968 visible_line_to_row_col: dict[int, tuple[int, int]] = {}
1970 # Maps (row, col) from the input to (y, x) screen coordinates.
1971 rowcol_to_yx: dict[tuple[int, int], tuple[int, int]] = {}
1973 def copy_line(
1974 line: StyleAndTextTuples,
1975 lineno: int,
1976 x: int,
1977 y: int,
1978 is_input: bool = False,
1979 ) -> tuple[int, int]:
1980 """
1981 Copy over a single line to the output screen. This can wrap over
1982 multiple lines in the output. It will call the prefix (prompt)
1983 function before every line.
1984 """
1985 if is_input:
1986 current_rowcol_to_yx = rowcol_to_yx
1987 else:
1988 current_rowcol_to_yx = {} # Throwaway dictionary.
1990 # Draw line prefix.
1991 if is_input and get_line_prefix:
1992 prompt = to_formatted_text(get_line_prefix(lineno, 0))
1993 x, y = copy_line(prompt, lineno, x, y, is_input=False)
1995 # Scroll horizontally.
1996 skipped = 0 # Characters skipped because of horizontal scrolling.
1997 if horizontal_scroll and is_input:
1998 h_scroll = horizontal_scroll
1999 line = explode_text_fragments(line)
2000 while h_scroll > 0 and line:
2001 h_scroll -= get_cwidth(line[0][1])
2002 skipped += 1
2003 del line[:1] # Remove first character.
2005 x -= h_scroll # When scrolling over double width character,
2006 # this can end up being negative.
2008 # Align this line. (Note that this doesn't work well when we use
2009 # get_line_prefix and that function returns variable width prefixes.)
2010 if align == WindowAlign.CENTER:
2011 line_width = fragment_list_width(line)
2012 if line_width < width:
2013 x += (width - line_width) // 2
2014 elif align == WindowAlign.RIGHT:
2015 line_width = fragment_list_width(line)
2016 if line_width < width:
2017 x += width - line_width
2019 col = 0
2020 wrap_count = 0
2021 for style, text, *_ in line:
2022 new_buffer_row = new_buffer[y + ypos]
2024 # Remember raw VT escape sequences. (E.g. FinalTerm's
2025 # escape sequences.)
2026 if "[ZeroWidthEscape]" in style:
2027 new_screen.zero_width_escapes[y + ypos][x + xpos] += text
2028 continue
2030 for c in text:
2031 char = _CHAR_CACHE[c, style]
2032 char_width = char.width
2034 # Wrap when the line width is exceeded.
2035 if wrap_lines and x + char_width > width:
2036 visible_line_to_row_col[y + 1] = (
2037 lineno,
2038 visible_line_to_row_col[y][1] + x,
2039 )
2040 y += 1
2041 wrap_count += 1
2042 x = 0
2044 # Insert line prefix (continuation prompt).
2045 if is_input and get_line_prefix:
2046 prompt = to_formatted_text(
2047 get_line_prefix(lineno, wrap_count)
2048 )
2049 x, y = copy_line(prompt, lineno, x, y, is_input=False)
2051 new_buffer_row = new_buffer[y + ypos]
2053 if y >= write_position.height:
2054 return x, y # Break out of all for loops.
2056 # Set character in screen and shift 'x'.
2057 if x >= 0 and y >= 0 and x < width:
2058 new_buffer_row[x + xpos] = char
2060 # When we print a multi width character, make sure
2061 # to erase the neighbours positions in the screen.
2062 # (The empty string if different from everything,
2063 # so next redraw this cell will repaint anyway.)
2064 if char_width > 1:
2065 for i in range(1, char_width):
2066 new_buffer_row[x + xpos + i] = empty_char
2068 # If this is a zero width characters, then it's
2069 # probably part of a decomposed unicode character.
2070 # See: https://en.wikipedia.org/wiki/Unicode_equivalence
2071 # Merge it in the previous cell.
2072 elif char_width == 0:
2073 # Handle all character widths. If the previous
2074 # character is a multiwidth character, then
2075 # merge it two positions back.
2076 for pw in [2, 1]: # Previous character width.
2077 if (
2078 x - pw >= 0
2079 and new_buffer_row[x + xpos - pw].width == pw
2080 ):
2081 prev_char = new_buffer_row[x + xpos - pw]
2082 char2 = _CHAR_CACHE[
2083 prev_char.char + c, prev_char.style
2084 ]
2085 new_buffer_row[x + xpos - pw] = char2
2087 # Keep track of write position for each character.
2088 current_rowcol_to_yx[lineno, col + skipped] = (
2089 y + ypos,
2090 x + xpos,
2091 )
2093 col += 1
2094 x += char_width
2095 return x, y
2097 # Copy content.
2098 def copy() -> int:
2099 y = -vertical_scroll_2
2100 lineno = vertical_scroll
2102 while y < write_position.height and lineno < line_count:
2103 # Take the next line and copy it in the real screen.
2104 line = ui_content.get_line(lineno)
2106 visible_line_to_row_col[y] = (lineno, horizontal_scroll)
2108 # Copy margin and actual line.
2109 x = 0
2110 x, y = copy_line(line, lineno, x, y, is_input=True)
2112 lineno += 1
2113 y += 1
2114 return y
2116 copy()
2118 def cursor_pos_to_screen_pos(row: int, col: int) -> Point:
2119 "Translate row/col from UIContent to real Screen coordinates."
2120 try:
2121 y, x = rowcol_to_yx[row, col]
2122 except KeyError:
2123 # Normally this should never happen. (It is a bug, if it happens.)
2124 # But to be sure, return (0, 0)
2125 return Point(x=0, y=0)
2127 # raise ValueError(
2128 # 'Invalid position. row=%r col=%r, vertical_scroll=%r, '
2129 # 'horizontal_scroll=%r, height=%r' %
2130 # (row, col, vertical_scroll, horizontal_scroll, write_position.height))
2131 else:
2132 return Point(x=x, y=y)
2134 # Set cursor and menu positions.
2135 if ui_content.cursor_position:
2136 screen_cursor_position = cursor_pos_to_screen_pos(
2137 ui_content.cursor_position.y, ui_content.cursor_position.x
2138 )
2140 if has_focus:
2141 new_screen.set_cursor_position(self, screen_cursor_position)
2143 if always_hide_cursor:
2144 new_screen.show_cursor = False
2145 else:
2146 new_screen.show_cursor = ui_content.show_cursor
2148 self._highlight_digraph(new_screen)
2150 if highlight_lines:
2151 self._highlight_cursorlines(
2152 new_screen,
2153 screen_cursor_position,
2154 xpos,
2155 ypos,
2156 width,
2157 write_position.height,
2158 )
2160 # Draw input characters from the input processor queue.
2161 if has_focus and ui_content.cursor_position:
2162 self._show_key_processor_key_buffer(new_screen)
2164 # Set menu position.
2165 if ui_content.menu_position:
2166 new_screen.set_menu_position(
2167 self,
2168 cursor_pos_to_screen_pos(
2169 ui_content.menu_position.y, ui_content.menu_position.x
2170 ),
2171 )
2173 # Update output screen height.
2174 new_screen.height = max(new_screen.height, ypos + write_position.height)
2176 return visible_line_to_row_col, rowcol_to_yx
2178 def _fill_bg(
2179 self, screen: Screen, write_position: WritePosition, erase_bg: bool
2180 ) -> None:
2181 """
2182 Erase/fill the background.
2183 (Useful for floats and when a `char` has been given.)
2184 """
2185 char: str | None
2186 if callable(self.char):
2187 char = self.char()
2188 else:
2189 char = self.char
2191 if erase_bg or char:
2192 wp = write_position
2193 char_obj = _CHAR_CACHE[char or " ", ""]
2195 for y in range(wp.ypos, wp.ypos + wp.height):
2196 row = screen.data_buffer[y]
2197 for x in range(wp.xpos, wp.xpos + wp.width):
2198 row[x] = char_obj
2200 def _apply_style(
2201 self, new_screen: Screen, write_position: WritePosition, parent_style: str
2202 ) -> None:
2203 # Apply `self.style`.
2204 style = parent_style + " " + to_str(self.style)
2206 new_screen.fill_area(write_position, style=style, after=False)
2208 # Apply the 'last-line' class to the last line of each Window. This can
2209 # be used to apply an 'underline' to the user control.
2210 wp = WritePosition(
2211 write_position.xpos,
2212 write_position.ypos + write_position.height - 1,
2213 write_position.width,
2214 1,
2215 )
2216 new_screen.fill_area(wp, "class:last-line", after=True)
2218 def _highlight_digraph(self, new_screen: Screen) -> None:
2219 """
2220 When we are in Vi digraph mode, put a question mark underneath the
2221 cursor.
2222 """
2223 digraph_char = self._get_digraph_char()
2224 if digraph_char:
2225 cpos = new_screen.get_cursor_position(self)
2226 new_screen.data_buffer[cpos.y][cpos.x] = _CHAR_CACHE[
2227 digraph_char, "class:digraph"
2228 ]
2230 def _show_key_processor_key_buffer(self, new_screen: Screen) -> None:
2231 """
2232 When the user is typing a key binding that consists of several keys,
2233 display the last pressed key if the user is in insert mode and the key
2234 is meaningful to be displayed.
2235 E.g. Some people want to bind 'jj' to escape in Vi insert mode. But the
2236 first 'j' needs to be displayed in order to get some feedback.
2237 """
2238 app = get_app()
2239 key_buffer = app.key_processor.key_buffer
2241 if key_buffer and _in_insert_mode() and not app.is_done:
2242 # The textual data for the given key. (Can be a VT100 escape
2243 # sequence.)
2244 data = key_buffer[-1].data
2246 # Display only if this is a 1 cell width character.
2247 if get_cwidth(data) == 1:
2248 cpos = new_screen.get_cursor_position(self)
2249 new_screen.data_buffer[cpos.y][cpos.x] = _CHAR_CACHE[
2250 data, "class:partial-key-binding"
2251 ]
2253 def _highlight_cursorlines(
2254 self, new_screen: Screen, cpos: Point, x: int, y: int, width: int, height: int
2255 ) -> None:
2256 """
2257 Highlight cursor row/column.
2258 """
2259 cursor_line_style = " class:cursor-line "
2260 cursor_column_style = " class:cursor-column "
2262 data_buffer = new_screen.data_buffer
2264 # Highlight cursor line.
2265 if self.cursorline():
2266 row = data_buffer[cpos.y]
2267 for x in range(x, x + width):
2268 original_char = row[x]
2269 row[x] = _CHAR_CACHE[
2270 original_char.char, original_char.style + cursor_line_style
2271 ]
2273 # Highlight cursor column.
2274 if self.cursorcolumn():
2275 for y2 in range(y, y + height):
2276 row = data_buffer[y2]
2277 original_char = row[cpos.x]
2278 row[cpos.x] = _CHAR_CACHE[
2279 original_char.char, original_char.style + cursor_column_style
2280 ]
2282 # Highlight color columns
2283 colorcolumns = self.colorcolumns
2284 if callable(colorcolumns):
2285 colorcolumns = colorcolumns()
2287 for cc in colorcolumns:
2288 assert isinstance(cc, ColorColumn)
2289 column = cc.position
2291 if column < x + width: # Only draw when visible.
2292 color_column_style = " " + cc.style
2294 for y2 in range(y, y + height):
2295 row = data_buffer[y2]
2296 original_char = row[column + x]
2297 row[column + x] = _CHAR_CACHE[
2298 original_char.char, original_char.style + color_column_style
2299 ]
2301 def _copy_margin(
2302 self,
2303 margin_content: UIContent,
2304 new_screen: Screen,
2305 write_position: WritePosition,
2306 move_x: int,
2307 width: int,
2308 ) -> None:
2309 """
2310 Copy characters from the margin screen to the real screen.
2311 """
2312 xpos = write_position.xpos + move_x
2313 ypos = write_position.ypos
2315 margin_write_position = WritePosition(xpos, ypos, width, write_position.height)
2316 self._copy_body(margin_content, new_screen, margin_write_position, 0, width)
2318 def _scroll(self, ui_content: UIContent, width: int, height: int) -> None:
2319 """
2320 Scroll body. Ensure that the cursor is visible.
2321 """
2322 if self.wrap_lines():
2323 func = self._scroll_when_linewrapping
2324 else:
2325 func = self._scroll_without_linewrapping
2327 func(ui_content, width, height)
2329 def _scroll_when_linewrapping(
2330 self, ui_content: UIContent, width: int, height: int
2331 ) -> None:
2332 """
2333 Scroll to make sure the cursor position is visible and that we maintain
2334 the requested scroll offset.
2336 Set `self.horizontal_scroll/vertical_scroll`.
2337 """
2338 scroll_offsets_bottom = self.scroll_offsets.bottom
2339 scroll_offsets_top = self.scroll_offsets.top
2341 # We don't have horizontal scrolling.
2342 self.horizontal_scroll = 0
2344 def get_line_height(lineno: int) -> int:
2345 return ui_content.get_height_for_line(lineno, width, self.get_line_prefix)
2347 # When there is no space, reset `vertical_scroll_2` to zero and abort.
2348 # This can happen if the margin is bigger than the window width.
2349 # Otherwise the text height will become "infinite" (a big number) and
2350 # the copy_line will spend a huge amount of iterations trying to render
2351 # nothing.
2352 if width <= 0:
2353 self.vertical_scroll = ui_content.cursor_position.y
2354 self.vertical_scroll_2 = 0
2355 return
2357 # If the current line consumes more than the whole window height,
2358 # then we have to scroll vertically inside this line. (We don't take
2359 # the scroll offsets into account for this.)
2360 # Also, ignore the scroll offsets in this case. Just set the vertical
2361 # scroll to this line.
2362 line_height = get_line_height(ui_content.cursor_position.y)
2363 if line_height > height - scroll_offsets_top:
2364 # Calculate the height of the text before the cursor (including
2365 # line prefixes).
2366 text_before_height = ui_content.get_height_for_line(
2367 ui_content.cursor_position.y,
2368 width,
2369 self.get_line_prefix,
2370 slice_stop=ui_content.cursor_position.x,
2371 )
2373 # Adjust scroll offset.
2374 self.vertical_scroll = ui_content.cursor_position.y
2375 self.vertical_scroll_2 = min(
2376 text_before_height - 1, # Keep the cursor visible.
2377 line_height
2378 - height, # Avoid blank lines at the bottom when scolling up again.
2379 self.vertical_scroll_2,
2380 )
2381 self.vertical_scroll_2 = max(
2382 0, text_before_height - height, self.vertical_scroll_2
2383 )
2384 return
2385 else:
2386 self.vertical_scroll_2 = 0
2388 # Current line doesn't consume the whole height. Take scroll offsets into account.
2389 def get_min_vertical_scroll() -> int:
2390 # Make sure that the cursor line is not below the bottom.
2391 # (Calculate how many lines can be shown between the cursor and the .)
2392 used_height = 0
2393 prev_lineno = ui_content.cursor_position.y
2395 for lineno in range(ui_content.cursor_position.y, -1, -1):
2396 used_height += get_line_height(lineno)
2398 if used_height > height - scroll_offsets_bottom:
2399 return prev_lineno
2400 else:
2401 prev_lineno = lineno
2402 return 0
2404 def get_max_vertical_scroll() -> int:
2405 # Make sure that the cursor line is not above the top.
2406 prev_lineno = ui_content.cursor_position.y
2407 used_height = 0
2409 for lineno in range(ui_content.cursor_position.y - 1, -1, -1):
2410 used_height += get_line_height(lineno)
2412 if used_height > scroll_offsets_top:
2413 return prev_lineno
2414 else:
2415 prev_lineno = lineno
2416 return prev_lineno
2418 def get_topmost_visible() -> int:
2419 """
2420 Calculate the upper most line that can be visible, while the bottom
2421 is still visible. We should not allow scroll more than this if
2422 `allow_scroll_beyond_bottom` is false.
2423 """
2424 prev_lineno = ui_content.line_count - 1
2425 used_height = 0
2426 for lineno in range(ui_content.line_count - 1, -1, -1):
2427 used_height += get_line_height(lineno)
2428 if used_height > height:
2429 return prev_lineno
2430 else:
2431 prev_lineno = lineno
2432 return prev_lineno
2434 # Scroll vertically. (Make sure that the whole line which contains the
2435 # cursor is visible.
2436 topmost_visible = get_topmost_visible()
2438 # Note: the `min(topmost_visible, ...)` is to make sure that we
2439 # don't require scrolling up because of the bottom scroll offset,
2440 # when we are at the end of the document.
2441 self.vertical_scroll = max(
2442 self.vertical_scroll, min(topmost_visible, get_min_vertical_scroll())
2443 )
2444 self.vertical_scroll = min(self.vertical_scroll, get_max_vertical_scroll())
2446 # Disallow scrolling beyond bottom?
2447 if not self.allow_scroll_beyond_bottom():
2448 self.vertical_scroll = min(self.vertical_scroll, topmost_visible)
2450 def _scroll_without_linewrapping(
2451 self, ui_content: UIContent, width: int, height: int
2452 ) -> None:
2453 """
2454 Scroll to make sure the cursor position is visible and that we maintain
2455 the requested scroll offset.
2457 Set `self.horizontal_scroll/vertical_scroll`.
2458 """
2459 cursor_position = ui_content.cursor_position or Point(x=0, y=0)
2461 # Without line wrapping, we will never have to scroll vertically inside
2462 # a single line.
2463 self.vertical_scroll_2 = 0
2465 if ui_content.line_count == 0:
2466 self.vertical_scroll = 0
2467 self.horizontal_scroll = 0
2468 return
2469 else:
2470 current_line_text = fragment_list_to_text(
2471 ui_content.get_line(cursor_position.y)
2472 )
2474 def do_scroll(
2475 current_scroll: int,
2476 scroll_offset_start: int,
2477 scroll_offset_end: int,
2478 cursor_pos: int,
2479 window_size: int,
2480 content_size: int,
2481 ) -> int:
2482 "Scrolling algorithm. Used for both horizontal and vertical scrolling."
2483 # Calculate the scroll offset to apply.
2484 # This can obviously never be more than have the screen size. Also, when the
2485 # cursor appears at the top or bottom, we don't apply the offset.
2486 scroll_offset_start = int(
2487 min(scroll_offset_start, window_size / 2, cursor_pos)
2488 )
2489 scroll_offset_end = int(
2490 min(scroll_offset_end, window_size / 2, content_size - 1 - cursor_pos)
2491 )
2493 # Prevent negative scroll offsets.
2494 if current_scroll < 0:
2495 current_scroll = 0
2497 # Scroll back if we scrolled to much and there's still space to show more of the document.
2498 if (
2499 not self.allow_scroll_beyond_bottom()
2500 and current_scroll > content_size - window_size
2501 ):
2502 current_scroll = max(0, content_size - window_size)
2504 # Scroll up if cursor is before visible part.
2505 if current_scroll > cursor_pos - scroll_offset_start:
2506 current_scroll = max(0, cursor_pos - scroll_offset_start)
2508 # Scroll down if cursor is after visible part.
2509 if current_scroll < (cursor_pos + 1) - window_size + scroll_offset_end:
2510 current_scroll = (cursor_pos + 1) - window_size + scroll_offset_end
2512 return current_scroll
2514 # When a preferred scroll is given, take that first into account.
2515 if self.get_vertical_scroll:
2516 self.vertical_scroll = self.get_vertical_scroll(self)
2517 assert isinstance(self.vertical_scroll, int)
2518 if self.get_horizontal_scroll:
2519 self.horizontal_scroll = self.get_horizontal_scroll(self)
2520 assert isinstance(self.horizontal_scroll, int)
2522 # Update horizontal/vertical scroll to make sure that the cursor
2523 # remains visible.
2524 offsets = self.scroll_offsets
2526 self.vertical_scroll = do_scroll(
2527 current_scroll=self.vertical_scroll,
2528 scroll_offset_start=offsets.top,
2529 scroll_offset_end=offsets.bottom,
2530 cursor_pos=ui_content.cursor_position.y,
2531 window_size=height,
2532 content_size=ui_content.line_count,
2533 )
2535 if self.get_line_prefix:
2536 current_line_prefix_width = fragment_list_width(
2537 to_formatted_text(self.get_line_prefix(ui_content.cursor_position.y, 0))
2538 )
2539 else:
2540 current_line_prefix_width = 0
2542 self.horizontal_scroll = do_scroll(
2543 current_scroll=self.horizontal_scroll,
2544 scroll_offset_start=offsets.left,
2545 scroll_offset_end=offsets.right,
2546 cursor_pos=get_cwidth(current_line_text[: ui_content.cursor_position.x]),
2547 window_size=width - current_line_prefix_width,
2548 # We can only analyse the current line. Calculating the width off
2549 # all the lines is too expensive.
2550 content_size=max(
2551 get_cwidth(current_line_text), self.horizontal_scroll + width
2552 ),
2553 )
2555 def _mouse_handler(self, mouse_event: MouseEvent) -> NotImplementedOrNone:
2556 """
2557 Mouse handler. Called when the UI control doesn't handle this
2558 particular event.
2560 Return `NotImplemented` if nothing was done as a consequence of this
2561 key binding (no UI invalidate required in that case).
2562 """
2563 if mouse_event.event_type == MouseEventType.SCROLL_DOWN:
2564 self._scroll_down()
2565 return None
2566 elif mouse_event.event_type == MouseEventType.SCROLL_UP:
2567 self._scroll_up()
2568 return None
2570 return NotImplemented
2572 def _scroll_down(self) -> None:
2573 "Scroll window down."
2574 info = self.render_info
2576 if info is None:
2577 return
2579 if self.vertical_scroll < info.content_height - info.window_height:
2580 if info.cursor_position.y <= info.configured_scroll_offsets.top:
2581 self.content.move_cursor_down()
2583 self.vertical_scroll += 1
2585 def _scroll_up(self) -> None:
2586 "Scroll window up."
2587 info = self.render_info
2589 if info is None:
2590 return
2592 if info.vertical_scroll > 0:
2593 # TODO: not entirely correct yet in case of line wrapping and long lines.
2594 if (
2595 info.cursor_position.y
2596 >= info.window_height - 1 - info.configured_scroll_offsets.bottom
2597 ):
2598 self.content.move_cursor_up()
2600 self.vertical_scroll -= 1
2602 def get_key_bindings(self) -> KeyBindingsBase | None:
2603 return self.content.get_key_bindings()
2605 def get_children(self) -> list[Container]:
2606 return []
2609class ConditionalContainer(Container):
2610 """
2611 Wrapper around any other container that can change the visibility. The
2612 received `filter` determines whether the given container should be
2613 displayed or not.
2615 :param content: :class:`.Container` instance.
2616 :param filter: :class:`.Filter` instance.
2617 """
2619 def __init__(self, content: AnyContainer, filter: FilterOrBool) -> None:
2620 self.content = to_container(content)
2621 self.filter = to_filter(filter)
2623 def __repr__(self) -> str:
2624 return f"ConditionalContainer({self.content!r}, filter={self.filter!r})"
2626 def reset(self) -> None:
2627 self.content.reset()
2629 def preferred_width(self, max_available_width: int) -> Dimension:
2630 if self.filter():
2631 return self.content.preferred_width(max_available_width)
2632 else:
2633 return Dimension.zero()
2635 def preferred_height(self, width: int, max_available_height: int) -> Dimension:
2636 if self.filter():
2637 return self.content.preferred_height(width, max_available_height)
2638 else:
2639 return Dimension.zero()
2641 def write_to_screen(
2642 self,
2643 screen: Screen,
2644 mouse_handlers: MouseHandlers,
2645 write_position: WritePosition,
2646 parent_style: str,
2647 erase_bg: bool,
2648 z_index: int | None,
2649 ) -> None:
2650 if self.filter():
2651 return self.content.write_to_screen(
2652 screen, mouse_handlers, write_position, parent_style, erase_bg, z_index
2653 )
2655 def get_children(self) -> list[Container]:
2656 return [self.content]
2659class DynamicContainer(Container):
2660 """
2661 Container class that dynamically returns any Container.
2663 :param get_container: Callable that returns a :class:`.Container` instance
2664 or any widget with a ``__pt_container__`` method.
2665 """
2667 def __init__(self, get_container: Callable[[], AnyContainer]) -> None:
2668 self.get_container = get_container
2670 def _get_container(self) -> Container:
2671 """
2672 Return the current container object.
2674 We call `to_container`, because `get_container` can also return a
2675 widget with a ``__pt_container__`` method.
2676 """
2677 obj = self.get_container()
2678 return to_container(obj)
2680 def reset(self) -> None:
2681 self._get_container().reset()
2683 def preferred_width(self, max_available_width: int) -> Dimension:
2684 return self._get_container().preferred_width(max_available_width)
2686 def preferred_height(self, width: int, max_available_height: int) -> Dimension:
2687 return self._get_container().preferred_height(width, max_available_height)
2689 def write_to_screen(
2690 self,
2691 screen: Screen,
2692 mouse_handlers: MouseHandlers,
2693 write_position: WritePosition,
2694 parent_style: str,
2695 erase_bg: bool,
2696 z_index: int | None,
2697 ) -> None:
2698 self._get_container().write_to_screen(
2699 screen, mouse_handlers, write_position, parent_style, erase_bg, z_index
2700 )
2702 def is_modal(self) -> bool:
2703 return False
2705 def get_key_bindings(self) -> KeyBindingsBase | None:
2706 # Key bindings will be collected when `layout.walk()` finds the child
2707 # container.
2708 return None
2710 def get_children(self) -> list[Container]:
2711 # Here we have to return the current active container itself, not its
2712 # children. Otherwise, we run into issues where `layout.walk()` will
2713 # never see an object of type `Window` if this contains a window. We
2714 # can't/shouldn't proxy the "isinstance" check.
2715 return [self._get_container()]
2718def to_container(container: AnyContainer) -> Container:
2719 """
2720 Make sure that the given object is a :class:`.Container`.
2721 """
2722 if isinstance(container, Container):
2723 return container
2724 elif hasattr(container, "__pt_container__"):
2725 return to_container(container.__pt_container__())
2726 else:
2727 raise ValueError(f"Not a container object: {container!r}")
2730def to_window(container: AnyContainer) -> Window:
2731 """
2732 Make sure that the given argument is a :class:`.Window`.
2733 """
2734 if isinstance(container, Window):
2735 return container
2736 elif hasattr(container, "__pt_container__"):
2737 return to_window(cast("MagicContainer", container).__pt_container__())
2738 else:
2739 raise ValueError(f"Not a Window object: {container!r}.")
2742def is_container(value: object) -> TypeGuard[AnyContainer]:
2743 """
2744 Checks whether the given value is a container object
2745 (for use in assert statements).
2746 """
2747 if isinstance(value, Container):
2748 return True
2749 if hasattr(value, "__pt_container__"):
2750 return is_container(cast("MagicContainer", value).__pt_container__())
2751 return False