Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.9/dist-packages/prompt_toolkit/layout/containers.py: 19%
968 statements
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-25 06:05 +0000
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-25 06:05 +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`."
175 TOP = "TOP"
176 CENTER = "CENTER"
177 BOTTOM = "BOTTOM"
178 JUSTIFY = "JUSTIFY"
181class HorizontalAlign(Enum):
182 "Alignment for `VSplit`."
183 LEFT = "LEFT"
184 CENTER = "CENTER"
185 RIGHT = "RIGHT"
186 JUSTIFY = "JUSTIFY"
189class _Split(Container):
190 """
191 The common parts of `VSplit` and `HSplit`.
192 """
194 def __init__(
195 self,
196 children: Sequence[AnyContainer],
197 window_too_small: Container | None = None,
198 padding: AnyDimension = Dimension.exact(0),
199 padding_char: str | None = None,
200 padding_style: str = "",
201 width: AnyDimension = None,
202 height: AnyDimension = None,
203 z_index: int | None = None,
204 modal: bool = False,
205 key_bindings: KeyBindingsBase | None = None,
206 style: str | Callable[[], str] = "",
207 ) -> None:
208 self.children = [to_container(c) for c in children]
209 self.window_too_small = window_too_small or _window_too_small()
210 self.padding = padding
211 self.padding_char = padding_char
212 self.padding_style = padding_style
214 self.width = width
215 self.height = height
216 self.z_index = z_index
218 self.modal = modal
219 self.key_bindings = key_bindings
220 self.style = style
222 def is_modal(self) -> bool:
223 return self.modal
225 def get_key_bindings(self) -> KeyBindingsBase | None:
226 return self.key_bindings
228 def get_children(self) -> list[Container]:
229 return self.children
232class HSplit(_Split):
233 """
234 Several layouts, one stacked above/under the other. ::
236 +--------------------+
237 | |
238 +--------------------+
239 | |
240 +--------------------+
242 By default, this doesn't display a horizontal line between the children,
243 but if this is something you need, then create a HSplit as follows::
245 HSplit(children=[ ... ], padding_char='-',
246 padding=1, padding_style='#ffff00')
248 :param children: List of child :class:`.Container` objects.
249 :param window_too_small: A :class:`.Container` object that is displayed if
250 there is not enough space for all the children. By default, this is a
251 "Window too small" message.
252 :param align: `VerticalAlign` value.
253 :param width: When given, use this width instead of looking at the children.
254 :param height: When given, use this height instead of looking at the children.
255 :param z_index: (int or None) When specified, this can be used to bring
256 element in front of floating elements. `None` means: inherit from parent.
257 :param style: A style string.
258 :param modal: ``True`` or ``False``.
259 :param key_bindings: ``None`` or a :class:`.KeyBindings` object.
261 :param padding: (`Dimension` or int), size to be used for the padding.
262 :param padding_char: Character to be used for filling in the padding.
263 :param padding_style: Style to applied to the padding.
264 """
266 def __init__(
267 self,
268 children: Sequence[AnyContainer],
269 window_too_small: Container | None = None,
270 align: VerticalAlign = VerticalAlign.JUSTIFY,
271 padding: AnyDimension = 0,
272 padding_char: str | None = None,
273 padding_style: str = "",
274 width: AnyDimension = None,
275 height: AnyDimension = None,
276 z_index: int | None = None,
277 modal: bool = False,
278 key_bindings: KeyBindingsBase | None = None,
279 style: str | Callable[[], str] = "",
280 ) -> None:
281 super().__init__(
282 children=children,
283 window_too_small=window_too_small,
284 padding=padding,
285 padding_char=padding_char,
286 padding_style=padding_style,
287 width=width,
288 height=height,
289 z_index=z_index,
290 modal=modal,
291 key_bindings=key_bindings,
292 style=style,
293 )
295 self.align = align
297 self._children_cache: SimpleCache[
298 tuple[Container, ...], list[Container]
299 ] = SimpleCache(maxsize=1)
300 self._remaining_space_window = Window() # Dummy window.
302 def preferred_width(self, max_available_width: int) -> Dimension:
303 if self.width is not None:
304 return to_dimension(self.width)
306 if self.children:
307 dimensions = [c.preferred_width(max_available_width) for c in self.children]
308 return max_layout_dimensions(dimensions)
309 else:
310 return Dimension()
312 def preferred_height(self, width: int, max_available_height: int) -> Dimension:
313 if self.height is not None:
314 return to_dimension(self.height)
316 dimensions = [
317 c.preferred_height(width, max_available_height) for c in self._all_children
318 ]
319 return sum_layout_dimensions(dimensions)
321 def reset(self) -> None:
322 for c in self.children:
323 c.reset()
325 @property
326 def _all_children(self) -> list[Container]:
327 """
328 List of child objects, including padding.
329 """
331 def get() -> list[Container]:
332 result: list[Container] = []
334 # Padding Top.
335 if self.align in (VerticalAlign.CENTER, VerticalAlign.BOTTOM):
336 result.append(Window(width=Dimension(preferred=0)))
338 # The children with padding.
339 for child in self.children:
340 result.append(child)
341 result.append(
342 Window(
343 height=self.padding,
344 char=self.padding_char,
345 style=self.padding_style,
346 )
347 )
348 if result:
349 result.pop()
351 # Padding right.
352 if self.align in (VerticalAlign.CENTER, VerticalAlign.TOP):
353 result.append(Window(width=Dimension(preferred=0)))
355 return result
357 return self._children_cache.get(tuple(self.children), get)
359 def write_to_screen(
360 self,
361 screen: Screen,
362 mouse_handlers: MouseHandlers,
363 write_position: WritePosition,
364 parent_style: str,
365 erase_bg: bool,
366 z_index: int | None,
367 ) -> None:
368 """
369 Render the prompt to a `Screen` instance.
371 :param screen: The :class:`~prompt_toolkit.layout.screen.Screen` class
372 to which the output has to be written.
373 """
374 sizes = self._divide_heights(write_position)
375 style = parent_style + " " + to_str(self.style)
376 z_index = z_index if self.z_index is None else self.z_index
378 if sizes is None:
379 self.window_too_small.write_to_screen(
380 screen, mouse_handlers, write_position, style, erase_bg, z_index
381 )
382 else:
383 #
384 ypos = write_position.ypos
385 xpos = write_position.xpos
386 width = write_position.width
388 # Draw child panes.
389 for s, c in zip(sizes, self._all_children):
390 c.write_to_screen(
391 screen,
392 mouse_handlers,
393 WritePosition(xpos, ypos, width, s),
394 style,
395 erase_bg,
396 z_index,
397 )
398 ypos += s
400 # Fill in the remaining space. This happens when a child control
401 # refuses to take more space and we don't have any padding. Adding a
402 # dummy child control for this (in `self._all_children`) is not
403 # desired, because in some situations, it would take more space, even
404 # when it's not required. This is required to apply the styling.
405 remaining_height = write_position.ypos + write_position.height - ypos
406 if remaining_height > 0:
407 self._remaining_space_window.write_to_screen(
408 screen,
409 mouse_handlers,
410 WritePosition(xpos, ypos, width, remaining_height),
411 style,
412 erase_bg,
413 z_index,
414 )
416 def _divide_heights(self, write_position: WritePosition) -> list[int] | None:
417 """
418 Return the heights for all rows.
419 Or None when there is not enough space.
420 """
421 if not self.children:
422 return []
424 width = write_position.width
425 height = write_position.height
427 # Calculate heights.
428 dimensions = [c.preferred_height(width, height) for c in self._all_children]
430 # Sum dimensions
431 sum_dimensions = sum_layout_dimensions(dimensions)
433 # If there is not enough space for both.
434 # Don't do anything.
435 if sum_dimensions.min > height:
436 return None
438 # Find optimal sizes. (Start with minimal size, increase until we cover
439 # the whole height.)
440 sizes = [d.min for d in dimensions]
442 child_generator = take_using_weights(
443 items=list(range(len(dimensions))), weights=[d.weight for d in dimensions]
444 )
446 i = next(child_generator)
448 # Increase until we meet at least the 'preferred' size.
449 preferred_stop = min(height, sum_dimensions.preferred)
450 preferred_dimensions = [d.preferred for d in dimensions]
452 while sum(sizes) < preferred_stop:
453 if sizes[i] < preferred_dimensions[i]:
454 sizes[i] += 1
455 i = next(child_generator)
457 # Increase until we use all the available space. (or until "max")
458 if not get_app().is_done:
459 max_stop = min(height, sum_dimensions.max)
460 max_dimensions = [d.max for d in dimensions]
462 while sum(sizes) < max_stop:
463 if sizes[i] < max_dimensions[i]:
464 sizes[i] += 1
465 i = next(child_generator)
467 return sizes
470class VSplit(_Split):
471 """
472 Several layouts, one stacked left/right of the other. ::
474 +---------+----------+
475 | | |
476 | | |
477 +---------+----------+
479 By default, this doesn't display a vertical line between the children, but
480 if this is something you need, then create a HSplit as follows::
482 VSplit(children=[ ... ], padding_char='|',
483 padding=1, padding_style='#ffff00')
485 :param children: List of child :class:`.Container` objects.
486 :param window_too_small: A :class:`.Container` object that is displayed if
487 there is not enough space for all the children. By default, this is a
488 "Window too small" message.
489 :param align: `HorizontalAlign` value.
490 :param width: When given, use this width instead of looking at the children.
491 :param height: When given, use this height instead of looking at the children.
492 :param z_index: (int or None) When specified, this can be used to bring
493 element in front of floating elements. `None` means: inherit from parent.
494 :param style: A style string.
495 :param modal: ``True`` or ``False``.
496 :param key_bindings: ``None`` or a :class:`.KeyBindings` object.
498 :param padding: (`Dimension` or int), size to be used for the padding.
499 :param padding_char: Character to be used for filling in the padding.
500 :param padding_style: Style to applied to the padding.
501 """
503 def __init__(
504 self,
505 children: Sequence[AnyContainer],
506 window_too_small: Container | None = None,
507 align: HorizontalAlign = HorizontalAlign.JUSTIFY,
508 padding: AnyDimension = 0,
509 padding_char: str | None = None,
510 padding_style: str = "",
511 width: AnyDimension = None,
512 height: AnyDimension = None,
513 z_index: int | None = None,
514 modal: bool = False,
515 key_bindings: KeyBindingsBase | None = None,
516 style: str | Callable[[], str] = "",
517 ) -> None:
518 super().__init__(
519 children=children,
520 window_too_small=window_too_small,
521 padding=padding,
522 padding_char=padding_char,
523 padding_style=padding_style,
524 width=width,
525 height=height,
526 z_index=z_index,
527 modal=modal,
528 key_bindings=key_bindings,
529 style=style,
530 )
532 self.align = align
534 self._children_cache: SimpleCache[
535 tuple[Container, ...], list[Container]
536 ] = SimpleCache(maxsize=1)
537 self._remaining_space_window = Window() # Dummy window.
539 def preferred_width(self, max_available_width: int) -> Dimension:
540 if self.width is not None:
541 return to_dimension(self.width)
543 dimensions = [
544 c.preferred_width(max_available_width) for c in self._all_children
545 ]
547 return sum_layout_dimensions(dimensions)
549 def preferred_height(self, width: int, max_available_height: int) -> Dimension:
550 if self.height is not None:
551 return to_dimension(self.height)
553 # At the point where we want to calculate the heights, the widths have
554 # already been decided. So we can trust `width` to be the actual
555 # `width` that's going to be used for the rendering. So,
556 # `divide_widths` is supposed to use all of the available width.
557 # Using only the `preferred` width caused a bug where the reported
558 # height was more than required. (we had a `BufferControl` which did
559 # wrap lines because of the smaller width returned by `_divide_widths`.
561 sizes = self._divide_widths(width)
562 children = self._all_children
564 if sizes is None:
565 return Dimension()
566 else:
567 dimensions = [
568 c.preferred_height(s, max_available_height)
569 for s, c in zip(sizes, children)
570 ]
571 return max_layout_dimensions(dimensions)
573 def reset(self) -> None:
574 for c in self.children:
575 c.reset()
577 @property
578 def _all_children(self) -> list[Container]:
579 """
580 List of child objects, including padding.
581 """
583 def get() -> list[Container]:
584 result: list[Container] = []
586 # Padding left.
587 if self.align in (HorizontalAlign.CENTER, HorizontalAlign.RIGHT):
588 result.append(Window(width=Dimension(preferred=0)))
590 # The children with padding.
591 for child in self.children:
592 result.append(child)
593 result.append(
594 Window(
595 width=self.padding,
596 char=self.padding_char,
597 style=self.padding_style,
598 )
599 )
600 if result:
601 result.pop()
603 # Padding right.
604 if self.align in (HorizontalAlign.CENTER, HorizontalAlign.LEFT):
605 result.append(Window(width=Dimension(preferred=0)))
607 return result
609 return self._children_cache.get(tuple(self.children), get)
611 def _divide_widths(self, width: int) -> list[int] | None:
612 """
613 Return the widths for all columns.
614 Or None when there is not enough space.
615 """
616 children = self._all_children
618 if not children:
619 return []
621 # Calculate widths.
622 dimensions = [c.preferred_width(width) for c in children]
623 preferred_dimensions = [d.preferred for d in dimensions]
625 # Sum dimensions
626 sum_dimensions = sum_layout_dimensions(dimensions)
628 # If there is not enough space for both.
629 # Don't do anything.
630 if sum_dimensions.min > width:
631 return None
633 # Find optimal sizes. (Start with minimal size, increase until we cover
634 # the whole width.)
635 sizes = [d.min for d in dimensions]
637 child_generator = take_using_weights(
638 items=list(range(len(dimensions))), weights=[d.weight for d in dimensions]
639 )
641 i = next(child_generator)
643 # Increase until we meet at least the 'preferred' size.
644 preferred_stop = min(width, sum_dimensions.preferred)
646 while sum(sizes) < preferred_stop:
647 if sizes[i] < preferred_dimensions[i]:
648 sizes[i] += 1
649 i = next(child_generator)
651 # Increase until we use all the available space.
652 max_dimensions = [d.max for d in dimensions]
653 max_stop = min(width, sum_dimensions.max)
655 while sum(sizes) < max_stop:
656 if sizes[i] < max_dimensions[i]:
657 sizes[i] += 1
658 i = next(child_generator)
660 return sizes
662 def write_to_screen(
663 self,
664 screen: Screen,
665 mouse_handlers: MouseHandlers,
666 write_position: WritePosition,
667 parent_style: str,
668 erase_bg: bool,
669 z_index: int | None,
670 ) -> None:
671 """
672 Render the prompt to a `Screen` instance.
674 :param screen: The :class:`~prompt_toolkit.layout.screen.Screen` class
675 to which the output has to be written.
676 """
677 if not self.children:
678 return
680 children = self._all_children
681 sizes = self._divide_widths(write_position.width)
682 style = parent_style + " " + to_str(self.style)
683 z_index = z_index if self.z_index is None else self.z_index
685 # If there is not enough space.
686 if sizes is None:
687 self.window_too_small.write_to_screen(
688 screen, mouse_handlers, write_position, style, erase_bg, z_index
689 )
690 return
692 # Calculate heights, take the largest possible, but not larger than
693 # write_position.height.
694 heights = [
695 child.preferred_height(width, write_position.height).preferred
696 for width, child in zip(sizes, children)
697 ]
698 height = max(write_position.height, min(write_position.height, max(heights)))
700 #
701 ypos = write_position.ypos
702 xpos = write_position.xpos
704 # Draw all child panes.
705 for s, c in zip(sizes, children):
706 c.write_to_screen(
707 screen,
708 mouse_handlers,
709 WritePosition(xpos, ypos, s, height),
710 style,
711 erase_bg,
712 z_index,
713 )
714 xpos += s
716 # Fill in the remaining space. This happens when a child control
717 # refuses to take more space and we don't have any padding. Adding a
718 # dummy child control for this (in `self._all_children`) is not
719 # desired, because in some situations, it would take more space, even
720 # when it's not required. This is required to apply the styling.
721 remaining_width = write_position.xpos + write_position.width - xpos
722 if remaining_width > 0:
723 self._remaining_space_window.write_to_screen(
724 screen,
725 mouse_handlers,
726 WritePosition(xpos, ypos, remaining_width, height),
727 style,
728 erase_bg,
729 z_index,
730 )
733class FloatContainer(Container):
734 """
735 Container which can contain another container for the background, as well
736 as a list of floating containers on top of it.
738 Example Usage::
740 FloatContainer(content=Window(...),
741 floats=[
742 Float(xcursor=True,
743 ycursor=True,
744 content=CompletionsMenu(...))
745 ])
747 :param z_index: (int or None) When specified, this can be used to bring
748 element in front of floating elements. `None` means: inherit from parent.
749 This is the z_index for the whole `Float` container as a whole.
750 """
752 def __init__(
753 self,
754 content: AnyContainer,
755 floats: list[Float],
756 modal: bool = False,
757 key_bindings: KeyBindingsBase | None = None,
758 style: str | Callable[[], str] = "",
759 z_index: int | None = None,
760 ) -> None:
761 self.content = to_container(content)
762 self.floats = floats
764 self.modal = modal
765 self.key_bindings = key_bindings
766 self.style = style
767 self.z_index = z_index
769 def reset(self) -> None:
770 self.content.reset()
772 for f in self.floats:
773 f.content.reset()
775 def preferred_width(self, max_available_width: int) -> Dimension:
776 return self.content.preferred_width(max_available_width)
778 def preferred_height(self, width: int, max_available_height: int) -> Dimension:
779 """
780 Return the preferred height of the float container.
781 (We don't care about the height of the floats, they should always fit
782 into the dimensions provided by the container.)
783 """
784 return self.content.preferred_height(width, max_available_height)
786 def write_to_screen(
787 self,
788 screen: Screen,
789 mouse_handlers: MouseHandlers,
790 write_position: WritePosition,
791 parent_style: str,
792 erase_bg: bool,
793 z_index: int | None,
794 ) -> None:
795 style = parent_style + " " + to_str(self.style)
796 z_index = z_index if self.z_index is None else self.z_index
798 self.content.write_to_screen(
799 screen, mouse_handlers, write_position, style, erase_bg, z_index
800 )
802 for number, fl in enumerate(self.floats):
803 # z_index of a Float is computed by summing the z_index of the
804 # container and the `Float`.
805 new_z_index = (z_index or 0) + fl.z_index
806 style = parent_style + " " + to_str(self.style)
808 # If the float that we have here, is positioned relative to the
809 # cursor position, but the Window that specifies the cursor
810 # position is not drawn yet, because it's a Float itself, we have
811 # to postpone this calculation. (This is a work-around, but good
812 # enough for now.)
813 postpone = fl.xcursor is not None or fl.ycursor is not None
815 if postpone:
816 new_z_index = (
817 number + 10**8
818 ) # Draw as late as possible, but keep the order.
819 screen.draw_with_z_index(
820 z_index=new_z_index,
821 draw_func=partial(
822 self._draw_float,
823 fl,
824 screen,
825 mouse_handlers,
826 write_position,
827 style,
828 erase_bg,
829 new_z_index,
830 ),
831 )
832 else:
833 self._draw_float(
834 fl,
835 screen,
836 mouse_handlers,
837 write_position,
838 style,
839 erase_bg,
840 new_z_index,
841 )
843 def _draw_float(
844 self,
845 fl: Float,
846 screen: Screen,
847 mouse_handlers: MouseHandlers,
848 write_position: WritePosition,
849 style: str,
850 erase_bg: bool,
851 z_index: int | None,
852 ) -> None:
853 "Draw a single Float."
854 # When a menu_position was given, use this instead of the cursor
855 # position. (These cursor positions are absolute, translate again
856 # relative to the write_position.)
857 # Note: This should be inside the for-loop, because one float could
858 # set the cursor position to be used for the next one.
859 cpos = screen.get_menu_position(
860 fl.attach_to_window or get_app().layout.current_window
861 )
862 cursor_position = Point(
863 x=cpos.x - write_position.xpos, y=cpos.y - write_position.ypos
864 )
866 fl_width = fl.get_width()
867 fl_height = fl.get_height()
868 width: int
869 height: int
870 xpos: int
871 ypos: int
873 # Left & width given.
874 if fl.left is not None and fl_width is not None:
875 xpos = fl.left
876 width = fl_width
877 # Left & right given -> calculate width.
878 elif fl.left is not None and fl.right is not None:
879 xpos = fl.left
880 width = write_position.width - fl.left - fl.right
881 # Width & right given -> calculate left.
882 elif fl_width is not None and fl.right is not None:
883 xpos = write_position.width - fl.right - fl_width
884 width = fl_width
885 # Near x position of cursor.
886 elif fl.xcursor:
887 if fl_width is None:
888 width = fl.content.preferred_width(write_position.width).preferred
889 width = min(write_position.width, width)
890 else:
891 width = fl_width
893 xpos = cursor_position.x
894 if xpos + width > write_position.width:
895 xpos = max(0, write_position.width - width)
896 # Only width given -> center horizontally.
897 elif fl_width:
898 xpos = int((write_position.width - fl_width) / 2)
899 width = fl_width
900 # Otherwise, take preferred width from float content.
901 else:
902 width = fl.content.preferred_width(write_position.width).preferred
904 if fl.left is not None:
905 xpos = fl.left
906 elif fl.right is not None:
907 xpos = max(0, write_position.width - width - fl.right)
908 else: # Center horizontally.
909 xpos = max(0, int((write_position.width - width) / 2))
911 # Trim.
912 width = min(width, write_position.width - xpos)
914 # Top & height given.
915 if fl.top is not None and fl_height is not None:
916 ypos = fl.top
917 height = fl_height
918 # Top & bottom given -> calculate height.
919 elif fl.top is not None and fl.bottom is not None:
920 ypos = fl.top
921 height = write_position.height - fl.top - fl.bottom
922 # Height & bottom given -> calculate top.
923 elif fl_height is not None and fl.bottom is not None:
924 ypos = write_position.height - fl_height - fl.bottom
925 height = fl_height
926 # Near cursor.
927 elif fl.ycursor:
928 ypos = cursor_position.y + (0 if fl.allow_cover_cursor else 1)
930 if fl_height is None:
931 height = fl.content.preferred_height(
932 width, write_position.height
933 ).preferred
934 else:
935 height = fl_height
937 # Reduce height if not enough space. (We can use the height
938 # when the content requires it.)
939 if height > write_position.height - ypos:
940 if write_position.height - ypos + 1 >= ypos:
941 # When the space below the cursor is more than
942 # the space above, just reduce the height.
943 height = write_position.height - ypos
944 else:
945 # Otherwise, fit the float above the cursor.
946 height = min(height, cursor_position.y)
947 ypos = cursor_position.y - height
949 # Only height given -> center vertically.
950 elif fl_height:
951 ypos = int((write_position.height - fl_height) / 2)
952 height = fl_height
953 # Otherwise, take preferred height from content.
954 else:
955 height = fl.content.preferred_height(width, write_position.height).preferred
957 if fl.top is not None:
958 ypos = fl.top
959 elif fl.bottom is not None:
960 ypos = max(0, write_position.height - height - fl.bottom)
961 else: # Center vertically.
962 ypos = max(0, int((write_position.height - height) / 2))
964 # Trim.
965 height = min(height, write_position.height - ypos)
967 # Write float.
968 # (xpos and ypos can be negative: a float can be partially visible.)
969 if height > 0 and width > 0:
970 wp = WritePosition(
971 xpos=xpos + write_position.xpos,
972 ypos=ypos + write_position.ypos,
973 width=width,
974 height=height,
975 )
977 if not fl.hide_when_covering_content or self._area_is_empty(screen, wp):
978 fl.content.write_to_screen(
979 screen,
980 mouse_handlers,
981 wp,
982 style,
983 erase_bg=not fl.transparent(),
984 z_index=z_index,
985 )
987 def _area_is_empty(self, screen: Screen, write_position: WritePosition) -> bool:
988 """
989 Return True when the area below the write position is still empty.
990 (For floats that should not hide content underneath.)
991 """
992 wp = write_position
994 for y in range(wp.ypos, wp.ypos + wp.height):
995 if y in screen.data_buffer:
996 row = screen.data_buffer[y]
998 for x in range(wp.xpos, wp.xpos + wp.width):
999 c = row[x]
1000 if c.char != " ":
1001 return False
1003 return True
1005 def is_modal(self) -> bool:
1006 return self.modal
1008 def get_key_bindings(self) -> KeyBindingsBase | None:
1009 return self.key_bindings
1011 def get_children(self) -> list[Container]:
1012 children = [self.content]
1013 children.extend(f.content for f in self.floats)
1014 return children
1017class Float:
1018 """
1019 Float for use in a :class:`.FloatContainer`.
1020 Except for the `content` parameter, all other options are optional.
1022 :param content: :class:`.Container` instance.
1024 :param width: :class:`.Dimension` or callable which returns a :class:`.Dimension`.
1025 :param height: :class:`.Dimension` or callable which returns a :class:`.Dimension`.
1027 :param left: Distance to the left edge of the :class:`.FloatContainer`.
1028 :param right: Distance to the right edge of the :class:`.FloatContainer`.
1029 :param top: Distance to the top of the :class:`.FloatContainer`.
1030 :param bottom: Distance to the bottom of the :class:`.FloatContainer`.
1032 :param attach_to_window: Attach to the cursor from this window, instead of
1033 the current window.
1034 :param hide_when_covering_content: Hide the float when it covers content underneath.
1035 :param allow_cover_cursor: When `False`, make sure to display the float
1036 below the cursor. Not on top of the indicated position.
1037 :param z_index: Z-index position. For a Float, this needs to be at least
1038 one. It is relative to the z_index of the parent container.
1039 :param transparent: :class:`.Filter` indicating whether this float needs to be
1040 drawn transparently.
1041 """
1043 def __init__(
1044 self,
1045 content: AnyContainer,
1046 top: int | None = None,
1047 right: int | None = None,
1048 bottom: int | None = None,
1049 left: int | None = None,
1050 width: int | Callable[[], int] | None = None,
1051 height: int | Callable[[], int] | None = None,
1052 xcursor: bool = False,
1053 ycursor: bool = False,
1054 attach_to_window: AnyContainer | None = None,
1055 hide_when_covering_content: bool = False,
1056 allow_cover_cursor: bool = False,
1057 z_index: int = 1,
1058 transparent: bool = False,
1059 ) -> None:
1060 assert z_index >= 1
1062 self.left = left
1063 self.right = right
1064 self.top = top
1065 self.bottom = bottom
1067 self.width = width
1068 self.height = height
1070 self.xcursor = xcursor
1071 self.ycursor = ycursor
1073 self.attach_to_window = (
1074 to_window(attach_to_window) if attach_to_window else None
1075 )
1077 self.content = to_container(content)
1078 self.hide_when_covering_content = hide_when_covering_content
1079 self.allow_cover_cursor = allow_cover_cursor
1080 self.z_index = z_index
1081 self.transparent = to_filter(transparent)
1083 def get_width(self) -> int | None:
1084 if callable(self.width):
1085 return self.width()
1086 return self.width
1088 def get_height(self) -> int | None:
1089 if callable(self.height):
1090 return self.height()
1091 return self.height
1093 def __repr__(self) -> str:
1094 return "Float(content=%r)" % self.content
1097class WindowRenderInfo:
1098 """
1099 Render information for the last render time of this control.
1100 It stores mapping information between the input buffers (in case of a
1101 :class:`~prompt_toolkit.layout.controls.BufferControl`) and the actual
1102 render position on the output screen.
1104 (Could be used for implementation of the Vi 'H' and 'L' key bindings as
1105 well as implementing mouse support.)
1107 :param ui_content: The original :class:`.UIContent` instance that contains
1108 the whole input, without clipping. (ui_content)
1109 :param horizontal_scroll: The horizontal scroll of the :class:`.Window` instance.
1110 :param vertical_scroll: The vertical scroll of the :class:`.Window` instance.
1111 :param window_width: The width of the window that displays the content,
1112 without the margins.
1113 :param window_height: The height of the window that displays the content.
1114 :param configured_scroll_offsets: The scroll offsets as configured for the
1115 :class:`Window` instance.
1116 :param visible_line_to_row_col: Mapping that maps the row numbers on the
1117 displayed screen (starting from zero for the first visible line) to
1118 (row, col) tuples pointing to the row and column of the :class:`.UIContent`.
1119 :param rowcol_to_yx: Mapping that maps (row, column) tuples representing
1120 coordinates of the :class:`UIContent` to (y, x) absolute coordinates at
1121 the rendered screen.
1122 """
1124 def __init__(
1125 self,
1126 window: Window,
1127 ui_content: UIContent,
1128 horizontal_scroll: int,
1129 vertical_scroll: int,
1130 window_width: int,
1131 window_height: int,
1132 configured_scroll_offsets: ScrollOffsets,
1133 visible_line_to_row_col: dict[int, tuple[int, int]],
1134 rowcol_to_yx: dict[tuple[int, int], tuple[int, int]],
1135 x_offset: int,
1136 y_offset: int,
1137 wrap_lines: bool,
1138 ) -> None:
1139 self.window = window
1140 self.ui_content = ui_content
1141 self.vertical_scroll = vertical_scroll
1142 self.window_width = window_width # Width without margins.
1143 self.window_height = window_height
1145 self.configured_scroll_offsets = configured_scroll_offsets
1146 self.visible_line_to_row_col = visible_line_to_row_col
1147 self.wrap_lines = wrap_lines
1149 self._rowcol_to_yx = rowcol_to_yx # row/col from input to absolute y/x
1150 # screen coordinates.
1151 self._x_offset = x_offset
1152 self._y_offset = y_offset
1154 @property
1155 def visible_line_to_input_line(self) -> dict[int, int]:
1156 return {
1157 visible_line: rowcol[0]
1158 for visible_line, rowcol in self.visible_line_to_row_col.items()
1159 }
1161 @property
1162 def cursor_position(self) -> Point:
1163 """
1164 Return the cursor position coordinates, relative to the left/top corner
1165 of the rendered screen.
1166 """
1167 cpos = self.ui_content.cursor_position
1168 try:
1169 y, x = self._rowcol_to_yx[cpos.y, cpos.x]
1170 except KeyError:
1171 # For `DummyControl` for instance, the content can be empty, and so
1172 # will `_rowcol_to_yx` be. Return 0/0 by default.
1173 return Point(x=0, y=0)
1174 else:
1175 return Point(x=x - self._x_offset, y=y - self._y_offset)
1177 @property
1178 def applied_scroll_offsets(self) -> ScrollOffsets:
1179 """
1180 Return a :class:`.ScrollOffsets` instance that indicates the actual
1181 offset. This can be less than or equal to what's configured. E.g, when
1182 the cursor is completely at the top, the top offset will be zero rather
1183 than what's configured.
1184 """
1185 if self.displayed_lines[0] == 0:
1186 top = 0
1187 else:
1188 # Get row where the cursor is displayed.
1189 y = self.input_line_to_visible_line[self.ui_content.cursor_position.y]
1190 top = min(y, self.configured_scroll_offsets.top)
1192 return ScrollOffsets(
1193 top=top,
1194 bottom=min(
1195 self.ui_content.line_count - self.displayed_lines[-1] - 1,
1196 self.configured_scroll_offsets.bottom,
1197 ),
1198 # For left/right, it probably doesn't make sense to return something.
1199 # (We would have to calculate the widths of all the lines and keep
1200 # double width characters in mind.)
1201 left=0,
1202 right=0,
1203 )
1205 @property
1206 def displayed_lines(self) -> list[int]:
1207 """
1208 List of all the visible rows. (Line numbers of the input buffer.)
1209 The last line may not be entirely visible.
1210 """
1211 return sorted(row for row, col in self.visible_line_to_row_col.values())
1213 @property
1214 def input_line_to_visible_line(self) -> dict[int, int]:
1215 """
1216 Return the dictionary mapping the line numbers of the input buffer to
1217 the lines of the screen. When a line spans several rows at the screen,
1218 the first row appears in the dictionary.
1219 """
1220 result: dict[int, int] = {}
1221 for k, v in self.visible_line_to_input_line.items():
1222 if v in result:
1223 result[v] = min(result[v], k)
1224 else:
1225 result[v] = k
1226 return result
1228 def first_visible_line(self, after_scroll_offset: bool = False) -> int:
1229 """
1230 Return the line number (0 based) of the input document that corresponds
1231 with the first visible line.
1232 """
1233 if after_scroll_offset:
1234 return self.displayed_lines[self.applied_scroll_offsets.top]
1235 else:
1236 return self.displayed_lines[0]
1238 def last_visible_line(self, before_scroll_offset: bool = False) -> int:
1239 """
1240 Like `first_visible_line`, but for the last visible line.
1241 """
1242 if before_scroll_offset:
1243 return self.displayed_lines[-1 - self.applied_scroll_offsets.bottom]
1244 else:
1245 return self.displayed_lines[-1]
1247 def center_visible_line(
1248 self, before_scroll_offset: bool = False, after_scroll_offset: bool = False
1249 ) -> int:
1250 """
1251 Like `first_visible_line`, but for the center visible line.
1252 """
1253 return (
1254 self.first_visible_line(after_scroll_offset)
1255 + (
1256 self.last_visible_line(before_scroll_offset)
1257 - self.first_visible_line(after_scroll_offset)
1258 )
1259 // 2
1260 )
1262 @property
1263 def content_height(self) -> int:
1264 """
1265 The full height of the user control.
1266 """
1267 return self.ui_content.line_count
1269 @property
1270 def full_height_visible(self) -> bool:
1271 """
1272 True when the full height is visible (There is no vertical scroll.)
1273 """
1274 return (
1275 self.vertical_scroll == 0
1276 and self.last_visible_line() == self.content_height
1277 )
1279 @property
1280 def top_visible(self) -> bool:
1281 """
1282 True when the top of the buffer is visible.
1283 """
1284 return self.vertical_scroll == 0
1286 @property
1287 def bottom_visible(self) -> bool:
1288 """
1289 True when the bottom of the buffer is visible.
1290 """
1291 return self.last_visible_line() == self.content_height - 1
1293 @property
1294 def vertical_scroll_percentage(self) -> int:
1295 """
1296 Vertical scroll as a percentage. (0 means: the top is visible,
1297 100 means: the bottom is visible.)
1298 """
1299 if self.bottom_visible:
1300 return 100
1301 else:
1302 return 100 * self.vertical_scroll // self.content_height
1304 def get_height_for_line(self, lineno: int) -> int:
1305 """
1306 Return the height of the given line.
1307 (The height that it would take, if this line became visible.)
1308 """
1309 if self.wrap_lines:
1310 return self.ui_content.get_height_for_line(
1311 lineno, self.window_width, self.window.get_line_prefix
1312 )
1313 else:
1314 return 1
1317class ScrollOffsets:
1318 """
1319 Scroll offsets for the :class:`.Window` class.
1321 Note that left/right offsets only make sense if line wrapping is disabled.
1322 """
1324 def __init__(
1325 self,
1326 top: int | Callable[[], int] = 0,
1327 bottom: int | Callable[[], int] = 0,
1328 left: int | Callable[[], int] = 0,
1329 right: int | Callable[[], int] = 0,
1330 ) -> None:
1331 self._top = top
1332 self._bottom = bottom
1333 self._left = left
1334 self._right = right
1336 @property
1337 def top(self) -> int:
1338 return to_int(self._top)
1340 @property
1341 def bottom(self) -> int:
1342 return to_int(self._bottom)
1344 @property
1345 def left(self) -> int:
1346 return to_int(self._left)
1348 @property
1349 def right(self) -> int:
1350 return to_int(self._right)
1352 def __repr__(self) -> str:
1353 return "ScrollOffsets(top={!r}, bottom={!r}, left={!r}, right={!r})".format(
1354 self._top,
1355 self._bottom,
1356 self._left,
1357 self._right,
1358 )
1361class ColorColumn:
1362 """
1363 Column for a :class:`.Window` to be colored.
1364 """
1366 def __init__(self, position: int, style: str = "class:color-column") -> None:
1367 self.position = position
1368 self.style = style
1371_in_insert_mode = vi_insert_mode | emacs_insert_mode
1374class WindowAlign(Enum):
1375 """
1376 Alignment of the Window content.
1378 Note that this is different from `HorizontalAlign` and `VerticalAlign`,
1379 which are used for the alignment of the child containers in respectively
1380 `VSplit` and `HSplit`.
1381 """
1383 LEFT = "LEFT"
1384 RIGHT = "RIGHT"
1385 CENTER = "CENTER"
1388class Window(Container):
1389 """
1390 Container that holds a control.
1392 :param content: :class:`.UIControl` instance.
1393 :param width: :class:`.Dimension` instance or callable.
1394 :param height: :class:`.Dimension` instance or callable.
1395 :param z_index: When specified, this can be used to bring element in front
1396 of floating elements.
1397 :param dont_extend_width: When `True`, don't take up more width then the
1398 preferred width reported by the control.
1399 :param dont_extend_height: When `True`, don't take up more width then the
1400 preferred height reported by the control.
1401 :param ignore_content_width: A `bool` or :class:`.Filter` instance. Ignore
1402 the :class:`.UIContent` width when calculating the dimensions.
1403 :param ignore_content_height: A `bool` or :class:`.Filter` instance. Ignore
1404 the :class:`.UIContent` height when calculating the dimensions.
1405 :param left_margins: A list of :class:`.Margin` instance to be displayed on
1406 the left. For instance: :class:`~prompt_toolkit.layout.NumberedMargin`
1407 can be one of them in order to show line numbers.
1408 :param right_margins: Like `left_margins`, but on the other side.
1409 :param scroll_offsets: :class:`.ScrollOffsets` instance, representing the
1410 preferred amount of lines/columns to be always visible before/after the
1411 cursor. When both top and bottom are a very high number, the cursor
1412 will be centered vertically most of the time.
1413 :param allow_scroll_beyond_bottom: A `bool` or
1414 :class:`.Filter` instance. When True, allow scrolling so far, that the
1415 top part of the content is not visible anymore, while there is still
1416 empty space available at the bottom of the window. In the Vi editor for
1417 instance, this is possible. You will see tildes while the top part of
1418 the body is hidden.
1419 :param wrap_lines: A `bool` or :class:`.Filter` instance. When True, don't
1420 scroll horizontally, but wrap lines instead.
1421 :param get_vertical_scroll: Callable that takes this window
1422 instance as input and returns a preferred vertical scroll.
1423 (When this is `None`, the scroll is only determined by the last and
1424 current cursor position.)
1425 :param get_horizontal_scroll: Callable that takes this window
1426 instance as input and returns a preferred vertical scroll.
1427 :param always_hide_cursor: A `bool` or
1428 :class:`.Filter` instance. When True, never display the cursor, even
1429 when the user control specifies a cursor position.
1430 :param cursorline: A `bool` or :class:`.Filter` instance. When True,
1431 display a cursorline.
1432 :param cursorcolumn: A `bool` or :class:`.Filter` instance. When True,
1433 display a cursorcolumn.
1434 :param colorcolumns: A list of :class:`.ColorColumn` instances that
1435 describe the columns to be highlighted, or a callable that returns such
1436 a list.
1437 :param align: :class:`.WindowAlign` value or callable that returns an
1438 :class:`.WindowAlign` value. alignment of content.
1439 :param style: A style string. Style to be applied to all the cells in this
1440 window. (This can be a callable that returns a string.)
1441 :param char: (string) Character to be used for filling the background. This
1442 can also be a callable that returns a character.
1443 :param get_line_prefix: None or a callable that returns formatted text to
1444 be inserted before a line. It takes a line number (int) and a
1445 wrap_count and returns formatted text. This can be used for
1446 implementation of line continuations, things like Vim "breakindent" and
1447 so on.
1448 """
1450 def __init__(
1451 self,
1452 content: UIControl | None = None,
1453 width: AnyDimension = None,
1454 height: AnyDimension = None,
1455 z_index: int | None = None,
1456 dont_extend_width: FilterOrBool = False,
1457 dont_extend_height: FilterOrBool = False,
1458 ignore_content_width: FilterOrBool = False,
1459 ignore_content_height: FilterOrBool = False,
1460 left_margins: Sequence[Margin] | None = None,
1461 right_margins: Sequence[Margin] | None = None,
1462 scroll_offsets: ScrollOffsets | None = None,
1463 allow_scroll_beyond_bottom: FilterOrBool = False,
1464 wrap_lines: FilterOrBool = False,
1465 get_vertical_scroll: Callable[[Window], int] | None = None,
1466 get_horizontal_scroll: Callable[[Window], int] | None = None,
1467 always_hide_cursor: FilterOrBool = False,
1468 cursorline: FilterOrBool = False,
1469 cursorcolumn: FilterOrBool = False,
1470 colorcolumns: (
1471 None | list[ColorColumn] | Callable[[], list[ColorColumn]]
1472 ) = None,
1473 align: WindowAlign | Callable[[], WindowAlign] = WindowAlign.LEFT,
1474 style: str | Callable[[], str] = "",
1475 char: None | str | Callable[[], str] = None,
1476 get_line_prefix: GetLinePrefixCallable | None = None,
1477 ) -> None:
1478 self.allow_scroll_beyond_bottom = to_filter(allow_scroll_beyond_bottom)
1479 self.always_hide_cursor = to_filter(always_hide_cursor)
1480 self.wrap_lines = to_filter(wrap_lines)
1481 self.cursorline = to_filter(cursorline)
1482 self.cursorcolumn = to_filter(cursorcolumn)
1484 self.content = content or DummyControl()
1485 self.dont_extend_width = to_filter(dont_extend_width)
1486 self.dont_extend_height = to_filter(dont_extend_height)
1487 self.ignore_content_width = to_filter(ignore_content_width)
1488 self.ignore_content_height = to_filter(ignore_content_height)
1489 self.left_margins = left_margins or []
1490 self.right_margins = right_margins or []
1491 self.scroll_offsets = scroll_offsets or ScrollOffsets()
1492 self.get_vertical_scroll = get_vertical_scroll
1493 self.get_horizontal_scroll = get_horizontal_scroll
1494 self.colorcolumns = colorcolumns or []
1495 self.align = align
1496 self.style = style
1497 self.char = char
1498 self.get_line_prefix = get_line_prefix
1500 self.width = width
1501 self.height = height
1502 self.z_index = z_index
1504 # Cache for the screens generated by the margin.
1505 self._ui_content_cache: SimpleCache[
1506 tuple[int, int, int], UIContent
1507 ] = SimpleCache(maxsize=8)
1508 self._margin_width_cache: SimpleCache[tuple[Margin, int], int] = SimpleCache(
1509 maxsize=1
1510 )
1512 self.reset()
1514 def __repr__(self) -> str:
1515 return "Window(content=%r)" % self.content
1517 def reset(self) -> None:
1518 self.content.reset()
1520 #: Scrolling position of the main content.
1521 self.vertical_scroll = 0
1522 self.horizontal_scroll = 0
1524 # Vertical scroll 2: this is the vertical offset that a line is
1525 # scrolled if a single line (the one that contains the cursor) consumes
1526 # all of the vertical space.
1527 self.vertical_scroll_2 = 0
1529 #: Keep render information (mappings between buffer input and render
1530 #: output.)
1531 self.render_info: WindowRenderInfo | None = None
1533 def _get_margin_width(self, margin: Margin) -> int:
1534 """
1535 Return the width for this margin.
1536 (Calculate only once per render time.)
1537 """
1539 # Margin.get_width, needs to have a UIContent instance.
1540 def get_ui_content() -> UIContent:
1541 return self._get_ui_content(width=0, height=0)
1543 def get_width() -> int:
1544 return margin.get_width(get_ui_content)
1546 key = (margin, get_app().render_counter)
1547 return self._margin_width_cache.get(key, get_width)
1549 def _get_total_margin_width(self) -> int:
1550 """
1551 Calculate and return the width of the margin (left + right).
1552 """
1553 return sum(self._get_margin_width(m) for m in self.left_margins) + sum(
1554 self._get_margin_width(m) for m in self.right_margins
1555 )
1557 def preferred_width(self, max_available_width: int) -> Dimension:
1558 """
1559 Calculate the preferred width for this window.
1560 """
1562 def preferred_content_width() -> int | None:
1563 """Content width: is only calculated if no exact width for the
1564 window was given."""
1565 if self.ignore_content_width():
1566 return None
1568 # Calculate the width of the margin.
1569 total_margin_width = self._get_total_margin_width()
1571 # Window of the content. (Can be `None`.)
1572 preferred_width = self.content.preferred_width(
1573 max_available_width - total_margin_width
1574 )
1576 if preferred_width is not None:
1577 # Include width of the margins.
1578 preferred_width += total_margin_width
1579 return preferred_width
1581 # Merge.
1582 return self._merge_dimensions(
1583 dimension=to_dimension(self.width),
1584 get_preferred=preferred_content_width,
1585 dont_extend=self.dont_extend_width(),
1586 )
1588 def preferred_height(self, width: int, max_available_height: int) -> Dimension:
1589 """
1590 Calculate the preferred height for this window.
1591 """
1593 def preferred_content_height() -> int | None:
1594 """Content height: is only calculated if no exact height for the
1595 window was given."""
1596 if self.ignore_content_height():
1597 return None
1599 total_margin_width = self._get_total_margin_width()
1600 wrap_lines = self.wrap_lines()
1602 return self.content.preferred_height(
1603 width - total_margin_width,
1604 max_available_height,
1605 wrap_lines,
1606 self.get_line_prefix,
1607 )
1609 return self._merge_dimensions(
1610 dimension=to_dimension(self.height),
1611 get_preferred=preferred_content_height,
1612 dont_extend=self.dont_extend_height(),
1613 )
1615 @staticmethod
1616 def _merge_dimensions(
1617 dimension: Dimension | None,
1618 get_preferred: Callable[[], int | None],
1619 dont_extend: bool = False,
1620 ) -> Dimension:
1621 """
1622 Take the Dimension from this `Window` class and the received preferred
1623 size from the `UIControl` and return a `Dimension` to report to the
1624 parent container.
1625 """
1626 dimension = dimension or Dimension()
1628 # When a preferred dimension was explicitly given to the Window,
1629 # ignore the UIControl.
1630 preferred: int | None
1632 if dimension.preferred_specified:
1633 preferred = dimension.preferred
1634 else:
1635 # Otherwise, calculate the preferred dimension from the UI control
1636 # content.
1637 preferred = get_preferred()
1639 # When a 'preferred' dimension is given by the UIControl, make sure
1640 # that it stays within the bounds of the Window.
1641 if preferred is not None:
1642 if dimension.max_specified:
1643 preferred = min(preferred, dimension.max)
1645 if dimension.min_specified:
1646 preferred = max(preferred, dimension.min)
1648 # When a `dont_extend` flag has been given, use the preferred dimension
1649 # also as the max dimension.
1650 max_: int | None
1651 min_: int | None
1653 if dont_extend and preferred is not None:
1654 max_ = min(dimension.max, preferred)
1655 else:
1656 max_ = dimension.max if dimension.max_specified else None
1658 min_ = dimension.min if dimension.min_specified else None
1660 return Dimension(
1661 min=min_, max=max_, preferred=preferred, weight=dimension.weight
1662 )
1664 def _get_ui_content(self, width: int, height: int) -> UIContent:
1665 """
1666 Create a `UIContent` instance.
1667 """
1669 def get_content() -> UIContent:
1670 return self.content.create_content(width=width, height=height)
1672 key = (get_app().render_counter, width, height)
1673 return self._ui_content_cache.get(key, get_content)
1675 def _get_digraph_char(self) -> str | None:
1676 "Return `False`, or the Digraph symbol to be used."
1677 app = get_app()
1678 if app.quoted_insert:
1679 return "^"
1680 if app.vi_state.waiting_for_digraph:
1681 if app.vi_state.digraph_symbol1:
1682 return app.vi_state.digraph_symbol1
1683 return "?"
1684 return None
1686 def write_to_screen(
1687 self,
1688 screen: Screen,
1689 mouse_handlers: MouseHandlers,
1690 write_position: WritePosition,
1691 parent_style: str,
1692 erase_bg: bool,
1693 z_index: int | None,
1694 ) -> None:
1695 """
1696 Write window to screen. This renders the user control, the margins and
1697 copies everything over to the absolute position at the given screen.
1698 """
1699 # If dont_extend_width/height was given. Then reduce width/height in
1700 # WritePosition if the parent wanted us to paint in a bigger area.
1701 # (This happens if this window is bundled with another window in a
1702 # HSplit/VSplit, but with different size requirements.)
1703 write_position = WritePosition(
1704 xpos=write_position.xpos,
1705 ypos=write_position.ypos,
1706 width=write_position.width,
1707 height=write_position.height,
1708 )
1710 if self.dont_extend_width():
1711 write_position.width = min(
1712 write_position.width,
1713 self.preferred_width(write_position.width).preferred,
1714 )
1716 if self.dont_extend_height():
1717 write_position.height = min(
1718 write_position.height,
1719 self.preferred_height(
1720 write_position.width, write_position.height
1721 ).preferred,
1722 )
1724 # Draw
1725 z_index = z_index if self.z_index is None else self.z_index
1727 draw_func = partial(
1728 self._write_to_screen_at_index,
1729 screen,
1730 mouse_handlers,
1731 write_position,
1732 parent_style,
1733 erase_bg,
1734 )
1736 if z_index is None or z_index <= 0:
1737 # When no z_index is given, draw right away.
1738 draw_func()
1739 else:
1740 # Otherwise, postpone.
1741 screen.draw_with_z_index(z_index=z_index, draw_func=draw_func)
1743 def _write_to_screen_at_index(
1744 self,
1745 screen: Screen,
1746 mouse_handlers: MouseHandlers,
1747 write_position: WritePosition,
1748 parent_style: str,
1749 erase_bg: bool,
1750 ) -> None:
1751 # Don't bother writing invisible windows.
1752 # (We save some time, but also avoid applying last-line styling.)
1753 if write_position.height <= 0 or write_position.width <= 0:
1754 return
1756 # Calculate margin sizes.
1757 left_margin_widths = [self._get_margin_width(m) for m in self.left_margins]
1758 right_margin_widths = [self._get_margin_width(m) for m in self.right_margins]
1759 total_margin_width = sum(left_margin_widths + right_margin_widths)
1761 # Render UserControl.
1762 ui_content = self.content.create_content(
1763 write_position.width - total_margin_width, write_position.height
1764 )
1765 assert isinstance(ui_content, UIContent)
1767 # Scroll content.
1768 wrap_lines = self.wrap_lines()
1769 self._scroll(
1770 ui_content, write_position.width - total_margin_width, write_position.height
1771 )
1773 # Erase background and fill with `char`.
1774 self._fill_bg(screen, write_position, erase_bg)
1776 # Resolve `align` attribute.
1777 align = self.align() if callable(self.align) else self.align
1779 # Write body
1780 visible_line_to_row_col, rowcol_to_yx = self._copy_body(
1781 ui_content,
1782 screen,
1783 write_position,
1784 sum(left_margin_widths),
1785 write_position.width - total_margin_width,
1786 self.vertical_scroll,
1787 self.horizontal_scroll,
1788 wrap_lines=wrap_lines,
1789 highlight_lines=True,
1790 vertical_scroll_2=self.vertical_scroll_2,
1791 always_hide_cursor=self.always_hide_cursor(),
1792 has_focus=get_app().layout.current_control == self.content,
1793 align=align,
1794 get_line_prefix=self.get_line_prefix,
1795 )
1797 # Remember render info. (Set before generating the margins. They need this.)
1798 x_offset = write_position.xpos + sum(left_margin_widths)
1799 y_offset = write_position.ypos
1801 render_info = WindowRenderInfo(
1802 window=self,
1803 ui_content=ui_content,
1804 horizontal_scroll=self.horizontal_scroll,
1805 vertical_scroll=self.vertical_scroll,
1806 window_width=write_position.width - total_margin_width,
1807 window_height=write_position.height,
1808 configured_scroll_offsets=self.scroll_offsets,
1809 visible_line_to_row_col=visible_line_to_row_col,
1810 rowcol_to_yx=rowcol_to_yx,
1811 x_offset=x_offset,
1812 y_offset=y_offset,
1813 wrap_lines=wrap_lines,
1814 )
1815 self.render_info = render_info
1817 # Set mouse handlers.
1818 def mouse_handler(mouse_event: MouseEvent) -> NotImplementedOrNone:
1819 """
1820 Wrapper around the mouse_handler of the `UIControl` that turns
1821 screen coordinates into line coordinates.
1822 Returns `NotImplemented` if no UI invalidation should be done.
1823 """
1824 # Don't handle mouse events outside of the current modal part of
1825 # the UI.
1826 if self not in get_app().layout.walk_through_modal_area():
1827 return NotImplemented
1829 # Find row/col position first.
1830 yx_to_rowcol = {v: k for k, v in rowcol_to_yx.items()}
1831 y = mouse_event.position.y
1832 x = mouse_event.position.x
1834 # If clicked below the content area, look for a position in the
1835 # last line instead.
1836 max_y = write_position.ypos + len(visible_line_to_row_col) - 1
1837 y = min(max_y, y)
1838 result: NotImplementedOrNone
1840 while x >= 0:
1841 try:
1842 row, col = yx_to_rowcol[y, x]
1843 except KeyError:
1844 # Try again. (When clicking on the right side of double
1845 # width characters, or on the right side of the input.)
1846 x -= 1
1847 else:
1848 # Found position, call handler of UIControl.
1849 result = self.content.mouse_handler(
1850 MouseEvent(
1851 position=Point(x=col, y=row),
1852 event_type=mouse_event.event_type,
1853 button=mouse_event.button,
1854 modifiers=mouse_event.modifiers,
1855 )
1856 )
1857 break
1858 else:
1859 # nobreak.
1860 # (No x/y coordinate found for the content. This happens in
1861 # case of a DummyControl, that does not have any content.
1862 # Report (0,0) instead.)
1863 result = self.content.mouse_handler(
1864 MouseEvent(
1865 position=Point(x=0, y=0),
1866 event_type=mouse_event.event_type,
1867 button=mouse_event.button,
1868 modifiers=mouse_event.modifiers,
1869 )
1870 )
1872 # If it returns NotImplemented, handle it here.
1873 if result == NotImplemented:
1874 result = self._mouse_handler(mouse_event)
1876 return result
1878 mouse_handlers.set_mouse_handler_for_range(
1879 x_min=write_position.xpos + sum(left_margin_widths),
1880 x_max=write_position.xpos + write_position.width - total_margin_width,
1881 y_min=write_position.ypos,
1882 y_max=write_position.ypos + write_position.height,
1883 handler=mouse_handler,
1884 )
1886 # Render and copy margins.
1887 move_x = 0
1889 def render_margin(m: Margin, width: int) -> UIContent:
1890 "Render margin. Return `Screen`."
1891 # Retrieve margin fragments.
1892 fragments = m.create_margin(render_info, width, write_position.height)
1894 # Turn it into a UIContent object.
1895 # already rendered those fragments using this size.)
1896 return FormattedTextControl(fragments).create_content(
1897 width + 1, write_position.height
1898 )
1900 for m, width in zip(self.left_margins, left_margin_widths):
1901 if width > 0: # (ConditionalMargin returns a zero width. -- Don't render.)
1902 # Create screen for margin.
1903 margin_content = render_margin(m, width)
1905 # Copy and shift X.
1906 self._copy_margin(margin_content, screen, write_position, move_x, width)
1907 move_x += width
1909 move_x = write_position.width - sum(right_margin_widths)
1911 for m, width in zip(self.right_margins, right_margin_widths):
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 # Apply 'self.style'
1920 self._apply_style(screen, write_position, parent_style)
1922 # Tell the screen that this user control has been painted at this
1923 # position.
1924 screen.visible_windows_to_write_positions[self] = write_position
1926 def _copy_body(
1927 self,
1928 ui_content: UIContent,
1929 new_screen: Screen,
1930 write_position: WritePosition,
1931 move_x: int,
1932 width: int,
1933 vertical_scroll: int = 0,
1934 horizontal_scroll: int = 0,
1935 wrap_lines: bool = False,
1936 highlight_lines: bool = False,
1937 vertical_scroll_2: int = 0,
1938 always_hide_cursor: bool = False,
1939 has_focus: bool = False,
1940 align: WindowAlign = WindowAlign.LEFT,
1941 get_line_prefix: Callable[[int, int], AnyFormattedText] | None = None,
1942 ) -> tuple[dict[int, tuple[int, int]], dict[tuple[int, int], tuple[int, int]]]:
1943 """
1944 Copy the UIContent into the output screen.
1945 Return (visible_line_to_row_col, rowcol_to_yx) tuple.
1947 :param get_line_prefix: None or a callable that takes a line number
1948 (int) and a wrap_count (int) and returns formatted text.
1949 """
1950 xpos = write_position.xpos + move_x
1951 ypos = write_position.ypos
1952 line_count = ui_content.line_count
1953 new_buffer = new_screen.data_buffer
1954 empty_char = _CHAR_CACHE["", ""]
1956 # Map visible line number to (row, col) of input.
1957 # 'col' will always be zero if line wrapping is off.
1958 visible_line_to_row_col: dict[int, tuple[int, int]] = {}
1960 # Maps (row, col) from the input to (y, x) screen coordinates.
1961 rowcol_to_yx: dict[tuple[int, int], tuple[int, int]] = {}
1963 def copy_line(
1964 line: StyleAndTextTuples,
1965 lineno: int,
1966 x: int,
1967 y: int,
1968 is_input: bool = False,
1969 ) -> tuple[int, int]:
1970 """
1971 Copy over a single line to the output screen. This can wrap over
1972 multiple lines in the output. It will call the prefix (prompt)
1973 function before every line.
1974 """
1975 if is_input:
1976 current_rowcol_to_yx = rowcol_to_yx
1977 else:
1978 current_rowcol_to_yx = {} # Throwaway dictionary.
1980 # Draw line prefix.
1981 if is_input and get_line_prefix:
1982 prompt = to_formatted_text(get_line_prefix(lineno, 0))
1983 x, y = copy_line(prompt, lineno, x, y, is_input=False)
1985 # Scroll horizontally.
1986 skipped = 0 # Characters skipped because of horizontal scrolling.
1987 if horizontal_scroll and is_input:
1988 h_scroll = horizontal_scroll
1989 line = explode_text_fragments(line)
1990 while h_scroll > 0 and line:
1991 h_scroll -= get_cwidth(line[0][1])
1992 skipped += 1
1993 del line[:1] # Remove first character.
1995 x -= h_scroll # When scrolling over double width character,
1996 # this can end up being negative.
1998 # Align this line. (Note that this doesn't work well when we use
1999 # get_line_prefix and that function returns variable width prefixes.)
2000 if align == WindowAlign.CENTER:
2001 line_width = fragment_list_width(line)
2002 if line_width < width:
2003 x += (width - line_width) // 2
2004 elif align == WindowAlign.RIGHT:
2005 line_width = fragment_list_width(line)
2006 if line_width < width:
2007 x += width - line_width
2009 col = 0
2010 wrap_count = 0
2011 for style, text, *_ in line:
2012 new_buffer_row = new_buffer[y + ypos]
2014 # Remember raw VT escape sequences. (E.g. FinalTerm's
2015 # escape sequences.)
2016 if "[ZeroWidthEscape]" in style:
2017 new_screen.zero_width_escapes[y + ypos][x + xpos] += text
2018 continue
2020 for c in text:
2021 char = _CHAR_CACHE[c, style]
2022 char_width = char.width
2024 # Wrap when the line width is exceeded.
2025 if wrap_lines and x + char_width > width:
2026 visible_line_to_row_col[y + 1] = (
2027 lineno,
2028 visible_line_to_row_col[y][1] + x,
2029 )
2030 y += 1
2031 wrap_count += 1
2032 x = 0
2034 # Insert line prefix (continuation prompt).
2035 if is_input and get_line_prefix:
2036 prompt = to_formatted_text(
2037 get_line_prefix(lineno, wrap_count)
2038 )
2039 x, y = copy_line(prompt, lineno, x, y, is_input=False)
2041 new_buffer_row = new_buffer[y + ypos]
2043 if y >= write_position.height:
2044 return x, y # Break out of all for loops.
2046 # Set character in screen and shift 'x'.
2047 if x >= 0 and y >= 0 and x < width:
2048 new_buffer_row[x + xpos] = char
2050 # When we print a multi width character, make sure
2051 # to erase the neighbours positions in the screen.
2052 # (The empty string if different from everything,
2053 # so next redraw this cell will repaint anyway.)
2054 if char_width > 1:
2055 for i in range(1, char_width):
2056 new_buffer_row[x + xpos + i] = empty_char
2058 # If this is a zero width characters, then it's
2059 # probably part of a decomposed unicode character.
2060 # See: https://en.wikipedia.org/wiki/Unicode_equivalence
2061 # Merge it in the previous cell.
2062 elif char_width == 0:
2063 # Handle all character widths. If the previous
2064 # character is a multiwidth character, then
2065 # merge it two positions back.
2066 for pw in [2, 1]: # Previous character width.
2067 if (
2068 x - pw >= 0
2069 and new_buffer_row[x + xpos - pw].width == pw
2070 ):
2071 prev_char = new_buffer_row[x + xpos - pw]
2072 char2 = _CHAR_CACHE[
2073 prev_char.char + c, prev_char.style
2074 ]
2075 new_buffer_row[x + xpos - pw] = char2
2077 # Keep track of write position for each character.
2078 current_rowcol_to_yx[lineno, col + skipped] = (
2079 y + ypos,
2080 x + xpos,
2081 )
2083 col += 1
2084 x += char_width
2085 return x, y
2087 # Copy content.
2088 def copy() -> int:
2089 y = -vertical_scroll_2
2090 lineno = vertical_scroll
2092 while y < write_position.height and lineno < line_count:
2093 # Take the next line and copy it in the real screen.
2094 line = ui_content.get_line(lineno)
2096 visible_line_to_row_col[y] = (lineno, horizontal_scroll)
2098 # Copy margin and actual line.
2099 x = 0
2100 x, y = copy_line(line, lineno, x, y, is_input=True)
2102 lineno += 1
2103 y += 1
2104 return y
2106 copy()
2108 def cursor_pos_to_screen_pos(row: int, col: int) -> Point:
2109 "Translate row/col from UIContent to real Screen coordinates."
2110 try:
2111 y, x = rowcol_to_yx[row, col]
2112 except KeyError:
2113 # Normally this should never happen. (It is a bug, if it happens.)
2114 # But to be sure, return (0, 0)
2115 return Point(x=0, y=0)
2117 # raise ValueError(
2118 # 'Invalid position. row=%r col=%r, vertical_scroll=%r, '
2119 # 'horizontal_scroll=%r, height=%r' %
2120 # (row, col, vertical_scroll, horizontal_scroll, write_position.height))
2121 else:
2122 return Point(x=x, y=y)
2124 # Set cursor and menu positions.
2125 if ui_content.cursor_position:
2126 screen_cursor_position = cursor_pos_to_screen_pos(
2127 ui_content.cursor_position.y, ui_content.cursor_position.x
2128 )
2130 if has_focus:
2131 new_screen.set_cursor_position(self, screen_cursor_position)
2133 if always_hide_cursor:
2134 new_screen.show_cursor = False
2135 else:
2136 new_screen.show_cursor = ui_content.show_cursor
2138 self._highlight_digraph(new_screen)
2140 if highlight_lines:
2141 self._highlight_cursorlines(
2142 new_screen,
2143 screen_cursor_position,
2144 xpos,
2145 ypos,
2146 width,
2147 write_position.height,
2148 )
2150 # Draw input characters from the input processor queue.
2151 if has_focus and ui_content.cursor_position:
2152 self._show_key_processor_key_buffer(new_screen)
2154 # Set menu position.
2155 if ui_content.menu_position:
2156 new_screen.set_menu_position(
2157 self,
2158 cursor_pos_to_screen_pos(
2159 ui_content.menu_position.y, ui_content.menu_position.x
2160 ),
2161 )
2163 # Update output screen height.
2164 new_screen.height = max(new_screen.height, ypos + write_position.height)
2166 return visible_line_to_row_col, rowcol_to_yx
2168 def _fill_bg(
2169 self, screen: Screen, write_position: WritePosition, erase_bg: bool
2170 ) -> None:
2171 """
2172 Erase/fill the background.
2173 (Useful for floats and when a `char` has been given.)
2174 """
2175 char: str | None
2176 if callable(self.char):
2177 char = self.char()
2178 else:
2179 char = self.char
2181 if erase_bg or char:
2182 wp = write_position
2183 char_obj = _CHAR_CACHE[char or " ", ""]
2185 for y in range(wp.ypos, wp.ypos + wp.height):
2186 row = screen.data_buffer[y]
2187 for x in range(wp.xpos, wp.xpos + wp.width):
2188 row[x] = char_obj
2190 def _apply_style(
2191 self, new_screen: Screen, write_position: WritePosition, parent_style: str
2192 ) -> None:
2193 # Apply `self.style`.
2194 style = parent_style + " " + to_str(self.style)
2196 new_screen.fill_area(write_position, style=style, after=False)
2198 # Apply the 'last-line' class to the last line of each Window. This can
2199 # be used to apply an 'underline' to the user control.
2200 wp = WritePosition(
2201 write_position.xpos,
2202 write_position.ypos + write_position.height - 1,
2203 write_position.width,
2204 1,
2205 )
2206 new_screen.fill_area(wp, "class:last-line", after=True)
2208 def _highlight_digraph(self, new_screen: Screen) -> None:
2209 """
2210 When we are in Vi digraph mode, put a question mark underneath the
2211 cursor.
2212 """
2213 digraph_char = self._get_digraph_char()
2214 if digraph_char:
2215 cpos = new_screen.get_cursor_position(self)
2216 new_screen.data_buffer[cpos.y][cpos.x] = _CHAR_CACHE[
2217 digraph_char, "class:digraph"
2218 ]
2220 def _show_key_processor_key_buffer(self, new_screen: Screen) -> None:
2221 """
2222 When the user is typing a key binding that consists of several keys,
2223 display the last pressed key if the user is in insert mode and the key
2224 is meaningful to be displayed.
2225 E.g. Some people want to bind 'jj' to escape in Vi insert mode. But the
2226 first 'j' needs to be displayed in order to get some feedback.
2227 """
2228 app = get_app()
2229 key_buffer = app.key_processor.key_buffer
2231 if key_buffer and _in_insert_mode() and not app.is_done:
2232 # The textual data for the given key. (Can be a VT100 escape
2233 # sequence.)
2234 data = key_buffer[-1].data
2236 # Display only if this is a 1 cell width character.
2237 if get_cwidth(data) == 1:
2238 cpos = new_screen.get_cursor_position(self)
2239 new_screen.data_buffer[cpos.y][cpos.x] = _CHAR_CACHE[
2240 data, "class:partial-key-binding"
2241 ]
2243 def _highlight_cursorlines(
2244 self, new_screen: Screen, cpos: Point, x: int, y: int, width: int, height: int
2245 ) -> None:
2246 """
2247 Highlight cursor row/column.
2248 """
2249 cursor_line_style = " class:cursor-line "
2250 cursor_column_style = " class:cursor-column "
2252 data_buffer = new_screen.data_buffer
2254 # Highlight cursor line.
2255 if self.cursorline():
2256 row = data_buffer[cpos.y]
2257 for x in range(x, x + width):
2258 original_char = row[x]
2259 row[x] = _CHAR_CACHE[
2260 original_char.char, original_char.style + cursor_line_style
2261 ]
2263 # Highlight cursor column.
2264 if self.cursorcolumn():
2265 for y2 in range(y, y + height):
2266 row = data_buffer[y2]
2267 original_char = row[cpos.x]
2268 row[cpos.x] = _CHAR_CACHE[
2269 original_char.char, original_char.style + cursor_column_style
2270 ]
2272 # Highlight color columns
2273 colorcolumns = self.colorcolumns
2274 if callable(colorcolumns):
2275 colorcolumns = colorcolumns()
2277 for cc in colorcolumns:
2278 assert isinstance(cc, ColorColumn)
2279 column = cc.position
2281 if column < x + width: # Only draw when visible.
2282 color_column_style = " " + cc.style
2284 for y2 in range(y, y + height):
2285 row = data_buffer[y2]
2286 original_char = row[column + x]
2287 row[column + x] = _CHAR_CACHE[
2288 original_char.char, original_char.style + color_column_style
2289 ]
2291 def _copy_margin(
2292 self,
2293 margin_content: UIContent,
2294 new_screen: Screen,
2295 write_position: WritePosition,
2296 move_x: int,
2297 width: int,
2298 ) -> None:
2299 """
2300 Copy characters from the margin screen to the real screen.
2301 """
2302 xpos = write_position.xpos + move_x
2303 ypos = write_position.ypos
2305 margin_write_position = WritePosition(xpos, ypos, width, write_position.height)
2306 self._copy_body(margin_content, new_screen, margin_write_position, 0, width)
2308 def _scroll(self, ui_content: UIContent, width: int, height: int) -> None:
2309 """
2310 Scroll body. Ensure that the cursor is visible.
2311 """
2312 if self.wrap_lines():
2313 func = self._scroll_when_linewrapping
2314 else:
2315 func = self._scroll_without_linewrapping
2317 func(ui_content, width, height)
2319 def _scroll_when_linewrapping(
2320 self, ui_content: UIContent, width: int, height: int
2321 ) -> None:
2322 """
2323 Scroll to make sure the cursor position is visible and that we maintain
2324 the requested scroll offset.
2326 Set `self.horizontal_scroll/vertical_scroll`.
2327 """
2328 scroll_offsets_bottom = self.scroll_offsets.bottom
2329 scroll_offsets_top = self.scroll_offsets.top
2331 # We don't have horizontal scrolling.
2332 self.horizontal_scroll = 0
2334 def get_line_height(lineno: int) -> int:
2335 return ui_content.get_height_for_line(lineno, width, self.get_line_prefix)
2337 # When there is no space, reset `vertical_scroll_2` to zero and abort.
2338 # This can happen if the margin is bigger than the window width.
2339 # Otherwise the text height will become "infinite" (a big number) and
2340 # the copy_line will spend a huge amount of iterations trying to render
2341 # nothing.
2342 if width <= 0:
2343 self.vertical_scroll = ui_content.cursor_position.y
2344 self.vertical_scroll_2 = 0
2345 return
2347 # If the current line consumes more than the whole window height,
2348 # then we have to scroll vertically inside this line. (We don't take
2349 # the scroll offsets into account for this.)
2350 # Also, ignore the scroll offsets in this case. Just set the vertical
2351 # scroll to this line.
2352 line_height = get_line_height(ui_content.cursor_position.y)
2353 if line_height > height - scroll_offsets_top:
2354 # Calculate the height of the text before the cursor (including
2355 # line prefixes).
2356 text_before_height = ui_content.get_height_for_line(
2357 ui_content.cursor_position.y,
2358 width,
2359 self.get_line_prefix,
2360 slice_stop=ui_content.cursor_position.x,
2361 )
2363 # Adjust scroll offset.
2364 self.vertical_scroll = ui_content.cursor_position.y
2365 self.vertical_scroll_2 = min(
2366 text_before_height - 1, # Keep the cursor visible.
2367 line_height
2368 - height, # Avoid blank lines at the bottom when scrolling up again.
2369 self.vertical_scroll_2,
2370 )
2371 self.vertical_scroll_2 = max(
2372 0, text_before_height - height, self.vertical_scroll_2
2373 )
2374 return
2375 else:
2376 self.vertical_scroll_2 = 0
2378 # Current line doesn't consume the whole height. Take scroll offsets into account.
2379 def get_min_vertical_scroll() -> int:
2380 # Make sure that the cursor line is not below the bottom.
2381 # (Calculate how many lines can be shown between the cursor and the .)
2382 used_height = 0
2383 prev_lineno = ui_content.cursor_position.y
2385 for lineno in range(ui_content.cursor_position.y, -1, -1):
2386 used_height += get_line_height(lineno)
2388 if used_height > height - scroll_offsets_bottom:
2389 return prev_lineno
2390 else:
2391 prev_lineno = lineno
2392 return 0
2394 def get_max_vertical_scroll() -> int:
2395 # Make sure that the cursor line is not above the top.
2396 prev_lineno = ui_content.cursor_position.y
2397 used_height = 0
2399 for lineno in range(ui_content.cursor_position.y - 1, -1, -1):
2400 used_height += get_line_height(lineno)
2402 if used_height > scroll_offsets_top:
2403 return prev_lineno
2404 else:
2405 prev_lineno = lineno
2406 return prev_lineno
2408 def get_topmost_visible() -> int:
2409 """
2410 Calculate the upper most line that can be visible, while the bottom
2411 is still visible. We should not allow scroll more than this if
2412 `allow_scroll_beyond_bottom` is false.
2413 """
2414 prev_lineno = ui_content.line_count - 1
2415 used_height = 0
2416 for lineno in range(ui_content.line_count - 1, -1, -1):
2417 used_height += get_line_height(lineno)
2418 if used_height > height:
2419 return prev_lineno
2420 else:
2421 prev_lineno = lineno
2422 return prev_lineno
2424 # Scroll vertically. (Make sure that the whole line which contains the
2425 # cursor is visible.
2426 topmost_visible = get_topmost_visible()
2428 # Note: the `min(topmost_visible, ...)` is to make sure that we
2429 # don't require scrolling up because of the bottom scroll offset,
2430 # when we are at the end of the document.
2431 self.vertical_scroll = max(
2432 self.vertical_scroll, min(topmost_visible, get_min_vertical_scroll())
2433 )
2434 self.vertical_scroll = min(self.vertical_scroll, get_max_vertical_scroll())
2436 # Disallow scrolling beyond bottom?
2437 if not self.allow_scroll_beyond_bottom():
2438 self.vertical_scroll = min(self.vertical_scroll, topmost_visible)
2440 def _scroll_without_linewrapping(
2441 self, ui_content: UIContent, width: int, height: int
2442 ) -> None:
2443 """
2444 Scroll to make sure the cursor position is visible and that we maintain
2445 the requested scroll offset.
2447 Set `self.horizontal_scroll/vertical_scroll`.
2448 """
2449 cursor_position = ui_content.cursor_position or Point(x=0, y=0)
2451 # Without line wrapping, we will never have to scroll vertically inside
2452 # a single line.
2453 self.vertical_scroll_2 = 0
2455 if ui_content.line_count == 0:
2456 self.vertical_scroll = 0
2457 self.horizontal_scroll = 0
2458 return
2459 else:
2460 current_line_text = fragment_list_to_text(
2461 ui_content.get_line(cursor_position.y)
2462 )
2464 def do_scroll(
2465 current_scroll: int,
2466 scroll_offset_start: int,
2467 scroll_offset_end: int,
2468 cursor_pos: int,
2469 window_size: int,
2470 content_size: int,
2471 ) -> int:
2472 "Scrolling algorithm. Used for both horizontal and vertical scrolling."
2473 # Calculate the scroll offset to apply.
2474 # This can obviously never be more than have the screen size. Also, when the
2475 # cursor appears at the top or bottom, we don't apply the offset.
2476 scroll_offset_start = int(
2477 min(scroll_offset_start, window_size / 2, cursor_pos)
2478 )
2479 scroll_offset_end = int(
2480 min(scroll_offset_end, window_size / 2, content_size - 1 - cursor_pos)
2481 )
2483 # Prevent negative scroll offsets.
2484 if current_scroll < 0:
2485 current_scroll = 0
2487 # Scroll back if we scrolled to much and there's still space to show more of the document.
2488 if (
2489 not self.allow_scroll_beyond_bottom()
2490 and current_scroll > content_size - window_size
2491 ):
2492 current_scroll = max(0, content_size - window_size)
2494 # Scroll up if cursor is before visible part.
2495 if current_scroll > cursor_pos - scroll_offset_start:
2496 current_scroll = max(0, cursor_pos - scroll_offset_start)
2498 # Scroll down if cursor is after visible part.
2499 if current_scroll < (cursor_pos + 1) - window_size + scroll_offset_end:
2500 current_scroll = (cursor_pos + 1) - window_size + scroll_offset_end
2502 return current_scroll
2504 # When a preferred scroll is given, take that first into account.
2505 if self.get_vertical_scroll:
2506 self.vertical_scroll = self.get_vertical_scroll(self)
2507 assert isinstance(self.vertical_scroll, int)
2508 if self.get_horizontal_scroll:
2509 self.horizontal_scroll = self.get_horizontal_scroll(self)
2510 assert isinstance(self.horizontal_scroll, int)
2512 # Update horizontal/vertical scroll to make sure that the cursor
2513 # remains visible.
2514 offsets = self.scroll_offsets
2516 self.vertical_scroll = do_scroll(
2517 current_scroll=self.vertical_scroll,
2518 scroll_offset_start=offsets.top,
2519 scroll_offset_end=offsets.bottom,
2520 cursor_pos=ui_content.cursor_position.y,
2521 window_size=height,
2522 content_size=ui_content.line_count,
2523 )
2525 if self.get_line_prefix:
2526 current_line_prefix_width = fragment_list_width(
2527 to_formatted_text(self.get_line_prefix(ui_content.cursor_position.y, 0))
2528 )
2529 else:
2530 current_line_prefix_width = 0
2532 self.horizontal_scroll = do_scroll(
2533 current_scroll=self.horizontal_scroll,
2534 scroll_offset_start=offsets.left,
2535 scroll_offset_end=offsets.right,
2536 cursor_pos=get_cwidth(current_line_text[: ui_content.cursor_position.x]),
2537 window_size=width - current_line_prefix_width,
2538 # We can only analyse the current line. Calculating the width off
2539 # all the lines is too expensive.
2540 content_size=max(
2541 get_cwidth(current_line_text), self.horizontal_scroll + width
2542 ),
2543 )
2545 def _mouse_handler(self, mouse_event: MouseEvent) -> NotImplementedOrNone:
2546 """
2547 Mouse handler. Called when the UI control doesn't handle this
2548 particular event.
2550 Return `NotImplemented` if nothing was done as a consequence of this
2551 key binding (no UI invalidate required in that case).
2552 """
2553 if mouse_event.event_type == MouseEventType.SCROLL_DOWN:
2554 self._scroll_down()
2555 return None
2556 elif mouse_event.event_type == MouseEventType.SCROLL_UP:
2557 self._scroll_up()
2558 return None
2560 return NotImplemented
2562 def _scroll_down(self) -> None:
2563 "Scroll window down."
2564 info = self.render_info
2566 if info is None:
2567 return
2569 if self.vertical_scroll < info.content_height - info.window_height:
2570 if info.cursor_position.y <= info.configured_scroll_offsets.top:
2571 self.content.move_cursor_down()
2573 self.vertical_scroll += 1
2575 def _scroll_up(self) -> None:
2576 "Scroll window up."
2577 info = self.render_info
2579 if info is None:
2580 return
2582 if info.vertical_scroll > 0:
2583 # TODO: not entirely correct yet in case of line wrapping and long lines.
2584 if (
2585 info.cursor_position.y
2586 >= info.window_height - 1 - info.configured_scroll_offsets.bottom
2587 ):
2588 self.content.move_cursor_up()
2590 self.vertical_scroll -= 1
2592 def get_key_bindings(self) -> KeyBindingsBase | None:
2593 return self.content.get_key_bindings()
2595 def get_children(self) -> list[Container]:
2596 return []
2599class ConditionalContainer(Container):
2600 """
2601 Wrapper around any other container that can change the visibility. The
2602 received `filter` determines whether the given container should be
2603 displayed or not.
2605 :param content: :class:`.Container` instance.
2606 :param filter: :class:`.Filter` instance.
2607 """
2609 def __init__(self, content: AnyContainer, filter: FilterOrBool) -> None:
2610 self.content = to_container(content)
2611 self.filter = to_filter(filter)
2613 def __repr__(self) -> str:
2614 return f"ConditionalContainer({self.content!r}, filter={self.filter!r})"
2616 def reset(self) -> None:
2617 self.content.reset()
2619 def preferred_width(self, max_available_width: int) -> Dimension:
2620 if self.filter():
2621 return self.content.preferred_width(max_available_width)
2622 else:
2623 return Dimension.zero()
2625 def preferred_height(self, width: int, max_available_height: int) -> Dimension:
2626 if self.filter():
2627 return self.content.preferred_height(width, max_available_height)
2628 else:
2629 return Dimension.zero()
2631 def write_to_screen(
2632 self,
2633 screen: Screen,
2634 mouse_handlers: MouseHandlers,
2635 write_position: WritePosition,
2636 parent_style: str,
2637 erase_bg: bool,
2638 z_index: int | None,
2639 ) -> None:
2640 if self.filter():
2641 return self.content.write_to_screen(
2642 screen, mouse_handlers, write_position, parent_style, erase_bg, z_index
2643 )
2645 def get_children(self) -> list[Container]:
2646 return [self.content]
2649class DynamicContainer(Container):
2650 """
2651 Container class that dynamically returns any Container.
2653 :param get_container: Callable that returns a :class:`.Container` instance
2654 or any widget with a ``__pt_container__`` method.
2655 """
2657 def __init__(self, get_container: Callable[[], AnyContainer]) -> None:
2658 self.get_container = get_container
2660 def _get_container(self) -> Container:
2661 """
2662 Return the current container object.
2664 We call `to_container`, because `get_container` can also return a
2665 widget with a ``__pt_container__`` method.
2666 """
2667 obj = self.get_container()
2668 return to_container(obj)
2670 def reset(self) -> None:
2671 self._get_container().reset()
2673 def preferred_width(self, max_available_width: int) -> Dimension:
2674 return self._get_container().preferred_width(max_available_width)
2676 def preferred_height(self, width: int, max_available_height: int) -> Dimension:
2677 return self._get_container().preferred_height(width, max_available_height)
2679 def write_to_screen(
2680 self,
2681 screen: Screen,
2682 mouse_handlers: MouseHandlers,
2683 write_position: WritePosition,
2684 parent_style: str,
2685 erase_bg: bool,
2686 z_index: int | None,
2687 ) -> None:
2688 self._get_container().write_to_screen(
2689 screen, mouse_handlers, write_position, parent_style, erase_bg, z_index
2690 )
2692 def is_modal(self) -> bool:
2693 return False
2695 def get_key_bindings(self) -> KeyBindingsBase | None:
2696 # Key bindings will be collected when `layout.walk()` finds the child
2697 # container.
2698 return None
2700 def get_children(self) -> list[Container]:
2701 # Here we have to return the current active container itself, not its
2702 # children. Otherwise, we run into issues where `layout.walk()` will
2703 # never see an object of type `Window` if this contains a window. We
2704 # can't/shouldn't proxy the "isinstance" check.
2705 return [self._get_container()]
2708def to_container(container: AnyContainer) -> Container:
2709 """
2710 Make sure that the given object is a :class:`.Container`.
2711 """
2712 if isinstance(container, Container):
2713 return container
2714 elif hasattr(container, "__pt_container__"):
2715 return to_container(container.__pt_container__())
2716 else:
2717 raise ValueError(f"Not a container object: {container!r}")
2720def to_window(container: AnyContainer) -> Window:
2721 """
2722 Make sure that the given argument is a :class:`.Window`.
2723 """
2724 if isinstance(container, Window):
2725 return container
2726 elif hasattr(container, "__pt_container__"):
2727 return to_window(cast("MagicContainer", container).__pt_container__())
2728 else:
2729 raise ValueError(f"Not a Window object: {container!r}.")
2732def is_container(value: object) -> TypeGuard[AnyContainer]:
2733 """
2734 Checks whether the given value is a container object
2735 (for use in assert statements).
2736 """
2737 if isinstance(value, Container):
2738 return True
2739 if hasattr(value, "__pt_container__"):
2740 return is_container(cast("MagicContainer", value).__pt_container__())
2741 return False