You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
			
				
					235 lines
				
				8.0 KiB
			
		
		
			
		
	
	
					235 lines
				
				8.0 KiB
			| 
											6 months ago
										 | import pyray as rl
 | ||
| 
											5 months ago
										 | import time
 | ||
| 
											5 months ago
										 | from openpilot.system.ui.lib.application import gui_app
 | ||
| 
											5 months ago
										 | from openpilot.system.ui.lib.text_measure import measure_text_cached
 | ||
| 
											4 months ago
										 | from openpilot.system.ui.widgets import Widget
 | ||
| 
											6 months ago
										 | 
 | ||
| 
											5 months ago
										 | PASSWORD_MASK_CHAR = "•"
 | ||
| 
											5 months ago
										 | PASSWORD_MASK_DELAY = 1.5  # Seconds to show character before masking
 | ||
| 
											5 months ago
										 | 
 | ||
|  | 
 | ||
| 
											5 months ago
										 | class InputBox(Widget):
 | ||
| 
											6 months ago
										 |   def __init__(self, max_text_size=255, password_mode=False):
 | ||
| 
											5 months ago
										 |     super().__init__()
 | ||
| 
											6 months ago
										 |     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
 | ||
| 
											5 months ago
										 |     self._repeat_rate = 4
 | ||
| 
											5 months ago
										 |     self._text_offset = 0
 | ||
|  |     self._visible_width = 0
 | ||
| 
											5 months ago
										 |     self._last_char_time = 0  # Track when last character was added
 | ||
| 
											5 months ago
										 |     self._masked_length = 0  # How many characters are currently masked
 | ||
| 
											6 months ago
										 | 
 | ||
|  |   @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)
 | ||
| 
											5 months ago
										 |     self._update_text_offset()
 | ||
| 
											6 months ago
										 | 
 | ||
|  |   def set_password_mode(self, password_mode):
 | ||
|  |     self._password_mode = password_mode
 | ||
|  | 
 | ||
|  |   def clear(self):
 | ||
|  |     self._input_text = ''
 | ||
|  |     self._cursor_position = 0
 | ||
| 
											5 months ago
										 |     self._text_offset = 0
 | ||
| 
											6 months ago
										 | 
 | ||
|  |   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
 | ||
| 
											5 months ago
										 |       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()
 | ||
| 
											5 months ago
										 |     display_text = self._get_display_text()
 | ||
| 
											5 months ago
										 |     padding = 10
 | ||
|  | 
 | ||
|  |     if self._cursor_position > 0:
 | ||
| 
											5 months ago
										 |       cursor_x = measure_text_cached(font, display_text[: self._cursor_position], self._font_size).x
 | ||
| 
											5 months ago
										 |     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
 | ||
| 
											6 months ago
										 | 
 | ||
|  |   def add_char_at_cursor(self, char):
 | ||
|  |     """Add a character at the current cursor position."""
 | ||
|  |     if len(self._input_text) < self._max_text_size:
 | ||
| 
											5 months ago
										 |       self._input_text = self._input_text[: self._cursor_position] + char + self._input_text[self._cursor_position:]
 | ||
| 
											6 months ago
										 |       self.set_cursor_position(self._cursor_position + 1)
 | ||
| 
											5 months ago
										 | 
 | ||
|  |       if self._password_mode:
 | ||
| 
											4 months ago
										 |         self._last_char_time = time.monotonic()
 | ||
| 
											5 months ago
										 | 
 | ||
| 
											6 months ago
										 |       return True
 | ||
|  |     return False
 | ||
|  | 
 | ||
|  |   def delete_char_before_cursor(self):
 | ||
|  |     """Delete the character before the cursor position (backspace)."""
 | ||
|  |     if self._cursor_position > 0:
 | ||
| 
											5 months ago
										 |       self._input_text = self._input_text[: self._cursor_position - 1] + self._input_text[self._cursor_position:]
 | ||
| 
											6 months ago
										 |       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):
 | ||
| 
											5 months ago
										 |       self._input_text = self._input_text[: self._cursor_position] + self._input_text[self._cursor_position + 1:]
 | ||
| 
											6 months ago
										 |       self.set_cursor_position(self._cursor_position)
 | ||
|  |       return True
 | ||
|  |     return False
 | ||
|  | 
 | ||
