bauiv1lib.colorpicker
Provides popup windows for choosing colors.
1# Released under the MIT License. See LICENSE for details. 2# 3"""Provides popup windows for choosing colors.""" 4 5from __future__ import annotations 6 7from typing import TYPE_CHECKING, override 8 9from bauiv1lib.popup import PopupWindow 10import bauiv1 as bui 11 12if TYPE_CHECKING: 13 from typing import Any, Sequence 14 15 16class ColorPicker(PopupWindow): 17 """A popup UI to select from a set of colors. 18 19 Passes the color to the delegate's color_picker_selected_color() method. 20 """ 21 22 def __init__( 23 self, 24 parent: bui.Widget, 25 position: tuple[float, float], 26 initial_color: Sequence[float] = (1.0, 1.0, 1.0), 27 delegate: Any = None, 28 scale: float | None = None, 29 offset: tuple[float, float] = (0.0, 0.0), 30 tag: Any = '', 31 ): 32 # pylint: disable=too-many-locals 33 assert bui.app.classic is not None 34 35 c_raw = bui.app.classic.get_player_colors() 36 assert len(c_raw) == 16 37 self.colors = [c_raw[0:4], c_raw[4:8], c_raw[8:12], c_raw[12:16]] 38 39 uiscale = bui.app.ui_v1.uiscale 40 if scale is None: 41 scale = ( 42 2.3 43 if uiscale is bui.UIScale.SMALL 44 else 1.65 if uiscale is bui.UIScale.MEDIUM else 1.23 45 ) 46 self._parent = parent 47 self._position = position 48 self._scale = scale 49 self._offset = offset 50 self._delegate = delegate 51 self._transitioning_out = False 52 self._tag = tag 53 self._initial_color = initial_color 54 55 # Create our _root_widget. 56 super().__init__( 57 position=position, 58 size=(210, 240), 59 scale=scale, 60 focus_position=(10, 10), 61 focus_size=(190, 220), 62 bg_color=(0.5, 0.5, 0.5), 63 offset=offset, 64 ) 65 rows: list[list[bui.Widget]] = [] 66 closest_dist = 9999.0 67 closest = (0, 0) 68 for y in range(4): 69 row: list[bui.Widget] = [] 70 rows.append(row) 71 for x in range(4): 72 color = self.colors[y][x] 73 dist = ( 74 abs(color[0] - initial_color[0]) 75 + abs(color[1] - initial_color[1]) 76 + abs(color[2] - initial_color[2]) 77 ) 78 if dist < closest_dist: 79 closest = (x, y) 80 closest_dist = dist 81 btn = bui.buttonwidget( 82 parent=self.root_widget, 83 position=(22 + 45 * x, 185 - 45 * y), 84 size=(35, 40), 85 label='', 86 button_type='square', 87 on_activate_call=bui.WeakCall(self._select, x, y), 88 autoselect=True, 89 color=color, 90 extra_touch_border_scale=0.0, 91 ) 92 row.append(btn) 93 other_button = bui.buttonwidget( 94 parent=self.root_widget, 95 position=(105 - 60, 13), 96 color=(0.7, 0.7, 0.7), 97 text_scale=0.5, 98 textcolor=(0.8, 0.8, 0.8), 99 size=(120, 30), 100 label=bui.Lstr( 101 resource='otherText', 102 fallback_resource='coopSelectWindow.customText', 103 ), 104 autoselect=True, 105 on_activate_call=bui.WeakCall(self._select_other), 106 ) 107 108 # Custom colors are limited to pro currently. 109 assert bui.app.classic is not None 110 if not bui.app.classic.accounts.have_pro(): 111 bui.imagewidget( 112 parent=self.root_widget, 113 position=(50, 12), 114 size=(30, 30), 115 texture=bui.gettexture('lock'), 116 draw_controller=other_button, 117 ) 118 119 # If their color is close to one of our swatches, select it. 120 # Otherwise select 'other'. 121 if closest_dist < 0.03: 122 bui.containerwidget( 123 edit=self.root_widget, 124 selected_child=rows[closest[1]][closest[0]], 125 ) 126 else: 127 bui.containerwidget( 128 edit=self.root_widget, selected_child=other_button 129 ) 130 131 def get_tag(self) -> Any: 132 """Return this popup's tag.""" 133 return self._tag 134 135 def _select_other(self) -> None: 136 from bauiv1lib import purchase 137 138 # Requires pro. 139 assert bui.app.classic is not None 140 if not bui.app.classic.accounts.have_pro(): 141 purchase.PurchaseWindow(items=['pro']) 142 self._transition_out() 143 return 144 ColorPickerExact( 145 parent=self._parent, 146 position=self._position, 147 initial_color=self._initial_color, 148 delegate=self._delegate, 149 scale=self._scale, 150 offset=self._offset, 151 tag=self._tag, 152 ) 153 154 # New picker now 'owns' the delegate; we shouldn't send it any 155 # more messages. 156 self._delegate = None 157 self._transition_out() 158 159 def _select(self, x: int, y: int) -> None: 160 if self._delegate: 161 self._delegate.color_picker_selected_color(self, self.colors[y][x]) 162 bui.apptimer(0.05, self._transition_out) 163 164 def _transition_out(self) -> None: 165 if not self._transitioning_out: 166 self._transitioning_out = True 167 if self._delegate is not None: 168 self._delegate.color_picker_closing(self) 169 bui.containerwidget(edit=self.root_widget, transition='out_scale') 170 171 @override 172 def on_popup_cancel(self) -> None: 173 if not self._transitioning_out: 174 bui.getsound('swish').play() 175 self._transition_out() 176 177 178class ColorPickerExact(PopupWindow): 179 """pops up a ui to select from a set of colors. 180 passes the color to the delegate's color_picker_selected_color() method""" 181 182 def __init__( 183 self, 184 parent: bui.Widget, 185 position: tuple[float, float], 186 initial_color: Sequence[float] = (1.0, 1.0, 1.0), 187 delegate: Any = None, 188 scale: float | None = None, 189 offset: tuple[float, float] = (0.0, 0.0), 190 tag: Any = '', 191 ): 192 # pylint: disable=too-many-locals 193 del parent # Unused var. 194 assert bui.app.classic is not None 195 196 c_raw = bui.app.classic.get_player_colors() 197 assert len(c_raw) == 16 198 self.colors = [c_raw[0:4], c_raw[4:8], c_raw[8:12], c_raw[12:16]] 199 200 uiscale = bui.app.ui_v1.uiscale 201 if scale is None: 202 scale = ( 203 2.3 204 if uiscale is bui.UIScale.SMALL 205 else 1.65 if uiscale is bui.UIScale.MEDIUM else 1.23 206 ) 207 self._delegate = delegate 208 self._transitioning_out = False 209 self._tag = tag 210 self._color = list(initial_color) 211 self._last_press_time = bui.apptime() 212 self._last_press_color_name: str | None = None 213 self._last_press_increasing: bool | None = None 214 self._hex_timer: bui.AppTimer | None = None 215 self._hex_prev_text: str = '#FFFFFF' 216 self._change_speed = 1.0 217 width = 180.0 218 height = 240.0 219 220 # Creates our _root_widget. 221 super().__init__( 222 position=position, 223 size=(width, height), 224 scale=scale, 225 focus_position=(10, 10), 226 focus_size=(width - 20, height - 20), 227 bg_color=(0.5, 0.5, 0.5), 228 offset=offset, 229 ) 230 self._swatch = bui.imagewidget( 231 parent=self.root_widget, 232 position=(width * 0.5 - 65 + 5, height - 95), 233 size=(130, 115), 234 texture=bui.gettexture('clayStroke'), 235 color=(1, 0, 0), 236 ) 237 self._hex_textbox = bui.textwidget( 238 parent=self.root_widget, 239 position=(width * 0.5 - 37.5 + 3, height - 51), 240 max_chars=9, 241 text='#FFFFFF', 242 autoselect=True, 243 size=(75, 30), 244 v_align='center', 245 editable=True, 246 maxwidth=70, 247 allow_clear_button=False, 248 force_internal_editing=True, 249 glow_type='uniform', 250 ) 251 252 x = 50 253 y = height - 90 254 self._label_r: bui.Widget 255 self._label_g: bui.Widget 256 self._label_b: bui.Widget 257 for color_name, color_val in [ 258 ('r', (1, 0.15, 0.15)), 259 ('g', (0.15, 1, 0.15)), 260 ('b', (0.15, 0.15, 1)), 261 ]: 262 txt = bui.textwidget( 263 parent=self.root_widget, 264 position=(x - 10, y), 265 size=(0, 0), 266 h_align='center', 267 color=color_val, 268 v_align='center', 269 text='0.12', 270 ) 271 setattr(self, '_label_' + color_name, txt) 272 for b_label, bhval, binc in [('-', 30, False), ('+', 75, True)]: 273 bui.buttonwidget( 274 parent=self.root_widget, 275 position=(x + bhval, y - 15), 276 scale=0.8, 277 repeat=True, 278 text_scale=1.3, 279 size=(40, 40), 280 label=b_label, 281 autoselect=True, 282 enable_sound=False, 283 on_activate_call=bui.WeakCall( 284 self._color_change_press, color_name, binc 285 ), 286 ) 287 y -= 42 288 289 btn = bui.buttonwidget( 290 parent=self.root_widget, 291 position=(width * 0.5 - 40, 10), 292 size=(80, 30), 293 text_scale=0.6, 294 color=(0.6, 0.6, 0.6), 295 textcolor=(0.7, 0.7, 0.7), 296 label=bui.Lstr(resource='doneText'), 297 on_activate_call=bui.WeakCall(self._transition_out), 298 autoselect=True, 299 ) 300 bui.containerwidget(edit=self.root_widget, start_button=btn) 301 302 # Unlike the swatch picker, we stay open and constantly push our 303 # color to the delegate, so start doing that. 304 self._update_for_color() 305 306 # Update our HEX stuff! 307 self._update_for_hex() 308 self._hex_timer = bui.AppTimer(0.025, self._update_for_hex, repeat=True) 309 310 def _update_for_hex(self) -> None: 311 """Update for any HEX or color change.""" 312 from typing import cast 313 314 hextext = cast(str, bui.textwidget(query=self._hex_textbox)) 315 hexcolor: tuple 316 # Check if our current hex text doesn't match with our old one. 317 # Convert our current hex text into a color if possible. 318 if hextext != self._hex_prev_text: 319 try: 320 hexcolor = hex_to_color(hextext) 321 if len(hexcolor) == 4: 322 r, g, b, a = hexcolor 323 del a # unused 324 else: 325 r, g, b = hexcolor 326 # Replace the color! 327 for i, ch in enumerate((r, g, b)): 328 self._color[i] = max(0.0, min(1.0, ch)) 329 self._update_for_color() 330 # Usually, a ValueError will occur if the provided hex 331 # is incomplete, which occurs when in the midst of typing it. 332 except ValueError: 333 pass 334 # Store the current text for our next comparison. 335 self._hex_prev_text = hextext 336 337 # noinspection PyUnresolvedReferences 338 def _update_for_color(self) -> None: 339 if not self.root_widget: 340 return 341 bui.imagewidget(edit=self._swatch, color=self._color) 342 343 # We generate these procedurally, so pylint misses them. 344 # FIXME: create static attrs instead. 345 # pylint: disable=consider-using-f-string 346 bui.textwidget(edit=self._label_r, text='%.2f' % self._color[0]) 347 bui.textwidget(edit=self._label_g, text='%.2f' % self._color[1]) 348 bui.textwidget(edit=self._label_b, text='%.2f' % self._color[2]) 349 if self._delegate is not None: 350 self._delegate.color_picker_selected_color(self, self._color) 351 352 # Show the HEX code of this color. 353 r, g, b = self._color 354 hexcode = color_to_hex(r, g, b, None) 355 self._hex_prev_text = hexcode 356 bui.textwidget( 357 edit=self._hex_textbox, 358 text=hexcode, 359 color=color_overlay_func(r, g, b), 360 ) 361 362 def _color_change_press(self, color_name: str, increasing: bool) -> None: 363 # If we get rapid-fire presses, eventually start moving faster. 364 current_time = bui.apptime() 365 since_last = current_time - self._last_press_time 366 if ( 367 since_last < 0.2 368 and self._last_press_color_name == color_name 369 and self._last_press_increasing == increasing 370 ): 371 self._change_speed += 0.25 372 else: 373 self._change_speed = 1.0 374 self._last_press_time = current_time 375 self._last_press_color_name = color_name 376 self._last_press_increasing = increasing 377 378 color_index = ('r', 'g', 'b').index(color_name) 379 offs = int(self._change_speed) * (0.01 if increasing else -0.01) 380 self._color[color_index] = max( 381 0.0, min(1.0, self._color[color_index] + offs) 382 ) 383 self._update_for_color() 384 385 def get_tag(self) -> Any: 386 """Return this popup's tag value.""" 387 return self._tag 388 389 def _transition_out(self) -> None: 390 # Kill our timer 391 self._hex_timer = None 392 if not self._transitioning_out: 393 self._transitioning_out = True 394 if self._delegate is not None: 395 self._delegate.color_picker_closing(self) 396 bui.containerwidget(edit=self.root_widget, transition='out_scale') 397 398 @override 399 def on_popup_cancel(self) -> None: 400 if not self._transitioning_out: 401 bui.getsound('swish').play() 402 self._transition_out() 403 404 405def hex_to_color(hex_color: str) -> tuple: 406 """Transforms an RGB / RGBA hex code into an rgb1/rgba1 tuple. 407 408 Args: 409 hex_color (str): The HEX color. 410 Raises: 411 ValueError: If the provided HEX color isn't 6 or 8 characters long. 412 Returns: 413 tuple: The color tuple divided by 255. 414 """ 415 # Remove the '#' from the string if provided. 416 if hex_color.startswith('#'): 417 hex_color = hex_color.lstrip('#') 418 # Check if this has a valid length. 419 hexlength = len(hex_color) 420 if not hexlength in [6, 8]: 421 raise ValueError(f'Invalid HEX color provided: "{hex_color}"') 422 423 # Convert the hex bytes to their true byte form. 424 ar, ag, ab, aa = ( 425 (int.from_bytes(bytes.fromhex(hex_color[0:2]))), 426 (int.from_bytes(bytes.fromhex(hex_color[2:4]))), 427 (int.from_bytes(bytes.fromhex(hex_color[4:6]))), 428 ( 429 (int.from_bytes(bytes.fromhex(hex_color[6:8]))) 430 if hexlength == 8 431 else None 432 ), 433 ) 434 # Divide all numbers by 255 and return. 435 nr, ng, nb, na = ( 436 x / 255 if x is not None else None for x in (ar, ag, ab, aa) 437 ) 438 return (nr, ng, nb, na) if aa is not None else (nr, ng, nb) 439 440 441def color_to_hex(r: float, g: float, b: float, a: float | None = 1.0) -> str: 442 """Converts an rgb1 tuple to a HEX color code. 443 444 Args: 445 r (float): Red. 446 g (float): Green. 447 b (float): Blue. 448 a (float, optional): Alpha. Defaults to 1.0. 449 450 Returns: 451 str: The hexified rgba values. 452 """ 453 # Turn our rgb1 to rgb255 454 nr, ng, nb, na = [ 455 int(min(255, x * 255)) if x is not None else x for x in [r, g, b, a] 456 ] 457 # Merge all values into their HEX representation. 458 hex_code = ( 459 f'#{nr:02x}{ng:02x}{nb:02x}{na:02x}' 460 if na is not None 461 else f'#{nr:02x}{ng:02x}{nb:02x}' 462 ) 463 return hex_code 464 465 466def color_overlay_func( 467 r: float, g: float, b: float, a: float | None = None 468) -> tuple: 469 """I could NOT come up with a better function name. 470 471 Args: 472 r (float): Red. 473 g (float): Green. 474 b (float): Blue. 475 a (float | None, optional): Alpha. Defaults to None. 476 477 Returns: 478 tuple: A brighter color if the provided one is dark, 479 and a darker one if it's darker. 480 """ 481 482 # Calculate the relative luminance using the formula for sRGB 483 # https://www.w3.org/TR/WCAG20/#relativeluminancedef 484 def relative_luminance(color: float) -> Any: 485 if color <= 0.03928: 486 return color / 12.92 487 return ((color + 0.055) / 1.055) ** 2.4 488 489 luminance = ( 490 0.2126 * relative_luminance(r) 491 + 0.7152 * relative_luminance(g) 492 + 0.0722 * relative_luminance(b) 493 ) 494 # Set our color multiplier depending on the provided color's luminance. 495 luminant = 1.65 if luminance < 0.33 else 0.2 496 # Multiply our given numbers, making sure 497 # they don't blend in the original bg. 498 avg = (0.7 - (r + g + b / 3)) + 0.15 499 r, g, b = [max(avg, x * luminant) for x in (r, g, b)] 500 # Include our alpha and ship it! 501 return (r, g, b, a) if a is not None else (r, g, b)
17class ColorPicker(PopupWindow): 18 """A popup UI to select from a set of colors. 19 20 Passes the color to the delegate's color_picker_selected_color() method. 21 """ 22 23 def __init__( 24 self, 25 parent: bui.Widget, 26 position: tuple[float, float], 27 initial_color: Sequence[float] = (1.0, 1.0, 1.0), 28 delegate: Any = None, 29 scale: float | None = None, 30 offset: tuple[float, float] = (0.0, 0.0), 31 tag: Any = '', 32 ): 33 # pylint: disable=too-many-locals 34 assert bui.app.classic is not None 35 36 c_raw = bui.app.classic.get_player_colors() 37 assert len(c_raw) == 16 38 self.colors = [c_raw[0:4], c_raw[4:8], c_raw[8:12], c_raw[12:16]] 39 40 uiscale = bui.app.ui_v1.uiscale 41 if scale is None: 42 scale = ( 43 2.3 44 if uiscale is bui.UIScale.SMALL 45 else 1.65 if uiscale is bui.UIScale.MEDIUM else 1.23 46 ) 47 self._parent = parent 48 self._position = position 49 self._scale = scale 50 self._offset = offset 51 self._delegate = delegate 52 self._transitioning_out = False 53 self._tag = tag 54 self._initial_color = initial_color 55 56 # Create our _root_widget. 57 super().__init__( 58 position=position, 59 size=(210, 240), 60 scale=scale, 61 focus_position=(10, 10), 62 focus_size=(190, 220), 63 bg_color=(0.5, 0.5, 0.5), 64 offset=offset, 65 ) 66 rows: list[list[bui.Widget]] = [] 67 closest_dist = 9999.0 68 closest = (0, 0) 69 for y in range(4): 70 row: list[bui.Widget] = [] 71 rows.append(row) 72 for x in range(4): 73 color = self.colors[y][x] 74 dist = ( 75 abs(color[0] - initial_color[0]) 76 + abs(color[1] - initial_color[1]) 77 + abs(color[2] - initial_color[2]) 78 ) 79 if dist < closest_dist: 80 closest = (x, y) 81 closest_dist = dist 82 btn = bui.buttonwidget( 83 parent=self.root_widget, 84 position=(22 + 45 * x, 185 - 45 * y), 85 size=(35, 40), 86 label='', 87 button_type='square', 88 on_activate_call=bui.WeakCall(self._select, x, y), 89 autoselect=True, 90 color=color, 91 extra_touch_border_scale=0.0, 92 ) 93 row.append(btn) 94 other_button = bui.buttonwidget( 95 parent=self.root_widget, 96 position=(105 - 60, 13), 97 color=(0.7, 0.7, 0.7), 98 text_scale=0.5, 99 textcolor=(0.8, 0.8, 0.8), 100 size=(120, 30), 101 label=bui.Lstr( 102 resource='otherText', 103 fallback_resource='coopSelectWindow.customText', 104 ), 105 autoselect=True, 106 on_activate_call=bui.WeakCall(self._select_other), 107 ) 108 109 # Custom colors are limited to pro currently. 110 assert bui.app.classic is not None 111 if not bui.app.classic.accounts.have_pro(): 112 bui.imagewidget( 113 parent=self.root_widget, 114 position=(50, 12), 115 size=(30, 30), 116 texture=bui.gettexture('lock'), 117 draw_controller=other_button, 118 ) 119 120 # If their color is close to one of our swatches, select it. 121 # Otherwise select 'other'. 122 if closest_dist < 0.03: 123 bui.containerwidget( 124 edit=self.root_widget, 125 selected_child=rows[closest[1]][closest[0]], 126 ) 127 else: 128 bui.containerwidget( 129 edit=self.root_widget, selected_child=other_button 130 ) 131 132 def get_tag(self) -> Any: 133 """Return this popup's tag.""" 134 return self._tag 135 136 def _select_other(self) -> None: 137 from bauiv1lib import purchase 138 139 # Requires pro. 140 assert bui.app.classic is not None 141 if not bui.app.classic.accounts.have_pro(): 142 purchase.PurchaseWindow(items=['pro']) 143 self._transition_out() 144 return 145 ColorPickerExact( 146 parent=self._parent, 147 position=self._position, 148 initial_color=self._initial_color, 149 delegate=self._delegate, 150 scale=self._scale, 151 offset=self._offset, 152 tag=self._tag, 153 ) 154 155 # New picker now 'owns' the delegate; we shouldn't send it any 156 # more messages. 157 self._delegate = None 158 self._transition_out() 159 160 def _select(self, x: int, y: int) -> None: 161 if self._delegate: 162 self._delegate.color_picker_selected_color(self, self.colors[y][x]) 163 bui.apptimer(0.05, self._transition_out) 164 165 def _transition_out(self) -> None: 166 if not self._transitioning_out: 167 self._transitioning_out = True 168 if self._delegate is not None: 169 self._delegate.color_picker_closing(self) 170 bui.containerwidget(edit=self.root_widget, transition='out_scale') 171 172 @override 173 def on_popup_cancel(self) -> None: 174 if not self._transitioning_out: 175 bui.getsound('swish').play() 176 self._transition_out()
A popup UI to select from a set of colors.
Passes the color to the delegate's color_picker_selected_color() method.
23 def __init__( 24 self, 25 parent: bui.Widget, 26 position: tuple[float, float], 27 initial_color: Sequence[float] = (1.0, 1.0, 1.0), 28 delegate: Any = None, 29 scale: float | None = None, 30 offset: tuple[float, float] = (0.0, 0.0), 31 tag: Any = '', 32 ): 33 # pylint: disable=too-many-locals 34 assert bui.app.classic is not None 35 36 c_raw = bui.app.classic.get_player_colors() 37 assert len(c_raw) == 16 38 self.colors = [c_raw[0:4], c_raw[4:8], c_raw[8:12], c_raw[12:16]] 39 40 uiscale = bui.app.ui_v1.uiscale 41 if scale is None: 42 scale = ( 43 2.3 44 if uiscale is bui.UIScale.SMALL 45 else 1.65 if uiscale is bui.UIScale.MEDIUM else 1.23 46 ) 47 self._parent = parent 48 self._position = position 49 self._scale = scale 50 self._offset = offset 51 self._delegate = delegate 52 self._transitioning_out = False 53 self._tag = tag 54 self._initial_color = initial_color 55 56 # Create our _root_widget. 57 super().__init__( 58 position=position, 59 size=(210, 240), 60 scale=scale, 61 focus_position=(10, 10), 62 focus_size=(190, 220), 63 bg_color=(0.5, 0.5, 0.5), 64 offset=offset, 65 ) 66 rows: list[list[bui.Widget]] = [] 67 closest_dist = 9999.0 68 closest = (0, 0) 69 for y in range(4): 70 row: list[bui.Widget] = [] 71 rows.append(row) 72 for x in range(4): 73 color = self.colors[y][x] 74 dist = ( 75 abs(color[0] - initial_color[0]) 76 + abs(color[1] - initial_color[1]) 77 + abs(color[2] - initial_color[2]) 78 ) 79 if dist < closest_dist: 80 closest = (x, y) 81 closest_dist = dist 82 btn = bui.buttonwidget( 83 parent=self.root_widget, 84 position=(22 + 45 * x, 185 - 45 * y), 85 size=(35, 40), 86 label='', 87 button_type='square', 88 on_activate_call=bui.WeakCall(self._select, x, y), 89 autoselect=True, 90 color=color, 91 extra_touch_border_scale=0.0, 92 ) 93 row.append(btn) 94 other_button = bui.buttonwidget( 95 parent=self.root_widget, 96 position=(105 - 60, 13), 97 color=(0.7, 0.7, 0.7), 98 text_scale=0.5, 99 textcolor=(0.8, 0.8, 0.8), 100 size=(120, 30), 101 label=bui.Lstr( 102 resource='otherText', 103 fallback_resource='coopSelectWindow.customText', 104 ), 105 autoselect=True, 106 on_activate_call=bui.WeakCall(self._select_other), 107 ) 108 109 # Custom colors are limited to pro currently. 110 assert bui.app.classic is not None 111 if not bui.app.classic.accounts.have_pro(): 112 bui.imagewidget( 113 parent=self.root_widget, 114 position=(50, 12), 115 size=(30, 30), 116 texture=bui.gettexture('lock'), 117 draw_controller=other_button, 118 ) 119 120 # If their color is close to one of our swatches, select it. 121 # Otherwise select 'other'. 122 if closest_dist < 0.03: 123 bui.containerwidget( 124 edit=self.root_widget, 125 selected_child=rows[closest[1]][closest[0]], 126 ) 127 else: 128 bui.containerwidget( 129 edit=self.root_widget, selected_child=other_button 130 )
172 @override 173 def on_popup_cancel(self) -> None: 174 if not self._transitioning_out: 175 bui.getsound('swish').play() 176 self._transition_out()
Called when the popup is canceled.
Cancels can occur due to clicking outside the window, hitting escape, etc.
Inherited Members
179class ColorPickerExact(PopupWindow): 180 """pops up a ui to select from a set of colors. 181 passes the color to the delegate's color_picker_selected_color() method""" 182 183 def __init__( 184 self, 185 parent: bui.Widget, 186 position: tuple[float, float], 187 initial_color: Sequence[float] = (1.0, 1.0, 1.0), 188 delegate: Any = None, 189 scale: float | None = None, 190 offset: tuple[float, float] = (0.0, 0.0), 191 tag: Any = '', 192 ): 193 # pylint: disable=too-many-locals 194 del parent # Unused var. 195 assert bui.app.classic is not None 196 197 c_raw = bui.app.classic.get_player_colors() 198 assert len(c_raw) == 16 199 self.colors = [c_raw[0:4], c_raw[4:8], c_raw[8:12], c_raw[12:16]] 200 201 uiscale = bui.app.ui_v1.uiscale 202 if scale is None: 203 scale = ( 204 2.3 205 if uiscale is bui.UIScale.SMALL 206 else 1.65 if uiscale is bui.UIScale.MEDIUM else 1.23 207 ) 208 self._delegate = delegate 209 self._transitioning_out = False 210 self._tag = tag 211 self._color = list(initial_color) 212 self._last_press_time = bui.apptime() 213 self._last_press_color_name: str | None = None 214 self._last_press_increasing: bool | None = None 215 self._hex_timer: bui.AppTimer | None = None 216 self._hex_prev_text: str = '#FFFFFF' 217 self._change_speed = 1.0 218 width = 180.0 219 height = 240.0 220 221 # Creates our _root_widget. 222 super().__init__( 223 position=position, 224 size=(width, height), 225 scale=scale, 226 focus_position=(10, 10), 227 focus_size=(width - 20, height - 20), 228 bg_color=(0.5, 0.5, 0.5), 229 offset=offset, 230 ) 231 self._swatch = bui.imagewidget( 232 parent=self.root_widget, 233 position=(width * 0.5 - 65 + 5, height - 95), 234 size=(130, 115), 235 texture=bui.gettexture('clayStroke'), 236 color=(1, 0, 0), 237 ) 238 self._hex_textbox = bui.textwidget( 239 parent=self.root_widget, 240 position=(width * 0.5 - 37.5 + 3, height - 51), 241 max_chars=9, 242 text='#FFFFFF', 243 autoselect=True, 244 size=(75, 30), 245 v_align='center', 246 editable=True, 247 maxwidth=70, 248 allow_clear_button=False, 249 force_internal_editing=True, 250 glow_type='uniform', 251 ) 252 253 x = 50 254 y = height - 90 255 self._label_r: bui.Widget 256 self._label_g: bui.Widget 257 self._label_b: bui.Widget 258 for color_name, color_val in [ 259 ('r', (1, 0.15, 0.15)), 260 ('g', (0.15, 1, 0.15)), 261 ('b', (0.15, 0.15, 1)), 262 ]: 263 txt = bui.textwidget( 264 parent=self.root_widget, 265 position=(x - 10, y), 266 size=(0, 0), 267 h_align='center', 268 color=color_val, 269 v_align='center', 270 text='0.12', 271 ) 272 setattr(self, '_label_' + color_name, txt) 273 for b_label, bhval, binc in [('-', 30, False), ('+', 75, True)]: 274 bui.buttonwidget( 275 parent=self.root_widget, 276 position=(x + bhval, y - 15), 277 scale=0.8, 278 repeat=True, 279 text_scale=1.3, 280 size=(40, 40), 281 label=b_label, 282 autoselect=True, 283 enable_sound=False, 284 on_activate_call=bui.WeakCall( 285 self._color_change_press, color_name, binc 286 ), 287 ) 288 y -= 42 289 290 btn = bui.buttonwidget( 291 parent=self.root_widget, 292 position=(width * 0.5 - 40, 10), 293 size=(80, 30), 294 text_scale=0.6, 295 color=(0.6, 0.6, 0.6), 296 textcolor=(0.7, 0.7, 0.7), 297 label=bui.Lstr(resource='doneText'), 298 on_activate_call=bui.WeakCall(self._transition_out), 299 autoselect=True, 300 ) 301 bui.containerwidget(edit=self.root_widget, start_button=btn) 302 303 # Unlike the swatch picker, we stay open and constantly push our 304 # color to the delegate, so start doing that. 305 self._update_for_color() 306 307 # Update our HEX stuff! 308 self._update_for_hex() 309 self._hex_timer = bui.AppTimer(0.025, self._update_for_hex, repeat=True) 310 311 def _update_for_hex(self) -> None: 312 """Update for any HEX or color change.""" 313 from typing import cast 314 315 hextext = cast(str, bui.textwidget(query=self._hex_textbox)) 316 hexcolor: tuple 317 # Check if our current hex text doesn't match with our old one. 318 # Convert our current hex text into a color if possible. 319 if hextext != self._hex_prev_text: 320 try: 321 hexcolor = hex_to_color(hextext) 322 if len(hexcolor) == 4: 323 r, g, b, a = hexcolor 324 del a # unused 325 else: 326 r, g, b = hexcolor 327 # Replace the color! 328 for i, ch in enumerate((r, g, b)): 329 self._color[i] = max(0.0, min(1.0, ch)) 330 self._update_for_color() 331 # Usually, a ValueError will occur if the provided hex 332 # is incomplete, which occurs when in the midst of typing it. 333 except ValueError: 334 pass 335 # Store the current text for our next comparison. 336 self._hex_prev_text = hextext 337 338 # noinspection PyUnresolvedReferences 339 def _update_for_color(self) -> None: 340 if not self.root_widget: 341 return 342 bui.imagewidget(edit=self._swatch, color=self._color) 343 344 # We generate these procedurally, so pylint misses them. 345 # FIXME: create static attrs instead. 346 # pylint: disable=consider-using-f-string 347 bui.textwidget(edit=self._label_r, text='%.2f' % self._color[0]) 348 bui.textwidget(edit=self._label_g, text='%.2f' % self._color[1]) 349 bui.textwidget(edit=self._label_b, text='%.2f' % self._color[2]) 350 if self._delegate is not None: 351 self._delegate.color_picker_selected_color(self, self._color) 352 353 # Show the HEX code of this color. 354 r, g, b = self._color 355 hexcode = color_to_hex(r, g, b, None) 356 self._hex_prev_text = hexcode 357 bui.textwidget( 358 edit=self._hex_textbox, 359 text=hexcode, 360 color=color_overlay_func(r, g, b), 361 ) 362 363 def _color_change_press(self, color_name: str, increasing: bool) -> None: 364 # If we get rapid-fire presses, eventually start moving faster. 365 current_time = bui.apptime() 366 since_last = current_time - self._last_press_time 367 if ( 368 since_last < 0.2 369 and self._last_press_color_name == color_name 370 and self._last_press_increasing == increasing 371 ): 372 self._change_speed += 0.25 373 else: 374 self._change_speed = 1.0 375 self._last_press_time = current_time 376 self._last_press_color_name = color_name 377 self._last_press_increasing = increasing 378 379 color_index = ('r', 'g', 'b').index(color_name) 380 offs = int(self._change_speed) * (0.01 if increasing else -0.01) 381 self._color[color_index] = max( 382 0.0, min(1.0, self._color[color_index] + offs) 383 ) 384 self._update_for_color() 385 386 def get_tag(self) -> Any: 387 """Return this popup's tag value.""" 388 return self._tag 389 390 def _transition_out(self) -> None: 391 # Kill our timer 392 self._hex_timer = None 393 if not self._transitioning_out: 394 self._transitioning_out = True 395 if self._delegate is not None: 396 self._delegate.color_picker_closing(self) 397 bui.containerwidget(edit=self.root_widget, transition='out_scale') 398 399 @override 400 def on_popup_cancel(self) -> None: 401 if not self._transitioning_out: 402 bui.getsound('swish').play() 403 self._transition_out()
pops up a ui to select from a set of colors. passes the color to the delegate's color_picker_selected_color() method
183 def __init__( 184 self, 185 parent: bui.Widget, 186 position: tuple[float, float], 187 initial_color: Sequence[float] = (1.0, 1.0, 1.0), 188 delegate: Any = None, 189 scale: float | None = None, 190 offset: tuple[float, float] = (0.0, 0.0), 191 tag: Any = '', 192 ): 193 # pylint: disable=too-many-locals 194 del parent # Unused var. 195 assert bui.app.classic is not None 196 197 c_raw = bui.app.classic.get_player_colors() 198 assert len(c_raw) == 16 199 self.colors = [c_raw[0:4], c_raw[4:8], c_raw[8:12], c_raw[12:16]] 200 201 uiscale = bui.app.ui_v1.uiscale 202 if scale is None: 203 scale = ( 204 2.3 205 if uiscale is bui.UIScale.SMALL 206 else 1.65 if uiscale is bui.UIScale.MEDIUM else 1.23 207 ) 208 self._delegate = delegate 209 self._transitioning_out = False 210 self._tag = tag 211 self._color = list(initial_color) 212 self._last_press_time = bui.apptime() 213 self._last_press_color_name: str | None = None 214 self._last_press_increasing: bool | None = None 215 self._hex_timer: bui.AppTimer | None = None 216 self._hex_prev_text: str = '#FFFFFF' 217 self._change_speed = 1.0 218 width = 180.0 219 height = 240.0 220 221 # Creates our _root_widget. 222 super().__init__( 223 position=position, 224 size=(width, height), 225 scale=scale, 226 focus_position=(10, 10), 227 focus_size=(width - 20, height - 20), 228 bg_color=(0.5, 0.5, 0.5), 229 offset=offset, 230 ) 231 self._swatch = bui.imagewidget( 232 parent=self.root_widget, 233 position=(width * 0.5 - 65 + 5, height - 95), 234 size=(130, 115), 235 texture=bui.gettexture('clayStroke'), 236 color=(1, 0, 0), 237 ) 238 self._hex_textbox = bui.textwidget( 239 parent=self.root_widget, 240 position=(width * 0.5 - 37.5 + 3, height - 51), 241 max_chars=9, 242 text='#FFFFFF', 243 autoselect=True, 244 size=(75, 30), 245 v_align='center', 246 editable=True, 247 maxwidth=70, 248 allow_clear_button=False, 249 force_internal_editing=True, 250 glow_type='uniform', 251 ) 252 253 x = 50 254 y = height - 90 255 self._label_r: bui.Widget 256 self._label_g: bui.Widget 257 self._label_b: bui.Widget 258 for color_name, color_val in [ 259 ('r', (1, 0.15, 0.15)), 260 ('g', (0.15, 1, 0.15)), 261 ('b', (0.15, 0.15, 1)), 262 ]: 263 txt = bui.textwidget( 264 parent=self.root_widget, 265 position=(x - 10, y), 266 size=(0, 0), 267 h_align='center', 268 color=color_val, 269 v_align='center', 270 text='0.12', 271 ) 272 setattr(self, '_label_' + color_name, txt) 273 for b_label, bhval, binc in [('-', 30, False), ('+', 75, True)]: 274 bui.buttonwidget( 275 parent=self.root_widget, 276 position=(x + bhval, y - 15), 277 scale=0.8, 278 repeat=True, 279 text_scale=1.3, 280 size=(40, 40), 281 label=b_label, 282 autoselect=True, 283 enable_sound=False, 284 on_activate_call=bui.WeakCall( 285 self._color_change_press, color_name, binc 286 ), 287 ) 288 y -= 42 289 290 btn = bui.buttonwidget( 291 parent=self.root_widget, 292 position=(width * 0.5 - 40, 10), 293 size=(80, 30), 294 text_scale=0.6, 295 color=(0.6, 0.6, 0.6), 296 textcolor=(0.7, 0.7, 0.7), 297 label=bui.Lstr(resource='doneText'), 298 on_activate_call=bui.WeakCall(self._transition_out), 299 autoselect=True, 300 ) 301 bui.containerwidget(edit=self.root_widget, start_button=btn) 302 303 # Unlike the swatch picker, we stay open and constantly push our 304 # color to the delegate, so start doing that. 305 self._update_for_color() 306 307 # Update our HEX stuff! 308 self._update_for_hex() 309 self._hex_timer = bui.AppTimer(0.025, self._update_for_hex, repeat=True)
399 @override 400 def on_popup_cancel(self) -> None: 401 if not self._transitioning_out: 402 bui.getsound('swish').play() 403 self._transition_out()
Called when the popup is canceled.
Cancels can occur due to clicking outside the window, hitting escape, etc.
Inherited Members
406def hex_to_color(hex_color: str) -> tuple: 407 """Transforms an RGB / RGBA hex code into an rgb1/rgba1 tuple. 408 409 Args: 410 hex_color (str): The HEX color. 411 Raises: 412 ValueError: If the provided HEX color isn't 6 or 8 characters long. 413 Returns: 414 tuple: The color tuple divided by 255. 415 """ 416 # Remove the '#' from the string if provided. 417 if hex_color.startswith('#'): 418 hex_color = hex_color.lstrip('#') 419 # Check if this has a valid length. 420 hexlength = len(hex_color) 421 if not hexlength in [6, 8]: 422 raise ValueError(f'Invalid HEX color provided: "{hex_color}"') 423 424 # Convert the hex bytes to their true byte form. 425 ar, ag, ab, aa = ( 426 (int.from_bytes(bytes.fromhex(hex_color[0:2]))), 427 (int.from_bytes(bytes.fromhex(hex_color[2:4]))), 428 (int.from_bytes(bytes.fromhex(hex_color[4:6]))), 429 ( 430 (int.from_bytes(bytes.fromhex(hex_color[6:8]))) 431 if hexlength == 8 432 else None 433 ), 434 ) 435 # Divide all numbers by 255 and return. 436 nr, ng, nb, na = ( 437 x / 255 if x is not None else None for x in (ar, ag, ab, aa) 438 ) 439 return (nr, ng, nb, na) if aa is not None else (nr, ng, nb)
Transforms an RGB / RGBA hex code into an rgb1/rgba1 tuple.
Args: hex_color (str): The HEX color. Raises: ValueError: If the provided HEX color isn't 6 or 8 characters long. Returns: tuple: The color tuple divided by 255.
442def color_to_hex(r: float, g: float, b: float, a: float | None = 1.0) -> str: 443 """Converts an rgb1 tuple to a HEX color code. 444 445 Args: 446 r (float): Red. 447 g (float): Green. 448 b (float): Blue. 449 a (float, optional): Alpha. Defaults to 1.0. 450 451 Returns: 452 str: The hexified rgba values. 453 """ 454 # Turn our rgb1 to rgb255 455 nr, ng, nb, na = [ 456 int(min(255, x * 255)) if x is not None else x for x in [r, g, b, a] 457 ] 458 # Merge all values into their HEX representation. 459 hex_code = ( 460 f'#{nr:02x}{ng:02x}{nb:02x}{na:02x}' 461 if na is not None 462 else f'#{nr:02x}{ng:02x}{nb:02x}' 463 ) 464 return hex_code
Converts an rgb1 tuple to a HEX color code.
Args: r (float): Red. g (float): Green. b (float): Blue. a (float, optional): Alpha. Defaults to 1.0.
Returns: str: The hexified rgba values.
467def color_overlay_func( 468 r: float, g: float, b: float, a: float | None = None 469) -> tuple: 470 """I could NOT come up with a better function name. 471 472 Args: 473 r (float): Red. 474 g (float): Green. 475 b (float): Blue. 476 a (float | None, optional): Alpha. Defaults to None. 477 478 Returns: 479 tuple: A brighter color if the provided one is dark, 480 and a darker one if it's darker. 481 """ 482 483 # Calculate the relative luminance using the formula for sRGB 484 # https://www.w3.org/TR/WCAG20/#relativeluminancedef 485 def relative_luminance(color: float) -> Any: 486 if color <= 0.03928: 487 return color / 12.92 488 return ((color + 0.055) / 1.055) ** 2.4 489 490 luminance = ( 491 0.2126 * relative_luminance(r) 492 + 0.7152 * relative_luminance(g) 493 + 0.0722 * relative_luminance(b) 494 ) 495 # Set our color multiplier depending on the provided color's luminance. 496 luminant = 1.65 if luminance < 0.33 else 0.2 497 # Multiply our given numbers, making sure 498 # they don't blend in the original bg. 499 avg = (0.7 - (r + g + b / 3)) + 0.15 500 r, g, b = [max(avg, x * luminant) for x in (r, g, b)] 501 # Include our alpha and ship it! 502 return (r, g, b, a) if a is not None else (r, g, b)
I could NOT come up with a better function name.
Args: r (float): Red. g (float): Green. b (float): Blue. a (float | None, optional): Alpha. Defaults to None.
Returns: tuple: A brighter color if the provided one is dark, and a darker one if it's darker.