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!

  1"""The module containing the ColorPicker widget, as well as some helpers it needs.
  2
  3To test out the widget, run `ptg --color`!
  4"""
  5
  6from __future__ import annotations
  7
  8from contextlib import suppress
  9from typing import Any
 10
 11from ..animations import animator
 12from ..ansi_interface import MouseAction, MouseEvent
 13from ..colors import str_to_color
 14from ..enums import HorizontalAlignment, SizePolicy
 15from ..regex import real_length
 16from . import boxes
 17from .base import Label, Widget
 18from .button import Button
 19from .containers import Container
 20from .pixel_matrix import PixelMatrix
 21
 22
 23def _get_xterm_matrix() -> list[list[str]]:
 24    """Creates a matrix containing all 255 xterm-255 colors.
 25
 26    The top row contains the normal & bright colors, with some
 27    space in between.
 28
 29    The second row contains all shades of black.
 30
 31    Finally, the third section is a table of all remaining colors.
 32    """
 33
 34    matrix: list[list[str]] = []
 35    for _ in range(11):
 36        current_row = []
 37        for _ in range(36):
 38            current_row.append("")
 39        matrix.append(current_row)
 40
 41    offset = 0
 42    for color in range(16):
 43        if color == 8:
 44            offset += 4
 45
 46        cursor = offset
 47        for _ in range(2):
 48            matrix[0][cursor] = str(color)
 49            cursor += 1
 50
 51        offset = cursor
 52
 53    offset = 7
 54    for color in range(23):
 55        cursor = offset
 56
 57        matrix[2][cursor] = str(232 + color)
 58        matrix[3][cursor] = str(min(232 + color + 1, 255))
 59        cursor += 1
 60
 61        offset = cursor
 62
 63    cursor = 16
 64    for row in range(5, 11):
 65        for column in range(37):
 66            if column == 36:
 67                continue
 68
 69            matrix[row][column] = str(cursor + column)
 70
 71        cursor += column
 72
 73        if cursor > 232:
 74            break
 75
 76    return matrix
 77
 78
 79class Joiner(Container):
 80    """A Container that stacks widgets horizontally, without filling up the available space.
 81
 82    This works slightly differently to Splitter, as that applies padding & custom widths to
 83    any Widget it finds. This works much more simply, and only joins their lines together as
 84    they come.
 85    """
 86
 87    parent_align = HorizontalAlignment.LEFT
 88
 89    chars = {"separator": " "}
 90
 91    def get_lines(self) -> list[str]:
 92        """Does magic"""
 93
 94        lines: list[str] = []
 95        separator = self._get_char("separator")
 96        assert isinstance(separator, str)
 97
 98        line = ""
 99        for widget in self._widgets:
