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