commit
5df6150894
23 changed files with 1181 additions and 773 deletions
@ -1 +1 @@ |
|||||||
Subproject commit c856a2c0bd2b3c75f86a73b051c0c4cc7159559e |
Subproject commit 95ee4edd17ecc6700eac12f2074de6b5478b9477 |
@ -1 +1 @@ |
|||||||
Subproject commit b4773f96b38a56089b28bf70b8073a9ddce6d847 |
Subproject commit 7eb5dba3dc9960373128244c10fac99bba91f630 |
@ -1 +1 @@ |
|||||||
7bf4ae5b92a3ad1f073f675e24e28babad0f2aa0 |
b31b7c5c29e6d30ccee2fa5105af778810fcd02e |
||||||
|
Binary file not shown.
Binary file not shown.
@ -0,0 +1,157 @@ |
|||||||
|
import pyray as rl |
||||||
|
from openpilot.system.ui.lib.application import gui_app |
||||||
|
|
||||||
|
|
||||||
|
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 = 5 |
||||||
|
|
||||||
|
@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) |
||||||
|
|
||||||
|
def set_password_mode(self, password_mode): |
||||||
|
self._password_mode = password_mode |
||||||
|
|
||||||
|
def clear(self): |
||||||
|
self._input_text = '' |
||||||
|
self._cursor_position = 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 |
||||||
|
|
||||||
|
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) |
||||||
|
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.LIGHTGRAY, border_color=rl.DARKGRAY, text_color=rl.BLACK, font_size=80): |
||||||
|
# Handle mouse input |
||||||
|
self._handle_mouse_input(rect, font_size) |
||||||
|
|
||||||
|
# Draw input box |
||||||
|
rl.draw_rectangle_rec(rect, color) |
||||||
|
rl.draw_rectangle_lines_ex(rect, 1, border_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 = "•" * len(self._input_text) if self._password_mode else self._input_text |
||||||
|
padding = 10 |
||||||
|
rl.draw_text_ex( |
||||||
|
font, |
||||||
|
display_text, |
||||||
|
rl.Vector2(int(rect.x + padding), 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 |
||||||
|
|
||||||
|
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.BLACK) |
||||||
|
|
||||||
|
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: |
||||||
|
text_width = rl.measure_text_ex(gui_app.font(), self._input_text, font_size, 0).x |
||||||
|
text_pos_x = rect.x + 10 |
||||||
|
|
||||||
|
if mouse_pos.x - text_pos_x > text_width: |
||||||
|
self.set_cursor_position(len(self._input_text)) |
||||||
|
else: |
||||||
|
click_ratio = (mouse_pos.x - text_pos_x) / text_width |
||||||
|
self.set_cursor_position(int(len(self._input_text) * click_ratio)) |
||||||
|
else: |
||||||
|
self.set_cursor_position(0) |
||||||
|
|
||||||
|
def _handle_keyboard_input(self): |
||||||
|
"""Process keyboard input.""" |
||||||
|
key = rl.get_key_pressed() |
||||||
|
|
||||||
|
# Handle key repeats |
||||||
|
if key == self._last_key_pressed and key != 0: |
||||||
|
self._key_press_time += 1 |
||||||
|
if self._key_press_time > self._repeat_delay and self._key_press_time % self._repeat_rate == 0: |
||||||
|
# Process repeated key |
||||||
|
pass |
||||||
|
else: |
||||||
|
return # Skip processing until repeat triggers |
||||||
|
else: |
||||||
|
self._last_key_pressed = key |
||||||
|
self._key_press_time = 0 |
||||||
|
|
||||||
|
# Handle navigation keys |
||||||
|
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)) |
||||||
|
|
||||||
|
# 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)) |
@ -0,0 +1,87 @@ |
|||||||
|
import pyray as rl |
||||||
|
from msgq.visionipc import VisionIpcClient, VisionStreamType |
||||||
|
from openpilot.system.ui.lib.application import gui_app |
||||||
|
|
||||||
|
FRAME_FRAGMENT_SHADER = """ |
||||||
|
#version 330 core |
||||||
|
in vec2 fragTexCoord; uniform sampler2D texture0, texture1; out vec4 fragColor; |
||||||
|
void main() { |
||||||
|
float y = texture(texture0, fragTexCoord).r; |
||||||
|
vec2 uv = texture(texture1, fragTexCoord).ra - 0.5; |
||||||
|
fragColor = vec4(y + 1.402*uv.y, y - 0.344*uv.x - 0.714*uv.y, y + 1.772*uv.x, 1.0); |
||||||
|
}""" |
||||||
|
|
||||||
|
|
||||||
|
class CameraView: |
||||||
|
def __init__(self, name: str, stream_type: VisionStreamType): |
||||||
|
self.client = VisionIpcClient(name, stream_type, False) |
||||||
|
self.shader = rl.load_shader_from_memory(rl.ffi.NULL, FRAME_FRAGMENT_SHADER) |
||||||
|
self.texture_y: rl.Texture | None = None |
||||||
|
self.texture_uv: rl.Texture | None = None |
||||||
|
self.frame = None |
||||||
|
|
||||||
|
def close(self): |
||||||
|
self._clear_textures() |
||||||
|
if self.shader and self.shader.id: |
||||||
|
rl.unload_shader(self.shader) |
||||||
|
|
||||||
|
def render(self, rect: rl.Rectangle): |
||||||
|
if not self._ensure_connection(): |
||||||
|
return |
||||||
|
|
||||||
|
buffer = self.client.recv(timeout_ms=0) |
||||||
|
self.frame = buffer if buffer else self.frame |
||||||
|
if not self.frame or not self.texture_y or not self.texture_uv: |
||||||
|
return |
||||||
|
|
||||||
|
y_data = self.frame.data[: self.frame.uv_offset] |
||||||
|
uv_data = self.frame.data[self.frame.uv_offset :] |
||||||
|
|
||||||
|
rl.update_texture(self.texture_y, rl.ffi.cast("void *", y_data.ctypes.data)) |
||||||
|
rl.update_texture(self.texture_uv, rl.ffi.cast("void *", uv_data.ctypes.data)) |
||||||
|
|
||||||
|
# Calculate scaling to maintain aspect ratio |
||||||
|
scale = min(rect.width / self.frame.width, rect.height / self.frame.height) |
||||||
|
x_offset = rect.x + (rect.width - (self.frame.width * scale)) / 2 |
||||||
|
y_offset = rect.y + (rect.height - (self.frame.height * scale)) / 2 |
||||||
|
src_rect = rl.Rectangle(0, 0, float(self.frame.width), float(self.frame.height)) |
||||||
|
dst_rect = rl.Rectangle(x_offset, y_offset, self.frame.width * scale, self.frame.height * scale) |
||||||
|
|
||||||
|
rl.begin_shader_mode(self.shader) |
||||||
|
rl.set_shader_value_texture(self.shader, rl.get_shader_location(self.shader, "texture1"), self.texture_uv) |
||||||
|
rl.draw_texture_pro(self.texture_y, src_rect, dst_rect, rl.Vector2(0, 0), 0.0, rl.WHITE) |
||||||
|
rl.end_shader_mode() |
||||||
|
|
||||||
|
def _ensure_connection(self) -> bool: |
||||||
|
if not self.client.is_connected(): |
||||||
|
self.frame = None |
||||||
|
if not self.client.connect(False) or not self.client.num_buffers: |
||||||
|
return False |
||||||
|
|
||||||
|
self._clear_textures() |
||||||
|
self.texture_y = rl.load_texture_from_image(rl.Image(None, int(self.client.stride), |
||||||
|
int(self.client.height), 1, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAYSCALE)) |
||||||
|
self.texture_uv = rl.load_texture_from_image(rl.Image(None, int(self.client.stride // 2), |
||||||
|
int(self.client.height // 2), 1, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAY_ALPHA)) |
||||||
|
return True |
||||||
|
|
||||||
|
def _clear_textures(self): |
||||||
|
if self.texture_y and self.texture_y.id: |
||||||
|
rl.unload_texture(self.texture_y) |
||||||
|
if self.texture_uv and self.texture_uv.id: |
||||||
|
rl.unload_texture(self.texture_uv) |
||||||
|
|
||||||
|
if __name__ == "__main__": |
||||||
|
gui_app.init_window("watch3") |
||||||
|
road_camera_view = CameraView("camerad", VisionStreamType.VISION_STREAM_ROAD) |
||||||
|
driver_camera_view = CameraView("camerad", VisionStreamType.VISION_STREAM_DRIVER) |
||||||
|
wide_road_camera_view = CameraView("camerad", VisionStreamType.VISION_STREAM_WIDE_ROAD) |
||||||
|
try: |
||||||
|
for _ in gui_app.render(): |
||||||
|
road_camera_view.render(rl.Rectangle(gui_app.width // 4, 0, gui_app.width // 2, gui_app.height // 2)) |
||||||
|
driver_camera_view.render(rl.Rectangle(0, gui_app.height // 2, gui_app.width // 2, gui_app.height // 2)) |
||||||
|
wide_road_camera_view.render(rl.Rectangle(gui_app.width // 2, gui_app.height // 2, gui_app.width // 2, gui_app.height // 2)) |
||||||
|
finally: |
||||||
|
road_camera_view.close() |
||||||
|
driver_camera_view.close() |
||||||
|
wide_road_camera_view.close() |
@ -0,0 +1,81 @@ |
|||||||
|
import pyray as rl |
||||||
|
|
||||||
|
from openpilot.system.ui.lib.button import gui_button, ButtonStyle, TextAlignment |
||||||
|
from openpilot.system.ui.lib.label import gui_label |
||||||
|
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel |
||||||
|
|
||||||
|
|
||||||
|
class MultiOptionDialog: |
||||||
|
def __init__(self, title, options, current=""): |
||||||
|
self._title = title |
||||||
|
self._options = options |
||||||
|
self._current = current if current in options else "" |
||||||
|
self._selection = self._current |
||||||
|
self._option_height = 80 |
||||||
|
self._padding = 20 |
||||||
|
self.scroll_panel = GuiScrollPanel() |
||||||
|
|
||||||
|
@property |
||||||
|
def selection(self): |
||||||
|
return self._selection |
||||||
|
|
||||||
|
def render(self, rect): |
||||||
|
title_rect = rl.Rectangle(rect.x + self._padding, rect.y + self._padding, rect.width - 2 * self._padding, 70) |
||||||
|
gui_label(title_rect, self._title, 70) |
||||||
|
|
||||||
|
options_y_start = rect.y + 120 |
||||||
|
options_height = len(self._options) * (self._option_height + 10) |
||||||
|
options_rect = rl.Rectangle(rect.x + self._padding, options_y_start, rect.width - 2 * self._padding, options_height) |
||||||
|
|
||||||
|
view_rect = rl.Rectangle( |
||||||
|
rect.x + self._padding, options_y_start, rect.width - 2 * self._padding, rect.height - 200 - 2 * self._padding |
||||||
|
) |
||||||
|
|
||||||
|
offset = self.scroll_panel.handle_scroll(view_rect, options_rect) |
||||||
|
is_click_valid = self.scroll_panel.is_click_valid() |
||||||
|
|
||||||
|
rl.begin_scissor_mode(int(view_rect.x), int(view_rect.y), int(view_rect.width), int(view_rect.height)) |
||||||
|
|
||||||
|
for i, option in enumerate(self._options): |
||||||
|
y_pos = view_rect.y + i * (self._option_height + 10) + offset.y |
||||||
|
item_rect = rl.Rectangle(view_rect.x, y_pos, view_rect.width, self._option_height) |
||||||
|
|
||||||
|
if not rl.check_collision_recs(item_rect, view_rect): |
||||||
|
continue |
||||||
|
|
||||||
|
is_selected = option == self._selection |
||||||
|
button_style = ButtonStyle.PRIMARY if is_selected else ButtonStyle.NORMAL |
||||||
|
|
||||||
|
if gui_button(item_rect, option, button_style=button_style, text_alignment=TextAlignment.LEFT) and is_click_valid: |
||||||
|
self._selection = option |
||||||
|
|
||||||
|
rl.end_scissor_mode() |
||||||
|
|
||||||
|
button_y = rect.y + rect.height - 80 - self._padding |
||||||
|
button_width = (rect.width - 3 * self._padding) / 2 |
||||||
|
|
||||||
|
cancel_rect = rl.Rectangle(rect.x + self._padding, button_y, button_width, 80) |
||||||
|
if gui_button(cancel_rect, "Cancel"): |
||||||
|
return 0 # Canceled |
||||||
|
|
||||||
|
select_rect = rl.Rectangle(rect.x + 2 * self._padding + button_width, button_y, button_width, 80) |
||||||
|
has_new_selection = self._selection != "" and self._selection != self._current |
||||||
|
|
||||||
|
if gui_button(select_rect, "Select", is_enabled=has_new_selection, button_style=ButtonStyle.PRIMARY): |
||||||
|
return 1 # Selected |
||||||
|
|
||||||
|
return -1 # Still active |
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__": |
||||||
|
from openpilot.system.ui.lib.application import gui_app |
||||||
|
|
||||||
|
gui_app.init_window("Multi Option Dialog Example") |
||||||
|
options = [f"Option {i}" for i in range(1, 11)] |
||||||
|
dialog = MultiOptionDialog("Choose an option", options, options[0]) |
||||||
|
|
||||||
|
for _ in gui_app.render(): |
||||||
|
result = dialog.render(rl.Rectangle(100, 100, 1024, 800)) |
||||||
|
if result >= 0: |
||||||
|
print(f"Selected: {dialog.selection}" if result > 0 else "Canceled") |
||||||
|
break |
Loading…
Reference in new issue