| 
											5 months ago
										 |   def _render(self, rect, color=rl.BLACK, border_color=rl.DARKGRAY, text_color=rl.WHITE, font_size=80):
 | ||
| 
											5 months ago
										 |     # Store dimensions for text offset calculations
 | ||
|  |     self._visible_width = rect.width
 | ||
|  |     self._font_size = font_size
 | ||
|  | 
 | ||
| 
											6 months ago
										 |     # 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()
 | ||
| 
											5 months ago
										 |     display_text = self._get_display_text()
 | ||
| 
											6 months ago
										 |     padding = 10
 | ||
| 
											5 months ago
										 | 
 | ||
|  |     # 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))
 | ||
| 
											6 months ago
										 |     rl.draw_text_ex(
 | ||
|  |       font,
 | ||
|  |       display_text,
 | ||
| 
											5 months ago
										 |       rl.Vector2(int(rect.x + padding - self._text_offset), int(rect.y + rect.height / 2 - font_size / 2)),
 | ||
| 
											6 months ago
										 |       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:
 | ||
| 
											5 months ago
										 |         cursor_x += measure_text_cached(font, display_text[: self._cursor_position], font_size).x
 | ||
| 
											6 months ago
										 | 
 | ||
| 
											5 months ago
										 |       # Apply text offset to cursor position
 | ||
|  |       cursor_x -= self._text_offset
 | ||
|  | 
 | ||
| 
											6 months ago
										 |       cursor_height = font_size + 4
 | ||
|  |       cursor_y = rect.y + rect.height / 2 - cursor_height / 2
 | ||
| 
											5 months ago
										 |       rl.draw_line(int(cursor_x), int(cursor_y), int(cursor_x), int(cursor_y + cursor_height), rl.WHITE)
 | ||
|  | 
 | ||
|  |     rl.end_scissor_mode()
 | ||
| 
											6 months ago
										 | 
 | ||
| 
											5 months ago
										 |   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)
 | ||
| 
											4 months ago
										 |     recent_edit = time.monotonic() - self._last_char_time < PASSWORD_MASK_DELAY
 | ||
| 
											5 months ago
										 |     if recent_edit and self._input_text:
 | ||
|  |       last_pos = max(0, self._cursor_position - 1)
 | ||
|  |       if last_pos < len(self._input_text):
 | ||
| 
											5 months ago
										 |         return masked_text[:last_pos] + self._input_text[last_pos] + masked_text[last_pos + 1:]
 | ||
| 
											5 months ago
										 | 
 | ||
|  |     return masked_text
 | ||
|  | 
 | ||
| 
											6 months ago
										 |   def _handle_mouse_input(self, rect, font_size):
 | ||
|  |     """Handle mouse clicks to position cursor."""
 | ||
|  |     mouse_pos = rl.get_mouse_position()
 | ||
| 
											5 months ago
										 |     if rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT) and rl.check_collision_point_rec(mouse_pos, rect):
 | ||
| 
											6 months ago
										 |       # Calculate cursor position from click
 | ||
|  |       if len(self._input_text) > 0:
 | ||
| 
											5 months ago
										 |         font = gui_app.font()
 | ||
| 
											5 months ago
										 |         display_text = self._get_display_text()
 | ||
| 
											5 months ago
										 | 
 | ||
|  |         # 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):
 | ||
| 
											5 months ago
										 |           char_width = measure_text_cached(font, display_text[:i], font_size).x
 | ||
| 
											5 months ago
										 |           distance = abs(relative_x - char_width)
 | ||
|  |           if distance < min_distance:
 | ||
|  |             min_distance = distance
 | ||
|  |             best_pos = i
 | ||
|  | 
 | ||
|  |         self.set_cursor_position(best_pos)
 | ||
| 
											6 months ago
										 |       else:
 | ||
| 
											6 months ago
										 |         self.set_cursor_position(0)
 | ||
|  | 
 | ||
|  |   def _handle_keyboard_input(self):
 | ||
| 
											5 months ago
										 |     # Handle navigation keys
 | ||
| 
											6 months ago
										 |     key = rl.get_key_pressed()
 | ||
| 
											5 months ago
										 |     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)
 | ||
| 
											6 months ago
										 |       else:
 | ||
| 
											5 months ago
										 |         self._last_key_pressed = 0
 | ||
| 
											6 months ago
										 | 
 | ||
| 
											5 months ago
										 |     # 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):
 | ||
| 
											6 months ago
										 |     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))
 |