Source code for bauiv1lib.colorpicker

# Released under the MIT License. See LICENSE for details.
#
"""Provides popup windows for choosing colors."""

from __future__ import annotations

from typing import TYPE_CHECKING, override

from bauiv1lib.popup import PopupWindow
import bauiv1 as bui

if TYPE_CHECKING:
    from typing import Any, Sequence


[docs] class ColorPicker(PopupWindow): """A popup UI to select from a set of colors. Passes the color to the delegate's color_picker_selected_color() method. """ def __init__( self, parent: bui.Widget, position: tuple[float, float], initial_color: Sequence[float] = (1.0, 1.0, 1.0), delegate: Any = None, scale: float | None = None, offset: tuple[float, float] = (0.0, 0.0), tag: Any = '', ): # pylint: disable=too-many-locals assert bui.app.classic is not None c_raw = bui.app.classic.get_player_colors() assert len(c_raw) == 16 self.colors = [c_raw[0:4], c_raw[4:8], c_raw[8:12], c_raw[12:16]] uiscale = bui.app.ui_v1.uiscale if scale is None: scale = ( 2.3 if uiscale is bui.UIScale.SMALL else 1.65 if uiscale is bui.UIScale.MEDIUM else 1.23 ) self._parent = parent self._position = position self._scale = scale self._offset = offset self._delegate = delegate self._transitioning_out = False self._tag = tag self._initial_color = initial_color # Create our _root_widget. super().__init__( position=position, size=(210, 240), scale=scale, focus_position=(10, 10), focus_size=(190, 220), bg_color=(0.5, 0.5, 0.5), offset=offset, ) rows: list[list[bui.Widget]] = [] closest_dist = 9999.0 closest = (0, 0) for y in range(4): row: list[bui.Widget] = [] rows.append(row) for x in range(4): color = self.colors[y][x] dist = ( abs(color[0] - initial_color[0]) + abs(color[1] - initial_color[1]) + abs(color[2] - initial_color[2]) ) if dist < closest_dist: closest = (x, y) closest_dist = dist btn = bui.buttonwidget( parent=self.root_widget, position=(22 + 45 * x, 185 - 45 * y), size=(35, 40), label='', button_type='square', on_activate_call=bui.WeakCall(self._select, x, y), autoselect=True, color=color, extra_touch_border_scale=0.0, ) row.append(btn) other_button = bui.buttonwidget( parent=self.root_widget, position=(105 - 60, 13), color=(0.7, 0.7, 0.7), text_scale=0.5, textcolor=(0.8, 0.8, 0.8), size=(120, 30), label=bui.Lstr( resource='otherText', fallback_resource='coopSelectWindow.customText', ), autoselect=True, on_activate_call=bui.WeakCall(self._select_other), ) # Custom colors are limited to pro currently. assert bui.app.classic is not None if not bui.app.classic.accounts.have_pro(): bui.imagewidget( parent=self.root_widget, position=(50, 12), size=(30, 30), texture=bui.gettexture('lock'), draw_controller=other_button, ) # If their color is close to one of our swatches, select it. # Otherwise select 'other'. if closest_dist < 0.03: bui.containerwidget( edit=self.root_widget, selected_child=rows[closest[1]][closest[0]], ) else: bui.containerwidget( edit=self.root_widget, selected_child=other_button )
[docs] def get_tag(self) -> Any: """Return this popup's tag.""" return self._tag
def _select_other(self) -> None: from bauiv1lib import purchase # Requires pro. assert bui.app.classic is not None if not bui.app.classic.accounts.have_pro(): purchase.PurchaseWindow(items=['pro']) self._transition_out() return ColorPickerExact( parent=self._parent, position=self._position, initial_color=self._initial_color, delegate=self._delegate, scale=self._scale, offset=self._offset, tag=self._tag, ) # New picker now 'owns' the delegate; we shouldn't send it any # more messages. self._delegate = None self._transition_out() def _select(self, x: int, y: int) -> None: if self._delegate: self._delegate.color_picker_selected_color(self, self.colors[y][x]) bui.apptimer(0.05, self._transition_out) def _transition_out(self) -> None: if not self._transitioning_out: self._transitioning_out = True if self._delegate is not None: self._delegate.color_picker_closing(self) bui.containerwidget(edit=self.root_widget, transition='out_scale')
[docs] @override def on_popup_cancel(self) -> None: if not self._transitioning_out: bui.getsound('swish').play() self._transition_out()
[docs] class ColorPickerExact(PopupWindow): """pops up a ui to select from a set of colors. passes the color to the delegate's color_picker_selected_color() method""" def __init__( self, parent: bui.Widget, position: tuple[float, float], initial_color: Sequence[float] = (1.0, 1.0, 1.0), delegate: Any = None, scale: float | None = None, offset: tuple[float, float] = (0.0, 0.0), tag: Any = '', ): # pylint: disable=too-many-locals del parent # Unused var. assert bui.app.classic is not None c_raw = bui.app.classic.get_player_colors() assert len(c_raw) == 16 self.colors = [c_raw[0:4], c_raw[4:8], c_raw[8:12], c_raw[12:16]] uiscale = bui.app.ui_v1.uiscale if scale is None: scale = ( 2.3 if uiscale is bui.UIScale.SMALL else 1.65 if uiscale is bui.UIScale.MEDIUM else 1.23 ) self._delegate = delegate self._transitioning_out = False self._tag = tag self._color = list(initial_color) self._last_press_time = bui.apptime() self._last_press_color_name: str | None = None self._last_press_increasing: bool | None = None self._hex_timer: bui.AppTimer | None = None self._hex_prev_text: str = '#FFFFFF' self._change_speed = 1.0 width = 180.0 height = 240.0 # Creates our _root_widget. super().__init__( position=position, size=(width, height), scale=scale, focus_position=(10, 10), focus_size=(width - 20, height - 20), bg_color=(0.5, 0.5, 0.5), offset=offset, ) self._swatch = bui.imagewidget( parent=self.root_widget, position=(width * 0.5 - 65 + 5, height - 95), size=(130, 115), texture=bui.gettexture('clayStroke'), color=(1, 0, 0), ) self._hex_textbox = bui.textwidget( parent=self.root_widget, position=(width * 0.5 - 37.5 + 3, height - 51), max_chars=9, text='#FFFFFF', autoselect=True, size=(75, 30), v_align='center', editable=True, maxwidth=70, allow_clear_button=False, force_internal_editing=True, glow_type='uniform', ) x = 50 y = height - 90 self._label_r: bui.Widget self._label_g: bui.Widget self._label_b: bui.Widget for color_name, color_val in [ ('r', (1, 0.15, 0.15)), ('g', (0.15, 1, 0.15)), ('b', (0.15, 0.15, 1)), ]: txt = bui.textwidget( parent=self.root_widget, position=(x - 10, y), size=(0, 0), h_align='center', color=color_val, v_align='center', text='0.12', ) setattr(self, '_label_' + color_name, txt) for b_label, bhval, binc in [('-', 30, False), ('+', 75, True)]: bui.buttonwidget( parent=self.root_widget, position=(x + bhval, y - 15), scale=0.8, repeat=True, text_scale=1.3, size=(40, 40), label=b_label, autoselect=True, enable_sound=False, on_activate_call=bui.WeakCall( self._color_change_press, color_name, binc ), ) y -= 42 btn = bui.buttonwidget( parent=self.root_widget, position=(width * 0.5 - 40, 10), size=(80, 30), text_scale=0.6, color=(0.6, 0.6, 0.6), textcolor=(0.7, 0.7, 0.7), label=bui.Lstr(resource='doneText'), on_activate_call=bui.WeakCall(self._transition_out), autoselect=True, ) bui.containerwidget(edit=self.root_widget, start_button=btn) # Unlike the swatch picker, we stay open and constantly push our # color to the delegate, so start doing that. self._update_for_color() # Update our HEX stuff! self._update_for_hex() self._hex_timer = bui.AppTimer(0.025, self._update_for_hex, repeat=True) def _update_for_hex(self) -> None: """Update for any HEX or color change.""" from typing import cast hextext = cast(str, bui.textwidget(query=self._hex_textbox)) hexcolor: tuple # Check if our current hex text doesn't match with our old one. # Convert our current hex text into a color if possible. if hextext != self._hex_prev_text: try: hexcolor = hex_to_color(hextext) if len(hexcolor) == 4: r, g, b, a = hexcolor del a # unused else: r, g, b = hexcolor # Replace the color! for i, ch in enumerate((r, g, b)): self._color[i] = max(0.0, min(1.0, ch)) self._update_for_color() # Usually, a ValueError will occur if the provided hex # is incomplete, which occurs when in the midst of typing it. except ValueError: pass # Store the current text for our next comparison. self._hex_prev_text = hextext # noinspection PyUnresolvedReferences def _update_for_color(self) -> None: if not self.root_widget: return bui.imagewidget(edit=self._swatch, color=self._color) # We generate these procedurally, so pylint misses them. # FIXME: create static attrs instead. # pylint: disable=consider-using-f-string bui.textwidget(edit=self._label_r, text='%.2f' % self._color[0]) bui.textwidget(edit=self._label_g, text='%.2f' % self._color[1]) bui.textwidget(edit=self._label_b, text='%.2f' % self._color[2]) if self._delegate is not None: self._delegate.color_picker_selected_color(self, self._color) # Show the HEX code of this color. r, g, b = self._color hexcode = color_to_hex(r, g, b, None) self._hex_prev_text = hexcode bui.textwidget( edit=self._hex_textbox, text=hexcode, color=color_overlay_func(r, g, b), ) def _color_change_press(self, color_name: str, increasing: bool) -> None: # If we get rapid-fire presses, eventually start moving faster. current_time = bui.apptime() since_last = current_time - self._last_press_time if ( since_last < 0.2 and self._last_press_color_name == color_name and self._last_press_increasing == increasing ): self._change_speed += 0.25 else: self._change_speed = 1.0 self._last_press_time = current_time self._last_press_color_name = color_name self._last_press_increasing = increasing color_index = ('r', 'g', 'b').index(color_name) offs = int(self._change_speed) * (0.01 if increasing else -0.01) self._color[color_index] = max( 0.0, min(1.0, self._color[color_index] + offs) ) self._update_for_color()
[docs] def get_tag(self) -> Any: """Return this popup's tag value.""" return self._tag
def _transition_out(self) -> None: # Kill our timer self._hex_timer = None if not self._transitioning_out: self._transitioning_out = True if self._delegate is not None: self._delegate.color_picker_closing(self) bui.containerwidget(edit=self.root_widget, transition='out_scale')
[docs] @override def on_popup_cancel(self) -> None: if not self._transitioning_out: bui.getsound('swish').play() self._transition_out()
[docs] def hex_to_color(hex_color: str) -> tuple: """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. """ # Remove the '#' from the string if provided. if hex_color.startswith('#'): hex_color = hex_color.lstrip('#') # Check if this has a valid length. hexlength = len(hex_color) if not hexlength in [6, 8]: raise ValueError(f'Invalid HEX color provided: "{hex_color}"') # Convert the hex bytes to their true byte form. ar, ag, ab, aa = ( (int.from_bytes(bytes.fromhex(hex_color[0:2]))), (int.from_bytes(bytes.fromhex(hex_color[2:4]))), (int.from_bytes(bytes.fromhex(hex_color[4:6]))), ( (int.from_bytes(bytes.fromhex(hex_color[6:8]))) if hexlength == 8 else None ), ) # Divide all numbers by 255 and return. nr, ng, nb, na = ( x / 255 if x is not None else None for x in (ar, ag, ab, aa) ) return (nr, ng, nb, na) if aa is not None else (nr, ng, nb)
[docs] def color_to_hex(r: float, g: float, b: float, a: float | None = 1.0) -> str: """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. """ # Turn our rgb1 to rgb255 nr, ng, nb, na = [ int(min(255, x * 255)) if x is not None else x for x in [r, g, b, a] ] # Merge all values into their HEX representation. hex_code = ( f'#{nr:02x}{ng:02x}{nb:02x}{na:02x}' if na is not None else f'#{nr:02x}{ng:02x}{nb:02x}' ) return hex_code
[docs] def color_overlay_func( r: float, g: float, b: float, a: float | None = None ) -> tuple: """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. """ # Calculate the relative luminance using the formula for sRGB # https://www.w3.org/TR/WCAG20/#relativeluminancedef def relative_luminance(color: float) -> Any: if color <= 0.03928: return color / 12.92 return ((color + 0.055) / 1.055) ** 2.4 luminance = ( 0.2126 * relative_luminance(r) + 0.7152 * relative_luminance(g) + 0.0722 * relative_luminance(b) ) # Set our color multiplier depending on the provided color's luminance. luminant = 1.65 if luminance < 0.33 else 0.2 # Multiply our given numbers, making sure # they don't blend in the original bg. avg = (0.7 - (r + g + b / 3)) + 0.15 r, g, b = [max(avg, x * luminant) for x in (r, g, b)] # Include our alpha and ship it! return (r, g, b, a) if a is not None else (r, g, b)