import pyray as rl import time from openpilot.system.ui.lib.application import gui_app PASSWORD_MASK_CHAR = "•" PASSWORD_MASK_DELAY = 1.5 # Seconds to show character before masking class InputBox: def __init__(self, max_text_size=255, password_mode=False): self._max_text_size = max_text_size self._input_text = "" self._cursor_position = 0 self._password_mode = password_mode self._blink_counter = 0 self._show_cursor = False self._last_key_pressed = 0 self._key_press_time = 0 self._repeat_delay = 30 self._repeat_rate = 4 self._text_offset = 0 self._visible_width = 0 self._last_char_time = 0 # Track when last character was added self._masked_length = 0 # How many characters are currently masked @property def text(self): return self._input_text @text.setter def text(self, value): self._input_text = value[: self._max_text_size] self._cursor_position = len(self._input_text) self._update_text_offset() def set_password_mode(self, password_mode): self._password_mode = password_mode def clear(self): self._input_text = '' self._cursor_position = 0 self._text_offset = 0 def set_cursor_position(self, position): """Set the cursor position and reset the blink counter.""" if 0 <= position <= len(self._input_text): self._cursor_position = position self._blink_counter = 0 self._show_cursor = True self._update_text_offset() def _update_text_offset(self): """Ensure the cursor is visible by adjusting text offset.""" if self._visible_width == 0: return font = gui_app.font() display_text = self._get_display_text() padding = 10 if self._cursor_position > 0: cursor_x = rl.measure_text_ex(font, display_text[: self._cursor_position], self._font_size, 0).x else: cursor_x = 0 visible_width = self._visible_width - (padding * 2) # Adjust offset if cursor would be outside visible area if cursor_x < self._text_offset: self._text_offset = max(0, cursor_x - padding) elif cursor_x > self._text_offset + visible_width: self._text_offset = cursor_x - visible_width + padding def add_char_at_cursor(self, char): """Add a character at the current cursor position.""" if len(self._input_text) < self._max_text_size: self._input_text = self._input_text[: self._cursor_position] + char + self._input_text[self._cursor_position :] self.set_cursor_position(self._cursor_position + 1) if self._password_mode: self._last_char_time = time.time() return True return False def delete_char_before_cursor(self): """Delete the character before the cursor position (backspace).""" if self._cursor_position > 0: self._input_text = self._input_text[: self._cursor_position - 1] + self._input_text[self._cursor_position :] self.set_cursor_position(self._cursor_position - 1) return True return False def delete_char_at_cursor(self): """Delete the character at the cursor position (delete).""" if self._cursor_position < len(self._input_text): self._input_text = self._input_text[: self._cursor_position] + self._input_text[self._cursor_position + 1 :] self.set_cursor_position(self._cursor_position) return True return False def render(self, rect, color=rl.BLACK, border_color=rl.DARKGRAY, text_color=rl.WHITE, font_size=80): # Store dimensions for text offset calculations self._visible_width = rect.width self._font_size = font_size # Handle mouse input self._handle_mouse_input(rect, font_size) # Draw input box rl.draw_rectangle_rec(rect, color) # Process keyboard input self._handle_keyboard_input() # Update cursor blink self._blink_counter += 1 if self._blink_counter >= 30: self._show_cursor = not self._show_cursor self._blink_counter = 0 # Display text font = gui_app.font() display_text = self._get_display_text() padding = 10 # Clip text within input box bounds buffer = 2 rl.begin_scissor_mode(int(rect.x + padding - buffer), int(rect.y), int(rect.width - padding * 2 + buffer * 2), int(rect.height)) rl.draw_text_ex( font, display_text, rl.Vector2(int(rect.x + padding - self._text_offset), int(rect.y + rect.height / 2 - font_size / 2)), font_size, 0, text_color, ) # Draw cursor if self._show_cursor: cursor_x = rect.x + padding if len(display_text) > 0 and self._cursor_position > 0: cursor_x += rl.measure_text_ex(font, display_text[: self._cursor_position], font_size, 0).x # Apply text offset to cursor position cursor_x -= self._text_offset cursor_height = font_size + 4 cursor_y = rect.y + rect.height / 2 - cursor_height / 2 rl.draw_line(int(cursor_x), int(cursor_y), int(cursor_x), int(cursor_y + cursor_height), rl.WHITE) rl.end_scissor_mode() def _get_display_text(self): """Get text to display, applying password masking with delay if needed.""" if not self._password_mode: return self._input_text # Show character at last edited position if within delay window masked_text = PASSWORD_MASK_CHAR * len(self._input_text) recent_edit = time.time() - self._last_char_time < PASSWORD_MASK_DELAY if recent_edit and self._input_text: last_pos = max(0, self._cursor_position - 1) if last_pos < len(self._input_text): return masked_text[:last_pos] + self._input_text[last_pos] + masked_text[last_pos + 1 :] return masked_text def _handle_mouse_input(self, rect, font_size): """Handle mouse clicks to position cursor.""" mouse_pos = rl.get_mouse_position() if rl.is_mouse_button_pressed(rl.MOUSE_LEFT_BUTTON) and rl.check_collision_point_rec(mouse_pos, rect): # Calculate cursor position from click if len(self._input_text) > 0: font = gui_app.font() display_text = self._get_display_text() # Find the closest character position to the click relative_x = mouse_pos.x - (rect.x + 10) + self._text_offset best_pos = 0 min_distance = float('inf') for i in range(len(self._input_text) + 1): char_width = rl.measure_text_ex(font, display_text[:i], font_size, 0).x distance = abs(relative_x - char_width) if distance < min_distance: min_distance = distance best_pos = i self.set_cursor_position(best_pos) else: self.set_cursor_position(0) def _handle_keyboard_input(self): # Handle navigation keys key = rl.get_key_pressed() if key != 0: self._process_key(key) if key in (rl.KEY_LEFT, rl.KEY_RIGHT, rl.KEY_BACKSPACE, rl.KEY_DELETE): self._last_key_pressed = key self._key_press_time = 0 # Handle repeats for held keys elif self._last_key_pressed != 0: if rl.is_key_down(self._last_key_pressed): self._key_press_time += 1 if self._key_press_time > self._repeat_delay and self._key_press_time % self._repeat_rate == 0: self._process_key(self._last_key_pressed) else: self._last_key_pressed = 0 # Handle text input char = rl.get_char_pressed() if char != 0 and char >= 32: # Filter out control characters self.add_char_at_cursor(chr(char)) def _process_key(self, key): if key == rl.KEY_LEFT: if self._cursor_position > 0: self.set_cursor_position(self._cursor_position - 1) elif key == rl.KEY_RIGHT: if self._cursor_position < len(self._input_text): self.set_cursor_position(self._cursor_position + 1) elif key == rl.KEY_BACKSPACE: self.delete_char_before_cursor() elif key == rl.KEY_DELETE: self.delete_char_at_cursor() elif key == rl.KEY_HOME: self.set_cursor_position(0) elif key == rl.KEY_END: self.set_cursor_position(len(self._input_text))