import pyray as rl from openpilot.system.ui.lib.button import gui_button from openpilot.system.ui.lib.label import gui_label # Constants for special keys BACKSPACE_KEY = "<-" ENTER_KEY = "Enter" SPACE_KEY = " " SHIFT_KEY = "↑" SHIFT_DOWN_KEY = "↓" NUMERIC_KEY = "123" SYMBOL_KEY = "#+=" ABC_KEY = "ABC" # Define keyboard layouts as a dictionary for easier access keyboard_layouts = { "lowercase": [ ["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"], ["a", "s", "d", "f", "g", "h", "j", "k", "l"], [SHIFT_KEY, "z", "x", "c", "v", "b", "n", "m", BACKSPACE_KEY], [NUMERIC_KEY, "/", "-", SPACE_KEY, ".", ENTER_KEY], ], "uppercase": [ ["Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P"], ["A", "S", "D", "F", "G", "H", "J", "K", "L"], [SHIFT_DOWN_KEY, "Z", "X", "C", "V", "B", "N", "M", BACKSPACE_KEY], [NUMERIC_KEY, "/", "-", SPACE_KEY, ".", ENTER_KEY], ], "numbers": [ ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"], ["-", "/", ":", ";", "(", ")", "$", "&", "@", "\""], [SYMBOL_KEY, ".", ",", "?", "!", "`", BACKSPACE_KEY], [ABC_KEY, SPACE_KEY, ".", ENTER_KEY], ], "specials": [ ["[", "]", "{", "}", "#", "%", "^", "*", "+", "="], ["_", "\\", "|", "~", "<", ">", "€", "£", "¥", "•"], [NUMERIC_KEY, ".", ",", "?", "!", "'", BACKSPACE_KEY], [ABC_KEY, SPACE_KEY, ".", ENTER_KEY], ], } class Keyboard: def __init__(self, max_text_size: int = 255): self._layout = keyboard_layouts["lowercase"] self._max_text_size = max_text_size self._string_pointer = rl.ffi.new("char[]", max_text_size) self._input_text: str = '' self._clear() @property def text(self): result = rl.ffi.string(self._string_pointer).decode('utf-8') self._clear() return result def render(self, rect, title, sub_title): gui_label(rl.Rectangle(rect.x, rect.y, rect.width, 95), title, 90) gui_label(rl.Rectangle(rect.x, rect.y + 95, rect.width, 60), sub_title, 55, rl.GRAY) if gui_button(rl.Rectangle(rect.x + rect.width - 300, rect.y, 300, 100), "Cancel"): self._clear() return 0 # Text box for input self._sync_string_pointer() rl.gui_text_box( rl.Rectangle(rect.x, rect.y + 160, rect.width, 100), self._string_pointer, self._max_text_size, True ) self._input_text = rl.ffi.string(self._string_pointer).decode('utf-8') h_space, v_space = 15, 15 row_y_start = rect.y + 300 # Starting Y position for the first row key_height = (rect.height - 300 - 3 * v_space) / 4 key_max_width = (rect.width - (len(self._layout[2]) - 1) * h_space) / len(self._layout[2]) # Iterate over the rows of keys in the current layout for row, keys in enumerate(self._layout): key_width = min((rect.width - (180 if row == 1 else 0) - h_space * (len(keys) - 1)) / len(keys), key_max_width) start_x = rect.x + (90 if row == 1 else 0) for i, key in enumerate(keys): if i > 0: start_x += h_space new_width = ( (key_width * 3 + h_space * 2) if key == SPACE_KEY else (key_width * 2 + h_space if key == ENTER_KEY else key_width) ) key_rect = rl.Rectangle(start_x, row_y_start + row * (key_height + v_space), new_width, key_height) start_x += new_width if gui_button(key_rect, key): if key == ENTER_KEY: return 1 else: self.handle_key_press(key) return -1 def handle_key_press(self, key): if key in (SHIFT_DOWN_KEY, ABC_KEY): self._layout = keyboard_layouts["lowercase"] elif key == SHIFT_KEY: self._layout = keyboard_layouts["uppercase"] elif key == NUMERIC_KEY: self._layout = keyboard_layouts["numbers"] elif key == SYMBOL_KEY: self._layout = keyboard_layouts["specials"] elif key == BACKSPACE_KEY and len(self._input_text) > 0: self._input_text = self._input_text[:-1] elif key != BACKSPACE_KEY and len(self._input_text) < self._max_text_size: self._input_text += key def _clear(self): self._input_text = '' self._string_pointer[0] = b'\0' def _sync_string_pointer(self): """Sync the C-string pointer with the internal Python string.""" encoded = self._input_text.encode("utf-8")[:self._max_text_size - 1] # Leave room for null terminator buffer = rl.ffi.buffer(self._string_pointer) buffer[:len(encoded)] = encoded self._string_pointer[len(encoded)] = b'\0' # Null terminate