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
  8
  9from bauiv1lib.popup import PopupWindow
 10import bauiv1 as bui
 11
 12if TYPE_CHECKING:
 13    from typing import Any, Sequence
 14
 15
 16class ColorPicker(PopupWindow):
 17    """A popup UI to select from a set of colors.
 18
 19    Passes the color to the delegate's color_picker_selected_color() method.
 20    """
 21
 22    def __init__(
 23        self,
 24        parent: bui.Widget,
 25        position: tuple[float, float],
 26        initial_color: Sequence[float] = (1.0, 1.0, 1.0),
 27        delegate: Any = None,
 28        scale: float | None = None,
 29        offset: tuple[float, float] = (0.0, 0.0),
 30        tag: Any = '',
 31    ):
 32        # pylint: disable=too-many-locals
 33        assert bui.app.classic is not None
 34
 35        c_raw = bui.app.classic.get_player_colors()
 36        assert len(c_raw) == 16
 37        self.colors = [c_raw[0:4], c_raw[4:8], c_raw[8:12], c_raw[12:16]]
 38
 39        uiscale = bui.app.ui_v1.uiscale
 40        if scale is None:
 41            scale = (
 42                2.3
 43                if uiscale is bui.UIScale.SMALL
 44                else 1.65
 45                if uiscale is bui.UIScale.MEDIUM
 46                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        # Custom colors are limited to pro currently.
111        assert bui.app.classic is not None
112        if 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 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    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
207                if uiscale is bui.UIScale.MEDIUM
208                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._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 - 50, height - 70),
234            size=(100, 70),
235            texture=bui.gettexture('buttonSquare'),
236            color=(1, 0, 0),
237        )
238        x = 50
239        y = height - 90
240        self._label_r: bui.Widget
241        self._label_g: bui.Widget
242        self._label_b: bui.Widget
243        for color_name, color_val in [
244            ('r', (1, 0.15, 0.15)),
245            ('g', (0.15, 1, 0.15)),
246            ('b', (0.15, 0.15, 1)),
247        ]:
248            txt = bui.textwidget(
249                parent=self.root_widget,
250                position=(x - 10, y),
251                size=(0, 0),
252                h_align='center',
253                color=color_val,
254                v_align='center',
255                text='0.12',
256            )
257            setattr(self, '_label_' + color_name, txt)
258            for b_label, bhval, binc in [('-', 30, False), ('+', 75, True)]:
259                bui.buttonwidget(
260                    parent=self.root_widget,
261                    position=(x + bhval, y - 15),
262                    scale=0.8,
263                    repeat=True,
264                    text_scale=1.3,
265                    size=(40, 40),
266                    label=b_label,
267                    autoselect=True,
268                    enable_sound=False,
269                    on_activate_call=bui.WeakCall(
270                        self._color_change_press, color_name, binc
271                    ),
272                )
273            y -= 42
274
275        btn = bui.buttonwidget(
276            parent=self.root_widget,
277            position=(width * 0.5 - 40, 10),
278            size=(80, 30),
279            text_scale=0.6,
280            color=(0.6, 0.6, 0.6),
281            textcolor=(0.7, 0.7, 0.7),
282            label=bui.Lstr(resource='doneText'),
283            on_activate_call=bui.WeakCall(self._transition_out),
284            autoselect=True,
285        )
286        bui.containerwidget(edit=self.root_widget, start_button=btn)
287
288        # Unlike the swatch picker, we stay open and constantly push our
289        # color to the delegate, so start doing that.
290        self._update_for_color()
291
292    # noinspection PyUnresolvedReferences
293    def _update_for_color(self) -> None:
294        if not self.root_widget:
295            return
296        bui.imagewidget(edit=self._swatch, color=self._color)
297
298        # We generate these procedurally, so pylint misses them.
299        # FIXME: create static attrs instead.
300        # pylint: disable=consider-using-f-string
301        bui.textwidget(edit=self._label_r, text='%.2f' % self._color[0])
302        bui.textwidget(edit=self._label_g, text='%.2f' % self._color[1])
303        bui.textwidget(edit=self._label_b, text='%.2f' % self._color[2])
304        if self._delegate is not None:
305            self._delegate.color_picker_selected_color(self, self._color)
306
307    def _color_change_press(self, color_name: str, increasing: bool) -> None:
308        # If we get rapid-fire presses, eventually start moving faster.
309        current_time = bui.apptime()
310        since_last = current_time - self._last_press_time
311        if (
312            since_last < 0.2
313            and self._last_press_color_name == color_name
314            and self._last_press_increasing == increasing
315        ):
316            self._change_speed += 0.25
317        else:
318            self._change_speed = 1.0
319        self._last_press_time = current_time
320        self._last_press_color_name = color_name
321        self._last_press_increasing = increasing
322
323        color_index = ('r', 'g', 'b').index(color_name)
324        offs = int(self._change_speed) * (0.01 if increasing else -0.01)
325        self._color[color_index] = max(
326            0.0, min(1.0, self._color[color_index] + offs)
327        )
328        self._update_for_color()
329
330    def get_tag(self) -> Any:
331        """Return this popup's tag value."""
332        return self._tag
333
334    def _transition_out(self) -> None:
335        if not self._transitioning_out:
336            self._transitioning_out = True
337            if self._delegate is not None:
338                self._delegate.color_picker_closing(self)
339            bui.containerwidget(edit=self.root_widget, transition='out_scale')
340
341    def on_popup_cancel(self) -> None:
342        if not self._transitioning_out:
343            bui.getsound('swish').play()
344        self._transition_out()
class ColorPicker(bauiv1lib.popup.PopupWindow):
 17class ColorPicker(PopupWindow):
 18    """A popup UI to select from a set of colors.
 19
 20    Passes the color to the delegate's color_picker_selected_color() method.
 21    """
 22
 23    def __init__(
 24        self,
 25        parent: bui.Widget,
 26        position: tuple[float, float],
 27        initial_color: Sequence[float] = (1.0, 1.0, 1.0),
 28        delegate: Any = None,
 29        scale: float | None = None,
 30        offset: tuple[float, float] = (0.0, 0.0),
 31        tag: Any = '',
 32    ):
 33        # pylint: disable=too-many-locals
 34        assert bui.app.classic is not None
 35
 36        c_raw = bui.app.classic.get_player_colors()
 37        assert len(c_raw) == 16
 38        self.colors = [c_raw[0:4], c_raw[4:8], c_raw[8:12], c_raw[12:16]]
 39
 40        uiscale = bui.app.ui_v1.uiscale
 41        if scale is None:
 42            scale = (
 43                2.3
 44                if uiscale is bui.UIScale.SMALL
 45                else 1.65
 46                if uiscale is bui.UIScale.MEDIUM
 47                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        # Custom colors are limited to pro currently.
112        assert bui.app.classic is not None
113        if 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 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    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.

ColorPicker( parent: _bauiv1.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 = '')
 23    def __init__(
 24        self,
 25        parent: bui.Widget,
 26        position: tuple[float, float],
 27        initial_color: Sequence[float] = (1.0, 1.0, 1.0),
 28        delegate: Any = None,
 29        scale: float | None = None,
 30        offset: tuple[float, float] = (0.0, 0.0),
 31        tag: Any = '',
 32    ):
 33        # pylint: disable=too-many-locals
 34        assert bui.app.classic is not None
 35
 36        c_raw = bui.app.classic.get_player_colors()
 37        assert len(c_raw) == 16
 38        self.colors = [c_raw[0:4], c_raw[4:8], c_raw[8:12], c_raw[12:16]]
 39
 40        uiscale = bui.app.ui_v1.uiscale
 41        if scale is None:
 42            scale = (
 43                2.3
 44                if uiscale is bui.UIScale.SMALL
 45                else 1.65
 46                if uiscale is bui.UIScale.MEDIUM
 47                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        # Custom colors are limited to pro currently.
112        assert bui.app.classic is not None
113        if 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            )
colors
def get_tag(self) -> Any:
134    def get_tag(self) -> Any:
135        """Return this popup's tag."""
136        return self._tag

Return this popup's tag.

def on_popup_cancel(self) -> None:
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.

class ColorPickerExact(bauiv1lib.popup.PopupWindow):
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
208                if uiscale is bui.UIScale.MEDIUM
209                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._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 - 50, height - 70),
235            size=(100, 70),
236            texture=bui.gettexture('buttonSquare'),
237            color=(1, 0, 0),
238        )
239        x = 50
240        y = height - 90
241        self._label_r: bui.Widget
242        self._label_g: bui.Widget
243        self._label_b: bui.Widget
244        for color_name, color_val in [
245            ('r', (1, 0.15, 0.15)),
246            ('g', (0.15, 1, 0.15)),
247            ('b', (0.15, 0.15, 1)),
248        ]:
249            txt = bui.textwidget(
250                parent=self.root_widget,
251                position=(x - 10, y),
252                size=(0, 0),
253                h_align='center',
254                color=color_val,
255                v_align='center',
256                text='0.12',
257            )
258            setattr(self, '_label_' + color_name, txt)
259            for b_label, bhval, binc in [('-', 30, False), ('+', 75, True)]:
260                bui.buttonwidget(
261                    parent=self.root_widget,
262                    position=(x + bhval, y - 15),
263                    scale=0.8,
264                    repeat=True,
265                    text_scale=1.3,
266                    size=(40, 40),
267                    label=b_label,
268                    autoselect=True,
269                    enable_sound=False,
270                    on_activate_call=bui.WeakCall(
271                        self._color_change_press, color_name, binc
272                    ),
273                )
274            y -= 42
275
276        btn = bui.buttonwidget(
277            parent=self.root_widget,
278            position=(width * 0.5 - 40, 10),
279            size=(80, 30),
280            text_scale=0.6,
281            color=(0.6, 0.6, 0.6),
282            textcolor=(0.7, 0.7, 0.7),
283            label=bui.Lstr(resource='doneText'),
284            on_activate_call=bui.WeakCall(self._transition_out),
285            autoselect=True,
286        )
287        bui.containerwidget(edit=self.root_widget, start_button=btn)
288
289        # Unlike the swatch picker, we stay open and constantly push our
290        # color to the delegate, so start doing that.
291        self._update_for_color()
292
293    # noinspection PyUnresolvedReferences
294    def _update_for_color(self) -> None:
295        if not self.root_widget:
296            return
297        bui.imagewidget(edit=self._swatch, color=self._color)
298
299        # We generate these procedurally, so pylint misses them.
300        # FIXME: create static attrs instead.
301        # pylint: disable=consider-using-f-string
302        bui.textwidget(edit=self._label_r, text='%.2f' % self._color[0])
303        bui.textwidget(edit=self._label_g, text='%.2f' % self._color[1])
304        bui.textwidget(edit=self._label_b, text='%.2f' % self._color[2])
305        if self._delegate is not None:
306            self._delegate.color_picker_selected_color(self, self._color)
307
308    def _color_change_press(self, color_name: str, increasing: bool) -> None:
309        # If we get rapid-fire presses, eventually start moving faster.
310        current_time = bui.apptime()
311        since_last = current_time - self._last_press_time
312        if (
313            since_last < 0.2
314            and self._last_press_color_name == color_name
315            and self._last_press_increasing == increasing
316        ):
317            self._change_speed += 0.25
318        else:
319            self._change_speed = 1.0
320        self._last_press_time = current_time
321        self._last_press_color_name = color_name
322        self._last_press_increasing = increasing
323
324        color_index = ('r', 'g', 'b').index(color_name)
325        offs = int(self._change_speed) * (0.01 if increasing else -0.01)
326        self._color[color_index] = max(
327            0.0, min(1.0, self._color[color_index] + offs)
328        )
329        self._update_for_color()
330
331    def get_tag(self) -> Any:
332        """Return this popup's tag value."""
333        return self._tag
334
335    def _transition_out(self) -> None:
336        if not self._transitioning_out:
337            self._transitioning_out = True
338            if self._delegate is not None:
339                self._delegate.color_picker_closing(self)
340            bui.containerwidget(edit=self.root_widget, transition='out_scale')
341
342    def on_popup_cancel(self) -> None:
343        if not self._transitioning_out:
344            bui.getsound('swish').play()
345        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

