pytermgui.widgets.color_picker

The module containing the ColorPicker widget, as well as some helpers it needs.

To test out the widget, run ptg --color!

View Source
"""The module containing the ColorPicker widget, as well as some helpers it needs.

To test out the widget, run `ptg --color`!
"""

from __future__ import annotations

from typing import Any
from contextlib import suppress

from . import boxes
from .layouts import Container
from .base import Label, Widget
from ..animator import animator
from .interactive import Button
from ..helpers import real_length
from .pixel_matrix import PixelMatrix
from ..enums import SizePolicy, HorizontalAlignment
from ..ansi_interface import MouseAction, MouseEvent


def _get_xterm_matrix() -> list[list[str]]:
    """Creates a matrix containing all 255 xterm-255 colors.

    The top row contains the normal & bright colors, with some
    space in between.

    The second row contains all shades of black.

    Finally, the third section is a table of all remaining colors.
    """

    matrix: list[list[str]] = []
    for _ in range(11):
        current_row = []
        for _ in range(36):
            current_row.append("")
        matrix.append(current_row)

    offset = 0
    for color in range(16):
        if color == 8:
            offset += 4

        cursor = offset
        for _ in range(2):
            matrix[0][cursor] = str(color)
            cursor += 1

        offset = cursor

    offset = 7
    for color in range(23):
        cursor = offset

        matrix[2][cursor] = str(232 + color)
        matrix[3][cursor] = str(min(232 + color + 1, 255))
        cursor += 1

        offset = cursor

    cursor = 16
    for row in range(5, 11):
        for column in range(37):
            if column == 36:
                continue

            matrix[row][column] = str(cursor + column)

        cursor += column

        if cursor > 232:
            break

    return matrix


class Joiner(Container):
    """A Container that stacks widgets horizontally, without filling up the available space.

    This works slightly differently to Splitter, as that applies padding & custom widths to
    any Widget it finds. This works much more simply, and only joins their lines together as
    they come.
    """

    parent_align = HorizontalAlignment.LEFT

    chars = {"separator": " "}

    def get_lines(self) -> list[str]:
        """Does magic"""

        lines: list[str] = []
        separator = self._get_char("separator")
        assert isinstance(separator, str)

        line = ""
        for widget in self._widgets:
            if len(line) > 0:
                line += separator

            widget.pos = (self.pos[0] + real_length(line), self.pos[1] + len(lines))
            widget_line = widget.get_lines()[0]

            if real_length(line + widget_line) >= self.width:
                lines.append(line)
                widget.pos = self.pos[0], self.pos[1] + len(lines)
                line = widget_line
                continue

            line += widget_line

        lines.append(line)
        self.height = len(lines)
        return lines


class _FadeInButton(Button):
    """A Button with a fade-in animation."""

    def __init__(self, *args: Any, **attrs: Any) -> None:
        """Initialize _FadeInButton.

        As this is nothing more than an extension on top of
        `pytermgui.widgets.interactive.Button`, check that documentation
        for more informationA.
        """

        super().__init__(*args, **attrs)
        self.onclick = self.remove_from_parent
        self.set_char("delimiter", ["", ""])

        self.set_style("label", lambda _, item: item)
        self._fade_progress = 0

        self.get_lines()
        animator.animate(
            self, "_fade_progress", startpoint=0, endpoint=self.width, duration=150
        )

    def remove_from_parent(self, _: Widget) -> None:
        """Removes self from parent, when possible."""

        def _on_finish(self) -> None:
            """Removes button on animation finish."""

            with suppress(ValueError):
                self.parent.remove(self)

        animator.animate(
            self,
            "_fade_progress",
            startpoint=self.width,
            endpoint=0,
            duration=150,
            finish_callback=_on_finish,
        )

    def get_lines(self) -> list[str]:
        """Gets the lines from Button, and cuts them off at self._fade_progress"""

        lines = super().get_lines()
        for i, line in enumerate(lines):
            lines[i] = line[: self._fade_progress].rstrip("\x1b") + "\x1b[0m"
        return lines


