bastd.ui.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
  9import ba
 10from bastd.ui.popup import PopupWindow
 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: ba.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        from ba.internal import get_player_colors
 34
 35        c_raw = 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 = ba.app.ui.uiscale
 40        if scale is None:
 41            scale = (
 42                2.3
 43                if uiscale is ba.UIScale.SMALL
 44                else 1.65
 45                if uiscale is ba.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        PopupWindow.__init__(
 59            self,
 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[ba.Widget]] = []
 69        closest_dist = 9999.0
 70        closest = (0, 0)
 71        for y in range(4):
 72            row: list[ba.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 = ba.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=ba.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 = ba.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=ba.Lstr(
104                resource='otherText',
105                fallback_resource='coopSelectWindow.customText',
106            ),
107            autoselect=True,
108            on_activate_call=ba.WeakCall(self._select_other),
109        )
110
111        # Custom colors are limited to pro currently.
112        if not ba.app.accounts_v1.have_pro():
113            ba.imagewidget(
114                parent=self.root_widget,
115                position=(50, 12),
116                size=(30, 30),
117                texture=ba.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            ba.containerwidget(
125                edit=self.root_widget,
126                selected_child=rows[closest[1]][closest[0]],
127            )
128        else:
129            ba.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 bastd.ui import purchase
139
140        # Requires pro.
141        if not ba.app.accounts_v1.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        ba.timer(0.05, self._transition_out, timetype=ba.TimeType.REAL)
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            ba.containerwidget(edit=self.root_widget, transition='out_scale')
171
172    def on_popup_cancel(self) -> None:
173        if not self._transitioning_out:
174            ba.playsound(ba.getsound('swish'))
175        self._transition_out()
176
177
178class ColorPickerExact(PopupWindow):
179    """pops up a ui to select from a set of colors.
180    passes the color to the delegate's color_picker_selected_color() method"""
181
182    def __init__(
183        self,
184        parent: ba.Widget,
185        position: tuple[float, float],
186        initial_color: Sequence[float] = (1.0, 1.0, 1.0),
187        delegate: Any = None,
188        scale: float | None = None,
189        offset: tuple[float, float] = (0.0, 0.0),
190        tag: Any = '',
191    ):
192        # pylint: disable=too-many-locals
193        del parent  # Unused var.
194        from ba.internal import get_player_colors
195
196        c_raw = get_player_colors()
197        assert len(c_raw) == 16
198        self.colors = [c_raw[0:4], c_raw[4:8], c_raw[8:12], c_raw[12:16]]
199
200        uiscale = ba.app.ui.uiscale
201        if scale is None:
202            scale = (
203                2.3
204                if uiscale is ba.UIScale.SMALL
205                else 1.65
206                if uiscale is ba.UIScale.MEDIUM
207                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 = ba.time(
214            ba.TimeType.REAL, ba.TimeFormat.MILLISECONDS
215        )
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        PopupWindow.__init__(
224            self,
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 = ba.imagewidget(
234            parent=self.root_widget,
235            position=(width * 0.5 - 50, height - 70),
236            size=(100, 70),
237            texture=ba.gettexture('buttonSquare'),
238            color=(1, 0, 0),
239        )
240        x = 50
241        y = height - 90
242        self._label_r: ba.Widget
243        self._label_g: ba.Widget
244        self._label_b: ba.Widget
245        for color_name, color_val in [
246            ('r', (1, 0.15, 0.15)),
247            ('g', (0.15, 1, 0.15)),
248            ('b', (0.15, 0.15, 1)),
249        ]:
250            txt = ba.textwidget(
251                parent=self.root_widget,
252                position=(x - 10, y),
253                size=(0, 0),
254                h_align='center',
255                color=color_val,
256                v_align='center',
257                text='0.12',
258            )
259            setattr(self, '_label_' + color_name, txt)
260            for b_label, bhval, binc in [('-', 30, False), ('+', 75, True)]:
261                ba.buttonwidget(
262                    parent=self.root_widget,
263                    position=(x + bhval, y - 15),
264                    scale=0.8,
265                    repeat=True,
266                    text_scale=1.3,
267                    size=(40, 40),
268                    label=b_label,
269                    autoselect=True,
270                    enable_sound=False,
271                    on_activate_call=ba.WeakCall(
272                        self._color_change_press, color_name, binc
273                    ),
274                )
275            y -= 42
276
277        btn = ba.buttonwidget(
278            parent=self.root_widget,
279            position=(width * 0.5 - 40, 10),
280            size=(80, 30),
281            text_scale=0.6,
282            color=(0.6, 0.6, 0.6),
283            textcolor=(0.7, 0.7, 0.7),
284            label=ba.Lstr(resource='doneText'),
285            on_activate_call=ba.WeakCall(self._transition_out),
286            autoselect=True,
287        )
288        ba.containerwidget(edit=self.root_widget, start_button=btn)
289
290        # Unlike the swatch picker, we stay open and constantly push our
291        # color to the delegate, so start doing that.
292        self._update_for_color()
293
294    # noinspection PyUnresolvedReferences
295    def _update_for_color(self) -> None:
296        if not self.root_widget:
297            return
298        ba.imagewidget(edit=self._swatch, color=self._color)
299
300        # We generate these procedurally, so pylint misses them.
301        # FIXME: create static attrs instead.
302        # pylint: disable=consider-using-f-string
303        ba.textwidget(edit=self._label_r, text='%.2f' % self._color[0])
304        ba.textwidget(edit=self._label_g, text='%.2f' % self._color[1])
305        ba.textwidget(edit=self._label_b, text='%.2f' % self._color[2])
306        if self._delegate is not None:
307            self._delegate.color_picker_selected_color(self, self._color)
308
309    def _color_change_press(self, color_name: str, increasing: bool) -> None:
310        # If we get rapid-fire presses, eventually start moving faster.
311        current_time = ba.time(ba.TimeType.REAL, ba.TimeFormat.MILLISECONDS)
312        since_last = current_time - self._last_press_time
313        if (
314            since_last < 200
315            and self._last_press_color_name == color_name
316            and self._last_press_increasing == increasing
317        ):
318            self._change_speed += 0.25
319        else:
320            self._change_speed = 1.0
321        self._last_press_time = current_time
322        self._last_press_color_name = color_name
323        self._last_press_increasing = increasing
324
325        color_index = ('r', 'g', 'b').index(color_name)
326        offs = int(self._change_speed) * (0.01 if increasing else -0.01)
327        self._color[color_index] = max(
328            0.0, min(1.0, self._color[color_index] + offs)
329        )
330        self._update_for_color()
331
332    def get_tag(self) -> Any:
333        """Return this popup's tag value."""
334        return self._tag
335
336    def _transition_out(self) -> None:
337        if not self._transitioning_out:
338            self._transitioning_out = True
339            if self._delegate is not None:
340                self._delegate.color_picker_closing(self)
341            ba.containerwidget(edit=self.root_widget, transition='out_scale')
342
343    def on_popup_cancel(self) -> None:
344        if not self._transitioning_out:
345            ba.playsound(ba.getsound('swish'))
346        self._transition_out()
class ColorPicker(bastd.ui.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: ba.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        from ba.internal import get_player_colors
 35
 36        c_raw = 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 = ba.app.ui.uiscale
 41        if scale is None:
 42            scale = (
 43                2.3
 44                if uiscale is ba.UIScale.SMALL
 45                else 1.65
 46                if uiscale is ba.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        PopupWindow.__init__(
 60            self,
 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[ba.Widget]] = []
 70        closest_dist = 9999.0
 71        closest = (0, 0)
 72        for y in range(4):
 73            row: list[ba.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 = ba.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=ba.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 = ba.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=ba.Lstr(
105                resource='otherText',
106                fallback_resource='coopSelectWindow.customText',
107            ),
108            autoselect=True,
109            on_activate_call=ba.WeakCall(self._select_other),
110        )
111
112        # Custom colors are limited to pro currently.
113        if not ba.app.accounts_v1.have_pro():
114            ba.imagewidget(
115                parent=self.root_widget,
116                position=(50, 12),
117                size=(30, 30),
118                texture=ba.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            ba.containerwidget(
126                edit=self.root_widget,
127                selected_child=rows[closest[1]][closest[0]],
128            )
129        else:
130            ba.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 bastd.ui import purchase
140
141        # Requires pro.
142        if not ba.app.accounts_v1.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        ba.timer(0.05, self._transition_out, timetype=ba.TimeType.REAL)
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            ba.containerwidget(edit=self.root_widget, transition='out_scale')
172
173    def on_popup_cancel(self) -> None:
174        if not self._transitioning_out:
175            ba.playsound(ba.getsound('swish'))
176        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: _ba.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: ba.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        from ba.internal import get_player_colors
 35
 36        c_raw = 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 = ba.app.ui.uiscale
 41        if scale is None:
 42            scale = (
 43                2.3
 44                if uiscale is ba.UIScale.SMALL
 45                else 1.65
 46                if uiscale is ba.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        PopupWindow.__init__(
 60            self,
 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[ba.Widget]] = []
 70        closest_dist = 9999.0
 71        closest = (0, 0)
 72        for y in range(4):
 73            row: list[ba.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 = ba.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=ba.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 = ba.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=ba.Lstr(
105                resource='otherText',
106                fallback_resource='coopSelectWindow.customText',
107            ),
108            autoselect=True,
109            on_activate_call=ba.WeakCall(self._select_other),
110        )
111
112        # Custom colors are limited to pro currently.
113        if not ba.app.accounts_v1.have_pro():
114            ba.imagewidget(
115                parent=self.root_widget,
116                position=(50, 12),
117                size=(30, 30),
118                texture=ba.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            ba.containerwidget(
126                edit=self.root_widget,
127                selected_child=rows[closest[1]][closest[0]],
128            )
129        else:
130            ba.containerwidget(
131                edit=self.root_widget, selected_child=other_button
132            )
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:
173    def on_popup_cancel(self) -> None:
174        if not self._transitioning_out:
175            ba.playsound(ba.getsound('swish'))
176        self._transition_out()

Called when the popup is canceled.

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

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

Return this popup's tag value.

def on_popup_cancel(self) -> None:
344    def on_popup_cancel(self) -> None:
345        if not self._transitioning_out:
346            ba.playsound(ba.getsound('swish'))
347        self._transition_out()

Called when the popup is canceled.

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