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

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
 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

Does magic

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

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

Updates self._output and gets widget lines.