class ColorPicker(Container):
    """A simple ColorPicker widget.

    This is used to visualize xterm-255 colors. RGB colors are not
    included here, as it is probably easier to use a web-based picker
    for those anyways.
    """

    size_policy = SizePolicy.STATIC

    def __init__(self, show_output: bool = True, **attrs: Any) -> None:
        """Initializes a ColorPicker.

        Attrs:
            show_output: Decides whether the output Container should be
                added. If not set, the widget will only display the
                PixelMatrix of colors.
        """

        super().__init__(**attrs)
        self.show_output = show_output

        self._matrix = PixelMatrix.from_matrix(_get_xterm_matrix())

        self.width = 72
        self.box = boxes.EMPTY

        self._add_widget(self._matrix, run_get_lines=False)

        self.chosen = Joiner()
        self._output = Container(self.chosen, "", "", "")
        self._output.height = 7
        self._output.box = boxes.Box([" ", "x", " "])

        if self.show_output:
            self._add_widget(self._output)

    def handle_mouse(self, event: MouseEvent) -> bool:
        """Handles mouse events.

        On hover, the widget will display the currently hovered
        color and some testing text.

        On click, it will add a _FadeInButton for the currently
        hovered color.

        Args:
            event: The event to handle.
        """

        if super().handle_mouse(event):
            return True

        if not self.show_output or not self._matrix.contains(event.position):
            return False

        if event.action is MouseAction.LEFT_CLICK:
            if self._matrix.selected_pixel is None:
                return True

            _, color = self._matrix.selected_pixel
            if len(color) == 0:
                return False

            # Why does mypy freak out about this?
            self.chosen += _FadeInButton(f"[black @{color}]{color:^5}")  # type: ignore
            return True

        return False

    def get_lines(self) -> list[str]:
        """Updates self._output and gets widget lines."""

        if self.show_output and self._matrix.selected_pixel is not None:
            _, color = self._matrix.selected_pixel
            if len(color) == 0:
                return super().get_lines()

            lines: list[Widget] = [
                Label(f"[black @{color}] {color} [/ {color}] {color}"),
                Label(
                    f"[{color} bold]Here[/bold italic] is "
                    + "[/italic underline]some[/underline dim] example[/dim] text"
                ),
            ]
            self._output.set_widgets(lines + [Label(), self.chosen])
            return super().get_lines()

        return super().get_lines()
View Source
class Joiner(Container):
    """A Container that stacks widgets horizontally, without filling up the available space.

    This works slightly differently to Splitter, as that applies padding & custom widths to
    any Widget it finds. This works much more simply, and only joins their lines together as
    they come.
    """

    parent_align = HorizontalAlignment.LEFT

    chars = {"separator": " "}

    def get_lines(self) -> list[str]:
        """Does magic"""

        lines: list[str] = []
        separator = self._get_char("separator")
        assert isinstance(separator, str)

        line = ""
        for widget in self._widgets:
            if len(line) > 0:
                line += separator

            widget.pos = (self.pos[0] + real_length(line), self.pos[1] + len(lines))
            widget_line = widget.get_lines()[0]

            if real_length(line + widget_line) >= self.width:
                lines.append(line)
                widget.pos = self.pos[0], self.pos[1] + len(lines)
                line = widget_line
                continue

            line += widget_line

        lines.append(line)
        self.height = len(lines)
        return lines

A Container that stacks widgets horizontally, without filling up the available space.

This works slightly differently to Splitter, as that applies padding & custom widths to any Widget it finds. This works much more simply, and only joins their lines together as they come.

#   parent_align = <HorizontalAlignment.LEFT: 0>
#   chars: dict[str, typing.Union[typing.List[str], str]] = {'separator': ' '}

Default characters for this class

#   def get_lines(self) -> list[str]:
View Source
    def get_lines(self) -> list[str]:
        """Does magic"""

        lines: list[str] = []
        separator = self._get_char("separator")
        assert isinstance(separator, str)

        line = ""
        for widget in self._widgets:
            if len(line) > 0:
                line += separator

            widget.pos = (self.pos[0] + real_length(line), self.pos[1] + len(lines))
            widget_line = widget.get_lines()[0]

            if real_length(line + widget_line) >= self.width:
                lines.append(line)
                widget.pos = self.pos[0], self.pos[1] + len(lines)
                line = widget_line
                continue

            line += widget_line

        lines.append(line)
        self.height = len(lines)
        return lines

Does magic

