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