ColorPickerExact( parent: _bauiv1.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 = '')
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
208                if uiscale is bui.UIScale.MEDIUM
209                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._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 - 50, height - 70),
235            size=(100, 70),
236            texture=bui.gettexture('buttonSquare'),
237            color=(1, 0, 0),
238        )
239        x = 50
240        y = height - 90
241        self._label_r: bui.Widget
242        self._label_g: bui.Widget
243        self._label_b: bui.Widget
244        for color_name, color_val in [
245            ('r', (1, 0.15, 0.15)),
246            ('g', (0.15, 1, 0.15)),
247            ('b', (0.15, 0.15, 1)),
248        ]:
249            txt = bui.textwidget(
250                parent=self.root_widget,
251                position=(x - 10, y),
252                size=(0, 0),
253                h_align='center',
254                color=color_val,
255                v_align='center',
256                text='0.12',
257            )
258            setattr(self, '_label_' + color_name, txt)
259            for b_label, bhval, binc in [('-', 30, False), ('+', 75, True)]:
260                bui.buttonwidget(
261                    parent=self.root_widget,
262                    position=(x + bhval, y - 15),
263                    scale=0.8,
264                    repeat=True,
265                    text_scale=1.3,
266                    size=(40, 40),
267                    label=b_label,
268                    autoselect=True,
269                    enable_sound=False,
270                    on_activate_call=bui.WeakCall(
271                        self._color_change_press, color_name, binc
272                    ),
273                )
274            y -= 42
275
276        btn = bui.buttonwidget(
277            parent=self.root_widget,
278            position=(width * 0.5 - 40, 10),
279            size=(80, 30),
280            text_scale=0.6,
281            color=(0.6, 0.6, 0.6),
282            textcolor=(0.7, 0.7, 0.7),
283            label=bui.Lstr(resource='doneText'),
284            on_activate_call=bui.WeakCall(self._transition_out),
285            autoselect=True,
286        )
287        bui.containerwidget(edit=self.root_widget, start_button=btn)
288
289        # Unlike the swatch picker, we stay open and constantly push our
290        # color to the delegate, so start doing that.
291        self._update_for_color()
colors
def get_tag(self) -> Any:
331    def get_tag(self) -> Any:
332        """Return this popup's tag value."""
333        return self._tag

Return this popup's tag value.

def on_popup_cancel(self) -> None:
342    def on_popup_cancel(self) -> None:
343        if not self._transitioning_out:
344            bui.getsound('swish').play()
345        self._transition_out()

Called when the popup is canceled.

Cancels can occur due to clicking outside the window, hitting escape, etc.