View Source
class ColorPicker(Container):
    """A simple ColorPicker widget.

    This is used to visualize xterm-255 colors. RGB colors are not
    included here, as it is probably easier to use a web-based picker
    for those anyways.
    """

    size_policy = SizePolicy.STATIC

    def __init__(self, show_output: bool = True, **attrs: Any) -> None:
        """Initializes a ColorPicker.

        Attrs:
            show_output: Decides whether the output Container should be
                added. If not set, the widget will only display the
                PixelMatrix of colors.
        """

        super().__init__(**attrs)
        self.show_output = show_output

        self._matrix = PixelMatrix.from_matrix(_get_xterm_matrix())

        self.width = 72
        self.box = boxes.EMPTY

        self._add_widget(self._matrix, run_get_lines=False)

        self.chosen = Joiner()
        self._output = Container(self.chosen, "", "", "")
        self._output.height = 7
        self._output.box = boxes.Box([" ", "x", " "])

        if self.show_output:
            self._add_widget(self._output)

    def handle_mouse(self, event: MouseEvent) -> bool:
        """Handles mouse events.

        On hover, the widget will display the currently hovered
        color and some testing text.

        On click, it will add a _FadeInButton for the currently
        hovered color.

        Args:
            event: The event to handle.
        """

        if super().handle_mouse(event):
            return True

        if not self.show_output or not self._matrix.contains(event.position):
            return False

        if event.action is MouseAction.LEFT_CLICK:
            if self._matrix.selected_pixel is None:
                return True

            _, color = self._matrix.selected_pixel
            if len(color) == 0:
                return False

            # Why does mypy freak out about this?
            self.chosen += _FadeInButton(f"[black @{color}]{color:^5}")  # type: ignore
            return True

        return False

    def get_lines(self) -> list[str]:
        """Updates self._output and gets widget lines."""

        if self.show_output and self._matrix.selected_pixel is not None:
            _, color = self._matrix.selected_pixel
            if len(color) == 0:
                return super().get_lines()

            lines: list[Widget] = [
                Label(f"[black @{color}] {color} [/ {color}] {color}"),
                Label(
                    f"[{color} bold]Here[/bold italic] is "
                    + "[/italic underline]some[/underline dim] example[/dim] text"
                ),
            ]
            self._output.set_widgets(lines + [Label(), self.chosen])
            return super().get_lines()

        return super().get_lines()

A simple ColorPicker widget.

This is used to visualize xterm-255 colors. RGB colors are not included here, as it is probably easier to use a web-based picker for those anyways.

#   ColorPicker(show_output: bool = True, **attrs: Any)
View Source
    def __init__(self, show_output: bool = True, **attrs: Any) -> None:
        """Initializes a ColorPicker.

        Attrs:
            show_output: Decides whether the output Container should be
                added. If not set, the widget will only display the
                PixelMatrix of colors.
        """

        super().__init__(**attrs)
        self.show_output = show_output

        self._matrix = PixelMatrix.from_matrix(_get_xterm_matrix())

        self.width = 72
        self.box = boxes.EMPTY

        self._add_widget(self._matrix, run_get_lines=False)

        self.chosen = Joiner()
        self._output = Container(self.chosen, "", "", "")
        self._output.height = 7
        self._output.box = boxes.Box([" ", "x", " "])

        if self.show_output:
            self._add_widget(self._output)

Initializes a ColorPicker.

Attrs

show_output: Decides whether the output Container should be added. If not set, the widget will only display the PixelMatrix of colors.

#   size_policy = <SizePolicy.STATIC: 1>

pytermgui.enums.SizePolicy to set widget's width according to

#   def handle_mouse(self, event: pytermgui.ansi_interface.MouseEvent) -> bool:
View Source
    def handle_mouse(self, event: MouseEvent) -> bool:
        """Handles mouse events.

        On hover, the widget will display the currently hovered
        color and some testing text.

        On click, it will add a _FadeInButton for the currently
        hovered color.

        Args:
            event: The event to handle.
        """

        if super().handle_mouse(event):
            return True

        if not self.show_output or not self._matrix.contains(event.position):
            return False

        if event.action is MouseAction.LEFT_CLICK:
            if self._matrix.selected_pixel is None:
                return True

            _, color = self._matrix.selected_pixel
            if len(color) == 0:
                return False

            # Why does mypy freak out about this?
            self.chosen += _FadeInButton(f"[black @{color}]{color:^5}")  # type: ignore
            return True

        return False

Handles mouse events.

On hover, the widget will display the currently hovered color and some testing text.

On click, it will add a _FadeInButton for the currently hovered color.

Args
  • event: The event to handle.
#   def get_lines(self) -> list[str]:
View Source
    def get_lines(self) -> list[str]:
        """Updates self._output and gets widget lines."""

        if self.show_output and self._matrix.selected_pixel is not None:
            _, color = self._matrix.selected_pixel
            if len(color) == 0:
                return super().get_lines()

            lines: list[Widget] = [
                Label(f"[black @{color}] {color} [/ {color}] {color}"),
                Label(
                    f"[{color} bold]Here[/bold italic] is "
                    + "[/italic underline]some[/underline dim] example[/dim] text"
                ),
            ]
            self._output.set_widgets(lines + [Label(), self.chosen])
            return super().get_lines()

        return super().get_lines()

Updates self._output and gets widget lines.