openpilot/system/ui/widgets/button.py
Vehicle Researcher c5d5c5d1f3 openpilot v0.10.1 release
date: 2025-10-24T00:30:59
master commit: 405631baf9685e171a0dd19547cb763f1b163d18
2025-10-24 00:31:03 -07:00

172 lines
6.7 KiB
Python

from collections.abc import Callable
from enum import IntEnum
import pyray as rl
from openpilot.system.ui.lib.application import FontWeight, MousePos
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.label import Label
class ButtonStyle(IntEnum):
NORMAL = 0 # Most common, neutral buttons
PRIMARY = 1 # For main actions
DANGER = 2 # For critical actions, like reboot or delete
TRANSPARENT = 3 # For buttons with transparent background and border
TRANSPARENT_WHITE_TEXT = 9 # For buttons with transparent background and border and white text
TRANSPARENT_WHITE_BORDER = 10 # For buttons with transparent background and white border and text
ACTION = 4
LIST_ACTION = 5 # For list items with action buttons
NO_EFFECT = 6
KEYBOARD = 7
FORGET_WIFI = 8
ICON_PADDING = 15
DEFAULT_BUTTON_FONT_SIZE = 60
ACTION_BUTTON_FONT_SIZE = 48
BUTTON_TEXT_COLOR = {
ButtonStyle.NORMAL: rl.Color(228, 228, 228, 255),
ButtonStyle.PRIMARY: rl.Color(228, 228, 228, 255),
ButtonStyle.DANGER: rl.Color(228, 228, 228, 255),
ButtonStyle.TRANSPARENT: rl.BLACK,
ButtonStyle.TRANSPARENT_WHITE_TEXT: rl.WHITE,
ButtonStyle.TRANSPARENT_WHITE_BORDER: rl.Color(228, 228, 228, 255),
ButtonStyle.ACTION: rl.BLACK,
ButtonStyle.LIST_ACTION: rl.Color(228, 228, 228, 255),
ButtonStyle.NO_EFFECT: rl.Color(228, 228, 228, 255),
ButtonStyle.KEYBOARD: rl.Color(221, 221, 221, 255),
ButtonStyle.FORGET_WIFI: rl.Color(51, 51, 51, 255),
}
BUTTON_DISABLED_TEXT_COLORS = {
ButtonStyle.TRANSPARENT_WHITE_TEXT: rl.WHITE,
}
BUTTON_BACKGROUND_COLORS = {
ButtonStyle.NORMAL: rl.Color(51, 51, 51, 255),
ButtonStyle.PRIMARY: rl.Color(70, 91, 234, 255),
ButtonStyle.DANGER: rl.Color(226, 44, 44, 255),
ButtonStyle.TRANSPARENT: rl.BLACK,
ButtonStyle.TRANSPARENT_WHITE_TEXT: rl.BLANK,
ButtonStyle.TRANSPARENT_WHITE_BORDER: rl.BLACK,
ButtonStyle.ACTION: rl.Color(189, 189, 189, 255),
ButtonStyle.LIST_ACTION: rl.Color(57, 57, 57, 255),
ButtonStyle.NO_EFFECT: rl.Color(51, 51, 51, 255),
ButtonStyle.KEYBOARD: rl.Color(68, 68, 68, 255),
ButtonStyle.FORGET_WIFI: rl.Color(189, 189, 189, 255),
}
BUTTON_PRESSED_BACKGROUND_COLORS = {
ButtonStyle.NORMAL: rl.Color(74, 74, 74, 255),
ButtonStyle.PRIMARY: rl.Color(48, 73, 244, 255),
ButtonStyle.DANGER: rl.Color(255, 36, 36, 255),
ButtonStyle.TRANSPARENT: rl.BLACK,
ButtonStyle.TRANSPARENT_WHITE_TEXT: rl.BLANK,
ButtonStyle.TRANSPARENT_WHITE_BORDER: rl.BLANK,
ButtonStyle.ACTION: rl.Color(130, 130, 130, 255),
ButtonStyle.LIST_ACTION: rl.Color(74, 74, 74, 74),
ButtonStyle.NO_EFFECT: rl.Color(51, 51, 51, 255),
ButtonStyle.KEYBOARD: rl.Color(51, 51, 51, 255),
ButtonStyle.FORGET_WIFI: rl.Color(130, 130, 130, 255),
}
BUTTON_DISABLED_BACKGROUND_COLORS = {
ButtonStyle.TRANSPARENT_WHITE_TEXT: rl.BLANK,
}
class Button(Widget):
def __init__(self,
text: str | Callable[[], str],
click_callback: Callable[[], None] | None = None,
font_size: int = DEFAULT_BUTTON_FONT_SIZE,
font_weight: FontWeight = FontWeight.MEDIUM,
button_style: ButtonStyle = ButtonStyle.NORMAL,
border_radius: int = 10,
text_alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
text_padding: int = 20,
icon=None,
elide_right: bool = False,
multi_touch: bool = False,
):
super().__init__()
self._button_style = button_style
self._border_radius = border_radius
self._background_color = BUTTON_BACKGROUND_COLORS[self._button_style]
self._label = Label(text, font_size, font_weight, text_alignment, text_padding=text_padding,
text_color=BUTTON_TEXT_COLOR[self._button_style], icon=icon, elide_right=elide_right)
self._click_callback = click_callback
self._multi_touch = multi_touch
def set_text(self, text):
self._label.set_text(text)
def set_button_style(self, button_style: ButtonStyle):
self._button_style = button_style
self._background_color = BUTTON_BACKGROUND_COLORS[self._button_style]
self._label.set_text_color(BUTTON_TEXT_COLOR[self._button_style])
def _update_state(self):
if self.enabled:
self._label.set_text_color(BUTTON_TEXT_COLOR[self._button_style])
if self.is_pressed:
self._background_color = BUTTON_PRESSED_BACKGROUND_COLORS[self._button_style]
else:
self._background_color = BUTTON_BACKGROUND_COLORS[self._button_style]
elif self._button_style != ButtonStyle.NO_EFFECT:
self._background_color = BUTTON_DISABLED_BACKGROUND_COLORS.get(self._button_style, rl.Color(51, 51, 51, 255))
self._label.set_text_color(BUTTON_DISABLED_TEXT_COLORS.get(self._button_style, rl.Color(228, 228, 228, 51)))
def _render(self, _):
roundness = self._border_radius / (min(self._rect.width, self._rect.height) / 2)
if self._button_style == ButtonStyle.TRANSPARENT_WHITE_BORDER:
rl.draw_rectangle_rounded(self._rect, roundness, 10, rl.BLACK)
rl.draw_rectangle_rounded_lines_ex(self._rect, roundness, 10, 2, rl.WHITE)
else:
rl.draw_rectangle_rounded(self._rect, roundness, 10, self._background_color)
self._label.render(self._rect)
class ButtonRadio(Button):
def __init__(self,
text: str,
icon,
click_callback: Callable[[], None] | None = None,
font_size: int = DEFAULT_BUTTON_FONT_SIZE,
text_alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
border_radius: int = 10,
text_padding: int = 20,
):
super().__init__(text, click_callback=click_callback, font_size=font_size,
border_radius=border_radius, text_padding=text_padding,
text_alignment=text_alignment)
self._text_padding = text_padding
self._icon = icon
self.selected = False
def _handle_mouse_release(self, mouse_pos: MousePos):
super()._handle_mouse_release(mouse_pos)
self.selected = not self.selected
def _update_state(self):
if self.selected:
self._background_color = BUTTON_BACKGROUND_COLORS[ButtonStyle.PRIMARY]
else:
self._background_color = BUTTON_BACKGROUND_COLORS[ButtonStyle.NORMAL]
def _render(self, _):
roundness = self._border_radius / (min(self._rect.width, self._rect.height) / 2)
rl.draw_rectangle_rounded(self._rect, roundness, 10, self._background_color)
self._label.render(self._rect)
if self._icon and self.selected:
icon_y = self._rect.y + (self._rect.height - self._icon.height) / 2
icon_x = self._rect.x + self._rect.width - self._icon.width - self._text_padding - ICON_PADDING
rl.draw_texture_v(self._icon, rl.Vector2(icon_x, icon_y), rl.WHITE if self.enabled else rl.Color(255, 255, 255, 100))