diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e9f4499..f5aaa9f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ EraOSBeta!) - Added a UI for customizing Series Length in Teams and Points-to-Win in FFA (Thanks EraOSBeta!) +- Implemented HEX code support to the advanced color picker (Thanks 3alTemp!) ### 1.7.32 (build 21741, api 8, 2023-12-20) - Fixed a screen message that no one will ever see (Thanks vishal332008?...) diff --git a/src/assets/ba_data/python/bauiv1lib/colorpicker.py b/src/assets/ba_data/python/bauiv1lib/colorpicker.py index 30887432..b6bbd2fb 100644 --- a/src/assets/ba_data/python/bauiv1lib/colorpicker.py +++ b/src/assets/ba_data/python/bauiv1lib/colorpicker.py @@ -43,9 +43,7 @@ class ColorPicker(PopupWindow): scale = ( 2.3 if uiscale is bui.UIScale.SMALL - else 1.65 - if uiscale is bui.UIScale.MEDIUM - else 1.23 + else 1.65 if uiscale is bui.UIScale.MEDIUM else 1.23 ) self._parent = parent self._position = position @@ -206,9 +204,7 @@ class ColorPickerExact(PopupWindow): scale = ( 2.3 if uiscale is bui.UIScale.SMALL - else 1.65 - if uiscale is bui.UIScale.MEDIUM - else 1.23 + else 1.65 if uiscale is bui.UIScale.MEDIUM else 1.23 ) self._delegate = delegate self._transitioning_out = False @@ -217,6 +213,8 @@ class ColorPickerExact(PopupWindow): 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 @@ -233,11 +231,25 @@ class ColorPickerExact(PopupWindow): ) self._swatch = bui.imagewidget( parent=self.root_widget, - position=(width * 0.5 - 50, height - 70), - size=(100, 70), - texture=bui.gettexture('buttonSquare'), + 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', + # on_return_press_call=self._done, + autoselect=True, + size=(75, 30), + v_align='center', + editable=True, + maxwidth=70, + force_internal_editing=True, + ) + x = 50 y = height - 90 self._label_r: bui.Widget @@ -292,6 +304,33 @@ class ColorPickerExact(PopupWindow): # 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[float, float, float, float] + # 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) + 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: @@ -307,6 +346,16 @@ class ColorPickerExact(PopupWindow): 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() @@ -335,6 +384,8 @@ class ColorPickerExact(PopupWindow): 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: @@ -346,3 +397,100 @@ class ColorPickerExact(PopupWindow): if not self._transitioning_out: bui.getsound('swish').play() self._transition_out() + + +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 255 + ), + ) + # 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) + + +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 + + +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)