| """ |
| 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 "Float(content=%r)" % self.content |
|
|
|
|
| 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 "ScrollOffsets(top={!r}, bottom={!r}, left={!r}, right={!r})".format( |
| self._top, |
| self._bottom, |
| self._left, |
| self._right, |
| ) |
|
|
|
|
| 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 "Window(content=%r)" % self.content |
|
|
| 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 |
|
|