100            if len(line) > 0:
101                line += separator
102
103            widget.pos = (self.pos[0] + real_length(line), self.pos[1] + len(lines))
104            widget_line = widget.get_lines()[0]
105
106            if real_length(line + widget_line) >= self.width:
107                lines.append(line)
108                widget.pos = self.pos[0], self.pos[1] + len(lines)
109                line = widget_line
110                continue
111
112            line += widget_line
113
114        lines.append(line)
115        self.height = len(lines)
116        return lines
117
118
119class _FadeInButton(Button):
120    """A Button with a fade-in animation."""
121
122    def __init__(self, *args: Any, **attrs: Any) -> None:
123        """Initialize _FadeInButton.
124
125        As this is nothing more than an extension on top of
126        `pytermgui.widgets.interactive.Button`, check that documentation
127        for more information.
128        """
129
130        super().__init__(*args, **attrs)
131        self.onclick = self.remove_from_parent
132        self.set_char("delimiter", ["", ""])
133
134        self._fade_progress = 0
135
136        self.get_lines()
137
138        # TODO: Why is that +2 needed?
139        animator.animate_attr(
140            target=self,
141            attr="_fade_progress",
142            start=0,
143            end=self.width + 2,
144            duration=150,
145        )
146
147    def remove_from_parent(self, _: Widget) -> None:
148        """Removes self from parent, when possible."""
149
150        def _on_finish(_: object) -> None:
151            """Removes button on animation finish."""
152
153            assert isinstance(self.parent, Container)
154
155            with suppress(ValueError):
156                self.parent.remove(self)
157
158        animator.animate_attr(
159            target=self,
160            attr="_fade_progress",
161            start=self.width,
162            end=0,
163            duration=150,
164            on_finish=_on_finish,
165        )
166
167    def get_lines(self) -> list[str]:
168        """Gets the lines from Button, and cuts them off at self._fade_progress"""
169
170        return [self.styles.label(self.label[: self._fade_progress])]
171
172
173class ColorPicker(Container):
174    """A simple ColorPicker widget.
175
176    This is used to visualize xterm-255 colors. RGB colors are not
177    included here, as it is probably easier to use a web-based picker
178    for those anyways.
179    """
180
181    size_policy = SizePolicy.STATIC
182
183    def __init__(self, show_output: bool = True, **attrs: Any) -> None:
184        """Initializes a ColorPicker.
185
186        Attrs:
187            show_output: Decides whether the output Container should be
188                added. If not set, the widget will only display the
189                PixelMatrix of colors.
190        """
191
192        super().__init__(**attrs)
193        self.show_output = show_output
194
195        self._matrix = PixelMatrix.from_matrix(_get_xterm_matrix())
196
197        self.width = 72
198        self.box = boxes.EMPTY
199
200        self._add_widget(self._matrix, run_get_lines=False)
201
202        self.chosen = Joiner()
203        self._output = Container(self.chosen, "", "", "")
204
205        if self.show_output:
206            self._add_widget(self._output)
207
208    @property
209    def selectables_length(self) -> int:
210        """Returns either the button count or 1."""
211
212        return max(super().selectables_length, 1)
213
214    def handle_mouse(self, event: MouseEvent) -> bool:
215        """Handles mouse events.
216
217        On hover, the widget will display the currently hovered
218        color and some testing text.
219
220        On click, it will add a _FadeInButton for the currently
221        hovered color.
222
223        Args:
224            event: The event to handle.
225        """
226
227        if super().handle_mouse(event):
228            return True
229
230        if not self.show_output or not self._matrix.contains(event.position):
231            return False
232
233        if event.action is MouseAction.LEFT_CLICK:
234            if self._matrix.selected_pixel is None:
235                return True
236
237            _, color = self._matrix.selected_pixel
238            if len(color) == 0:
239                return False
240
241            button = _FadeInButton(f"{color:^5}", width=5)
242            button.styles.label = f"black @{color}"
243            self.chosen.lazy_add(button)
244
245            return True
246
247        return False
248
249    def get_lines(self) -> list[str]:
250        """Updates self._output and gets widget lines."""
251
252        if self.show_output and self._matrix.selected_pixel is not None:
253            _, color = self._matrix.selected_pixel
254            if len(color) == 0:
255                return super().get_lines()
256
257            color_obj = str_to_color(color)
258            rgb = color_obj.rgb
259            hex_ = color_obj.hex
260            lines: list[Widget] = [
261                Label(f"[black @{color}] {color} [/ {color}] {color}"),
262                Label(
263                    f"[{color} bold]Here[/bold italic] is "
264                    + "[/italic underline]some[/underline dim] example[/dim] text"
265                ),
266                Label(),
267                Label(
268                    f"RGB: [{';'.join(map(str, rgb))}]"
269                    + f"rgb({rgb[0]:>3}, {rgb[1]:>3}, {rgb[2]:>3})"
270                ),
271                Label(f"HEX: [{hex_}]{hex_}"),
272            ]
273            self._output.set_widgets(lines + [Label(), self.chosen])
274
275            return super().get_lines()
276
277        return super().get_lines()
class Joiner(pytermgui.widgets.containers.Container):
 80class Joiner(Container):
 81    """A Container that stacks widgets horizontally, without filling up the available space.
 82
 83    This works slightly differently to Splitter, as that applies padding & custom widths to
 84    any Widget it finds. This works much more simply, and only joins their lines together as
 85    they come.
 86    """
 87
 88    parent_align = HorizontalAlignment.LEFT
 89
 90    chars = {"separator": " "}
 91
 92    def get_lines(self) -> list[str]:
 93        """Does magic"""
 94
 95        lines: list[str] = []
 96        separator = self._get_char("separator")
 97        assert isinstance(separator, str)
 98
 99        line = ""
