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.
123 lines
3.9 KiB
123 lines
3.9 KiB
#!/usr/bin/env python3
|
|
import os
|
|
import re
|
|
import threading
|
|
import time
|
|
import pyray as rl
|
|
from openpilot.system.hardware import HARDWARE, PC
|
|
from openpilot.system.ui.lib.button import gui_button, ButtonStyle
|
|
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
|
|
from openpilot.system.ui.lib.application import gui_app
|
|
|
|
MARGIN = 50
|
|
SPACING = 50
|
|
FONT_SIZE = 72
|
|
LINE_HEIGHT = 80
|
|
BUTTON_SIZE = rl.Vector2(310, 160)
|
|
|
|
DEMO_TEXT = """This is a sample text that will be wrapped and scrolled if necessary.
|
|
The text is long enough to demonstrate scrolling and word wrapping.""" * 30
|
|
|
|
def wrap_text(text, font_size, max_width):
|
|
lines = []
|
|
font = gui_app.font()
|
|
|
|
for paragraph in text.split("\n"):
|
|
if not paragraph.strip():
|
|
# Don't add empty lines first, ensuring wrap_text("") returns []
|
|
if lines:
|
|
lines.append("")
|
|
continue
|
|
indent = re.match(r"^\s*", paragraph).group()
|
|
current_line = indent
|
|
for word in paragraph.split():
|
|
test_line = current_line + word + " "
|
|
if rl.measure_text_ex(font, test_line, font_size, 0).x <= max_width:
|
|
current_line = test_line
|
|
else:
|
|
lines.append(current_line)
|
|
current_line = word + " "
|
|
current_line = current_line.rstrip()
|
|
if current_line:
|
|
lines.append(current_line)
|
|
|
|
return lines
|
|
|
|
|
|
class TextWindowRenderer:
|
|
def __init__(self, text: str):
|
|
self._textarea_rect = rl.Rectangle(MARGIN, MARGIN, gui_app.width - MARGIN * 2, gui_app.height - MARGIN * 2)
|
|
self._wrapped_lines = wrap_text(text, FONT_SIZE, self._textarea_rect.width - 20)
|
|
self._content_rect = rl.Rectangle(0, 0, self._textarea_rect.width - 20, len(self._wrapped_lines) * LINE_HEIGHT)
|
|
self._scroll_panel = GuiScrollPanel(show_vertical_scroll_bar=True)
|
|
|
|
def render(self):
|
|
scroll = self._scroll_panel.handle_scroll(self._textarea_rect, self._content_rect)
|
|
rl.begin_scissor_mode(int(self._textarea_rect.x), int(self._textarea_rect.y), int(self._textarea_rect.width), int(self._textarea_rect.height))
|
|
for i, line in enumerate(self._wrapped_lines):
|
|
position = rl.Vector2(self._textarea_rect.x + scroll.x, self._textarea_rect.y + scroll.y + i * LINE_HEIGHT)
|
|
if position.y + LINE_HEIGHT < self._textarea_rect.y or position.y > self._textarea_rect.y + self._textarea_rect.height:
|
|
continue
|
|
rl.draw_text_ex(gui_app.font(), line, position, FONT_SIZE, 0, rl.WHITE)
|
|
rl.end_scissor_mode()
|
|
|
|
button_bounds = rl.Rectangle(gui_app.width - MARGIN - BUTTON_SIZE.x, gui_app.height - MARGIN - BUTTON_SIZE.y, BUTTON_SIZE.x, BUTTON_SIZE.y)
|
|
ret = gui_button(button_bounds, "Exit" if PC else "Reboot", button_style=ButtonStyle.TRANSPARENT)
|
|
if ret:
|
|
if PC:
|
|
gui_app.request_close()
|
|
else:
|
|
HARDWARE.reboot()
|
|
return ret
|
|
|
|
|
|
class TextWindow:
|
|
def __init__(self, text: str):
|
|
self._text = text
|
|
|
|
self._renderer: TextWindowRenderer | None = None
|
|
self._stop_event = threading.Event()
|
|
self._thread = threading.Thread(target=self._run)
|
|
self._thread.start()
|
|
|
|
# wait for the renderer to be initialized
|
|
while self._renderer is None and self._thread.is_alive():
|
|
time.sleep(0.01)
|
|
|
|
def wait_for_exit(self):
|
|
while self._thread.is_alive():
|
|
time.sleep(0.01)
|
|
|
|
def _run(self):
|
|
if os.getenv("CI") is not None:
|
|
return
|
|
gui_app.init_window("Text")
|
|
self._renderer = renderer = TextWindowRenderer(self._text)
|
|
try:
|
|
for _ in gui_app.render():
|
|
if self._stop_event.is_set():
|
|
break
|
|
renderer.render()
|
|
finally:
|
|
gui_app.close()
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def close(self):
|
|
if self._thread.is_alive():
|
|
self._stop_event.set()
|
|
self._thread.join(timeout=2.0)
|
|
if self._thread.is_alive():
|
|
print("WARNING: failed to join text window thread")
|
|
|
|
def __del__(self):
|
|
self.close()
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
self.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
with TextWindow(DEMO_TEXT):
|
|
time.sleep(5)
|
|
|