1"""
2Container for the layout.
3(Containers can contain other containers or user interface controls.)
4"""
5
6from __future__ import annotations
7
8from abc import ABCMeta, abstractmethod
9from enum import Enum
10from functools import partial
11from typing import TYPE_CHECKING, Callable, Sequence, Union, cast
12
13from prompt_toolkit.application.current import get_app
14from prompt_toolkit.cache import SimpleCache
15from prompt_toolkit.data_structures import Point
16from prompt_toolkit.filters import (
17 FilterOrBool,
18 emacs_insert_mode,
19 to_filter,
20 vi_insert_mode,
21)
22from prompt_toolkit.formatted_text import (
23 AnyFormattedText,
24 StyleAndTextTuples,
25 to_formatted_text,
26)
27from prompt_toolkit.formatted_text.utils import (
28 fragment_list_to_text,
29 fragment_list_width,
30)
31from prompt_toolkit.key_binding import KeyBindingsBase
32from prompt_toolkit.mouse_events import MouseEvent, MouseEventType
33from prompt_toolkit.utils import get_cwidth, take_using_weights, to_int, to_str
34
35from .controls import (
36 DummyControl,
37 FormattedTextControl,
38 GetLinePrefixCallable,
39 UIContent,
40 UIControl,
41)
42from .dimension import (
43 AnyDimension,
44 Dimension,
45 max_layout_dimensions,
46 sum_layout_dimensions,
47 to_dimension,
48)
49from .margins import Margin
50from .mouse_handlers import MouseHandlers
51from .screen import _CHAR_CACHE, Screen, WritePosition
52from .utils import explode_text_fragments
53
54if TYPE_CHECKING:
55 from typing_extensions import Protocol, TypeGuard
56
57 from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone
58
59
60__all__ = [
61 "AnyContainer",
62 "Container",
63 "HorizontalAlign",
64 "VerticalAlign",
65 "HSplit",
66 "VSplit",
67 "FloatContainer",
68 "Float",
69 "WindowAlign",
70 "Window",
71 "WindowRenderInfo",
72 "ConditionalContainer",
73 "ScrollOffsets",
74 "ColorColumn",
75 "to_container",
76 "to_window",
77 "is_container",
78 "DynamicContainer",
79]
80
81
82class Container(metaclass=ABCMeta):
83 """
84 Base class for user interface layout.
85 """
86
87 @abstractmethod
88 def reset(self) -> None:
89 """
90 Reset the state of this container and all the children.
91 (E.g. reset scroll offsets, etc...)
92 """
93
94 @abstractmethod
95 def preferred_width(self, max_available_width: int) -> Dimension:
96 """
97 Return a :class:`~prompt_toolkit.layout.Dimension` that represents the
98 desired width for this container.
99 """
100
101 @abstractmethod
102 def preferred_height(self, width: int, max_available_height: int) -> Dimension:
103 """
104 Return a :class:`~prompt_toolkit.layout.Dimension` that represents the
105 desired height for this container.
106 """
107
108 @abstractmethod
109 def write_to_screen(
110 self,
111 screen: Screen,
112 mouse_handlers: MouseHandlers,
113 write_position: WritePosition,
114 parent_style: str,
115 erase_bg: bool,
116 z_index: int | None,
117 ) -> None:
118 """
119 Write the actual content to the screen.
120
121 :param screen: :class:`~prompt_toolkit.layout.screen.Screen`
122 :param mouse_handlers: :class:`~prompt_toolkit.layout.mouse_handlers.MouseHandlers`.
123 :param parent_style: Style string to pass to the :class:`.Window`
124 object. This will be applied to all content of the windows.
125 :class:`.VSplit` and :class:`.HSplit` can use it to pass their
126 style down to the windows that they contain.
127 :param z_index: Used for propagating z_index from parent to child.
128 """
129
130 def is_modal(self) -> bool:
131 """
132 When this container is modal, key bindings from parent containers are
133 not taken into account if a user control in this container is focused.
134 """
135 return False
136
137 def get_key_bindings(self) -> KeyBindingsBase | None:
138 """
139 Returns a :class:`.KeyBindings` object. These bindings become active when any
140 user control in this container has the focus, except if any containers
141 between this container and the focused user control is modal.
142 """
143 return None
144
145 @abstractmethod
146 def get_children(self) -> list[Container]:
147 """
148 Return the list of child :class:`.Container` objects.
149 """
150 return []
151
152
153if TYPE_CHECKING:
154
155 class MagicContainer(Protocol):
156 """
157 Any object that implements ``__pt_container__`` represents a container.
158 """
159
160 def __pt_container__(self) -> AnyContainer: ...
161
162
163AnyContainer = Union[Container, "MagicContainer"]
164
165
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 )
171
172
173class VerticalAlign(Enum):
174 "Alignment for `HSplit`."
175
176 TOP = "TOP"
177 CENTER = "CENTER"
178 BOTTOM = "BOTTOM"
179 JUSTIFY = "JUSTIFY"
180
181
182class HorizontalAlign(Enum):
183 "Alignment for `VSplit`."
184
185 LEFT = "LEFT"
186 CENTER = "CENTER"
187 RIGHT = "RIGHT"
188 JUSTIFY = "JUSTIFY"
189
190
191class _Split(Container):
192 """
193 The common parts of `VSplit` and `HSplit`.
194 """
195
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
215
216 self.width = width
217 self.height = height
218 self.z_index = z_index
219
220 self.modal = modal
221 self.key_bindings = key_bindings
222 self.style = style
223
224 def is_modal(self) -> bool:
225 return self.modal
226
227 def get_key_bindings(self) -> KeyBindingsBase | None:
228 return self.key_bindings
229
230 def get_children(self) -> list[Container]:
231 return self.children
232
233
234class HSplit(_Split):
235 """
236 Several layouts, one stacked above/under the other. ::
237
238 +--------------------+
239 | |
240 +--------------------+
241 | |
242 +--------------------+
243
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::
246
247 HSplit(children=[ ... ], padding_char='-',
248 padding=1, padding_style='#ffff00')
249
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.
262
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 """
267
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 )
296
297 self.align = align
298
299 self._children_cache: SimpleCache[tuple[Container, ...], list[Container]] = (
300 SimpleCache(maxsize=1)
301 )
302 self._remaining_space_window = Window() # Dummy window.
303
304 def preferred_width(self, max_available_width: int) -> Dimension:
305 if self.width is not None:
306 return to_dimension(self.width)
307
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()
313
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)
317
318 dimensions = [
319 c.preferred_height(width, max_available_height) for c in self._all_children
320 ]
321 return sum_layout_dimensions(dimensions)
322
323 def reset(self) -> None:
324 for c in self.children:
325 c.reset()
326
327 @property
328 def _all_children(self) -> list[Container]:
329 """
330 List of child objects, including padding.
331 """
332
333 def get() -> list[Container]:
334 result: list[Container] = []
335
336 # Padding Top.
337 if self.align in (VerticalAlign.CENTER, VerticalAlign.BOTTOM):
338 result.append(Window(width=Dimension(preferred=0)))
339
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()
352
353 # Padding right.
354 if self.align in (VerticalAlign.CENTER, VerticalAlign.TOP):
355 result.append(Window(width=Dimension(preferred=0)))
356
357 return result
358
359 return self._children_cache.get(tuple(self.children), get)
360
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.
372
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
379
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
389
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
401
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 )
417
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 []
425
426 width = write_position.width
427 height = write_position.height
428
429 # Calculate heights.
430 dimensions = [c.preferred_height(width, height) for c in self._all_children]
431
432 # Sum dimensions
433 sum_dimensions = sum_layout_dimensions(dimensions)
434
435 # If there is not enough space for both.
436 # Don't do anything.
437 if sum_dimensions.min > height:
438 return None
439
440 # Find optimal sizes. (Start with minimal size, increase until we cover
441 # the whole height.)
442 sizes = [d.min for d in dimensions]
443
444 child_generator = take_using_weights(
445 items=list(range(len(dimensions))), weights=[d.weight for d in dimensions]
446 )
447
448 i = next(child_generator)
449
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]
453
454 while sum(sizes) < preferred_stop:
455 if sizes[i] < preferred_dimensions[i]:
456 sizes[i] += 1
457 i = next(child_generator)
458
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]
463
464 while sum(sizes) < max_stop:
465 if sizes[i] < max_dimensions[i]:
466 sizes[i] += 1
467 i = next(child_generator)
468
469 return sizes
470
471
472class VSplit(_Split):
473 """
474 Several layouts, one stacked left/right of the other. ::
475
476 +---------+----------+
477 | | |
478 | | |
479 +---------+----------+
480
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::
483
484 VSplit(children=[ ... ], padding_char='|',
485 padding=1, padding_style='#ffff00')
486
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.
499
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 """
504
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 )
533
534 self.align = align
535
536 self._children_cache: SimpleCache[tuple[Container, ...], list[Container]] = (
537 SimpleCache(maxsize=1)
538 )
539 self._remaining_space_window = Window() # Dummy window.
540
541 def preferred_width(self, max_available_width: int) -> Dimension:
542 if self.width is not None:
543 return to_dimension(self.width)
544
545 dimensions = [
546 c.preferred_width(max_available_width) for c in self._all_children
547 ]
548
549 return sum_layout_dimensions(dimensions)
550
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)
554
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`.
562
563 sizes = self._divide_widths(width)
564 children = self._all_children
565
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)
574
575 def reset(self) -> None:
576 for c in self.children:
577 c.reset()
578
579 @property
580 def _all_children(self) -> list[Container]:
581 """
582 List of child objects, including padding.
583 """
584
585 def get() -> list[Container]:
586 result: list[Container] = []
587
588 # Padding left.
589 if self.align in (HorizontalAlign.CENTER, HorizontalAlign.RIGHT):
590 result.append(Window(width=Dimension(preferred=0)))
591
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()
604
605 # Padding right.
606 if self.align in (HorizontalAlign.CENTER, HorizontalAlign.LEFT):
607 result.append(Window(width=Dimension(preferred=0)))
608
609 return result
610
611 return self._children_cache.get(tuple(self.children), get)
612
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
619
620 if not children:
621 return []
622
623 # Calculate widths.
624 dimensions = [c.preferred_width(width) for c in children]
625 preferred_dimensions = [d.preferred for d in dimensions]
626
627 # Sum dimensions
628 sum_dimensions = sum_layout_dimensions(dimensions)
629
630 # If there is not enough space for both.
631 # Don't do anything.
632 if sum_dimensions.min > width:
633 return None
634
635 # Find optimal sizes. (Start with minimal size, increase until we cover
636 # the whole width.)
637 sizes = [d.min for d in dimensions]
638
639 child_generator = take_using_weights(
640 items=list(range(len(dimensions))), weights=[d.weight for d in dimensions]
641 )
642
643 i = next(child_generator)
644
645 # Increase until we meet at least the 'preferred' size.
646 preferred_stop = min(width, sum_dimensions.preferred)
647
648 while sum(sizes) < preferred_stop:
649 if sizes[i] < preferred_dimensions[i]:
650 sizes[i] += 1
651 i = next(child_generator)
652
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)
656
657 while sum(sizes) < max_stop:
658 if sizes[i] < max_dimensions[i]:
659 sizes[i] += 1
660 i = next(child_generator)
661
662 return sizes
663
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.
675
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
681
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
686
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
693
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)))
701
702 #
703 ypos = write_position.ypos
704 xpos = write_position.xpos
705
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
717
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 )
733
734
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.
739
740 Example Usage::
741
742 FloatContainer(content=Window(...),
743 floats=[
744 Float(xcursor=True,
745 ycursor=True,
746 content=CompletionsMenu(...))
747 ])
748
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 """
753
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
765
766 self.modal = modal
767 self.key_bindings = key_bindings
768 self.style = style
769 self.z_index = z_index
770
771 def reset(self) -> None:
772 self.content.reset()
773
774 for f in self.floats:
775 f.content.reset()
776
777 def preferred_width(self, max_available_width: int) -> Dimension:
778 return self.content.preferred_width(max_available_width)
779
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)
787
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
799
800 self.content.write_to_screen(
801 screen, mouse_handlers, write_position, style, erase_bg, z_index
802 )
803
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)
809
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
816
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 )
844
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 )
867
868 fl_width = fl.get_width()
869 fl_height = fl.get_height()
870 width: int
871 height: int
872 xpos: int
873 ypos: int
874
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
894
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
905
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))
912
913 # Trim.
914 width = min(width, write_position.width - xpos)
915
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)
931
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
938
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
950
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
958
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))
965
966 # Trim.
967 height = min(height, write_position.height - ypos)
968
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 )
978
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 )
988
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
995
996 for y in range(wp.ypos, wp.ypos + wp.height):
997 if y in screen.data_buffer:
998 row = screen.data_buffer[y]
999
1000 for x in range(wp.xpos, wp.xpos + wp.width):
1001 c = row[x]
1002 if c.char != " ":
1003 return False
1004
1005 return True
1006
1007 def is_modal(self) -> bool:
1008 return self.modal
1009
1010 def get_key_bindings(self) -> KeyBindingsBase | None:
1011 return self.key_bindings
1012
1013 def get_children(self) -> list[Container]:
1014 children = [self.content]
1015 children.extend(f.content for f in self.floats)
1016 return children
1017
1018
1019class Float:
1020 """
1021 Float for use in a :class:`.FloatContainer`.
1022 Except for the `content` parameter, all other options are optional.
1023
1024 :param content: :class:`.Container` instance.
1025
1026 :param width: :class:`.Dimension` or callable which returns a :class:`.Dimension`.
1027 :param height: :class:`.Dimension` or callable which returns a :class:`.Dimension`.
1028
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`.
1033
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 """
1044
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
1063
1064 self.left = left
1065 self.right = right
1066 self.top = top
1067 self.bottom = bottom
1068
1069 self.width = width
1070 self.height = height
1071
1072 self.xcursor = xcursor
1073 self.ycursor = ycursor
1074
1075 self.attach_to_window = (
1076 to_window(attach_to_window) if attach_to_window else None
1077 )
1078
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)
1084
1085 def get_width(self) -> int | None:
1086 if callable(self.width):
1087 return self.width()
1088 return self.width
1089
1090 def get_height(self) -> int | None:
1091 if callable(self.height):
1092 return self.height()
1093 return self.height
1094
1095 def __repr__(self) -> str:
1096 return f"Float(content={self.content!r})"
1097
1098
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.
1105
1106 (Could be used for implementation of the Vi 'H' and 'L' key bindings as
1107 well as implementing mouse support.)
1108
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 """
1125
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
1146
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
1150
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
1155
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 }
1162
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)
1178
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)
1193
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 )
1206
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())
1214
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
1229
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]
1239
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]
1248
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 )
1263
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
1270
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 )
1280
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
1287
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
1294
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
1305
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
1317
1318
1319class ScrollOffsets:
1320 """
1321 Scroll offsets for the :class:`.Window` class.
1322
1323 Note that left/right offsets only make sense if line wrapping is disabled.
1324 """
1325
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
1337
1338 @property
1339 def top(self) -> int:
1340 return to_int(self._top)
1341
1342 @property
1343 def bottom(self) -> int:
1344 return to_int(self._bottom)
1345
1346 @property
1347 def left(self) -> int:
1348 return to_int(self._left)
1349
1350 @property
1351 def right(self) -> int:
1352 return to_int(self._right)
1353
1354 def __repr__(self) -> str:
1355 return f"ScrollOffsets(top={self._top!r}, bottom={self._bottom!r}, left={self._left!r}, right={self._right!r})"
1356
1357
1358class ColorColumn:
1359 """
1360 Column for a :class:`.Window` to be colored.
1361 """
1362
1363 def __init__(self, position: int, style: str = "class:color-column") -> None:
1364 self.position = position
1365 self.style = style
1366
1367
1368_in_insert_mode = vi_insert_mode | emacs_insert_mode
1369
1370
1371class WindowAlign(Enum):
1372 """
1373 Alignment of the Window content.
1374
1375 Note that this is different from `HorizontalAlign` and `VerticalAlign`,
1376 which are used for the alignment of the child containers in respectively
1377 `VSplit` and `HSplit`.
1378 """
1379
1380 LEFT = "LEFT"
1381 RIGHT = "RIGHT"
1382 CENTER = "CENTER"
1383
1384
1385class Window(Container):
1386 """
1387 Container that holds a control.
1388
1389 :param content: :class:`.UIControl` instance.
1390 :param width: :class:`.Dimension` instance or callable.
1391 :param height: :class:`.Dimension` instance or callable.
1392 :param z_index: When specified, this can be used to bring element in front
1393 of floating elements.
1394 :param dont_extend_width: When `True`, don't take up more width then the
1395 preferred width reported by the control.
1396 :param dont_extend_height: When `True`, don't take up more width then the
1397 preferred height reported by the control.
1398 :param ignore_content_width: A `bool` or :class:`.Filter` instance. Ignore
1399 the :class:`.UIContent` width when calculating the dimensions.
1400 :param ignore_content_height: A `bool` or :class:`.Filter` instance. Ignore
1401 the :class:`.UIContent` height when calculating the dimensions.
1402 :param left_margins: A list of :class:`.Margin` instance to be displayed on
1403 the left. For instance: :class:`~prompt_toolkit.layout.NumberedMargin`
1404 can be one of them in order to show line numbers.
1405 :param right_margins: Like `left_margins`, but on the other side.
1406 :param scroll_offsets: :class:`.ScrollOffsets` instance, representing the
1407 preferred amount of lines/columns to be always visible before/after the
1408 cursor. When both top and bottom are a very high number, the cursor
1409 will be centered vertically most of the time.
1410 :param allow_scroll_beyond_bottom: A `bool` or
1411 :class:`.Filter` instance. When True, allow scrolling so far, that the
1412 top part of the content is not visible anymore, while there is still
1413 empty space available at the bottom of the window. In the Vi editor for
1414 instance, this is possible. You will see tildes while the top part of
1415 the body is hidden.
1416 :param wrap_lines: A `bool` or :class:`.Filter` instance. When True, don't
1417 scroll horizontally, but wrap lines instead.
1418 :param get_vertical_scroll: Callable that takes this window
1419 instance as input and returns a preferred vertical scroll.
1420 (When this is `None`, the scroll is only determined by the last and
1421 current cursor position.)
1422 :param get_horizontal_scroll: Callable that takes this window
1423 instance as input and returns a preferred vertical scroll.
1424 :param always_hide_cursor: A `bool` or
1425 :class:`.Filter` instance. When True, never display the cursor, even
1426 when the user control specifies a cursor position.
1427 :param cursorline: A `bool` or :class:`.Filter` instance. When True,
1428 display a cursorline.
1429 :param cursorcolumn: A `bool` or :class:`.Filter` instance. When True,
1430 display a cursorcolumn.
1431 :param colorcolumns: A list of :class:`.ColorColumn` instances that
1432 describe the columns to be highlighted, or a callable that returns such
1433 a list.
1434 :param align: :class:`.WindowAlign` value or callable that returns an
1435 :class:`.WindowAlign` value. alignment of content.
1436 :param style: A style string. Style to be applied to all the cells in this
1437 window. (This can be a callable that returns a string.)
1438 :param char: (string) Character to be used for filling the background. This
1439 can also be a callable that returns a character.
1440 :param get_line_prefix: None or a callable that returns formatted text to
1441 be inserted before a line. It takes a line number (int) and a
1442 wrap_count and returns formatted text. This can be used for
1443 implementation of line continuations, things like Vim "breakindent" and
1444 so on.
1445 """
1446
1447 def __init__(
1448 self,
1449 content: UIControl | None = None,
1450 width: AnyDimension = None,
1451 height: AnyDimension = None,
1452 z_index: int | None = None,
1453 dont_extend_width: FilterOrBool = False,
1454 dont_extend_height: FilterOrBool = False,
1455 ignore_content_width: FilterOrBool = False,
1456 ignore_content_height: FilterOrBool = False,
1457 left_margins: Sequence[Margin] | None = None,
1458 right_margins: Sequence[Margin] | None = None,
1459 scroll_offsets: ScrollOffsets | None = None,
1460 allow_scroll_beyond_bottom: FilterOrBool = False,
1461 wrap_lines: FilterOrBool = False,
1462 get_vertical_scroll: Callable[[Window], int] | None = None,
1463 get_horizontal_scroll: Callable[[Window], int] | None = None,
1464 always_hide_cursor: FilterOrBool = False,
1465 cursorline: FilterOrBool = False,
1466 cursorcolumn: FilterOrBool = False,
1467 colorcolumns: (
1468 None | list[ColorColumn] | Callable[[], list[ColorColumn]]
1469 ) = None,
1470 align: WindowAlign | Callable[[], WindowAlign] = WindowAlign.LEFT,
1471 style: str | Callable[[], str] = "",
1472 char: None | str | Callable[[], str] = None,
1473 get_line_prefix: GetLinePrefixCallable | None = None,
1474 ) -> None:
1475 self.allow_scroll_beyond_bottom = to_filter(allow_scroll_beyond_bottom)
1476 self.always_hide_cursor = to_filter(always_hide_cursor)
1477 self.wrap_lines = to_filter(wrap_lines)
1478 self.cursorline = to_filter(cursorline)
1479 self.cursorcolumn = to_filter(cursorcolumn)
1480
1481 self.content = content or DummyControl()
1482 self.dont_extend_width = to_filter(dont_extend_width)
1483 self.dont_extend_height = to_filter(dont_extend_height)
1484 self.ignore_content_width = to_filter(ignore_content_width)
1485 self.ignore_content_height = to_filter(ignore_content_height)
1486 self.left_margins = left_margins or []
1487 self.right_margins = right_margins or []
1488 self.scroll_offsets = scroll_offsets or ScrollOffsets()
1489 self.get_vertical_scroll = get_vertical_scroll
1490 self.get_horizontal_scroll = get_horizontal_scroll
1491 self.colorcolumns = colorcolumns or []
1492 self.align = align
1493 self.style = style
1494 self.char = char
1495 self.get_line_prefix = get_line_prefix
1496
1497 self.width = width
1498 self.height = height
1499 self.z_index = z_index
1500
1501 # Cache for the screens generated by the margin.
1502 self._ui_content_cache: SimpleCache[tuple[int, int, int], UIContent] = (
1503 SimpleCache(maxsize=8)
1504 )
1505 self._margin_width_cache: SimpleCache[tuple[Margin, int], int] = SimpleCache(
1506 maxsize=1
1507 )
1508
1509 self.reset()
1510
1511 def __repr__(self) -> str:
1512 return f"Window(content={self.content!r})"
1513
1514 def reset(self) -> None:
1515 self.content.reset()
1516
1517 #: Scrolling position of the main content.
1518 self.vertical_scroll = 0
1519 self.horizontal_scroll = 0
1520
1521 # Vertical scroll 2: this is the vertical offset that a line is
1522 # scrolled if a single line (the one that contains the cursor) consumes
1523 # all of the vertical space.
1524 self.vertical_scroll_2 = 0
1525
1526 #: Keep render information (mappings between buffer input and render
1527 #: output.)
1528 self.render_info: WindowRenderInfo | None = None
1529
1530 def _get_margin_width(self, margin: Margin) -> int:
1531 """
1532 Return the width for this margin.
1533 (Calculate only once per render time.)
1534 """
1535
1536 # Margin.get_width, needs to have a UIContent instance.
1537 def get_ui_content() -> UIContent:
1538 return self._get_ui_content(width=0, height=0)
1539
1540 def get_width() -> int:
1541 return margin.get_width(get_ui_content)
1542
1543 key = (margin, get_app().render_counter)
1544 return self._margin_width_cache.get(key, get_width)
1545
1546 def _get_total_margin_width(self) -> int:
1547 """
1548 Calculate and return the width of the margin (left + right).
1549 """
1550 return sum(self._get_margin_width(m) for m in self.left_margins) + sum(
1551 self._get_margin_width(m) for m in self.right_margins
1552 )
1553
1554 def preferred_width(self, max_available_width: int) -> Dimension:
1555 """
1556 Calculate the preferred width for this window.
1557 """
1558
1559 def preferred_content_width() -> int | None:
1560 """Content width: is only calculated if no exact width for the
1561 window was given."""
1562 if self.ignore_content_width():
1563 return None
1564
1565 # Calculate the width of the margin.
1566 total_margin_width = self._get_total_margin_width()
1567
1568 # Window of the content. (Can be `None`.)
1569 preferred_width = self.content.preferred_width(
1570 max_available_width - total_margin_width
1571 )
1572
1573 if preferred_width is not None:
1574 # Include width of the margins.
1575 preferred_width += total_margin_width
1576 return preferred_width
1577
1578 # Merge.
1579 return self._merge_dimensions(
1580 dimension=to_dimension(self.width),
1581 get_preferred=preferred_content_width,
1582 dont_extend=self.dont_extend_width(),
1583 )
1584
1585 def preferred_height(self, width: int, max_available_height: int) -> Dimension:
1586 """
1587 Calculate the preferred height for this window.
1588 """
1589
1590 def preferred_content_height() -> int | None:
1591 """Content height: is only calculated if no exact height for the
1592 window was given."""
1593 if self.ignore_content_height():
1594 return None
1595
1596 total_margin_width = self._get_total_margin_width()
1597 wrap_lines = self.wrap_lines()
1598
1599 return self.content.preferred_height(
1600 width - total_margin_width,
1601 max_available_height,
1602 wrap_lines,
1603 self.get_line_prefix,
1604 )
1605
1606 return self._merge_dimensions(
1607 dimension=to_dimension(self.height),
1608 get_preferred=preferred_content_height,
1609 dont_extend=self.dont_extend_height(),
1610 )
1611
1612 @staticmethod
1613 def _merge_dimensions(
1614 dimension: Dimension | None,
1615 get_preferred: Callable[[], int | None],
1616 dont_extend: bool = False,
1617 ) -> Dimension:
1618 """
1619 Take the Dimension from this `Window` class and the received preferred
1620 size from the `UIControl` and return a `Dimension` to report to the
1621 parent container.
1622 """
1623 dimension = dimension or Dimension()
1624
1625 # When a preferred dimension was explicitly given to the Window,
1626 # ignore the UIControl.
1627 preferred: int | None
1628
1629 if dimension.preferred_specified:
1630 preferred = dimension.preferred
1631 else:
1632 # Otherwise, calculate the preferred dimension from the UI control
1633 # content.
1634 preferred = get_preferred()
1635
1636 # When a 'preferred' dimension is given by the UIControl, make sure
1637 # that it stays within the bounds of the Window.
1638 if preferred is not None:
1639 if dimension.max_specified:
1640 preferred = min(preferred, dimension.max)
1641
1642 if dimension.min_specified:
1643 preferred = max(preferred, dimension.min)
1644
1645 # When a `dont_extend` flag has been given, use the preferred dimension
1646 # also as the max dimension.
1647 max_: int | None
1648 min_: int | None
1649
1650 if dont_extend and preferred is not None:
1651 max_ = min(dimension.max, preferred)
1652 else:
1653 max_ = dimension.max if dimension.max_specified else None
1654
1655 min_ = dimension.min if dimension.min_specified else None
1656
1657 return Dimension(
1658 min=min_, max=max_, preferred=preferred, weight=dimension.weight
1659 )
1660
1661 def _get_ui_content(self, width: int, height: int) -> UIContent:
1662 """
1663 Create a `UIContent` instance.
1664 """
1665
1666 def get_content() -> UIContent:
1667 return self.content.create_content(width=width, height=height)
1668
1669 key = (get_app().render_counter, width, height)
1670 return self._ui_content_cache.get(key, get_content)
1671
1672 def _get_digraph_char(self) -> str | None:
1673 "Return `False`, or the Digraph symbol to be used."
1674 app = get_app()
1675 if app.quoted_insert:
1676 return "^"
1677 if app.vi_state.waiting_for_digraph:
1678 if app.vi_state.digraph_symbol1:
1679 return app.vi_state.digraph_symbol1
1680 return "?"
1681 return None
1682
1683 def write_to_screen(
1684 self,
1685 screen: Screen,
1686 mouse_handlers: MouseHandlers,
1687 write_position: WritePosition,
1688 parent_style: str,
1689 erase_bg: bool,
1690 z_index: int | None,
1691 ) -> None:
1692 """
1693 Write window to screen. This renders the user control, the margins and
1694 copies everything over to the absolute position at the given screen.
1695 """
1696 # If dont_extend_width/height was given. Then reduce width/height in
1697 # WritePosition if the parent wanted us to paint in a bigger area.
1698 # (This happens if this window is bundled with another window in a
1699 # HSplit/VSplit, but with different size requirements.)
1700 write_position = WritePosition(
1701 xpos=write_position.xpos,
1702 ypos=write_position.ypos,
1703 width=write_position.width,
1704 height=write_position.height,
1705 )
1706
1707 if self.dont_extend_width():
1708 write_position.width = min(
1709 write_position.width,
1710 self.preferred_width(write_position.width).preferred,
1711 )
1712
1713 if self.dont_extend_height():
1714 write_position.height = min(
1715 write_position.height,
1716 self.preferred_height(
1717 write_position.width, write_position.height
1718 ).preferred,
1719 )
1720
1721 # Draw
1722 z_index = z_index if self.z_index is None else self.z_index
1723
1724 draw_func = partial(
1725 self._write_to_screen_at_index,
1726 screen,
1727 mouse_handlers,
1728 write_position,
1729 parent_style,
1730 erase_bg,
1731 )
1732
1733 if z_index is None or z_index <= 0:
1734 # When no z_index is given, draw right away.
1735 draw_func()
1736 else:
1737 # Otherwise, postpone.
1738 screen.draw_with_z_index(z_index=z_index, draw_func=draw_func)
1739
1740 def _write_to_screen_at_index(
1741 self,
1742 screen: Screen,
1743 mouse_handlers: MouseHandlers,
1744 write_position: WritePosition,
1745 parent_style: str,
1746 erase_bg: bool,
1747 ) -> None:
1748 # Don't bother writing invisible windows.
1749 # (We save some time, but also avoid applying last-line styling.)
1750 if write_position.height <= 0 or write_position.width <= 0:
1751 return
1752
1753 # Calculate margin sizes.
1754 left_margin_widths = [self._get_margin_width(m) for m in self.left_margins]
1755 right_margin_widths = [self._get_margin_width(m) for m in self.right_margins]
1756 total_margin_width = sum(left_margin_widths + right_margin_widths)
1757
1758 # Render UserControl.
1759 ui_content = self.content.create_content(
1760 write_position.width - total_margin_width, write_position.height
1761 )
1762 assert isinstance(ui_content, UIContent)
1763
1764 # Scroll content.
1765 wrap_lines = self.wrap_lines()
1766 self._scroll(
1767 ui_content, write_position.width - total_margin_width, write_position.height
1768 )
1769
1770 # Erase background and fill with `char`.
1771 self._fill_bg(screen, write_position, erase_bg)
1772
1773 # Resolve `align` attribute.
1774 align = self.align() if callable(self.align) else self.align
1775
1776 # Write body
1777 visible_line_to_row_col, rowcol_to_yx = self._copy_body(
1778 ui_content,
1779 screen,
1780 write_position,
1781 sum(left_margin_widths),
1782 write_position.width - total_margin_width,
1783 self.vertical_scroll,
1784 self.horizontal_scroll,
1785 wrap_lines=wrap_lines,
1786 highlight_lines=True,
1787 vertical_scroll_2=self.vertical_scroll_2,
1788 always_hide_cursor=self.always_hide_cursor(),
1789 has_focus=get_app().layout.current_control == self.content,
1790 align=align,
1791 get_line_prefix=self.get_line_prefix,
1792 )
1793
1794 # Remember render info. (Set before generating the margins. They need this.)
1795 x_offset = write_position.xpos + sum(left_margin_widths)
1796 y_offset = write_position.ypos
1797
1798 render_info = WindowRenderInfo(
1799 window=self,
1800 ui_content=ui_content,
1801 horizontal_scroll=self.horizontal_scroll,
1802 vertical_scroll=self.vertical_scroll,
1803 window_width=write_position.width - total_margin_width,
1804 window_height=write_position.height,
1805 configured_scroll_offsets=self.scroll_offsets,
1806 visible_line_to_row_col=visible_line_to_row_col,
1807 rowcol_to_yx=rowcol_to_yx,
1808 x_offset=x_offset,
1809 y_offset=y_offset,
1810 wrap_lines=wrap_lines,
1811 )
1812 self.render_info = render_info
1813
1814 # Set mouse handlers.
1815 def mouse_handler(mouse_event: MouseEvent) -> NotImplementedOrNone:
1816 """
1817 Wrapper around the mouse_handler of the `UIControl` that turns
1818 screen coordinates into line coordinates.
1819 Returns `NotImplemented` if no UI invalidation should be done.
1820 """
1821 # Don't handle mouse events outside of the current modal part of
1822 # the UI.
1823 if self not in get_app().layout.walk_through_modal_area():
1824 return NotImplemented
1825
1826 # Find row/col position first.
1827 yx_to_rowcol = {v: k for k, v in rowcol_to_yx.items()}
1828 y = mouse_event.position.y
1829 x = mouse_event.position.x
1830
1831 # If clicked below the content area, look for a position in the
1832 # last line instead.
1833 max_y = write_position.ypos + len(visible_line_to_row_col) - 1
1834 y = min(max_y, y)
1835 result: NotImplementedOrNone
1836
1837 while x >= 0:
1838 try:
1839 row, col = yx_to_rowcol[y, x]
1840 except KeyError:
1841 # Try again. (When clicking on the right side of double
1842 # width characters, or on the right side of the input.)
1843 x -= 1
1844 else:
1845 # Found position, call handler of UIControl.
1846 result = self.content.mouse_handler(
1847 MouseEvent(
1848 position=Point(x=col, y=row),
1849 event_type=mouse_event.event_type,
1850 button=mouse_event.button,
1851 modifiers=mouse_event.modifiers,
1852 )
1853 )
1854 break
1855 else:
1856 # nobreak.
1857 # (No x/y coordinate found for the content. This happens in
1858 # case of a DummyControl, that does not have any content.
1859 # Report (0,0) instead.)
1860 result = self.content.mouse_handler(
1861 MouseEvent(
1862 position=Point(x=0, y=0),
1863 event_type=mouse_event.event_type,
1864 button=mouse_event.button,
1865 modifiers=mouse_event.modifiers,
1866 )
1867 )
1868
1869 # If it returns NotImplemented, handle it here.
1870 if result == NotImplemented:
1871 result = self._mouse_handler(mouse_event)
1872
1873 return result
1874
1875 mouse_handlers.set_mouse_handler_for_range(
1876 x_min=write_position.xpos + sum(left_margin_widths),
1877 x_max=write_position.xpos + write_position.width - total_margin_width,
1878 y_min=write_position.ypos,
1879 y_max=write_position.ypos + write_position.height,
1880 handler=mouse_handler,
1881 )
1882
1883 # Render and copy margins.
1884 move_x = 0
1885
1886 def render_margin(m: Margin, width: int) -> UIContent:
1887 "Render margin. Return `Screen`."
1888 # Retrieve margin fragments.
1889 fragments = m.create_margin(render_info, width, write_position.height)
1890
1891 # Turn it into a UIContent object.
1892 # already rendered those fragments using this size.)
1893 return FormattedTextControl(fragments).create_content(
1894 width + 1, write_position.height
1895 )
1896
1897 for m, width in zip(self.left_margins, left_margin_widths):
1898 if width > 0: # (ConditionalMargin returns a zero width. -- Don't render.)
1899 # Create screen for margin.
1900 margin_content = render_margin(m, width)
1901
1902 # Copy and shift X.
1903 self._copy_margin(margin_content, screen, write_position, move_x, width)
1904 move_x += width
1905
1906 move_x = write_position.width - sum(right_margin_widths)
1907
1908 for m, width in zip(self.right_margins, right_margin_widths):
1909 # Create screen for margin.
1910 margin_content = render_margin(m, width)
1911
1912 # Copy and shift X.
1913 self._copy_margin(margin_content, screen, write_position, move_x, width)
1914 move_x += width
1915
1916 # Apply 'self.style'
1917 self._apply_style(screen, write_position, parent_style)
1918
1919 # Tell the screen that this user control has been painted at this
1920 # position.
1921 screen.visible_windows_to_write_positions[self] = write_position
1922
1923 def _copy_body(
1924 self,
1925 ui_content: UIContent,
1926 new_screen: Screen,
1927 write_position: WritePosition,
1928 move_x: int,
1929 width: int,
1930 vertical_scroll: int = 0,
1931 horizontal_scroll: int = 0,
1932 wrap_lines: bool = False,
1933 highlight_lines: bool = False,
1934 vertical_scroll_2: int = 0,
1935 always_hide_cursor: bool = False,
1936 has_focus: bool = False,
1937 align: WindowAlign = WindowAlign.LEFT,
1938 get_line_prefix: Callable[[int, int], AnyFormattedText] | None = None,
1939 ) -> tuple[dict[int, tuple[int, int]], dict[tuple[int, int], tuple[int, int]]]:
1940 """
1941 Copy the UIContent into the output screen.
1942 Return (visible_line_to_row_col, rowcol_to_yx) tuple.
1943
1944 :param get_line_prefix: None or a callable that takes a line number
1945 (int) and a wrap_count (int) and returns formatted text.
1946 """
1947 xpos = write_position.xpos + move_x
1948 ypos = write_position.ypos
1949 line_count = ui_content.line_count
1950 new_buffer = new_screen.data_buffer
1951 empty_char = _CHAR_CACHE["", ""]
1952
1953 # Map visible line number to (row, col) of input.
1954 # 'col' will always be zero if line wrapping is off.
1955 visible_line_to_row_col: dict[int, tuple[int, int]] = {}
1956
1957 # Maps (row, col) from the input to (y, x) screen coordinates.
1958 rowcol_to_yx: dict[tuple[int, int], tuple[int, int]] = {}
1959
1960 def copy_line(
1961 line: StyleAndTextTuples,
1962 lineno: int,
1963 x: int,
1964 y: int,
1965 is_input: bool = False,
1966 ) -> tuple[int, int]:
1967 """
1968 Copy over a single line to the output screen. This can wrap over
1969 multiple lines in the output. It will call the prefix (prompt)
1970 function before every line.
1971 """
1972 if is_input:
1973 current_rowcol_to_yx = rowcol_to_yx
1974 else:
1975 current_rowcol_to_yx = {} # Throwaway dictionary.
1976
1977 # Draw line prefix.
1978 if is_input and get_line_prefix:
1979 prompt = to_formatted_text(get_line_prefix(lineno, 0))
1980 x, y = copy_line(prompt, lineno, x, y, is_input=False)
1981
1982 # Scroll horizontally.
1983 skipped = 0 # Characters skipped because of horizontal scrolling.
1984 if horizontal_scroll and is_input:
1985 h_scroll = horizontal_scroll
1986 line = explode_text_fragments(line)
1987 while h_scroll > 0 and line:
1988 h_scroll -= get_cwidth(line[0][1])
1989 skipped += 1
1990 del line[:1] # Remove first character.
1991
1992 x -= h_scroll # When scrolling over double width character,
1993 # this can end up being negative.
1994
1995 # Align this line. (Note that this doesn't work well when we use
1996 # get_line_prefix and that function returns variable width prefixes.)
1997 if align == WindowAlign.CENTER:
1998 line_width = fragment_list_width(line)
1999 if line_width < width:
2000 x += (width - line_width) // 2
2001 elif align == WindowAlign.RIGHT:
2002 line_width = fragment_list_width(line)
2003 if line_width < width:
2004 x += width - line_width
2005
2006 col = 0
2007 wrap_count = 0
2008 for style, text, *_ in line:
2009 new_buffer_row = new_buffer[y + ypos]
2010
2011 # Remember raw VT escape sequences. (E.g. FinalTerm's
2012 # escape sequences.)
2013 if "[ZeroWidthEscape]" in style:
2014 new_screen.zero_width_escapes[y + ypos][x + xpos] += text
2015 continue
2016
2017 for c in text:
2018 char = _CHAR_CACHE[c, style]
2019 char_width = char.width
2020
2021 # Wrap when the line width is exceeded.
2022 if wrap_lines and x + char_width > width:
2023 visible_line_to_row_col[y + 1] = (
2024 lineno,
2025 visible_line_to_row_col[y][1] + x,
2026 )
2027 y += 1
2028 wrap_count += 1
2029 x = 0
2030
2031 # Insert line prefix (continuation prompt).
2032 if is_input and get_line_prefix:
2033 prompt = to_formatted_text(
2034 get_line_prefix(lineno, wrap_count)
2035 )
2036 x, y = copy_line(prompt, lineno, x, y, is_input=False)
2037
2038 new_buffer_row = new_buffer[y + ypos]
2039
2040 if y >= write_position.height:
2041 return x, y # Break out of all for loops.
2042
2043 # Set character in screen and shift 'x'.
2044 if x >= 0 and y >= 0 and x < width:
2045 new_buffer_row[x + xpos] = char
2046
2047 # When we print a multi width character, make sure
2048 # to erase the neighbors positions in the screen.
2049 # (The empty string if different from everything,
2050 # so next redraw this cell will repaint anyway.)
2051 if char_width > 1:
2052 for i in range(1, char_width):
2053 new_buffer_row[x + xpos + i] = empty_char
2054
2055 # If this is a zero width characters, then it's
2056 # probably part of a decomposed unicode character.
2057 # See: https://en.wikipedia.org/wiki/Unicode_equivalence
2058 # Merge it in the previous cell.
2059 elif char_width == 0:
2060 # Handle all character widths. If the previous
2061 # character is a multiwidth character, then
2062 # merge it two positions back.
2063 for pw in [2, 1]: # Previous character width.
2064 if (
2065 x - pw >= 0
2066 and new_buffer_row[x + xpos - pw].width == pw
2067 ):
2068 prev_char = new_buffer_row[x + xpos - pw]
2069 char2 = _CHAR_CACHE[
2070 prev_char.char + c, prev_char.style
2071 ]
2072 new_buffer_row[x + xpos - pw] = char2
2073
2074 # Keep track of write position for each character.
2075 current_rowcol_to_yx[lineno, col + skipped] = (
2076 y + ypos,
2077 x + xpos,
2078 )
2079
2080 col += 1
2081 x += char_width
2082 return x, y
2083
2084 # Copy content.
2085 def copy() -> int:
2086 y = -vertical_scroll_2
2087 lineno = vertical_scroll
2088
2089 while y < write_position.height and lineno < line_count:
2090 # Take the next line and copy it in the real screen.
2091 line = ui_content.get_line(lineno)
2092
2093 visible_line_to_row_col[y] = (lineno, horizontal_scroll)
2094
2095 # Copy margin and actual line.
2096 x = 0
2097 x, y = copy_line(line, lineno, x, y, is_input=True)
2098
2099 lineno += 1
2100 y += 1
2101 return y
2102
2103 copy()
2104
2105 def cursor_pos_to_screen_pos(row: int, col: int) -> Point:
2106 "Translate row/col from UIContent to real Screen coordinates."
2107 try:
2108 y, x = rowcol_to_yx[row, col]
2109 except KeyError:
2110 # Normally this should never happen. (It is a bug, if it happens.)
2111 # But to be sure, return (0, 0)
2112 return Point(x=0, y=0)
2113
2114 # raise ValueError(
2115 # 'Invalid position. row=%r col=%r, vertical_scroll=%r, '
2116 # 'horizontal_scroll=%r, height=%r' %
2117 # (row, col, vertical_scroll, horizontal_scroll, write_position.height))
2118 else:
2119 return Point(x=x, y=y)
2120
2121 # Set cursor and menu positions.
2122 if ui_content.cursor_position:
2123 screen_cursor_position = cursor_pos_to_screen_pos(
2124 ui_content.cursor_position.y, ui_content.cursor_position.x
2125 )
2126
2127 if has_focus:
2128 new_screen.set_cursor_position(self, screen_cursor_position)
2129
2130 if always_hide_cursor:
2131 new_screen.show_cursor = False
2132 else:
2133 new_screen.show_cursor = ui_content.show_cursor
2134
2135 self._highlight_digraph(new_screen)
2136
2137 if highlight_lines:
2138 self._highlight_cursorlines(
2139 new_screen,
2140 screen_cursor_position,
2141 xpos,
2142 ypos,
2143 width,
2144 write_position.height,
2145 )
2146
2147 # Draw input characters from the input processor queue.
2148 if has_focus and ui_content.cursor_position:
2149 self._show_key_processor_key_buffer(new_screen)
2150
2151 # Set menu position.
2152 if ui_content.menu_position:
2153 new_screen.set_menu_position(
2154 self,
2155 cursor_pos_to_screen_pos(
2156 ui_content.menu_position.y, ui_content.menu_position.x
2157 ),
2158 )
2159
2160 # Update output screen height.
2161 new_screen.height = max(new_screen.height, ypos + write_position.height)
2162
2163 return visible_line_to_row_col, rowcol_to_yx
2164
2165 def _fill_bg(
2166 self, screen: Screen, write_position: WritePosition, erase_bg: bool
2167 ) -> None:
2168 """
2169 Erase/fill the background.
2170 (Useful for floats and when a `char` has been given.)
2171 """
2172 char: str | None
2173 if callable(self.char):
2174 char = self.char()
2175 else:
2176 char = self.char
2177
2178 if erase_bg or char:
2179 wp = write_position
2180 char_obj = _CHAR_CACHE[char or " ", ""]
2181
2182 for y in range(wp.ypos, wp.ypos + wp.height):
2183 row = screen.data_buffer[y]
2184 for x in range(wp.xpos, wp.xpos + wp.width):
2185 row[x] = char_obj
2186
2187 def _apply_style(
2188 self, new_screen: Screen, write_position: WritePosition, parent_style: str
2189 ) -> None:
2190 # Apply `self.style`.
2191 style = parent_style + " " + to_str(self.style)
2192
2193 new_screen.fill_area(write_position, style=style, after=False)
2194
2195 # Apply the 'last-line' class to the last line of each Window. This can
2196 # be used to apply an 'underline' to the user control.
2197 wp = WritePosition(
2198 write_position.xpos,
2199 write_position.ypos + write_position.height - 1,
2200 write_position.width,
2201 1,
2202 )
2203 new_screen.fill_area(wp, "class:last-line", after=True)
2204
2205 def _highlight_digraph(self, new_screen: Screen) -> None:
2206 """
2207 When we are in Vi digraph mode, put a question mark underneath the
2208 cursor.
2209 """
2210 digraph_char = self._get_digraph_char()
2211 if digraph_char:
2212 cpos = new_screen.get_cursor_position(self)
2213 new_screen.data_buffer[cpos.y][cpos.x] = _CHAR_CACHE[
2214 digraph_char, "class:digraph"
2215 ]
2216
2217 def _show_key_processor_key_buffer(self, new_screen: Screen) -> None:
2218 """
2219 When the user is typing a key binding that consists of several keys,
2220 display the last pressed key if the user is in insert mode and the key
2221 is meaningful to be displayed.
2222 E.g. Some people want to bind 'jj' to escape in Vi insert mode. But the
2223 first 'j' needs to be displayed in order to get some feedback.
2224 """
2225 app = get_app()
2226 key_buffer = app.key_processor.key_buffer
2227
2228 if key_buffer and _in_insert_mode() and not app.is_done:
2229 # The textual data for the given key. (Can be a VT100 escape
2230 # sequence.)
2231 data = key_buffer[-1].data
2232
2233 # Display only if this is a 1 cell width character.
2234 if get_cwidth(data) == 1:
2235 cpos = new_screen.get_cursor_position(self)
2236 new_screen.data_buffer[cpos.y][cpos.x] = _CHAR_CACHE[
2237 data, "class:partial-key-binding"
2238 ]
2239
2240 def _highlight_cursorlines(
2241 self, new_screen: Screen, cpos: Point, x: int, y: int, width: int, height: int
2242 ) -> None:
2243 """
2244 Highlight cursor row/column.
2245 """
2246 cursor_line_style = " class:cursor-line "
2247 cursor_column_style = " class:cursor-column "
2248
2249 data_buffer = new_screen.data_buffer
2250
2251 # Highlight cursor line.
2252 if self.cursorline():
2253 row = data_buffer[cpos.y]
2254 for x in range(x, x + width):
2255 original_char = row[x]
2256 row[x] = _CHAR_CACHE[
2257 original_char.char, original_char.style + cursor_line_style
2258 ]
2259
2260 # Highlight cursor column.
2261 if self.cursorcolumn():
2262 for y2 in range(y, y + height):
2263 row = data_buffer[y2]
2264 original_char = row[cpos.x]
2265 row[cpos.x] = _CHAR_CACHE[
2266 original_char.char, original_char.style + cursor_column_style
2267 ]
2268
2269 # Highlight color columns
2270 colorcolumns = self.colorcolumns
2271 if callable(colorcolumns):
2272 colorcolumns = colorcolumns()
2273
2274 for cc in colorcolumns:
2275 assert isinstance(cc, ColorColumn)
2276 column = cc.position
2277
2278 if column < x + width: # Only draw when visible.
2279 color_column_style = " " + cc.style
2280
2281 for y2 in range(y, y + height):
2282 row = data_buffer[y2]
2283 original_char = row[column + x]
2284 row[column + x] = _CHAR_CACHE[
2285 original_char.char, original_char.style + color_column_style
2286 ]
2287
2288 def _copy_margin(
2289 self,
2290 margin_content: UIContent,
2291 new_screen: Screen,
2292 write_position: WritePosition,
2293 move_x: int,
2294 width: int,
2295 ) -> None:
2296 """
2297 Copy characters from the margin screen to the real screen.
2298 """
2299 xpos = write_position.xpos + move_x
2300 ypos = write_position.ypos
2301
2302 margin_write_position = WritePosition(xpos, ypos, width, write_position.height)
2303 self._copy_body(margin_content, new_screen, margin_write_position, 0, width)
2304
2305 def _scroll(self, ui_content: UIContent, width: int, height: int) -> None:
2306 """
2307 Scroll body. Ensure that the cursor is visible.
2308 """
2309 if self.wrap_lines():
2310 func = self._scroll_when_linewrapping
2311 else:
2312 func = self._scroll_without_linewrapping
2313
2314 func(ui_content, width, height)
2315
2316 def _scroll_when_linewrapping(
2317 self, ui_content: UIContent, width: int, height: int
2318 ) -> None:
2319 """
2320 Scroll to make sure the cursor position is visible and that we maintain
2321 the requested scroll offset.
2322
2323 Set `self.horizontal_scroll/vertical_scroll`.
2324 """
2325 scroll_offsets_bottom = self.scroll_offsets.bottom
2326 scroll_offsets_top = self.scroll_offsets.top
2327
2328 # We don't have horizontal scrolling.
2329 self.horizontal_scroll = 0
2330
2331 def get_line_height(lineno: int) -> int:
2332 return ui_content.get_height_for_line(lineno, width, self.get_line_prefix)
2333
2334 # When there is no space, reset `vertical_scroll_2` to zero and abort.
2335 # This can happen if the margin is bigger than the window width.
2336 # Otherwise the text height will become "infinite" (a big number) and
2337 # the copy_line will spend a huge amount of iterations trying to render
2338 # nothing.
2339 if width <= 0:
2340 self.vertical_scroll = ui_content.cursor_position.y
2341 self.vertical_scroll_2 = 0
2342 return
2343
2344 # If the current line consumes more than the whole window height,
2345 # then we have to scroll vertically inside this line. (We don't take
2346 # the scroll offsets into account for this.)
2347 # Also, ignore the scroll offsets in this case. Just set the vertical
2348 # scroll to this line.
2349 line_height = get_line_height(ui_content.cursor_position.y)
2350 if line_height > height - scroll_offsets_top:
2351 # Calculate the height of the text before the cursor (including
2352 # line prefixes).
2353 text_before_height = ui_content.get_height_for_line(
2354 ui_content.cursor_position.y,
2355 width,
2356 self.get_line_prefix,
2357 slice_stop=ui_content.cursor_position.x,
2358 )
2359
2360 # Adjust scroll offset.
2361 self.vertical_scroll = ui_content.cursor_position.y
2362 self.vertical_scroll_2 = min(
2363 text_before_height - 1, # Keep the cursor visible.
2364 line_height
2365 - height, # Avoid blank lines at the bottom when scrolling up again.
2366 self.vertical_scroll_2,
2367 )
2368 self.vertical_scroll_2 = max(
2369 0, text_before_height - height, self.vertical_scroll_2
2370 )
2371 return
2372 else:
2373 self.vertical_scroll_2 = 0
2374
2375 # Current line doesn't consume the whole height. Take scroll offsets into account.
2376 def get_min_vertical_scroll() -> int:
2377 # Make sure that the cursor line is not below the bottom.
2378 # (Calculate how many lines can be shown between the cursor and the .)
2379 used_height = 0
2380 prev_lineno = ui_content.cursor_position.y
2381
2382 for lineno in range(ui_content.cursor_position.y, -1, -1):
2383 used_height += get_line_height(lineno)
2384
2385 if used_height > height - scroll_offsets_bottom:
2386 return prev_lineno
2387 else:
2388 prev_lineno = lineno
2389 return 0
2390
2391 def get_max_vertical_scroll() -> int:
2392 # Make sure that the cursor line is not above the top.
2393 prev_lineno = ui_content.cursor_position.y
2394 used_height = 0
2395
2396 for lineno in range(ui_content.cursor_position.y - 1, -1, -1):
2397 used_height += get_line_height(lineno)
2398
2399 if used_height > scroll_offsets_top:
2400 return prev_lineno
2401 else:
2402 prev_lineno = lineno
2403 return prev_lineno
2404
2405 def get_topmost_visible() -> int:
2406 """
2407 Calculate the upper most line that can be visible, while the bottom
2408 is still visible. We should not allow scroll more than this if
2409 `allow_scroll_beyond_bottom` is false.
2410 """
2411 prev_lineno = ui_content.line_count - 1
2412 used_height = 0
2413 for lineno in range(ui_content.line_count - 1, -1, -1):
2414 used_height += get_line_height(lineno)
2415 if used_height > height:
2416 return prev_lineno
2417 else:
2418 prev_lineno = lineno
2419 return prev_lineno
2420
2421 # Scroll vertically. (Make sure that the whole line which contains the
2422 # cursor is visible.
2423 topmost_visible = get_topmost_visible()
2424
2425 # Note: the `min(topmost_visible, ...)` is to make sure that we
2426 # don't require scrolling up because of the bottom scroll offset,
2427 # when we are at the end of the document.
2428 self.vertical_scroll = max(
2429 self.vertical_scroll, min(topmost_visible, get_min_vertical_scroll())
2430 )
2431 self.vertical_scroll = min(self.vertical_scroll, get_max_vertical_scroll())
2432
2433 # Disallow scrolling beyond bottom?
2434 if not self.allow_scroll_beyond_bottom():
2435 self.vertical_scroll = min(self.vertical_scroll, topmost_visible)
2436
2437 def _scroll_without_linewrapping(
2438 self, ui_content: UIContent, width: int, height: int
2439 ) -> None:
2440 """
2441 Scroll to make sure the cursor position is visible and that we maintain
2442 the requested scroll offset.
2443
2444 Set `self.horizontal_scroll/vertical_scroll`.
2445 """
2446 cursor_position = ui_content.cursor_position or Point(x=0, y=0)
2447
2448 # Without line wrapping, we will never have to scroll vertically inside
2449 # a single line.
2450 self.vertical_scroll_2 = 0
2451
2452 if ui_content.line_count == 0:
2453 self.vertical_scroll = 0
2454 self.horizontal_scroll = 0
2455 return
2456 else:
2457 current_line_text = fragment_list_to_text(
2458 ui_content.get_line(cursor_position.y)
2459 )
2460
2461 def do_scroll(
2462 current_scroll: int,
2463 scroll_offset_start: int,
2464 scroll_offset_end: int,
2465 cursor_pos: int,
2466 window_size: int,
2467 content_size: int,
2468 ) -> int:
2469 "Scrolling algorithm. Used for both horizontal and vertical scrolling."
2470 # Calculate the scroll offset to apply.
2471 # This can obviously never be more than have the screen size. Also, when the
2472 # cursor appears at the top or bottom, we don't apply the offset.
2473 scroll_offset_start = int(
2474 min(scroll_offset_start, window_size / 2, cursor_pos)
2475 )
2476 scroll_offset_end = int(
2477 min(scroll_offset_end, window_size / 2, content_size - 1 - cursor_pos)
2478 )
2479
2480 # Prevent negative scroll offsets.
2481 if current_scroll < 0:
2482 current_scroll = 0
2483
2484 # Scroll back if we scrolled to much and there's still space to show more of the document.
2485 if (
2486 not self.allow_scroll_beyond_bottom()
2487 and current_scroll > content_size - window_size
2488 ):
2489 current_scroll = max(0, content_size - window_size)
2490
2491 # Scroll up if cursor is before visible part.
2492 if current_scroll > cursor_pos - scroll_offset_start:
2493 current_scroll = max(0, cursor_pos - scroll_offset_start)
2494
2495 # Scroll down if cursor is after visible part.
2496 if current_scroll < (cursor_pos + 1) - window_size + scroll_offset_end:
2497 current_scroll = (cursor_pos + 1) - window_size + scroll_offset_end
2498
2499 return current_scroll
2500
2501 # When a preferred scroll is given, take that first into account.
2502 if self.get_vertical_scroll:
2503 self.vertical_scroll = self.get_vertical_scroll(self)
2504 assert isinstance(self.vertical_scroll, int)
2505 if self.get_horizontal_scroll:
2506 self.horizontal_scroll = self.get_horizontal_scroll(self)
2507 assert isinstance(self.horizontal_scroll, int)
2508
2509 # Update horizontal/vertical scroll to make sure that the cursor
2510 # remains visible.
2511 offsets = self.scroll_offsets
2512
2513 self.vertical_scroll = do_scroll(
2514 current_scroll=self.vertical_scroll,
2515 scroll_offset_start=offsets.top,
2516 scroll_offset_end=offsets.bottom,
2517 cursor_pos=ui_content.cursor_position.y,
2518 window_size=height,
2519 content_size=ui_content.line_count,
2520 )
2521
2522 if self.get_line_prefix:
2523 current_line_prefix_width = fragment_list_width(
2524 to_formatted_text(self.get_line_prefix(ui_content.cursor_position.y, 0))
2525 )
2526 else:
2527 current_line_prefix_width = 0
2528
2529 self.horizontal_scroll = do_scroll(
2530 current_scroll=self.horizontal_scroll,
2531 scroll_offset_start=offsets.left,
2532 scroll_offset_end=offsets.right,
2533 cursor_pos=get_cwidth(current_line_text[: ui_content.cursor_position.x]),
2534 window_size=width - current_line_prefix_width,
2535 # We can only analyze the current line. Calculating the width off
2536 # all the lines is too expensive.
2537 content_size=max(
2538 get_cwidth(current_line_text), self.horizontal_scroll + width
2539 ),
2540 )
2541
2542 def _mouse_handler(self, mouse_event: MouseEvent) -> NotImplementedOrNone:
2543 """
2544 Mouse handler. Called when the UI control doesn't handle this
2545 particular event.
2546
2547 Return `NotImplemented` if nothing was done as a consequence of this
2548 key binding (no UI invalidate required in that case).
2549 """
2550 if mouse_event.event_type == MouseEventType.SCROLL_DOWN:
2551 self._scroll_down()
2552 return None
2553 elif mouse_event.event_type == MouseEventType.SCROLL_UP:
2554 self._scroll_up()
2555 return None
2556
2557 return NotImplemented
2558
2559 def _scroll_down(self) -> None:
2560 "Scroll window down."
2561 info = self.render_info
2562
2563 if info is None:
2564 return
2565
2566 if self.vertical_scroll < info.content_height - info.window_height:
2567 if info.cursor_position.y <= info.configured_scroll_offsets.top:
2568 self.content.move_cursor_down()
2569
2570 self.vertical_scroll += 1
2571
2572 def _scroll_up(self) -> None:
2573 "Scroll window up."
2574 info = self.render_info
2575
2576 if info is None:
2577 return
2578
2579 if info.vertical_scroll > 0:
2580 # TODO: not entirely correct yet in case of line wrapping and long lines.
2581 if (
2582 info.cursor_position.y
2583 >= info.window_height - 1 - info.configured_scroll_offsets.bottom
2584 ):
2585 self.content.move_cursor_up()
2586
2587 self.vertical_scroll -= 1
2588
2589 def get_key_bindings(self) -> KeyBindingsBase | None:
2590 return self.content.get_key_bindings()
2591
2592 def get_children(self) -> list[Container]:
2593 return []
2594
2595
2596class ConditionalContainer(Container):
2597 """
2598 Wrapper around any other container that can change the visibility. The
2599 received `filter` determines whether the given container should be
2600 displayed or not.
2601
2602 :param content: :class:`.Container` instance.
2603 :param filter: :class:`.Filter` instance.
2604 """
2605
2606 def __init__(self, content: AnyContainer, filter: FilterOrBool) -> None:
2607 self.content = to_container(content)
2608 self.filter = to_filter(filter)
2609
2610 def __repr__(self) -> str:
2611 return f"ConditionalContainer({self.content!r}, filter={self.filter!r})"
2612
2613 def reset(self) -> None:
2614 self.content.reset()
2615
2616 def preferred_width(self, max_available_width: int) -> Dimension:
2617 if self.filter():
2618 return self.content.preferred_width(max_available_width)
2619 else:
2620 return Dimension.zero()
2621
2622 def preferred_height(self, width: int, max_available_height: int) -> Dimension:
2623 if self.filter():
2624 return self.content.preferred_height(width, max_available_height)
2625 else:
2626 return Dimension.zero()
2627
2628 def write_to_screen(
2629 self,
2630 screen: Screen,
2631 mouse_handlers: MouseHandlers,
2632 write_position: WritePosition,
2633 parent_style: str,
2634 erase_bg: bool,
2635 z_index: int | None,
2636 ) -> None:
2637 if self.filter():
2638 return self.content.write_to_screen(
2639 screen, mouse_handlers, write_position, parent_style, erase_bg, z_index
2640 )
2641
2642 def get_children(self) -> list[Container]:
2643 return [self.content]
2644
2645
2646class DynamicContainer(Container):
2647 """
2648 Container class that dynamically returns any Container.
2649
2650 :param get_container: Callable that returns a :class:`.Container` instance
2651 or any widget with a ``__pt_container__`` method.
2652 """
2653
2654 def __init__(self, get_container: Callable[[], AnyContainer]) -> None:
2655 self.get_container = get_container
2656
2657 def _get_container(self) -> Container:
2658 """
2659 Return the current container object.
2660
2661 We call `to_container`, because `get_container` can also return a
2662 widget with a ``__pt_container__`` method.
2663 """
2664 obj = self.get_container()
2665 return to_container(obj)
2666
2667 def reset(self) -> None:
2668 self._get_container().reset()
2669
2670 def preferred_width(self, max_available_width: int) -> Dimension:
2671 return self._get_container().preferred_width(max_available_width)
2672
2673 def preferred_height(self, width: int, max_available_height: int) -> Dimension:
2674 return self._get_container().preferred_height(width, max_available_height)
2675
2676 def write_to_screen(
2677 self,
2678 screen: Screen,
2679 mouse_handlers: MouseHandlers,
2680 write_position: WritePosition,
2681 parent_style: str,
2682 erase_bg: bool,
2683 z_index: int | None,
2684 ) -> None:
2685 self._get_container().write_to_screen(
2686 screen, mouse_handlers, write_position, parent_style, erase_bg, z_index
2687 )
2688
2689 def is_modal(self) -> bool:
2690 return False
2691
2692 def get_key_bindings(self) -> KeyBindingsBase | None:
2693 # Key bindings will be collected when `layout.walk()` finds the child
2694 # container.
2695 return None
2696
2697 def get_children(self) -> list[Container]:
2698 # Here we have to return the current active container itself, not its
2699 # children. Otherwise, we run into issues where `layout.walk()` will
2700 # never see an object of type `Window` if this contains a window. We
2701 # can't/shouldn't proxy the "isinstance" check.
2702 return [self._get_container()]
2703
2704
2705def to_container(container: AnyContainer) -> Container:
2706 """
2707 Make sure that the given object is a :class:`.Container`.
2708 """
2709 if isinstance(container, Container):
2710 return container
2711 elif hasattr(container, "__pt_container__"):
2712 return to_container(container.__pt_container__())
2713 else:
2714 raise ValueError(f"Not a container object: {container!r}")
2715
2716
2717def to_window(container: AnyContainer) -> Window:
2718 """
2719 Make sure that the given argument is a :class:`.Window`.
2720 """
2721 if isinstance(container, Window):
2722 return container
2723 elif hasattr(container, "__pt_container__"):
2724 return to_window(cast("MagicContainer", container).__pt_container__())
2725 else:
2726 raise ValueError(f"Not a Window object: {container!r}.")
2727
2728
2729def is_container(value: object) -> TypeGuard[AnyContainer]:
2730 """
2731 Checks whether the given value is a container object
2732 (for use in assert statements).
2733 """
2734 if isinstance(value, Container):
2735 return True
2736 if hasattr(value, "__pt_container__"):
2737 return is_container(cast("MagicContainer", value).__pt_container__())
2738 return False