100        for widget in self._widgets:
101            if len(line) > 0:
102                line += separator
103
104            widget.pos = (self.pos[0] + real_length(line), self.pos[1] + len(lines))
105            widget_line = widget.get_lines()[0]
106
107            if real_length(line + widget_line) >= self.width:
108                lines.append(line)
109                widget.pos = self.pos[0], self.pos[1] + len(lines)
110                line = widget_line
111                continue
112
113            line += widget_line
114
115        lines.append(line)
116        self.height = len(lines)
117        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]:
 92    def get_lines(self) -> list[str]:
 93        """Does magic"""
 94
 95        lines: list[str] = []
 96        separator = self._get_char("separator")
 97        assert isinstance(separator, str)
 98
 99        line = ""
100        for widget in self._widgets:
101            if len(line) > 0:
102                line += separator
103
104            widget.pos = (self.pos[0] + real_length(line), self.pos[1] + len(lines))
105            widget_line = widget.get_lines()[0]
106
107            if real_length(line + widget_line) >= self.width:
108                lines.append(line)
109                widget.pos = self.pos[0], self.pos[1] + len(lines)
110                line = widget_line
111                continue
112
113            line += widget_line
114
115        lines.append(line)
116        self.height = len(lines)
117        return lines

Does magic

class ColorPicker(pytermgui.widgets.containers.Container):
174class ColorPicker(Container):
175    """A simple ColorPicker widget.
176
177    This is used to visualize xterm-255 colors. RGB colors are not
178    included here, as it is probably easier to use a web-based picker
179    for those anyways.
180    """
181
182    size_policy = SizePolicy.STATIC
183
184    def __init__(self, show_output: bool = True, **attrs: Any) -> None:
185        """Initializes a ColorPicker.
186
187        Attrs:
188            show_output: Decides whether the output Container should be
189                added. If not set, the widget will only display the
190                PixelMatrix of colors.
191        """
192
193        super().__init__(**attrs)
194        self.show_output = show_output
195
196        self._matrix = PixelMatrix.from_matrix(_get_xterm_matrix())
197
198        self.width = 72
199        self.box = boxes.EMPTY
200
201        self._add_widget(self._matrix, run_get_lines=False)
202
203        self.chosen = Joiner()
204        self._output = Container(self.chosen, "", "", "")
205
206        if self.show_output:
207            self._add_widget(self._output)
208
209    @property
210    def selectables_length(self) -> int:
211        """Returns either the button count or 1."""
212
213        return max(super().selectables_length, 1)
214
215    def handle_mouse(self, event: MouseEvent) -> bool:
216        """Handles mouse events.
217
218        On hover, the widget will display the currently hovered
219        color and some testing text.
220
221        On click, it will add a _FadeInButton for the currently
222        hovered color.
223
224        Args:
225            event: The event to handle.
226        """
227
228        if super().handle_mouse(event):
229            return True
230
231        if not self.show_output or not self._matrix.contains(event.position):
232            return False
233
234        if event.action is MouseAction.LEFT_CLICK:
235            if self._matrix.selected_pixel is None:
236                return True
237
238            _, color = self._matrix.selected_pixel
239            if len(color) == 0:
240                return False
241
242            button = _FadeInButton(f"{color:^5}", width=5)
243            button.styles.label = f"black @{color}"
244            self.chosen.lazy_add(button)
245
246            return True
247
248        return False
249
250    def get_lines(self) -> list[str]:
251        """Updates self._output and gets widget lines."""
252
253        if self.show_output and self._matrix.selected_pixel is not None:
254            _, color = self._matrix.selected_pixel
255            if len(color) == 0:
256                return super().get_lines()
257
258            color_obj = str_to_color(color)
259            rgb = color_obj.rgb
260            hex_ = color_obj.hex
261            lines: list[Widget] = [
262                Label(f"[black @{color}] {color} [/ {color}] {color}"),
263                Label(
264                    f"[{color} bold]Here[/bold italic] is "
265                    + "[/italic underline]some[/underline dim] example[/dim] text"
266                ),
267                Label(),
268                Label(
269                    f"RGB: [{';'.join(map(str, rgb))}]"
270                    + f"rgb({rgb[0]:>3}, {rgb[1]:>3}, {rgb[2]:>3})"
271                ),
272                Label(f"HEX: [{hex_}]{hex_}"),
273            ]
274            self._output.set_widgets(lines + [Label(), self.chosen])
275
276            return super().get_lines()
277
278        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)
184    def __init__(self, show_output: bool = True, **attrs: Any) -> None:
185        """Initializes a ColorPicker.
186
187        Attrs:
188            show_output: Decides whether the output Container should be
189                added. If not set, the widget will only display the
190                PixelMatrix of colors.
191        """
192
193        super().__init__(**attrs)
194        self.show_output = show_output
195
196        self._matrix = PixelMatrix.from_matrix(_get_xterm_matrix())
197
198        self.width = 72
199        self.box = boxes.EMPTY
200
201        self._add_widget(self._matrix, run_get_lines=False)
202
203        self.chosen = Joiner()
204        self._output = Container(self.chosen, "", "", "")
205
206        if self.show_output:
207            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

