| | """ |
| | Container for the layout. |
| | (Containers can contain other containers or user interface controls.) |
| | """ |
| |
|
| | from __future__ import annotations |
| |
|
| | from abc import ABCMeta, abstractmethod |
| | from enum import Enum |
| | from functools import partial |
| | from typing import TYPE_CHECKING, Callable, Sequence, Union, cast |
| |
|
| | from prompt_toolkit.application.current import get_app |
| | from prompt_toolkit.cache import SimpleCache |
| | from prompt_toolkit.data_structures import Point |
| | from prompt_toolkit.filters import ( |
| | FilterOrBool, |
| | emacs_insert_mode, |
| | to_filter, |
| | vi_insert_mode, |
| | ) |
| | from prompt_toolkit.formatted_text import ( |
| | AnyFormattedText, |
| | StyleAndTextTuples, |
| | to_formatted_text, |
| | ) |
| | from prompt_toolkit.formatted_text.utils import ( |
| | fragment_list_to_text, |
| | fragment_list_width, |
| | ) |
| | from prompt_toolkit.key_binding import KeyBindingsBase |
| | from prompt_toolkit.mouse_events import MouseEvent, MouseEventType |
| | from prompt_toolkit.utils import get_cwidth, take_using_weights, to_int, to_str |
| |
|
| | from .controls import ( |
| | DummyControl, |
| | FormattedTextControl, |
| | GetLinePrefixCallable, |
| | UIContent, |
| | UIControl, |
| | ) |
| | from .dimension import ( |
| | AnyDimension, |
| | Dimension, |
| | max_layout_dimensions, |
| | sum_layout_dimensions, |
| | to_dimension, |
| | ) |
| | from .margins import Margin |
| | from .mouse_handlers import MouseHandlers |
| | from .screen import _CHAR_CACHE, Screen, WritePosition |
| | from .utils import explode_text_fragments |
| |
|
| | if TYPE_CHECKING: |
| | from typing_extensions import Protocol, TypeGuard |
| |
|
| | from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone |
| |
|
| |
|
| | __all__ = [ |
| | "AnyContainer", |
| | "Container", |
| | "HorizontalAlign", |
| | "VerticalAlign", |
| | "HSplit", |
| | "VSplit", |
| | "FloatContainer", |
| | "Float", |
| | "WindowAlign", |
| | "Window", |
| | "WindowRenderInfo", |
| | "ConditionalContainer", |
| | "ScrollOffsets", |
| | "ColorColumn", |
| | "to_container", |
| | "to_window", |
| | "is_container", |
| | "DynamicContainer", |
| | ] |
| |
|
| |
|
| | class Container(metaclass=ABCMeta): |
| | """ |
| | Base class for user interface layout. |
| | """ |
| |
|
| | @abstractmethod |
| | def reset(self) -> None: |
| | """ |
| | Reset the state of this container and all the children. |
| | (E.g. reset scroll offsets, etc...) |
| | """ |
| |
|
| | @abstractmethod |
| | def preferred_width(self, max_available_width: int) -> Dimension: |
| | """ |
| | Return a :class:`~prompt_toolkit.layout.Dimension` that represents the |
| | desired width for this container. |
| | """ |
| |
|
| | @abstractmethod |
| | def preferred_height(self, width: int, max_available_height: int) -> Dimension: |
| | """ |
| | Return a :class:`~prompt_toolkit.layout.Dimension` that represents the |
| | desired height for this container. |
| | """ |
| |
|
| | @abstractmethod |
| | def write_to_screen( |
| | self, |
| | screen: Screen, |
| | mouse_handlers: MouseHandlers, |
| | write_position: WritePosition, |
| | parent_style: str, |
| | erase_bg: bool, |
| | z_index: int | None, |
| | ) -> None: |
| | """ |
| | Write the actual content to the screen. |
| | |
| | :param screen: :class:`~prompt_toolkit.layout.screen.Screen` |
| | :param mouse_handlers: :class:`~prompt_toolkit.layout.mouse_handlers.MouseHandlers`. |
| | :param parent_style: Style string to pass to the :class:`.Window` |
| | object. This will be applied to all content of the windows. |
| | :class:`.VSplit` and :class:`.HSplit` can use it to pass their |
| | style down to the windows that they contain. |
| | :param z_index: Used for propagating z_index from parent to child. |
| | """ |
| |
|
| | def is_modal(self) -> bool: |
| | """ |
| | When this container is modal, key bindings from parent containers are |
| | not taken into account if a user control in this container is focused. |
| | """ |
| | return False |
| |
|
| | def get_key_bindings(self) -> KeyBindingsBase | None: |
| | """ |
| | Returns a :class:`.KeyBindings` object. These bindings become active when any |
| | user control in this container has the focus, except if any containers |
| | between this container and the focused user control is modal. |
| | """ |
| | return None |
| |
|
| | @abstractmethod |
| | def get_children(self) -> list[Container]: |
| | """ |
| | Return the list of child :class:`.Container` objects. |
| | """ |
| | return [] |
| |
|
| |
|
| | if TYPE_CHECKING: |
| |
|
| | class MagicContainer(Protocol): |
| | """ |
| | Any object that implements ``__pt_container__`` represents a container. |
| | """ |
| |
|
| | def __pt_container__(self) -> AnyContainer: ... |
| |
|
| |
|
| | AnyContainer = Union[Container, "MagicContainer"] |
| |
|
| |
|
| | def _window_too_small() -> Window: |
| | "Create a `Window` that displays the 'Window too small' text." |
| | return Window( |
| | FormattedTextControl(text=[("class:window-too-small", " Window too small... ")]) |
| | ) |
| |
|
| |
|
| | class VerticalAlign(Enum): |
| | "Alignment for `HSplit`." |
| |
|
| | TOP = "TOP" |
| | CENTER = "CENTER" |
| | BOTTOM = "BOTTOM" |
| | JUSTIFY = "JUSTIFY" |
| |
|
| |
|
| | class HorizontalAlign(Enum): |
| | "Alignment for `VSplit`." |
| |
|
| | LEFT = "LEFT" |
| | CENTER = "CENTER" |
| | RIGHT = "RIGHT" |
| | JUSTIFY = "JUSTIFY" |
| |
|
| |
|
| | class _Split(Container): |
| | """ |
| | The common parts of `VSplit` and `HSplit`. |
| | """ |
| |
|
| | def __init__( |
| | self, |
| | children: Sequence[AnyContainer], |
| | window_too_small: Container | None = None, |
| | padding: AnyDimension = Dimension.exact(0), |
| | padding_char: str | None = None, |
| | padding_style: str = "", |
| | width: AnyDimension = None, |
| | height: AnyDimension = None, |
| | z_index: int | None = None, |
| | modal: bool = False, |
| | key_bindings: KeyBindingsBase | None = None, |
| | style: str | Callable[[], str] = "", |
| | ) -> None: |
| | self.children = [to_container(c) for c in children] |
| | self.window_too_small = window_too_small or _window_too_small() |
| | self.padding = padding |
| | self.padding_char = padding_char |
| | self.padding_style = padding_style |
| |
|
| | self.width = width |
| | self.height = height |
| | self.z_index = z_index |
| |
|
| | self.modal = modal |
| | self.key_bindings = key_bindings |
| | self.style = style |
| |
|
| | def is_modal(self) -> bool: |
| | return self.modal |
| |
|
| | def get_key_bindings(self) -> KeyBindingsBase | None: |
| | return self.key_bindings |
| |
|
| | def get_children(self) -> list[Container]: |
| | return self.children |
| |
|
| |
|
| | class HSplit(_Split): |
| | """ |
| | Several layouts, one stacked above/under the other. :: |
| | |
| | +--------------------+ |
| | | | |
| | +--------------------+ |
| | | | |
| | +--------------------+ |
| | |
| | By default, this doesn't display a horizontal line between the children, |
| | but if this is something you need, then create a HSplit as follows:: |
| | |
| | HSplit(children=[ ... ], padding_char='-', |
| | padding=1, padding_style='#ffff00') |
| | |
| | :param children: List of child :class:`.Container` objects. |
| | :param window_too_small: A :class:`.Container` object that is displayed if |
| | there is not enough space for all the children. By default, this is a |
| | "Window too small" message. |
| | :param align: `VerticalAlign` value. |
| | :param width: When given, use this width instead of looking at the children. |
| | :param height: When given, use this height instead of looking at the children. |
| | :param z_index: (int or None) When specified, this can be used to bring |
| | element in front of floating elements. `None` means: inherit from parent. |
| | :param style: A style string. |
| | :param modal: ``True`` or ``False``. |
| | :param key_bindings: ``None`` or a :class:`.KeyBindings` object. |
| | |
| | :param padding: (`Dimension` or int), size to be used for the padding. |
| | :param padding_char: Character to be used for filling in the padding. |
| | :param padding_style: Style to applied to the padding. |
| | """ |
| |
|
| | def __init__( |
| | self, |
| | children: Sequence[AnyContainer], |
| | window_too_small: Container | None = None, |
| | align: VerticalAlign = VerticalAlign.JUSTIFY, |
| | padding: AnyDimension = 0, |
| | padding_char: str | None = None, |
| | padding_style: str = "", |
| | width: AnyDimension = None, |
| | height: AnyDimension = None, |
| | z_index: int | None = None, |
| | modal: bool = False, |
| | key_bindings: KeyBindingsBase | None = None, |
| | style: str | Callable[[], str] = "", |
| | ) -> None: |
| | super().__init__( |
| | children=children, |
| | window_too_small=window_too_small, |
| | padding=padding, |
| | padding_char=padding_char, |
| | padding_style=padding_style, |
| | width=width, |
| | height=height, |
| | z_index=z_index, |
| | modal=modal, |
| | key_bindings=key_bindings, |
| | style=style, |
| | ) |
| |
|
| | self.align = align |
| |
|
| | self._children_cache: SimpleCache[tuple[Container, ...], list[Container]] = ( |
| | SimpleCache(maxsize=1) |
| | ) |
| | self._remaining_space_window = Window() |
| |
|
| | def preferred_width(self, max_available_width: int) -> Dimension: |
| | if self.width is not None: |
| | return to_dimension(self.width) |
| |
|
| | if self.children: |
| | dimensions = [c.preferred_width(max_available_width) for c in self.children] |
| | return max_layout_dimensions(dimensions) |
| | else: |
| | return Dimension() |
| |
|
| | def preferred_height(self, width: int, max_available_height: int) -> Dimension: |
| | if self.height is not None: |
| | return to_dimension(self.height) |
| |
|
| | dimensions = [ |
| | c.preferred_height(width, max_available_height) for c in self._all_children |
| | ] |
| | return sum_layout_dimensions(dimensions) |
| |
|
| | def reset(self) -> None: |
| | for c in self.children: |
| | c.reset() |
| |
|
| | @property |
| | def _all_children(self) -> list[Container]: |
| | """ |
| | List of child objects, including padding. |
| | """ |
| |
|
| | def get() -> list[Container]: |
| | result: list[Container] = [] |
| |
|
| | |
| | if self.align in (VerticalAlign.CENTER, VerticalAlign.BOTTOM): |
| | result.append(Window(width=Dimension(preferred=0))) |
| |
|
| | |
| | for child in self.children: |
| | result.append(child) |
| | result.append( |
| | Window( |
| | height=self.padding, |
| | char=self.padding_char, |
| | style=self.padding_style, |
| | ) |
| | ) |
| | if result: |
| | result.pop() |
| |
|
| | |
| | if self.align in (VerticalAlign.CENTER, VerticalAlign.TOP): |
| | result.append(Window(width=Dimension(preferred=0))) |
| |
|
| | return result |
| |
|
| | return self._children_cache.get(tuple(self.children), get) |
| |
|
| | def write_to_screen( |
| | self, |
| | screen: Screen, |
| | mouse_handlers: MouseHandlers, |
| | write_position: WritePosition, |
| | parent_style: str, |
| | erase_bg: bool, |
| | z_index: int | None, |
| | ) -> None: |
| | """ |
| | Render the prompt to a `Screen` instance. |
| | |
| | :param screen: The :class:`~prompt_toolkit.layout.screen.Screen` class |
| | to which the output has to be written. |
| | """ |
| | sizes = self._divide_heights(write_position) |
| | style = parent_style + " " + to_str(self.style) |
| | z_index = z_index if self.z_index is None else self.z_index |
| |
|
| | if sizes is None: |
| | self.window_too_small.write_to_screen( |
| | screen, mouse_handlers, write_position, style, erase_bg, z_index |
| | ) |
| | else: |
| | |
| | ypos = write_position.ypos |
| | xpos = write_position.xpos |
| | width = write_position.width |
| |
|
| | |
| | for s, c in zip(sizes, self._all_children): |
| | c.write_to_screen( |
| | screen, |
| | mouse_handlers, |
| | WritePosition(xpos, ypos, width, s), |
| | style, |
| | erase_bg, |
| | z_index, |
| | ) |
| | ypos += s |
| |
|
| | |
| | |
| | |
| | |
| | |
| | remaining_height = write_position.ypos + write_position.height - ypos |
| | if remaining_height > 0: |
| | self._remaining_space_window.write_to_screen( |
| | screen, |
| | mouse_handlers, |
| | WritePosition(xpos, ypos, width, remaining_height), |
| | style, |
| | erase_bg, |
| | z_index, |
| | ) |
| |
|
| | def _divide_heights(self, write_position: WritePosition) -> list[int] | None: |
| | """ |
| | Return the heights for all rows. |
| | Or None when there is not enough space. |
| | """ |
| | if not self.children: |
| | return [] |
| |
|
| | width = write_position.width |
| | height = write_position.height |
| |
|
| | |
| | dimensions = [c.preferred_height(width, height) for c in self._all_children] |
| |
|
| | |
| | sum_dimensions = sum_layout_dimensions(dimensions) |
| |
|
| | |
| | |
| | if sum_dimensions.min > height: |
| | return None |
| |
|
| | |
| | |
| | sizes = [d.min for d in dimensions] |
| |
|
| | child_generator = take_using_weights( |
| | items=list(range(len(dimensions))), weights=[d.weight for d in dimensions] |
| | ) |
| |
|
| | i = next(child_generator) |
| |
|
| | |
| | preferred_stop = min(height, sum_dimensions.preferred) |
| | preferred_dimensions = [d.preferred for d in dimensions] |
| |
|
| | while sum(sizes) < preferred_stop: |
| | if sizes[i] < preferred_dimensions[i]: |
| | sizes[i] += 1 |
| | i = next(child_generator) |
| |
|
| | |
| | if not get_app().is_done: |
| | max_stop = min(height, sum_dimensions.max) |
| | max_dimensions = [d.max for d in dimensions] |
| |
|
| | while sum(sizes) < max_stop: |
| | if sizes[i] < max_dimensions[i]: |
| | sizes[i] += 1 |
| | i = next(child_generator) |
| |
|
| | return sizes |
| |
|
| |
|
| | class VSplit(_Split): |
| | """ |
| | Several layouts, one stacked left/right of the other. :: |
| | |
| | +---------+----------+ |
| | | | | |
| | | | | |
| | +---------+----------+ |
| | |
| | By default, this doesn't display a vertical line between the children, but |
| | if this is something you need, then create a HSplit as follows:: |
| | |
| | VSplit(children=[ ... ], padding_char='|', |
| | padding=1, padding_style='#ffff00') |
| | |
| | :param children: List of child :class:`.Container` objects. |
| | :param window_too_small: A :class:`.Container` object that is displayed if |
| | there is not enough space for all the children. By default, this is a |
| | "Window too small" message. |
| | :param align: `HorizontalAlign` value. |
| | :param width: When given, use this width instead of looking at the children. |
| | :param height: When given, use this height instead of looking at the children. |
| | :param z_index: (int or None) When specified, this can be used to bring |
| | element in front of floating elements. `None` means: inherit from parent. |
| | :param style: A style string. |
| | :param modal: ``True`` or ``False``. |
| | :param key_bindings: ``None`` or a :class:`.KeyBindings` object. |
| | |
| | :param padding: (`Dimension` or int), size to be used for the padding. |
| | :param padding_char: Character to be used for filling in the padding. |
| | :param padding_style: Style to applied to the padding. |
| | """ |
| |
|
| | def __init__( |
| | self, |
| | children: Sequence[AnyContainer], |
| | window_too_small: Container | None = None, |
| | align: HorizontalAlign = HorizontalAlign.JUSTIFY, |
| | padding: AnyDimension = 0, |
| | padding_char: str | None = None, |
| | padding_style: str = "", |
| | width: AnyDimension = None, |
| | height: AnyDimension = None, |
| | z_index: int | None = None, |
| | modal: bool = False, |
| | key_bindings: KeyBindingsBase | None = None, |
| | style: str | Callable[[], str] = "", |
| | ) -> None: |
| | super().__init__( |
| | children=children, |
| | window_too_small=window_too_small, |
| | padding=padding, |
| | padding_char=padding_char, |
| | padding_style=padding_style, |
| | width=width, |
| | height=height, |
| | z_index=z_index, |
| | modal=modal, |
| | key_bindings=key_bindings, |
| | style=style, |
| | ) |
| |
|
| | self.align = align |
| |
|
| | self._children_cache: SimpleCache[tuple[Container, ...], list[Container]] = ( |
| | SimpleCache(maxsize=1) |
| | ) |
| | self._remaining_space_window = Window() |
| |
|
| | def preferred_width(self, max_available_width: int) -> Dimension: |
| | if self.width is not None: |
| | return to_dimension(self.width) |
| |
|
| | dimensions = [ |
| | c.preferred_width(max_available_width) for c in self._all_children |
| | ] |
| |
|
| | return sum_layout_dimensions(dimensions) |
| |
|
| | def preferred_height(self, width: int, max_available_height: int) -> Dimension: |
| | if self.height is not None: |
| | return to_dimension(self.height) |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | sizes = self._divide_widths(width) |
| | children = self._all_children |
| |
|
| | if sizes is None: |
| | return Dimension() |
| | else: |
| | dimensions = [ |
| | c.preferred_height(s, max_available_height) |
| | for s, c in zip(sizes, children) |
| | ] |
| | return max_layout_dimensions(dimensions) |
| |
|
| | def reset(self) -> None: |
| | for c in self.children: |
| | c.reset() |
| |
|
| | @property |
| | def _all_children(self) -> list[Container]: |
| | """ |
| | List of child objects, including padding. |
| | """ |
| |
|
| | def get() -> list[Container]: |
| | result: list[Container] = [] |
| |
|
| | |
| | if self.align in (HorizontalAlign.CENTER, HorizontalAlign.RIGHT): |
| | result.append(Window(width=Dimension(preferred=0))) |
| |
|
| | |
| | for child in self.children: |
| | result.append(child) |
| | result.append( |
| | Window( |
| | width=self.padding, |
| | char=self.padding_char, |
| | style=self.padding_style, |
| | ) |
| | ) |
| | if result: |
| | result.pop() |
| |
|
| | |
| | if self.align in (HorizontalAlign.CENTER, HorizontalAlign.LEFT): |
| | result.append(Window(width=Dimension(preferred=0))) |
| |
|
| | return result |
| |
|
| | return self._children_cache.get(tuple(self.children), get) |
| |
|
| | def _divide_widths(self, width: int) -> list[int] | None: |
| | """ |
| | Return the widths for all columns. |
| | Or None when there is not enough space. |
| | """ |
| | children = self._all_children |
| |
|
| | if not children: |
| | return [] |
| |
|
| | |
| | dimensions = [c.preferred_width(width) for c in children] |
| | preferred_dimensions = [d.preferred for d in dimensions] |
| |
|
| | |
| | sum_dimensions = sum_layout_dimensions(dimensions) |
| |
|
| | |
| | |
| | if sum_dimensions.min > width: |
| | return None |
| |
|
| | |
| | |
| | sizes = [d.min for d in dimensions] |
| |
|
| | child_generator = take_using_weights( |
| | items=list(range(len(dimensions))), weights=[d.weight for d in dimensions] |
| | ) |
| |
|
| | i = next(child_generator) |
| |
|
| | |
| | preferred_stop = min(width, sum_dimensions.preferred) |
| |
|
| | while sum(sizes) < preferred_stop: |
| | if sizes[i] < preferred_dimensions[i]: |
| | sizes[i] += 1 |
| | i = next(child_generator) |
| |
|
| | |
| | max_dimensions = [d.max for d in dimensions] |
| | max_stop = min(width, sum_dimensions.max) |
| |
|
| | while sum(sizes) < max_stop: |
| | if sizes[i] < max_dimensions[i]: |
| | sizes[i] += 1 |
| | i = next(child_generator) |
| |
|
| | return sizes |
| |
|
| | def write_to_screen( |
| | self, |
| | screen: Screen, |
| | mouse_handlers: MouseHandlers, |
| | write_position: WritePosition, |
| | parent_style: str, |
| | erase_bg: bool, |
| | z_index: int | None, |
| | ) -> None: |
| | """ |
| | Render the prompt to a `Screen` instance. |
| | |
| | :param screen: The :class:`~prompt_toolkit.layout.screen.Screen` class |
| | to which the output has to be written. |
| | """ |
| | if not self.children: |
| | return |
| |
|
| | children = self._all_children |
| | sizes = self._divide_widths(write_position.width) |
| | style = parent_style + " " + to_str(self.style) |
| | z_index = z_index if self.z_index is None else self.z_index |
| |
|
| | |
| | if sizes is None: |
| | self.window_too_small.write_to_screen( |
| | screen, mouse_handlers, write_position, style, erase_bg, z_index |
| | ) |
| | return |
| |
|
| | |
| | |
| | heights = [ |
| | child.preferred_height(width, write_position.height).preferred |
| | for width, child in zip(sizes, children) |
| | ] |
| | height = max(write_position.height, min(write_position.height, max(heights))) |
| |
|
| | |
| | ypos = write_position.ypos |
| | xpos = write_position.xpos |
| |
|
| | |
| | for s, c in zip(sizes, children): |
| | c.write_to_screen( |
| | screen, |
| | mouse_handlers, |
| | WritePosition(xpos, ypos, s, height), |
| | style, |
| | erase_bg, |
| | z_index, |
| | ) |
| | xpos += s |
| |
|
| | |
| | |
| | |
| | |
| | |
| | remaining_width = write_position.xpos + write_position.width - xpos |
| | if remaining_width > 0: |
| | self._remaining_space_window.write_to_screen( |
| | screen, |
| | mouse_handlers, |
| | WritePosition(xpos, ypos, remaining_width, height), |
| | style, |
| | erase_bg, |
| | z_index, |
| | ) |
| |
|
| |
|
| | class FloatContainer(Container): |
| | """ |
| | Container which can contain another container for the background, as well |
| | as a list of floating containers on top of it. |
| | |
| | Example Usage:: |
| | |
| | FloatContainer(content=Window(...), |
| | floats=[ |
| | Float(xcursor=True, |
| | ycursor=True, |
| | content=CompletionsMenu(...)) |
| | ]) |
| | |
| | :param z_index: (int or None) When specified, this can be used to bring |
| | element in front of floating elements. `None` means: inherit from parent. |
| | This is the z_index for the whole `Float` container as a whole. |
| | """ |
| |
|
| | def __init__( |
| | self, |
| | content: AnyContainer, |
| | floats: list[Float], |
| | modal: bool = False, |
| | key_bindings: KeyBindingsBase | None = None, |
| | style: str | Callable[[], str] = "", |
| | z_index: int | None = None, |
| | ) -> None: |
| | self.content = to_container(content) |
| | self.floats = floats |
| |
|
| | self.modal = modal |
| | self.key_bindings = key_bindings |
| | self.style = style |
| | self.z_index = z_index |
| |
|
| | def reset(self) -> None: |
| | self.content.reset() |
| |
|
| | for f in self.floats: |
| | f.content.reset() |
| |
|
| | def preferred_width(self, max_available_width: int) -> Dimension: |
| | return self.content.preferred_width(max_available_width) |
| |
|
| | def preferred_height(self, width: int, max_available_height: int) -> Dimension: |
| | """ |
| | Return the preferred height of the float container. |
| | (We don't care about the height of the floats, they should always fit |
| | into the dimensions provided by the container.) |
| | """ |
| | return self.content.preferred_height(width, max_available_height) |
| |
|
| | def write_to_screen( |
| | self, |
| | screen: Screen, |
| | mouse_handlers: MouseHandlers, |
| | write_position: WritePosition, |
| | parent_style: str, |
| | erase_bg: bool, |
| | z_index: int | None, |
| | ) -> None: |
| | style = parent_style + " " + to_str(self.style) |
| | z_index = z_index if self.z_index is None else self.z_index |
| |
|
| | self.content.write_to_screen( |
| | screen, mouse_handlers, write_position, style, erase_bg, z_index |
| | ) |
| |
|
| | for number, fl in enumerate(self.floats): |
| | |
| | |
| | new_z_index = (z_index or 0) + fl.z_index |
| | style = parent_style + " " + to_str(self.style) |
| |
|
| | |
| | |
| | |
| | |
| | |
| | postpone = fl.xcursor is not None or fl.ycursor is not None |
| |
|
| | if postpone: |
| | new_z_index = ( |
| | number + 10**8 |
| | ) |
| | screen.draw_with_z_index( |
| | z_index=new_z_index, |
| | draw_func=partial( |
| | self._draw_float, |
| | fl, |
| | screen, |
| | mouse_handlers, |
| | write_position, |
| | style, |
| | erase_bg, |
| | new_z_index, |
| | ), |
| | ) |
| | else: |
| | self._draw_float( |
| | fl, |
| | screen, |
| | mouse_handlers, |
| | write_position, |
| | style, |
| | erase_bg, |
| | new_z_index, |
| | ) |
| |
|
| | def _draw_float( |
| | self, |
| | fl: Float, |
| | screen: Screen, |
| | mouse_handlers: MouseHandlers, |
| | write_position: WritePosition, |
| | style: str, |
| | erase_bg: bool, |
| | z_index: int | None, |
| | ) -> None: |
| | "Draw a single Float." |
| | |
| | |
| | |
| | |
| | |
| | cpos = screen.get_menu_position( |
| | fl.attach_to_window or get_app().layout.current_window |
| | ) |
| | cursor_position = Point( |
| | x=cpos.x - write_position.xpos, y=cpos.y - write_position.ypos |
| | ) |
| |
|
| | fl_width = fl.get_width() |
| | fl_height = fl.get_height() |
| | width: int |
| | height: int |
| | xpos: int |
| | ypos: int |
| |
|
| | |
| | if fl.left is not None and fl_width is not None: |
| | xpos = fl.left |
| | width = fl_width |
| | |
| | elif fl.left is not None and fl.right is not None: |
| | xpos = fl.left |
| | width = write_position.width - fl.left - fl.right |
| | |
| | elif fl_width is not None and fl.right is not None: |
| | xpos = write_position.width - fl.right - fl_width |
| | width = fl_width |
| | |
| | elif fl.xcursor: |
| | if fl_width is None: |
| | width = fl.content.preferred_width(write_position.width).preferred |
| | width = min(write_position.width, width) |
| | else: |
| | width = fl_width |
| |
|
| | xpos = cursor_position.x |
| | if xpos + width > write_position.width: |
| | xpos = max(0, write_position.width - width) |
| | |
| | elif fl_width: |
| | xpos = int((write_position.width - fl_width) / 2) |
| | width = fl_width |
| | |
| | else: |
| | width = fl.content.preferred_width(write_position.width).preferred |
| |
|
| | if fl.left is not None: |
| | xpos = fl.left |
| | elif fl.right is not None: |
| | xpos = max(0, write_position.width - width - fl.right) |
| | else: |
| | xpos = max(0, int((write_position.width - width) / 2)) |
| |
|
| | |
| | width = min(width, write_position.width - xpos) |
| |
|
| | |
| | if fl.top is not None and fl_height is not None: |
| | ypos = fl.top |
| | height = fl_height |
| | |
| | elif fl.top is not None and fl.bottom is not None: |
| | ypos = fl.top |
| | height = write_position.height - fl.top - fl.bottom |
| | |
| | elif fl_height is not None and fl.bottom is not None: |
| | ypos = write_position.height - fl_height - fl.bottom |
| | height = fl_height |
| | |
| | elif fl.ycursor: |
| | ypos = cursor_position.y + (0 if fl.allow_cover_cursor else 1) |
| |
|
| | if fl_height is None: |
| | height = fl.content.preferred_height( |
| | width, write_position.height |
| | ).preferred |
| | else: |
| | height = fl_height |
| |
|
| | |
| | |
| | if height > write_position.height - ypos: |
| | if write_position.height - ypos + 1 >= ypos: |
| | |
| | |
| | height = write_position.height - ypos |
| | else: |
| | |
| | height = min(height, cursor_position.y) |
| | ypos = cursor_position.y - height |
| |
|
| | |
| | elif fl_height: |
| | ypos = int((write_position.height - fl_height) / 2) |
| | height = fl_height |
| | |
| | else: |
| | height = fl.content.preferred_height(width, write_position.height).preferred |
| |
|
| | if fl.top is not None: |
| | ypos = fl.top |
| | elif fl.bottom is not None: |
| | ypos = max(0, write_position.height - height - fl.bottom) |
| | else: |
| | ypos = max(0, int((write_position.height - height) / 2)) |
| |
|
| | |
| | height = min(height, write_position.height - ypos) |
| |
|
| | |
| | |
| | if height > 0 and width > 0: |
| | wp = WritePosition( |
| | xpos=xpos + write_position.xpos, |
| | ypos=ypos + write_position.ypos, |
| | width=width, |
| | height=height, |
| | ) |
| |
|
| | if not fl.hide_when_covering_content or self._area_is_empty(screen, wp): |
| | fl.content.write_to_screen( |
| | screen, |
| | mouse_handlers, |
| | wp, |
| | style, |
| | erase_bg=not fl.transparent(), |
| | z_index=z_index, |
| | ) |
| |
|
| | def _area_is_empty(self, screen: Screen, write_position: WritePosition) -> bool: |
| | """ |
| | Return True when the area below the write position is still empty. |
| | (For floats that should not hide content underneath.) |
| | """ |
| | wp = write_position |
| |
|
| | for y in range(wp.ypos, wp.ypos + wp.height): |
| | if y in screen.data_buffer: |
| | row = screen.data_buffer[y] |
| |
|
| | for x in range(wp.xpos, wp.xpos + wp.width): |
| | c = row[x] |
| | if c.char != " ": |
| | return False |
| |
|
| | return True |
| |
|
| | def is_modal(self) -> bool: |
| | return self.modal |
| |
|
| | def get_key_bindings(self) -> KeyBindingsBase | None: |
| | return self.key_bindings |
| |
|
| | def get_children(self) -> list[Container]: |
| | children = [self.content] |
| | children.extend(f.content for f in self.floats) |
| | return children |
| |
|
| |
|
| | class Float: |
| | """ |
| | Float for use in a :class:`.FloatContainer`. |
| | Except for the `content` parameter, all other options are optional. |
| | |
| | :param content: :class:`.Container` instance. |
| | |
| | :param width: :class:`.Dimension` or callable which returns a :class:`.Dimension`. |
| | :param height: :class:`.Dimension` or callable which returns a :class:`.Dimension`. |
| | |
| | :param left: Distance to the left edge of the :class:`.FloatContainer`. |
| | :param right: Distance to the right edge of the :class:`.FloatContainer`. |
| | :param top: Distance to the top of the :class:`.FloatContainer`. |
| | :param bottom: Distance to the bottom of the :class:`.FloatContainer`. |
| | |
| | :param attach_to_window: Attach to the cursor from this window, instead of |
| | the current window. |
| | :param hide_when_covering_content: Hide the float when it covers content underneath. |
| | :param allow_cover_cursor: When `False`, make sure to display the float |
| | below the cursor. Not on top of the indicated position. |
| | :param z_index: Z-index position. For a Float, this needs to be at least |
| | one. It is relative to the z_index of the parent container. |
| | :param transparent: :class:`.Filter` indicating whether this float needs to be |
| | drawn transparently. |
| | """ |
| |
|
| | def __init__( |
| | self, |
| | content: AnyContainer, |
| | top: int | None = None, |
| | right: int | None = None, |
| | bottom: int | None = None, |
| | left: int | None = None, |
| | width: int | Callable[[], int] | None = None, |
| | height: int | Callable[[], int] | None = None, |
| | xcursor: bool = False, |
| | ycursor: bool = False, |
| | attach_to_window: AnyContainer | None = None, |
| | hide_when_covering_content: bool = False, |
| | allow_cover_cursor: bool = False, |
| | z_index: int = 1, |
| | transparent: bool = False, |
| | ) -> None: |
| | assert z_index >= 1 |
| |
|
| | self.left = left |
| | self.right = right |
| | self.top = top |
| | self.bottom = bottom |
| |
|
| | self.width = width |
| | self.height = height |
| |
|
| | self.xcursor = xcursor |
| | self.ycursor = ycursor |
| |
|
| | self.attach_to_window = ( |
| | to_window(attach_to_window) if attach_to_window else None |
| | ) |
| |
|
| | self.content = to_container(content) |
| | self.hide_when_covering_content = hide_when_covering_content |
| | self.allow_cover_cursor = allow_cover_cursor |
| | self.z_index = z_index |
| | self.transparent = to_filter(transparent) |
| |
|
| | def get_width(self) -> int | None: |
| | if callable(self.width): |
| | return self.width() |
| | return self.width |
| |
|
| | def get_height(self) -> int | None: |
| | if callable(self.height): |
| | return self.height() |
| | return self.height |
| |
|
| | def __repr__(self) -> str: |
| | return f"Float(content={self.content!r})" |
| |
|
| |
|
| | class WindowRenderInfo: |
| | """ |
| | Render information for the last render time of this control. |
| | It stores mapping information between the input buffers (in case of a |
| | :class:`~prompt_toolkit.layout.controls.BufferControl`) and the actual |
| | render position on the output screen. |
| | |
| | (Could be used for implementation of the Vi 'H' and 'L' key bindings as |
| | well as implementing mouse support.) |
| | |
| | :param ui_content: The original :class:`.UIContent` instance that contains |
| | the whole input, without clipping. (ui_content) |
| | :param horizontal_scroll: The horizontal scroll of the :class:`.Window` instance. |
| | :param vertical_scroll: The vertical scroll of the :class:`.Window` instance. |
| | :param window_width: The width of the window that displays the content, |
| | without the margins. |
| | :param window_height: The height of the window that displays the content. |
| | :param configured_scroll_offsets: The scroll offsets as configured for the |
| | :class:`Window` instance. |
| | :param visible_line_to_row_col: Mapping that maps the row numbers on the |
| | displayed screen (starting from zero for the first visible line) to |
| | (row, col) tuples pointing to the row and column of the :class:`.UIContent`. |
| | :param rowcol_to_yx: Mapping that maps (row, column) tuples representing |
| | coordinates of the :class:`UIContent` to (y, x) absolute coordinates at |
| | the rendered screen. |
| | """ |
| |
|
| | def __init__( |
| | self, |
| | window: Window, |
| | ui_content: UIContent, |
| | horizontal_scroll: int, |
| | vertical_scroll: int, |
| | window_width: int, |
| | window_height: int, |
| | configured_scroll_offsets: ScrollOffsets, |
| | visible_line_to_row_col: dict[int, tuple[int, int]], |
| | rowcol_to_yx: dict[tuple[int, int], tuple[int, int]], |
| | x_offset: int, |
| | y_offset: int, |
| | wrap_lines: bool, |
| | ) -> None: |
| | self.window = window |
| | self.ui_content = ui_content |
| | self.vertical_scroll = vertical_scroll |
| | self.window_width = window_width |
| | self.window_height = window_height |
| |
|
| | self.configured_scroll_offsets = configured_scroll_offsets |
| | self.visible_line_to_row_col = visible_line_to_row_col |
| | self.wrap_lines = wrap_lines |
| |
|
| | self._rowcol_to_yx = rowcol_to_yx |
| | |
| | self._x_offset = x_offset |
| | self._y_offset = y_offset |
| |
|
| | @property |
| | def visible_line_to_input_line(self) -> dict[int, int]: |
| | return { |
| | visible_line: rowcol[0] |
| | for visible_line, rowcol in self.visible_line_to_row_col.items() |
| | } |
| |
|
| | @property |
| | def cursor_position(self) -> Point: |
| | """ |
| | Return the cursor position coordinates, relative to the left/top corner |
| | of the rendered screen. |
| | """ |
| | cpos = self.ui_content.cursor_position |
| | try: |
| | y, x = self._rowcol_to_yx[cpos.y, cpos.x] |
| | except KeyError: |
| | |
| | |
| | return Point(x=0, y=0) |
| | else: |
| | return Point(x=x - self._x_offset, y=y - self._y_offset) |
| |
|
| | @property |
| | def applied_scroll_offsets(self) -> ScrollOffsets: |
| | """ |
| | Return a :class:`.ScrollOffsets` instance that indicates the actual |
| | offset. This can be less than or equal to what's configured. E.g, when |
| | the cursor is completely at the top, the top offset will be zero rather |
| | than what's configured. |
| | """ |
| | if self.displayed_lines[0] == 0: |
| | top = 0 |
| | else: |
| | |
| | y = self.input_line_to_visible_line[self.ui_content.cursor_position.y] |
| | top = min(y, self.configured_scroll_offsets.top) |
| |
|
| | return ScrollOffsets( |
| | top=top, |
| | bottom=min( |
| | self.ui_content.line_count - self.displayed_lines[-1] - 1, |
| | self.configured_scroll_offsets.bottom, |
| | ), |
| | |
| | |
| | |
| | left=0, |
| | right=0, |
| | ) |
| |
|
| | @property |
| | def displayed_lines(self) -> list[int]: |
| | """ |
| | List of all the visible rows. (Line numbers of the input buffer.) |
| | The last line may not be entirely visible. |
| | """ |
| | return sorted(row for row, col in self.visible_line_to_row_col.values()) |
| |
|
| | @property |
| | def input_line_to_visible_line(self) -> dict[int, int]: |
| | """ |
| | Return the dictionary mapping the line numbers of the input buffer to |
| | the lines of the screen. When a line spans several rows at the screen, |
| | the first row appears in the dictionary. |
| | """ |
| | result: dict[int, int] = {} |
| | for k, v in self.visible_line_to_input_line.items(): |
| | if v in result: |
| | result[v] = min(result[v], k) |
| | else: |
| | result[v] = k |
| | return result |
| |
|
| | def first_visible_line(self, after_scroll_offset: bool = False) -> int: |
| | """ |
| | Return the line number (0 based) of the input document that corresponds |
| | with the first visible line. |
| | """ |
| | if after_scroll_offset: |
| | return self.displayed_lines[self.applied_scroll_offsets.top] |
| | else: |
| | return self.displayed_lines[0] |
| |
|
| | def last_visible_line(self, before_scroll_offset: bool = False) -> int: |
| | """ |
| | Like `first_visible_line`, but for the last visible line. |
| | """ |
| | if before_scroll_offset: |
| | return self.displayed_lines[-1 - self.applied_scroll_offsets.bottom] |
| | else: |
| | return self.displayed_lines[-1] |
| |
|
| | def center_visible_line( |
| | self, before_scroll_offset: bool = False, after_scroll_offset: bool = False |
| | ) -> int: |
| | """ |
| | Like `first_visible_line`, but for the center visible line. |
| | """ |
| | return ( |
| | self.first_visible_line(after_scroll_offset) |
| | + ( |
| | self.last_visible_line(before_scroll_offset) |
| | - self.first_visible_line(after_scroll_offset) |
| | ) |
| | // 2 |
| | ) |
| |
|
| | @property |
| | def content_height(self) -> int: |
| | """ |
| | The full height of the user control. |
| | """ |
| | return self.ui_content.line_count |
| |
|
| | @property |
| | def full_height_visible(self) -> bool: |
| | """ |
| | True when the full height is visible (There is no vertical scroll.) |
| | """ |
| | return ( |
| | self.vertical_scroll == 0 |
| | and self.last_visible_line() == self.content_height |
| | ) |
| |
|
| | @property |
| | def top_visible(self) -> bool: |
| | """ |
| | True when the top of the buffer is visible. |
| | """ |
| | return self.vertical_scroll == 0 |
| |
|
| | @property |
| | def bottom_visible(self) -> bool: |
| | """ |
| | True when the bottom of the buffer is visible. |
| | """ |
| | return self.last_visible_line() == self.content_height - 1 |
| |
|
| | @property |
| | def vertical_scroll_percentage(self) -> int: |
| | """ |
| | Vertical scroll as a percentage. (0 means: the top is visible, |
| | 100 means: the bottom is visible.) |
| | """ |
| | if self.bottom_visible: |
| | return 100 |
| | else: |
| | return 100 * self.vertical_scroll // self.content_height |
| |
|
| | def get_height_for_line(self, lineno: int) -> int: |
| | """ |
| | Return the height of the given line. |
| | (The height that it would take, if this line became visible.) |
| | """ |
| | if self.wrap_lines: |
| | return self.ui_content.get_height_for_line( |
| | lineno, self.window_width, self.window.get_line_prefix |
| | ) |
| | else: |
| | return 1 |
| |
|
| |
|
| | class ScrollOffsets: |
| | """ |
| | Scroll offsets for the :class:`.Window` class. |
| | |
| | Note that left/right offsets only make sense if line wrapping is disabled. |
| | """ |
| |
|
| | def __init__( |
| | self, |
| | top: int | Callable[[], int] = 0, |
| | bottom: int | Callable[[], int] = 0, |
| | left: int | Callable[[], int] = 0, |
| | right: int | Callable[[], int] = 0, |
| | ) -> None: |
| | self._top = top |
| | self._bottom = bottom |
| | self._left = left |
| | self._right = right |
| |
|
| | @property |
| | def top(self) -> int: |
| | return to_int(self._top) |
| |
|
| | @property |
| | def bottom(self) -> int: |
| | return to_int(self._bottom) |
| |
|
| | @property |
| | def left(self) -> int: |
| | return to_int(self._left) |
| |
|
| | @property |
| | def right(self) -> int: |
| | return to_int(self._right) |
| |
|
| | def __repr__(self) -> str: |
| | return f"ScrollOffsets(top={self._top!r}, bottom={self._bottom!r}, left={self._left!r}, right={self._right!r})" |
| |
|
| |
|
| | class ColorColumn: |
| | """ |
| | Column for a :class:`.Window` to be colored. |
| | """ |
| |
|
| | def __init__(self, position: int, style: str = "class:color-column") -> None: |
| | self.position = position |
| | self.style = style |
| |
|
| |
|
| | _in_insert_mode = vi_insert_mode | emacs_insert_mode |
| |
|
| |
|
| | class WindowAlign(Enum): |
| | """ |
| | Alignment of the Window content. |
| | |
| | Note that this is different from `HorizontalAlign` and `VerticalAlign`, |
| | which are used for the alignment of the child containers in respectively |
| | `VSplit` and `HSplit`. |
| | """ |
| |
|
| | LEFT = "LEFT" |
| | RIGHT = "RIGHT" |
| | CENTER = "CENTER" |
| |
|
| |
|
| | class Window(Container): |
| | """ |
| | Container that holds a control. |
| | |
| | :param content: :class:`.UIControl` instance. |
| | :param width: :class:`.Dimension` instance or callable. |
| | :param height: :class:`.Dimension` instance or callable. |
| | :param z_index: When specified, this can be used to bring element in front |
| | of floating elements. |
| | :param dont_extend_width: When `True`, don't take up more width then the |
| | preferred width reported by the control. |
| | :param dont_extend_height: When `True`, don't take up more width then the |
| | preferred height reported by the control. |
| | :param ignore_content_width: A `bool` or :class:`.Filter` instance. Ignore |
| | the :class:`.UIContent` width when calculating the dimensions. |
| | :param ignore_content_height: A `bool` or :class:`.Filter` instance. Ignore |
| | the :class:`.UIContent` height when calculating the dimensions. |
| | :param left_margins: A list of :class:`.Margin` instance to be displayed on |
| | the left. For instance: :class:`~prompt_toolkit.layout.NumberedMargin` |
| | can be one of them in order to show line numbers. |
| | :param right_margins: Like `left_margins`, but on the other side. |
| | :param scroll_offsets: :class:`.ScrollOffsets` instance, representing the |
| | preferred amount of lines/columns to be always visible before/after the |
| | cursor. When both top and bottom are a very high number, the cursor |
| | will be centered vertically most of the time. |
| | :param allow_scroll_beyond_bottom: A `bool` or |
| | :class:`.Filter` instance. When True, allow scrolling so far, that the |
| | top part of the content is not visible anymore, while there is still |
| | empty space available at the bottom of the window. In the Vi editor for |
| | instance, this is possible. You will see tildes while the top part of |
| | the body is hidden. |
| | :param wrap_lines: A `bool` or :class:`.Filter` instance. When True, don't |
| | scroll horizontally, but wrap lines instead. |
| | :param get_vertical_scroll: Callable that takes this window |
| | instance as input and returns a preferred vertical scroll. |
| | (When this is `None`, the scroll is only determined by the last and |
| | current cursor position.) |
| | :param get_horizontal_scroll: Callable that takes this window |
| | instance as input and returns a preferred vertical scroll. |
| | :param always_hide_cursor: A `bool` or |
| | :class:`.Filter` instance. When True, never display the cursor, even |
| | when the user control specifies a cursor position. |
| | :param cursorline: A `bool` or :class:`.Filter` instance. When True, |
| | display a cursorline. |
| | :param cursorcolumn: A `bool` or :class:`.Filter` instance. When True, |
| | display a cursorcolumn. |
| | :param colorcolumns: A list of :class:`.ColorColumn` instances that |
| | describe the columns to be highlighted, or a callable that returns such |
| | a list. |
| | :param align: :class:`.WindowAlign` value or callable that returns an |
| | :class:`.WindowAlign` value. alignment of content. |
| | :param style: A style string. Style to be applied to all the cells in this |
| | window. (This can be a callable that returns a string.) |
| | :param char: (string) Character to be used for filling the background. This |
| | can also be a callable that returns a character. |
| | :param get_line_prefix: None or a callable that returns formatted text to |
| | be inserted before a line. It takes a line number (int) and a |
| | wrap_count and returns formatted text. This can be used for |
| | implementation of line continuations, things like Vim "breakindent" and |
| | so on. |
| | """ |
| |
|
| | def __init__( |
| | self, |
| | content: UIControl | None = None, |
| | width: AnyDimension = None, |
| | height: AnyDimension = None, |
| | z_index: int | None = None, |
| | dont_extend_width: FilterOrBool = False, |
| | dont_extend_height: FilterOrBool = False, |
| | ignore_content_width: FilterOrBool = False, |
| | ignore_content_height: FilterOrBool = False, |
| | left_margins: Sequence[Margin] | None = None, |
| | right_margins: Sequence[Margin] | None = None, |
| | scroll_offsets: ScrollOffsets | None = None, |
| | allow_scroll_beyond_bottom: FilterOrBool = False, |
| | wrap_lines: FilterOrBool = False, |
| | get_vertical_scroll: Callable[[Window], int] | None = None, |
| | get_horizontal_scroll: Callable[[Window], int] | None = None, |
| | always_hide_cursor: FilterOrBool = False, |
| | cursorline: FilterOrBool = False, |
| | cursorcolumn: FilterOrBool = False, |
| | colorcolumns: ( |
| | None | list[ColorColumn] | Callable[[], list[ColorColumn]] |
| | ) = None, |
| | align: WindowAlign | Callable[[], WindowAlign] = WindowAlign.LEFT, |
| | style: str | Callable[[], str] = "", |
| | char: None | str | Callable[[], str] = None, |
| | get_line_prefix: GetLinePrefixCallable | None = None, |
| | ) -> None: |
| | self.allow_scroll_beyond_bottom = to_filter(allow_scroll_beyond_bottom) |
| | self.always_hide_cursor = to_filter(always_hide_cursor) |
| | self.wrap_lines = to_filter(wrap_lines) |
| | self.cursorline = to_filter(cursorline) |
| | self.cursorcolumn = to_filter(cursorcolumn) |
| |
|
| | self.content = content or DummyControl() |
| | self.dont_extend_width = to_filter(dont_extend_width) |
| | self.dont_extend_height = to_filter(dont_extend_height) |
| | self.ignore_content_width = to_filter(ignore_content_width) |
| | self.ignore_content_height = to_filter(ignore_content_height) |
| | self.left_margins = left_margins or [] |
| | self.right_margins = right_margins or [] |
| | self.scroll_offsets = scroll_offsets or ScrollOffsets() |
| | self.get_vertical_scroll = get_vertical_scroll |
| | self.get_horizontal_scroll = get_horizontal_scroll |
| | self.colorcolumns = colorcolumns or [] |
| | self.align = align |
| | self.style = style |
| | self.char = char |
| | self.get_line_prefix = get_line_prefix |
| |
|
| | self.width = width |
| | self.height = height |
| | self.z_index = z_index |
| |
|
| | |
| | self._ui_content_cache: SimpleCache[tuple[int, int, int], UIContent] = ( |
| | SimpleCache(maxsize=8) |
| | ) |
| | self._margin_width_cache: SimpleCache[tuple[Margin, int], int] = SimpleCache( |
| | maxsize=1 |
| | ) |
| |
|
| | self.reset() |
| |
|
| | def __repr__(self) -> str: |
| | return f"Window(content={self.content!r})" |
| |
|
| | def reset(self) -> None: |
| | self.content.reset() |
| |
|
| | |
| | self.vertical_scroll = 0 |
| | self.horizontal_scroll = 0 |
| |
|
| | |
| | |
| | |
| | self.vertical_scroll_2 = 0 |
| |
|
| | |
| | |
| | self.render_info: WindowRenderInfo | None = None |
| |
|
| | def _get_margin_width(self, margin: Margin) -> int: |
| | """ |
| | Return the width for this margin. |
| | (Calculate only once per render time.) |
| | """ |
| |
|
| | |
| | def get_ui_content() -> UIContent: |
| | return self._get_ui_content(width=0, height=0) |
| |
|
| | def get_width() -> int: |
| | return margin.get_width(get_ui_content) |
| |
|
| | key = (margin, get_app().render_counter) |
| | return self._margin_width_cache.get(key, get_width) |
| |
|
| | def _get_total_margin_width(self) -> int: |
| | """ |
| | Calculate and return the width of the margin (left + right). |
| | """ |
| | return sum(self._get_margin_width(m) for m in self.left_margins) + sum( |
| | self._get_margin_width(m) for m in self.right_margins |
| | ) |
| |
|
| | def preferred_width(self, max_available_width: int) -> Dimension: |
| | """ |
| | Calculate the preferred width for this window. |
| | """ |
| |
|
| | def preferred_content_width() -> int | None: |
| | """Content width: is only calculated if no exact width for the |
| | window was given.""" |
| | if self.ignore_content_width(): |
| | return None |
| |
|
| | |
| | total_margin_width = self._get_total_margin_width() |
| |
|
| | |
| | preferred_width = self.content.preferred_width( |
| | max_available_width - total_margin_width |
| | ) |
| |
|
| | if preferred_width is not None: |
| | |
| | preferred_width += total_margin_width |
| | return preferred_width |
| |
|
| | |
| | return self._merge_dimensions( |
| | dimension=to_dimension(self.width), |
| | get_preferred=preferred_content_width, |
| | dont_extend=self.dont_extend_width(), |
| | ) |
| |
|
| | def preferred_height(self, width: int, max_available_height: int) -> Dimension: |
| | """ |
| | Calculate the preferred height for this window. |
| | """ |
| |
|
| | def preferred_content_height() -> int | None: |
| | """Content height: is only calculated if no exact height for the |
| | window was given.""" |
| | if self.ignore_content_height(): |
| | return None |
| |
|
| | total_margin_width = self._get_total_margin_width() |
| | wrap_lines = self.wrap_lines() |
| |
|
| | return self.content.preferred_height( |
| | width - total_margin_width, |
| | max_available_height, |
| | wrap_lines, |
| | self.get_line_prefix, |
| | ) |
| |
|
| | return self._merge_dimensions( |
| | dimension=to_dimension(self.height), |
| | get_preferred=preferred_content_height, |
| | dont_extend=self.dont_extend_height(), |
| | ) |
| |
|
| | @staticmethod |
| | def _merge_dimensions( |
| | dimension: Dimension | None, |
| | get_preferred: Callable[[], int | None], |
| | dont_extend: bool = False, |
| | ) -> Dimension: |
| | """ |
| | Take the Dimension from this `Window` class and the received preferred |
| | size from the `UIControl` and return a `Dimension` to report to the |
| | parent container. |
| | """ |
| | dimension = dimension or Dimension() |
| |
|
| | |
| | |
| | preferred: int | None |
| |
|
| | if dimension.preferred_specified: |
| | preferred = dimension.preferred |
| | else: |
| | |
| | |
| | preferred = get_preferred() |
| |
|
| | |
| | |
| | if preferred is not None: |
| | if dimension.max_specified: |
| | preferred = min(preferred, dimension.max) |
| |
|
| | if dimension.min_specified: |
| | preferred = max(preferred, dimension.min) |
| |
|
| | |
| | |
| | max_: int | None |
| | min_: int | None |
| |
|
| | if dont_extend and preferred is not None: |
| | max_ = min(dimension.max, preferred) |
| | else: |
| | max_ = dimension.max if dimension.max_specified else None |
| |
|
| | min_ = dimension.min if dimension.min_specified else None |
| |
|
| | return Dimension( |
| | min=min_, max=max_, preferred=preferred, weight=dimension.weight |
| | ) |
| |
|
| | def _get_ui_content(self, width: int, height: int) -> UIContent: |
| | """ |
| | Create a `UIContent` instance. |
| | """ |
| |
|
| | def get_content() -> UIContent: |
| | return self.content.create_content(width=width, height=height) |
| |
|
| | key = (get_app().render_counter, width, height) |
| | return self._ui_content_cache.get(key, get_content) |
| |
|
| | def _get_digraph_char(self) -> str | None: |
| | "Return `False`, or the Digraph symbol to be used." |
| | app = get_app() |
| | if app.quoted_insert: |
| | return "^" |
| | if app.vi_state.waiting_for_digraph: |
| | if app.vi_state.digraph_symbol1: |
| | return app.vi_state.digraph_symbol1 |
| | return "?" |
| | return None |
| |
|
| | def write_to_screen( |
| | self, |
| | screen: Screen, |
| | mouse_handlers: MouseHandlers, |
| | write_position: WritePosition, |
| | parent_style: str, |
| | erase_bg: bool, |
| | z_index: int | None, |
| | ) -> None: |
| | """ |
| | Write window to screen. This renders the user control, the margins and |
| | copies everything over to the absolute position at the given screen. |
| | """ |
| | |
| | |
| | |
| | |
| | write_position = WritePosition( |
| | xpos=write_position.xpos, |
| | ypos=write_position.ypos, |
| | width=write_position.width, |
| | height=write_position.height, |
| | ) |
| |
|
| | if self.dont_extend_width(): |
| | write_position.width = min( |
| | write_position.width, |
| | self.preferred_width(write_position.width).preferred, |
| | ) |
| |
|
| | if self.dont_extend_height(): |
| | write_position.height = min( |
| | write_position.height, |
| | self.preferred_height( |
| | write_position.width, write_position.height |
| | ).preferred, |
| | ) |
| |
|
| | |
| | z_index = z_index if self.z_index is None else self.z_index |
| |
|
| | draw_func = partial( |
| | self._write_to_screen_at_index, |
| | screen, |
| | mouse_handlers, |
| | write_position, |
| | parent_style, |
| | erase_bg, |
| | ) |
| |
|
| | if z_index is None or z_index <= 0: |
| | |
| | draw_func() |
| | else: |
| | |
| | screen.draw_with_z_index(z_index=z_index, draw_func=draw_func) |
| |
|
| | def _write_to_screen_at_index( |
| | self, |
| | screen: Screen, |
| | mouse_handlers: MouseHandlers, |
| | write_position: WritePosition, |
| | parent_style: str, |
| | erase_bg: bool, |
| | ) -> None: |
| | |
| | |
| | if write_position.height <= 0 or write_position.width <= 0: |
| | return |
| |
|
| | |
| | left_margin_widths = [self._get_margin_width(m) for m in self.left_margins] |
| | right_margin_widths = [self._get_margin_width(m) for m in self.right_margins] |
| | total_margin_width = sum(left_margin_widths + right_margin_widths) |
| |
|
| | |
| | ui_content = self.content.create_content( |
| | write_position.width - total_margin_width, write_position.height |
| | ) |
| | assert isinstance(ui_content, UIContent) |
| |
|
| | |
| | wrap_lines = self.wrap_lines() |
| | self._scroll( |
| | ui_content, write_position.width - total_margin_width, write_position.height |
| | ) |
| |
|
| | |
| | self._fill_bg(screen, write_position, erase_bg) |
| |
|
| | |
| | align = self.align() if callable(self.align) else self.align |
| |
|
| | |
| | visible_line_to_row_col, rowcol_to_yx = self._copy_body( |
| | ui_content, |
| | screen, |
| | write_position, |
| | sum(left_margin_widths), |
| | write_position.width - total_margin_width, |
| | self.vertical_scroll, |
| | self.horizontal_scroll, |
| | wrap_lines=wrap_lines, |
| | highlight_lines=True, |
| | vertical_scroll_2=self.vertical_scroll_2, |
| | always_hide_cursor=self.always_hide_cursor(), |
| | has_focus=get_app().layout.current_control == self.content, |
| | align=align, |
| | get_line_prefix=self.get_line_prefix, |
| | ) |
| |
|
| | |
| | x_offset = write_position.xpos + sum(left_margin_widths) |
| | y_offset = write_position.ypos |
| |
|
| | render_info = WindowRenderInfo( |
| | window=self, |
| | ui_content=ui_content, |
| | horizontal_scroll=self.horizontal_scroll, |
| | vertical_scroll=self.vertical_scroll, |
| | window_width=write_position.width - total_margin_width, |
| | window_height=write_position.height, |
| | configured_scroll_offsets=self.scroll_offsets, |
| | visible_line_to_row_col=visible_line_to_row_col, |
| | rowcol_to_yx=rowcol_to_yx, |
| | x_offset=x_offset, |
| | y_offset=y_offset, |
| | wrap_lines=wrap_lines, |
| | ) |
| | self.render_info = render_info |
| |
|
| | |
| | def mouse_handler(mouse_event: MouseEvent) -> NotImplementedOrNone: |
| | """ |
| | Wrapper around the mouse_handler of the `UIControl` that turns |
| | screen coordinates into line coordinates. |
| | Returns `NotImplemented` if no UI invalidation should be done. |
| | """ |
| | |
| | |
| | if self not in get_app().layout.walk_through_modal_area(): |
| | return NotImplemented |
| |
|
| | |
| | yx_to_rowcol = {v: k for k, v in rowcol_to_yx.items()} |
| | y = mouse_event.position.y |
| | x = mouse_event.position.x |
| |
|
| | |
| | |
| | max_y = write_position.ypos + len(visible_line_to_row_col) - 1 |
| | y = min(max_y, y) |
| | result: NotImplementedOrNone |
| |
|
| | while x >= 0: |
| | try: |
| | row, col = yx_to_rowcol[y, x] |
| | except KeyError: |
| | |
| | |
| | x -= 1 |
| | else: |
| | |
| | result = self.content.mouse_handler( |
| | MouseEvent( |
| | position=Point(x=col, y=row), |
| | event_type=mouse_event.event_type, |
| | button=mouse_event.button, |
| | modifiers=mouse_event.modifiers, |
| | ) |
| | ) |
| | break |
| | else: |
| | |
| | |
| | |
| | |
| | result = self.content.mouse_handler( |
| | MouseEvent( |
| | position=Point(x=0, y=0), |
| | event_type=mouse_event.event_type, |
| | button=mouse_event.button, |
| | modifiers=mouse_event.modifiers, |
| | ) |
| | ) |
| |
|
| | |
| | if result == NotImplemented: |
| | result = self._mouse_handler(mouse_event) |
| |
|
| | return result |
| |
|
| | mouse_handlers.set_mouse_handler_for_range( |
| | x_min=write_position.xpos + sum(left_margin_widths), |
| | x_max=write_position.xpos + write_position.width - total_margin_width, |
| | y_min=write_position.ypos, |
| | y_max=write_position.ypos + write_position.height, |
| | handler=mouse_handler, |
| | ) |
| |
|
| | |
| | move_x = 0 |
| |
|
| | def render_margin(m: Margin, width: int) -> UIContent: |
| | "Render margin. Return `Screen`." |
| | |
| | fragments = m.create_margin(render_info, width, write_position.height) |
| |
|
| | |
| | |
| | return FormattedTextControl(fragments).create_content( |
| | width + 1, write_position.height |
| | ) |
| |
|
| | for m, width in zip(self.left_margins, left_margin_widths): |
| | if width > 0: |
| | |
| | margin_content = render_margin(m, width) |
| |
|
| | |
| | self._copy_margin(margin_content, screen, write_position, move_x, width) |
| | move_x += width |
| |
|
| | move_x = write_position.width - sum(right_margin_widths) |
| |
|
| | for m, width in zip(self.right_margins, right_margin_widths): |
| | |
| | margin_content = render_margin(m, width) |
| |
|
| | |
| | self._copy_margin(margin_content, screen, write_position, move_x, width) |
| | move_x += width |
| |
|
| | |
| | self._apply_style(screen, write_position, parent_style) |
| |
|
| | |
| | |
| | screen.visible_windows_to_write_positions[self] = write_position |
| |
|
| | def _copy_body( |
| | self, |
| | ui_content: UIContent, |
| | new_screen: Screen, |
| | write_position: WritePosition, |
| | move_x: int, |
| | width: int, |
| | vertical_scroll: int = 0, |
| | horizontal_scroll: int = 0, |
| | wrap_lines: bool = False, |
| | highlight_lines: bool = False, |
| | vertical_scroll_2: int = 0, |
| | always_hide_cursor: bool = False, |
| | has_focus: bool = False, |
| | align: WindowAlign = WindowAlign.LEFT, |
| | get_line_prefix: Callable[[int, int], AnyFormattedText] | None = None, |
| | ) -> tuple[dict[int, tuple[int, int]], dict[tuple[int, int], tuple[int, int]]]: |
| | """ |
| | Copy the UIContent into the output screen. |
| | Return (visible_line_to_row_col, rowcol_to_yx) tuple. |
| | |
| | :param get_line_prefix: None or a callable that takes a line number |
| | (int) and a wrap_count (int) and returns formatted text. |
| | """ |
| | xpos = write_position.xpos + move_x |
| | ypos = write_position.ypos |
| | line_count = ui_content.line_count |
| | new_buffer = new_screen.data_buffer |
| | empty_char = _CHAR_CACHE["", ""] |
| |
|
| | |
| | |
| | visible_line_to_row_col: dict[int, tuple[int, int]] = {} |
| |
|
| | |
| | rowcol_to_yx: dict[tuple[int, int], tuple[int, int]] = {} |
| |
|
| | def copy_line( |
| | line: StyleAndTextTuples, |
| | lineno: int, |
| | x: int, |
| | y: int, |
| | is_input: bool = False, |
| | ) -> tuple[int, int]: |
| | """ |
| | Copy over a single line to the output screen. This can wrap over |
| | multiple lines in the output. It will call the prefix (prompt) |
| | function before every line. |
| | """ |
| | if is_input: |
| | current_rowcol_to_yx = rowcol_to_yx |
| | else: |
| | current_rowcol_to_yx = {} |
| |
|
| | |
| | if is_input and get_line_prefix: |
| | prompt = to_formatted_text(get_line_prefix(lineno, 0)) |
| | x, y = copy_line(prompt, lineno, x, y, is_input=False) |
| |
|
| | |
| | skipped = 0 |
| | if horizontal_scroll and is_input: |
| | h_scroll = horizontal_scroll |
| | line = explode_text_fragments(line) |
| | while h_scroll > 0 and line: |
| | h_scroll -= get_cwidth(line[0][1]) |
| | skipped += 1 |
| | del line[:1] |
| |
|
| | x -= h_scroll |
| | |
| |
|
| | |
| | |
| | if align == WindowAlign.CENTER: |
| | line_width = fragment_list_width(line) |
| | if line_width < width: |
| | x += (width - line_width) // 2 |
| | elif align == WindowAlign.RIGHT: |
| | line_width = fragment_list_width(line) |
| | if line_width < width: |
| | x += width - line_width |
| |
|
| | col = 0 |
| | wrap_count = 0 |
| | for style, text, *_ in line: |
| | new_buffer_row = new_buffer[y + ypos] |
| |
|
| | |
| | |
| | if "[ZeroWidthEscape]" in style: |
| | new_screen.zero_width_escapes[y + ypos][x + xpos] += text |
| | continue |
| |
|
| | for c in text: |
| | char = _CHAR_CACHE[c, style] |
| | char_width = char.width |
| |
|
| | |
| | if wrap_lines and x + char_width > width: |
| | visible_line_to_row_col[y + 1] = ( |
| | lineno, |
| | visible_line_to_row_col[y][1] + x, |
| | ) |
| | y += 1 |
| | wrap_count += 1 |
| | x = 0 |
| |
|
| | |
| | if is_input and get_line_prefix: |
| | prompt = to_formatted_text( |
| | get_line_prefix(lineno, wrap_count) |
| | ) |
| | x, y = copy_line(prompt, lineno, x, y, is_input=False) |
| |
|
| | new_buffer_row = new_buffer[y + ypos] |
| |
|
| | if y >= write_position.height: |
| | return x, y |
| |
|
| | |
| | if x >= 0 and y >= 0 and x < width: |
| | new_buffer_row[x + xpos] = char |
| |
|
| | |
| | |
| | |
| | |
| | if char_width > 1: |
| | for i in range(1, char_width): |
| | new_buffer_row[x + xpos + i] = empty_char |
| |
|
| | |
| | |
| | |
| | |
| | elif char_width == 0: |
| | |
| | |
| | |
| | for pw in [2, 1]: |
| | if ( |
| | x - pw >= 0 |
| | and new_buffer_row[x + xpos - pw].width == pw |
| | ): |
| | prev_char = new_buffer_row[x + xpos - pw] |
| | char2 = _CHAR_CACHE[ |
| | prev_char.char + c, prev_char.style |
| | ] |
| | new_buffer_row[x + xpos - pw] = char2 |
| |
|
| | |
| | current_rowcol_to_yx[lineno, col + skipped] = ( |
| | y + ypos, |
| | x + xpos, |
| | ) |
| |
|
| | col += 1 |
| | x += char_width |
| | return x, y |
| |
|
| | |
| | def copy() -> int: |
| | y = -vertical_scroll_2 |
| | lineno = vertical_scroll |
| |
|
| | while y < write_position.height and lineno < line_count: |
| | |
| | line = ui_content.get_line(lineno) |
| |
|
| | visible_line_to_row_col[y] = (lineno, horizontal_scroll) |
| |
|
| | |
| | x = 0 |
| | x, y = copy_line(line, lineno, x, y, is_input=True) |
| |
|
| | lineno += 1 |
| | y += 1 |
| | return y |
| |
|
| | copy() |
| |
|
| | def cursor_pos_to_screen_pos(row: int, col: int) -> Point: |
| | "Translate row/col from UIContent to real Screen coordinates." |
| | try: |
| | y, x = rowcol_to_yx[row, col] |
| | except KeyError: |
| | |
| | |
| | return Point(x=0, y=0) |
| |
|
| | |
| | |
| | |
| | |
| | else: |
| | return Point(x=x, y=y) |
| |
|
| | |
| | if ui_content.cursor_position: |
| | screen_cursor_position = cursor_pos_to_screen_pos( |
| | ui_content.cursor_position.y, ui_content.cursor_position.x |
| | ) |
| |
|
| | if has_focus: |
| | new_screen.set_cursor_position(self, screen_cursor_position) |
| |
|
| | if always_hide_cursor: |
| | new_screen.show_cursor = False |
| | else: |
| | new_screen.show_cursor = ui_content.show_cursor |
| |
|
| | self._highlight_digraph(new_screen) |
| |
|
| | if highlight_lines: |
| | self._highlight_cursorlines( |
| | new_screen, |
| | screen_cursor_position, |
| | xpos, |
| | ypos, |
| | width, |
| | write_position.height, |
| | ) |
| |
|
| | |
| | if has_focus and ui_content.cursor_position: |
| | self._show_key_processor_key_buffer(new_screen) |
| |
|
| | |
| | if ui_content.menu_position: |
| | new_screen.set_menu_position( |
| | self, |
| | cursor_pos_to_screen_pos( |
| | ui_content.menu_position.y, ui_content.menu_position.x |
| | ), |
| | ) |
| |
|
| | |
| | new_screen.height = max(new_screen.height, ypos + write_position.height) |
| |
|
| | return visible_line_to_row_col, rowcol_to_yx |
| |
|
| | def _fill_bg( |
| | self, screen: Screen, write_position: WritePosition, erase_bg: bool |
| | ) -> None: |
| | """ |
| | Erase/fill the background. |
| | (Useful for floats and when a `char` has been given.) |
| | """ |
| | char: str | None |
| | if callable(self.char): |
| | char = self.char() |
| | else: |
| | char = self.char |
| |
|
| | if erase_bg or char: |
| | wp = write_position |
| | char_obj = _CHAR_CACHE[char or " ", ""] |
| |
|
| | for y in range(wp.ypos, wp.ypos + wp.height): |
| | row = screen.data_buffer[y] |
| | for x in range(wp.xpos, wp.xpos + wp.width): |
| | row[x] = char_obj |
| |
|
| | def _apply_style( |
| | self, new_screen: Screen, write_position: WritePosition, parent_style: str |
| | ) -> None: |
| | |
| | style = parent_style + " " + to_str(self.style) |
| |
|
| | new_screen.fill_area(write_position, style=style, after=False) |
| |
|
| | |
| | |
| | wp = WritePosition( |
| | write_position.xpos, |
| | write_position.ypos + write_position.height - 1, |
| | write_position.width, |
| | 1, |
| | ) |
| | new_screen.fill_area(wp, "class:last-line", after=True) |
| |
|
| | def _highlight_digraph(self, new_screen: Screen) -> None: |
| | """ |
| | When we are in Vi digraph mode, put a question mark underneath the |
| | cursor. |
| | """ |
| | digraph_char = self._get_digraph_char() |
| | if digraph_char: |
| | cpos = new_screen.get_cursor_position(self) |
| | new_screen.data_buffer[cpos.y][cpos.x] = _CHAR_CACHE[ |
| | digraph_char, "class:digraph" |
| | ] |
| |
|
| | def _show_key_processor_key_buffer(self, new_screen: Screen) -> None: |
| | """ |
| | When the user is typing a key binding that consists of several keys, |
| | display the last pressed key if the user is in insert mode and the key |
| | is meaningful to be displayed. |
| | E.g. Some people want to bind 'jj' to escape in Vi insert mode. But the |
| | first 'j' needs to be displayed in order to get some feedback. |
| | """ |
| | app = get_app() |
| | key_buffer = app.key_processor.key_buffer |
| |
|
| | if key_buffer and _in_insert_mode() and not app.is_done: |
| | |
| | |
| | data = key_buffer[-1].data |
| |
|
| | |
| | if get_cwidth(data) == 1: |
| | cpos = new_screen.get_cursor_position(self) |
| | new_screen.data_buffer[cpos.y][cpos.x] = _CHAR_CACHE[ |
| | data, "class:partial-key-binding" |
| | ] |
| |
|
| | def _highlight_cursorlines( |
| | self, new_screen: Screen, cpos: Point, x: int, y: int, width: int, height: int |
| | ) -> None: |
| | """ |
| | Highlight cursor row/column. |
| | """ |
| | cursor_line_style = " class:cursor-line " |
| | cursor_column_style = " class:cursor-column " |
| |
|
| | data_buffer = new_screen.data_buffer |
| |
|
| | |
| | if self.cursorline(): |
| | row = data_buffer[cpos.y] |
| | for x in range(x, x + width): |
| | original_char = row[x] |
| | row[x] = _CHAR_CACHE[ |
| | original_char.char, original_char.style + cursor_line_style |
| | ] |
| |
|
| | |
| | if self.cursorcolumn(): |
| | for y2 in range(y, y + height): |
| | row = data_buffer[y2] |
| | original_char = row[cpos.x] |
| | row[cpos.x] = _CHAR_CACHE[ |
| | original_char.char, original_char.style + cursor_column_style |
| | ] |
| |
|
| | |
| | colorcolumns = self.colorcolumns |
| | if callable(colorcolumns): |
| | colorcolumns = colorcolumns() |
| |
|
| | for cc in colorcolumns: |
| | assert isinstance(cc, ColorColumn) |
| | column = cc.position |
| |
|
| | if column < x + width: |
| | color_column_style = " " + cc.style |
| |
|
| | for y2 in range(y, y + height): |
| | row = data_buffer[y2] |
| | original_char = row[column + x] |
| | row[column + x] = _CHAR_CACHE[ |
| | original_char.char, original_char.style + color_column_style |
| | ] |
| |
|
| | def _copy_margin( |
| | self, |
| | margin_content: UIContent, |
| | new_screen: Screen, |
| | write_position: WritePosition, |
| | move_x: int, |
| | width: int, |
| | ) -> None: |
| | """ |
| | Copy characters from the margin screen to the real screen. |
| | """ |
| | xpos = write_position.xpos + move_x |
| | ypos = write_position.ypos |
| |
|
| | margin_write_position = WritePosition(xpos, ypos, width, write_position.height) |
| | self._copy_body(margin_content, new_screen, margin_write_position, 0, width) |
| |
|
| | def _scroll(self, ui_content: UIContent, width: int, height: int) -> None: |
| | """ |
| | Scroll body. Ensure that the cursor is visible. |
| | """ |
| | if self.wrap_lines(): |
| | func = self._scroll_when_linewrapping |
| | else: |
| | func = self._scroll_without_linewrapping |
| |
|
| | func(ui_content, width, height) |
| |
|
| | def _scroll_when_linewrapping( |
| | self, ui_content: UIContent, width: int, height: int |
| | ) -> None: |
| | """ |
| | Scroll to make sure the cursor position is visible and that we maintain |
| | the requested scroll offset. |
| | |
| | Set `self.horizontal_scroll/vertical_scroll`. |
| | """ |
| | scroll_offsets_bottom = self.scroll_offsets.bottom |
| | scroll_offsets_top = self.scroll_offsets.top |
| |
|
| | |
| | self.horizontal_scroll = 0 |
| |
|
| | def get_line_height(lineno: int) -> int: |
| | return ui_content.get_height_for_line(lineno, width, self.get_line_prefix) |
| |
|
| | |
| | |
| | |
| | |
| | |
| | if width <= 0: |
| | self.vertical_scroll = ui_content.cursor_position.y |
| | self.vertical_scroll_2 = 0 |
| | return |
| |
|
| | |
| | |
| | |
| | |
| | |
| | line_height = get_line_height(ui_content.cursor_position.y) |
| | if line_height > height - scroll_offsets_top: |
| | |
| | |
| | text_before_height = ui_content.get_height_for_line( |
| | ui_content.cursor_position.y, |
| | width, |
| | self.get_line_prefix, |
| | slice_stop=ui_content.cursor_position.x, |
| | ) |
| |
|
| | |
| | self.vertical_scroll = ui_content.cursor_position.y |
| | self.vertical_scroll_2 = min( |
| | text_before_height - 1, |
| | line_height |
| | - height, |
| | self.vertical_scroll_2, |
| | ) |
| | self.vertical_scroll_2 = max( |
| | 0, text_before_height - height, self.vertical_scroll_2 |
| | ) |
| | return |
| | else: |
| | self.vertical_scroll_2 = 0 |
| |
|
| | |
| | def get_min_vertical_scroll() -> int: |
| | |
| | |
| | used_height = 0 |
| | prev_lineno = ui_content.cursor_position.y |
| |
|
| | for lineno in range(ui_content.cursor_position.y, -1, -1): |
| | used_height += get_line_height(lineno) |
| |
|
| | if used_height > height - scroll_offsets_bottom: |
| | return prev_lineno |
| | else: |
| | prev_lineno = lineno |
| | return 0 |
| |
|
| | def get_max_vertical_scroll() -> int: |
| | |
| | prev_lineno = ui_content.cursor_position.y |
| | used_height = 0 |
| |
|
| | for lineno in range(ui_content.cursor_position.y - 1, -1, -1): |
| | used_height += get_line_height(lineno) |
| |
|
| | if used_height > scroll_offsets_top: |
| | return prev_lineno |
| | else: |
| | prev_lineno = lineno |
| | return prev_lineno |
| |
|
| | def get_topmost_visible() -> int: |
| | """ |
| | Calculate the upper most line that can be visible, while the bottom |
| | is still visible. We should not allow scroll more than this if |
| | `allow_scroll_beyond_bottom` is false. |
| | """ |
| | prev_lineno = ui_content.line_count - 1 |
| | used_height = 0 |
| | for lineno in range(ui_content.line_count - 1, -1, -1): |
| | used_height += get_line_height(lineno) |
| | if used_height > height: |
| | return prev_lineno |
| | else: |
| | prev_lineno = lineno |
| | return prev_lineno |
| |
|
| | |
| | |
| | topmost_visible = get_topmost_visible() |
| |
|
| | |
| | |
| | |
| | self.vertical_scroll = max( |
| | self.vertical_scroll, min(topmost_visible, get_min_vertical_scroll()) |
| | ) |
| | self.vertical_scroll = min(self.vertical_scroll, get_max_vertical_scroll()) |
| |
|
| | |
| | if not self.allow_scroll_beyond_bottom(): |
| | self.vertical_scroll = min(self.vertical_scroll, topmost_visible) |
| |
|
| | def _scroll_without_linewrapping( |
| | self, ui_content: UIContent, width: int, height: int |
| | ) -> None: |
| | """ |
| | Scroll to make sure the cursor position is visible and that we maintain |
| | the requested scroll offset. |
| | |
| | Set `self.horizontal_scroll/vertical_scroll`. |
| | """ |
| | cursor_position = ui_content.cursor_position or Point(x=0, y=0) |
| |
|
| | |
| | |
| | self.vertical_scroll_2 = 0 |
| |
|
| | if ui_content.line_count == 0: |
| | self.vertical_scroll = 0 |
| | self.horizontal_scroll = 0 |
| | return |
| | else: |
| | current_line_text = fragment_list_to_text( |
| | ui_content.get_line(cursor_position.y) |
| | ) |
| |
|
| | def do_scroll( |
| | current_scroll: int, |
| | scroll_offset_start: int, |
| | scroll_offset_end: int, |
| | cursor_pos: int, |
| | window_size: int, |
| | content_size: int, |
| | ) -> int: |
| | "Scrolling algorithm. Used for both horizontal and vertical scrolling." |
| | |
| | |
| | |
| | scroll_offset_start = int( |
| | min(scroll_offset_start, window_size / 2, cursor_pos) |
| | ) |
| | scroll_offset_end = int( |
| | min(scroll_offset_end, window_size / 2, content_size - 1 - cursor_pos) |
| | ) |
| |
|
| | |
| | if current_scroll < 0: |
| | current_scroll = 0 |
| |
|
| | |
| | if ( |
| | not self.allow_scroll_beyond_bottom() |
| | and current_scroll > content_size - window_size |
| | ): |
| | current_scroll = max(0, content_size - window_size) |
| |
|
| | |
| | if current_scroll > cursor_pos - scroll_offset_start: |
| | current_scroll = max(0, cursor_pos - scroll_offset_start) |
| |
|
| | |
| | if current_scroll < (cursor_pos + 1) - window_size + scroll_offset_end: |
| | current_scroll = (cursor_pos + 1) - window_size + scroll_offset_end |
| |
|
| | return current_scroll |
| |
|
| | |
| | if self.get_vertical_scroll: |
| | self.vertical_scroll = self.get_vertical_scroll(self) |
| | assert isinstance(self.vertical_scroll, int) |
| | if self.get_horizontal_scroll: |
| | self.horizontal_scroll = self.get_horizontal_scroll(self) |
| | assert isinstance(self.horizontal_scroll, int) |
| |
|
| | |
| | |
| | offsets = self.scroll_offsets |
| |
|
| | self.vertical_scroll = do_scroll( |
| | current_scroll=self.vertical_scroll, |
| | scroll_offset_start=offsets.top, |
| | scroll_offset_end=offsets.bottom, |
| | cursor_pos=ui_content.cursor_position.y, |
| | window_size=height, |
| | content_size=ui_content.line_count, |
| | ) |
| |
|
| | if self.get_line_prefix: |
| | current_line_prefix_width = fragment_list_width( |
| | to_formatted_text(self.get_line_prefix(ui_content.cursor_position.y, 0)) |
| | ) |
| | else: |
| | current_line_prefix_width = 0 |
| |
|
| | self.horizontal_scroll = do_scroll( |
| | current_scroll=self.horizontal_scroll, |
| | scroll_offset_start=offsets.left, |
| | scroll_offset_end=offsets.right, |
| | cursor_pos=get_cwidth(current_line_text[: ui_content.cursor_position.x]), |
| | window_size=width - current_line_prefix_width, |
| | |
| | |
| | content_size=max( |
| | get_cwidth(current_line_text), self.horizontal_scroll + width |
| | ), |
| | ) |
| |
|
| | def _mouse_handler(self, mouse_event: MouseEvent) -> NotImplementedOrNone: |
| | """ |
| | Mouse handler. Called when the UI control doesn't handle this |
| | particular event. |
| | |
| | Return `NotImplemented` if nothing was done as a consequence of this |
| | key binding (no UI invalidate required in that case). |
| | """ |
| | if mouse_event.event_type == MouseEventType.SCROLL_DOWN: |
| | self._scroll_down() |
| | return None |
| | elif mouse_event.event_type == MouseEventType.SCROLL_UP: |
| | self._scroll_up() |
| | return None |
| |
|
| | return NotImplemented |
| |
|
| | def _scroll_down(self) -> None: |
| | "Scroll window down." |
| | info = self.render_info |
| |
|
| | if info is None: |
| | return |
| |
|
| | if self.vertical_scroll < info.content_height - info.window_height: |
| | if info.cursor_position.y <= info.configured_scroll_offsets.top: |
| | self.content.move_cursor_down() |
| |
|
| | self.vertical_scroll += 1 |
| |
|
| | def _scroll_up(self) -> None: |
| | "Scroll window up." |
| | info = self.render_info |
| |
|
| | if info is None: |
| | return |
| |
|
| | if info.vertical_scroll > 0: |
| | |
| | if ( |
| | info.cursor_position.y |
| | >= info.window_height - 1 - info.configured_scroll_offsets.bottom |
| | ): |
| | self.content.move_cursor_up() |
| |
|
| | self.vertical_scroll -= 1 |
| |
|
| | def get_key_bindings(self) -> KeyBindingsBase | None: |
| | return self.content.get_key_bindings() |
| |
|
| | def get_children(self) -> list[Container]: |
| | return [] |
| |
|
| |
|
| | class ConditionalContainer(Container): |
| | """ |
| | Wrapper around any other container that can change the visibility. The |
| | received `filter` determines whether the given container should be |
| | displayed or not. |
| | |
| | :param content: :class:`.Container` instance. |
| | :param filter: :class:`.Filter` instance. |
| | """ |
| |
|
| | def __init__(self, content: AnyContainer, filter: FilterOrBool) -> None: |
| | self.content = to_container(content) |
| | self.filter = to_filter(filter) |
| |
|
| | def __repr__(self) -> str: |
| | return f"ConditionalContainer({self.content!r}, filter={self.filter!r})" |
| |
|
| | def reset(self) -> None: |
| | self.content.reset() |
| |
|
| | def preferred_width(self, max_available_width: int) -> Dimension: |
| | if self.filter(): |
| | return self.content.preferred_width(max_available_width) |
| | else: |
| | return Dimension.zero() |
| |
|
| | def preferred_height(self, width: int, max_available_height: int) -> Dimension: |
| | if self.filter(): |
| | return self.content.preferred_height(width, max_available_height) |
| | else: |
| | return Dimension.zero() |
| |
|
| | def write_to_screen( |
| | self, |
| | screen: Screen, |
| | mouse_handlers: MouseHandlers, |
| | write_position: WritePosition, |
| | parent_style: str, |
| | erase_bg: bool, |
| | z_index: int | None, |
| | ) -> None: |
| | if self.filter(): |
| | return self.content.write_to_screen( |
| | screen, mouse_handlers, write_position, parent_style, erase_bg, z_index |
| | ) |
| |
|
| | def get_children(self) -> list[Container]: |
| | return [self.content] |
| |
|
| |
|
| | class DynamicContainer(Container): |
| | """ |
| | Container class that dynamically returns any Container. |
| | |
| | :param get_container: Callable that returns a :class:`.Container` instance |
| | or any widget with a ``__pt_container__`` method. |
| | """ |
| |
|
| | def __init__(self, get_container: Callable[[], AnyContainer]) -> None: |
| | self.get_container = get_container |
| |
|
| | def _get_container(self) -> Container: |
| | """ |
| | Return the current container object. |
| | |
| | We call `to_container`, because `get_container` can also return a |
| | widget with a ``__pt_container__`` method. |
| | """ |
| | obj = self.get_container() |
| | return to_container(obj) |
| |
|
| | def reset(self) -> None: |
| | self._get_container().reset() |
| |
|
| | def preferred_width(self, max_available_width: int) -> Dimension: |
| | return self._get_container().preferred_width(max_available_width) |
| |
|
| | def preferred_height(self, width: int, max_available_height: int) -> Dimension: |
| | return self._get_container().preferred_height(width, max_available_height) |
| |
|
| | def write_to_screen( |
| | self, |
| | screen: Screen, |
| | mouse_handlers: MouseHandlers, |
| | write_position: WritePosition, |
| | parent_style: str, |
| | erase_bg: bool, |
| | z_index: int | None, |
| | ) -> None: |
| | self._get_container().write_to_screen( |
| | screen, mouse_handlers, write_position, parent_style, erase_bg, z_index |
| | ) |
| |
|
| | def is_modal(self) -> bool: |
| | return False |
| |
|
| | def get_key_bindings(self) -> KeyBindingsBase | None: |
| | |
| | |
| | return None |
| |
|
| | def get_children(self) -> list[Container]: |
| | |
| | |
| | |
| | |
| | return [self._get_container()] |
| |
|
| |
|
| | def to_container(container: AnyContainer) -> Container: |
| | """ |
| | Make sure that the given object is a :class:`.Container`. |
| | """ |
| | if isinstance(container, Container): |
| | return container |
| | elif hasattr(container, "__pt_container__"): |
| | return to_container(container.__pt_container__()) |
| | else: |
| | raise ValueError(f"Not a container object: {container!r}") |
| |
|
| |
|
| | def to_window(container: AnyContainer) -> Window: |
| | """ |
| | Make sure that the given argument is a :class:`.Window`. |
| | """ |
| | if isinstance(container, Window): |
| | return container |
| | elif hasattr(container, "__pt_container__"): |
| | return to_window(cast("MagicContainer", container).__pt_container__()) |
| | else: |
| | raise ValueError(f"Not a Window object: {container!r}.") |
| |
|
| |
|
| | def is_container(value: object) -> TypeGuard[AnyContainer]: |
| | """ |
| | Checks whether the given value is a container object |
| | (for use in assert statements). |
| | """ |
| | if isinstance(value, Container): |
| | return True |
| | if hasattr(value, "__pt_container__"): |
| | return is_container(cast("MagicContainer", value).__pt_container__()) |
| | return False |
| |
|