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.
		
		
		
		
		
			
		
			
				
					
					
						
							233 lines
						
					
					
						
							7.9 KiB
						
					
					
				
			
		
		
	
	
							233 lines
						
					
					
						
							7.9 KiB
						
					
					
				import pyray as rl
 | 
						|
import time
 | 
						|
from openpilot.system.ui.lib.application import gui_app, Widget
 | 
						|
from openpilot.system.ui.lib.text_measure import measure_text_cached
 | 
						|
 | 
						|
PASSWORD_MASK_CHAR = "•"
 | 
						|
PASSWORD_MASK_DELAY = 1.5  # Seconds to show character before masking
 | 
						|
 | 
						|
 | 
						|
class InputBox(Widget):
 | 
						|
  def __init__(self, max_text_size=255, password_mode=False):
 | 
						|
    super().__init__()
 | 
						|
    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 = measure_text_cached(font, display_text[: self._cursor_position], self._font_size).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 += measure_text_cached(font, display_text[: self._cursor_position], font_size).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.MouseButton.MOUSE_BUTTON_LEFT) 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 = measure_text_cached(font, display_text[:i], font_size).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))
 | 
						|
 |