selectables_length: int

Returns either the button count or 1.

def handle_mouse(self, event: pytermgui.ansi_interface.MouseEvent) -> bool:
215    def handle_mouse(self, event: MouseEvent) -> bool:
216        """Handles mouse events.
217
218        On hover, the widget will display the currently hovered
219        color and some testing text.
220
221        On click, it will add a _FadeInButton for the currently
222        hovered color.
223
224        Args:
225            event: The event to handle.
226        """
227
228        if super().handle_mouse(event):
229            return True
230
231        if not self.show_output or not self._matrix.contains(event.position):
232            return False
233
234        if event.action is MouseAction.LEFT_CLICK:
235            if self._matrix.selected_pixel is None:
236                return True
237
238            _, color = self._matrix.selected_pixel
239            if len(color) == 0:
240                return False
241
242            button = _FadeInButton(f"{color:^5}", width=5)
243            button.styles.label = f"black @{color}"
244            self.chosen.lazy_add(button)
245
246            return True
247
248        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]:
250    def get_lines(self) -> list[str]:
251        """Updates self._output and gets widget lines."""
252
253        if self.show_output and self._matrix.selected_pixel is not None:
254            _, color = self._matrix.selected_pixel
255            if len(color) == 0:
256                return super().get_lines()
257
258            color_obj = str_to_color(color)
259            rgb = color_obj.rgb
260            hex_ = color_obj.hex
261            lines: list[Widget] = [
262                Label(f"[black @{color}] {color} [/ {color}] {color}"),
263                Label(
264                    f"[{color} bold]Here[/bold italic] is "
265                    + "[/italic underline]some[/underline dim] example[/dim] text"
266                ),
267                Label(),
268                Label(
269                    f"RGB: [{';'.join(map(str, rgb))}]"
270                    + f"rgb({rgb[0]:>3}, {rgb[1]:>3}, {rgb[2]:>3})"
271                ),
272                Label(f"HEX: [{hex_}]{hex_}"),
273            ]
274            self._output.set_widgets(lines + [Label(), self.chosen])
275
276            return super().get_lines()
277
278        return super().get_lines()

Updates self._output and gets widget lines.