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()
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.
Default characters for this class
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
Inherited Members
- pytermgui.widgets.containers.Container
- Container
- styles
- keys
- serialized
- vertical_align
- allow_fullscreen
- overflow
- sidelength
- content_dimensions
- selectables
- selectables_length
- selected
- box
- get_change
- lazy_add
- set_widgets
- serialize
- pop
- remove
- set_recursive_depth
- select
- center
- handle_mouse
- execute_binding
- handle_key
- wipe
- debug
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.
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.
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.
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.