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