mirror of
https://github.com/MoreTore/openpilot.git
synced 2026-08-05 00:05:59 +08:00
ui: Initial UI rewrite using pyray (spinner and text window) (#34583)
* pyray init version * remove c++ code * cleanup * restruct the directory layout * improve GuiApplication * smooth out the texture after resize * use atexit to close app * rename FontSize->FontWeight * make files executable * use Inter Regular for FrontWeight.NORMAL * set FLAG_VSYNC_HINT to avoid tearing while scrolling * smoother scrolling * mange textures in gui_app
This commit is contained in:
@@ -352,7 +352,6 @@ SConscript(['rednose/SConscript'])
|
||||
|
||||
# Build system services
|
||||
SConscript([
|
||||
'system/ui/SConscript',
|
||||
'system/proclogd/SConscript',
|
||||
'system/ubloxd/SConscript',
|
||||
'system/loggerd/SConscript',
|
||||
|
||||
@@ -53,7 +53,6 @@ function run_tests() {
|
||||
run "check_shebang_scripts_are_executable" python3 -m pre_commit_hooks.check_shebang_scripts_are_executable $ALL_FILES
|
||||
run "check_shebang_format" $DIR/check_shebang_format.sh $ALL_FILES
|
||||
run "check_nomerge_comments" $DIR/check_nomerge_comments.sh $ALL_FILES
|
||||
run "check_raylib_includes" $DIR/check_raylib_includes.sh $ALL_FILES
|
||||
|
||||
if [[ -z "$FAST" ]]; then
|
||||
run "mypy" mypy $PYTHON_FILES
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
spinner
|
||||
@@ -1,20 +0,0 @@
|
||||
import subprocess
|
||||
|
||||
Import('env', 'arch', 'common')
|
||||
|
||||
renv = env.Clone()
|
||||
|
||||
rayutil = env.Library("rayutil", ['raylib/util.cc'], LIBS='raylib')
|
||||
linked_libs = ['raylib', rayutil, common]
|
||||
renv['LIBPATH'] += [f'#third_party/raylib/{arch}/']
|
||||
|
||||
mac_frameworks = []
|
||||
if arch == "Darwin":
|
||||
mac_frameworks += ['OpenCL', 'CoreVideo', 'Cocoa', 'GLUT', 'CoreFoundation', 'OpenGL', 'IOKit']
|
||||
elif arch == 'larch64':
|
||||
linked_libs += ['GLESv2', 'GL', 'EGL', 'wayland-client', 'wayland-egl']
|
||||
else:
|
||||
linked_libs += ['OpenCL', 'dl', 'pthread']
|
||||
|
||||
if arch != 'aarch64':
|
||||
renv.Program("spinner", ["raylib/spinner.cc"], LIBS=linked_libs, FRAMEWORKS=mac_frameworks)
|
||||
@@ -0,0 +1,100 @@
|
||||
import atexit
|
||||
import os
|
||||
import pyray as rl
|
||||
from enum import IntEnum
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
|
||||
DEFAULT_TEXT_SIZE = 60
|
||||
DEFAULT_FPS = 60
|
||||
FONT_DIR = os.path.join(BASEDIR, "selfdrive/assets/fonts")
|
||||
|
||||
class FontWeight(IntEnum):
|
||||
BLACK = 0
|
||||
BOLD = 1
|
||||
EXTRA_BOLD = 2
|
||||
EXTRA_LIGHT = 3
|
||||
MEDIUM = 4
|
||||
NORMAL = 5
|
||||
SEMI_BOLD= 6
|
||||
THIN = 7
|
||||
|
||||
|
||||
class GuiApplication:
|
||||
def __init__(self, width: int, height: int):
|
||||
self._fonts: dict[FontWeight, rl.Font] = {}
|
||||
self._width = width
|
||||
self._height = height
|
||||
self._textures: list[rl.Texture] = []
|
||||
|
||||
def init_window(self, title: str, fps: int=DEFAULT_FPS):
|
||||
atexit.register(self.close) # Automatically call close() on exit
|
||||
|
||||
rl.set_config_flags(rl.ConfigFlags.FLAG_MSAA_4X_HINT | rl.ConfigFlags.FLAG_VSYNC_HINT)
|
||||
rl.init_window(self._width, self._height, title)
|
||||
rl.set_target_fps(fps)
|
||||
|
||||
self._set_styles()
|
||||
self._load_fonts()
|
||||
|
||||
def load_texture_from_image(self, file_name: str, width: int, height: int):
|
||||
"""Load and resize a texture, storing it for later automatic unloading."""
|
||||
image = rl.load_image(file_name)
|
||||
rl.image_resize(image, width, height)
|
||||
texture = rl.load_texture_from_image(image)
|
||||
# Set texture filtering to smooth the result
|
||||
rl.set_texture_filter(texture, rl.TextureFilter.TEXTURE_FILTER_BILINEAR)
|
||||
|
||||
rl.unload_image(image)
|
||||
|
||||
self._textures.append(texture)
|
||||
return texture
|
||||
|
||||
def close(self):
|
||||
for texture in self._textures:
|
||||
rl.unload_texture(texture)
|
||||
|
||||
for font in self._fonts.values():
|
||||
rl.unload_font(font)
|
||||
|
||||
rl.close_window()
|
||||
|
||||
def font(self, font_wight: FontWeight=FontWeight.NORMAL):
|
||||
return self._fonts[font_wight]
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
return self._width
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
return self._height
|
||||
|
||||
def _load_fonts(self):
|
||||
font_files = (
|
||||
"Inter-Black.ttf",
|
||||
"Inter-Bold.ttf",
|
||||
"Inter-ExtraBold.ttf",
|
||||
"Inter-ExtraLight.ttf",
|
||||
"Inter-Medium.ttf",
|
||||
"Inter-Regular.ttf",
|
||||
"Inter-SemiBold.ttf",
|
||||
"Inter-Thin.ttf"
|
||||
)
|
||||
|
||||
for index, font_file in enumerate(font_files):
|
||||
font = rl.load_font_ex(os.path.join(FONT_DIR, font_file), 120, None, 0)
|
||||
rl.set_texture_filter(font.texture, rl.TextureFilter.TEXTURE_FILTER_BILINEAR)
|
||||
self._fonts[index] = font
|
||||
|
||||
rl.gui_set_font(self._fonts[FontWeight.NORMAL])
|
||||
|
||||
def _set_styles(self):
|
||||
rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiControlProperty.BORDER_WIDTH, 0)
|
||||
rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_SIZE, DEFAULT_TEXT_SIZE)
|
||||
rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.BACKGROUND_COLOR, rl.color_to_int(rl.BLACK))
|
||||
rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiControlProperty.TEXT_COLOR_NORMAL, rl.color_to_int(rl.Color(200, 200, 200, 255)))
|
||||
rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.BACKGROUND_COLOR, rl.color_to_int(rl.Color(30, 30, 30, 255)))
|
||||
rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiControlProperty.BASE_COLOR_NORMAL, rl.color_to_int(rl.Color(50, 50, 50, 255)))
|
||||
|
||||
|
||||
gui_app = GuiApplication(2160, 1080)
|
||||
@@ -0,0 +1,14 @@
|
||||
|
||||
import pyray as rl
|
||||
from openpilot.system.ui.lib.utils import GuiStyleContext
|
||||
|
||||
BUTTON_DEFAULT_BG_COLOR = rl.Color(51, 51, 51, 255)
|
||||
|
||||
def gui_button(rect, text, bg_color=BUTTON_DEFAULT_BG_COLOR):
|
||||
styles = [
|
||||
(rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_ALIGNMENT_VERTICAL, rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE),
|
||||
(rl.GuiControl.DEFAULT, rl.GuiControlProperty.BASE_COLOR_NORMAL, rl.color_to_int(bg_color))
|
||||
]
|
||||
|
||||
with GuiStyleContext(styles):
|
||||
return rl.gui_button(rect, text)
|
||||
@@ -0,0 +1,13 @@
|
||||
import pyray as rl
|
||||
from openpilot.system.ui.lib.utils import GuiStyleContext
|
||||
|
||||
def gui_label(rect, text, font_size):
|
||||
styles = [
|
||||
(rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_SIZE, font_size),
|
||||
(rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_LINE_SPACING, font_size),
|
||||
(rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_ALIGNMENT_VERTICAL, rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP),
|
||||
(rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_WRAP_MODE, rl.GuiTextWrapMode.TEXT_WRAP_WORD)
|
||||
]
|
||||
|
||||
with GuiStyleContext(styles):
|
||||
rl.gui_label(rect, text)
|
||||
@@ -0,0 +1,40 @@
|
||||
import pyray as rl
|
||||
from cffi import FFI
|
||||
|
||||
MOUSE_WHEEL_SCROLL_SPEED = 30
|
||||
|
||||
class GuiScrollPanel:
|
||||
def __init__(self, bounds: rl.Rectangle, content: rl.Rectangle, show_vertical_scroll_bar: bool = False):
|
||||
self._dragging: bool = False
|
||||
self._last_mouse_y: float = 0.0
|
||||
self._bounds = bounds
|
||||
self._content = content
|
||||
self._scroll = rl.Vector2(0, 0)
|
||||
self._view = rl.Rectangle(0, 0, 0, 0)
|
||||
self._show_vertical_scroll_bar: bool = show_vertical_scroll_bar
|
||||
|
||||
def handle_scroll(self)-> rl.Vector2:
|
||||
mouse_pos = rl.get_mouse_position()
|
||||
if rl.check_collision_point_rec(mouse_pos, self._bounds) and rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT):
|
||||
if not self._dragging:
|
||||
self._dragging = True
|
||||
self._last_mouse_y = mouse_pos.y
|
||||
|
||||
if self._dragging:
|
||||
if rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT):
|
||||
delta_y = mouse_pos.y - self._last_mouse_y
|
||||
self._scroll.y += delta_y
|
||||
self._last_mouse_y = mouse_pos.y
|
||||
else:
|
||||
self._dragging = False
|
||||
|
||||
wheel_move = rl.get_mouse_wheel_move()
|
||||
if self._show_vertical_scroll_bar:
|
||||
self._scroll.y += wheel_move * (MOUSE_WHEEL_SCROLL_SPEED - 20)
|
||||
rl.gui_scroll_panel(self._bounds, FFI().NULL, self._content, self._scroll, self._view)
|
||||
else:
|
||||
self._scroll.y += wheel_move * MOUSE_WHEEL_SCROLL_SPEED
|
||||
max_scroll_y = self._content.height - self._bounds.height
|
||||
self._scroll.y = max(min(self._scroll.y, 0), -max_scroll_y)
|
||||
|
||||
return self._scroll
|
||||
@@ -0,0 +1,17 @@
|
||||
import pyray as rl
|
||||
|
||||
class GuiStyleContext:
|
||||
def __init__(self, styles: list[tuple[int, int, int]]):
|
||||
"""styles is a list of tuples (control, prop, new_value)"""
|
||||
self.styles = styles
|
||||
self.prev_styles: list[tuple[int, int, int]] = []
|
||||
|
||||
def __enter__(self):
|
||||
for control, prop, new_value in self.styles:
|
||||
prev_value = rl.gui_get_style(control, prop)
|
||||
self.prev_styles.append((control, prop, prev_value))
|
||||
rl.gui_set_style(control, prop, new_value)
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
for control, prop, prev_value in self.prev_styles:
|
||||
rl.gui_set_style(control, prop, prev_value)
|
||||
@@ -1,5 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#define OPENPILOT_RAYLIB
|
||||
|
||||
#include "third_party/raylib/include/raylib.h"
|
||||
@@ -1,66 +0,0 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <iostream>
|
||||
|
||||
#include "system/ui/raylib/util.h"
|
||||
|
||||
constexpr int kProgressBarWidth = 1000;
|
||||
constexpr int kProgressBarHeight = 20;
|
||||
constexpr float kRotationRate = 12.0f;
|
||||
constexpr int kMargin = 200;
|
||||
constexpr int kTextureSize = 360;
|
||||
constexpr int kFontSize = 80;
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
App app("spinner", 30);
|
||||
|
||||
// Turn off input buffering for std::cin
|
||||
std::cin.sync_with_stdio(false);
|
||||
std::cin.tie(nullptr);
|
||||
|
||||
Texture2D commaTexture = LoadTextureResized("../../selfdrive/assets/img_spinner_comma.png", kTextureSize);
|
||||
Texture2D spinnerTexture = LoadTextureResized("../../selfdrive/assets/img_spinner_track.png", kTextureSize);
|
||||
|
||||
float rotation = 0.0f;
|
||||
std::string userInput;
|
||||
|
||||
while (!WindowShouldClose()) {
|
||||
BeginDrawing();
|
||||
ClearBackground(RAYLIB_BLACK);
|
||||
|
||||
rotation = fmod(rotation + kRotationRate, 360.0f);
|
||||
Vector2 center = {GetScreenWidth() / 2.0f, GetScreenHeight() / 2.0f};
|
||||
const Vector2 spinnerOrigin{kTextureSize / 2.0f, kTextureSize / 2.0f};
|
||||
const Vector2 commaPosition{center.x - kTextureSize / 2.0f, center.y - kTextureSize / 2.0f};
|
||||
|
||||
// Draw rotating spinner and static comma logo
|
||||
DrawTexturePro(spinnerTexture, {0, 0, (float)kTextureSize, (float)kTextureSize},
|
||||
{center.x, center.y, (float)kTextureSize, (float)kTextureSize},
|
||||
spinnerOrigin, rotation, RAYLIB_WHITE);
|
||||
DrawTextureV(commaTexture, commaPosition, RAYLIB_WHITE);
|
||||
|
||||
// Check for user input
|
||||
if (std::cin.rdbuf()->in_avail() > 0) {
|
||||
std::getline(std::cin, userInput);
|
||||
}
|
||||
|
||||
// Display either a progress bar or user input text based on input
|
||||
if (!userInput.empty()) {
|
||||
float yPos = GetScreenHeight() - kMargin - kProgressBarHeight;
|
||||
if (std::all_of(userInput.begin(), userInput.end(), ::isdigit)) {
|
||||
Rectangle bar = {center.x - kProgressBarWidth / 2.0f, yPos, kProgressBarWidth, kProgressBarHeight};
|
||||
DrawRectangleRounded(bar, 0.5f, 10, RAYLIB_GRAY);
|
||||
|
||||
int progress = std::clamp(std::stoi(userInput), 0, 100);
|
||||
bar.width *= progress / 100.0f;
|
||||
DrawRectangleRounded(bar, 0.5f, 10, RAYLIB_RAYWHITE);
|
||||
} else {
|
||||
Vector2 textSize = MeasureTextEx(app.getFont(), userInput.c_str(), kFontSize, 1.0);
|
||||
DrawTextEx(app.getFont(), userInput.c_str(), {center.x - textSize.x / 2, yPos}, kFontSize, 1.0, RAYLIB_WHITE);
|
||||
}
|
||||
}
|
||||
|
||||
EndDrawing();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
#include "system/ui/raylib/util.h"
|
||||
|
||||
#include <array>
|
||||
#include <filesystem>
|
||||
|
||||
#undef GREEN
|
||||
#undef RED
|
||||
#undef YELLOW
|
||||
#include "common/swaglog.h"
|
||||
#include "system/hardware/hw.h"
|
||||
|
||||
constexpr std::array<const char *, static_cast<int>(FontWeight::Count)> FONT_FILE_PATHS = {
|
||||
"../../selfdrive/assets/fonts/Inter-Black.ttf",
|
||||
"../../selfdrive/assets/fonts/Inter-Bold.ttf",
|
||||
"../../selfdrive/assets/fonts/Inter-ExtraBold.ttf",
|
||||
"../../selfdrive/assets/fonts/Inter-ExtraLight.ttf",
|
||||
"../../selfdrive/assets/fonts/Inter-Medium.ttf",
|
||||
"../../selfdrive/assets/fonts/Inter-Regular.ttf",
|
||||
"../../selfdrive/assets/fonts/Inter-SemiBold.ttf",
|
||||
"../../selfdrive/assets/fonts/Inter-Thin.ttf",
|
||||
};
|
||||
|
||||
Texture2D LoadTextureResized(const char *fileName, int size) {
|
||||
Image img = LoadImage(fileName);
|
||||
ImageResize(&img, size, size);
|
||||
Texture2D texture = LoadTextureFromImage(img);
|
||||
return texture;
|
||||
}
|
||||
|
||||
App *pApp = nullptr;
|
||||
|
||||
App::App(const char *title, int fps) {
|
||||
// Ensure the current dir matches the exectuable's directory
|
||||
auto self_path = util::readlink("/proc/self/exe");
|
||||
auto exe_dir = std::filesystem::path(self_path).parent_path();
|
||||
chdir(exe_dir.c_str());
|
||||
|
||||
Hardware::set_display_power(true);
|
||||
Hardware::set_brightness(65);
|
||||
|
||||
// SetTraceLogLevel(LOG_NONE);
|
||||
InitWindow(2160, 1080, title);
|
||||
SetTargetFPS(fps);
|
||||
|
||||
// Load fonts
|
||||
fonts_.reserve(FONT_FILE_PATHS.size());
|
||||
for (int i = 0; i < FONT_FILE_PATHS.size(); ++i) {
|
||||
fonts_.push_back(LoadFontEx(FONT_FILE_PATHS[i], 120, nullptr, 250));
|
||||
}
|
||||
|
||||
pApp = this;
|
||||
}
|
||||
|
||||
App::~App() {
|
||||
for (auto &font : fonts_) {
|
||||
UnloadFont(font);
|
||||
}
|
||||
|
||||
CloseWindow();
|
||||
pApp = nullptr;
|
||||
}
|
||||
|
||||
const Font &App::getFont(FontWeight weight) const {
|
||||
return fonts_[static_cast<int>(weight)];
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "system/ui/raylib/raylib.h"
|
||||
|
||||
enum class FontWeight {
|
||||
Normal,
|
||||
Bold,
|
||||
ExtraBold,
|
||||
ExtraLight,
|
||||
Medium,
|
||||
Regular,
|
||||
SemiBold,
|
||||
Thin,
|
||||
Count // To represent the total number of fonts
|
||||
};
|
||||
|
||||
Texture2D LoadTextureResized(const char *fileName, int size);
|
||||
|
||||
class App {
|
||||
public:
|
||||
App(const char *title, int fps);
|
||||
~App();
|
||||
const Font &getFont(FontWeight weight = FontWeight::Normal) const;
|
||||
|
||||
protected:
|
||||
std::vector<Font> fonts_;
|
||||
};
|
||||
|
||||
// Global pointer to the App instance
|
||||
extern App *pApp;
|
||||
Executable
+76
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python3
|
||||
import pyray as rl
|
||||
import os
|
||||
import select
|
||||
import sys
|
||||
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
|
||||
# Constants
|
||||
PROGRESS_BAR_WIDTH = 1000
|
||||
PROGRESS_BAR_HEIGHT = 20
|
||||
ROTATION_RATE = 12.0
|
||||
MARGIN = 200
|
||||
TEXTURE_SIZE = 360
|
||||
FONT_SIZE = 80
|
||||
|
||||
def clamp(value, min_value, max_value):
|
||||
return max(min(value, max_value), min_value)
|
||||
|
||||
def check_input_non_blocking():
|
||||
if sys.stdin in select.select([sys.stdin], [], [], 0)[0]:
|
||||
return sys.stdin.readline().strip()
|
||||
return ""
|
||||
|
||||
def main():
|
||||
gui_app.init_window("Spinner")
|
||||
|
||||
# Load textures
|
||||
comma_texture = gui_app.load_texture_from_image(os.path.join(BASEDIR, "selfdrive/assets/img_spinner_comma.png"), TEXTURE_SIZE, TEXTURE_SIZE)
|
||||
spinner_texture = gui_app.load_texture_from_image(os.path.join(BASEDIR, "selfdrive/assets/img_spinner_track.png"), TEXTURE_SIZE, TEXTURE_SIZE)
|
||||
|
||||
# Initial values
|
||||
rotation = 0.0
|
||||
user_input = ""
|
||||
center = rl.Vector2(gui_app.width / 2.0, gui_app.height / 2.0)
|
||||
spinner_origin = rl.Vector2(TEXTURE_SIZE / 2.0, TEXTURE_SIZE / 2.0)
|
||||
comma_position = rl.Vector2(center.x - TEXTURE_SIZE / 2.0, center.y - TEXTURE_SIZE / 2.0)
|
||||
|
||||
while not rl.window_should_close():
|
||||
rl.begin_drawing()
|
||||
rl.clear_background(rl.BLACK)
|
||||
|
||||
# Update rotation
|
||||
rotation = (rotation + ROTATION_RATE) % 360.0
|
||||
|
||||
# Draw rotating spinner and static comma logo
|
||||
rl.draw_texture_pro(spinner_texture, rl.Rectangle(0, 0, TEXTURE_SIZE, TEXTURE_SIZE),
|
||||
rl.Rectangle(center.x, center.y, TEXTURE_SIZE, TEXTURE_SIZE),
|
||||
spinner_origin, rotation, rl.WHITE)
|
||||
rl.draw_texture_v(comma_texture, comma_position, rl.WHITE)
|
||||
|
||||
# Read user input
|
||||
if input_str := check_input_non_blocking():
|
||||
user_input = input_str
|
||||
|
||||
# Display progress bar or text based on user input
|
||||
if user_input:
|
||||
y_pos = rl.get_screen_height() - MARGIN - PROGRESS_BAR_HEIGHT
|
||||
if user_input.isdigit():
|
||||
progress = clamp(int(user_input), 0, 100)
|
||||
bar = rl.Rectangle(center.x - PROGRESS_BAR_WIDTH / 2.0, y_pos, PROGRESS_BAR_WIDTH, PROGRESS_BAR_HEIGHT)
|
||||
rl.draw_rectangle_rounded(bar, 0.5, 10, rl.GRAY)
|
||||
|
||||
bar.width *= progress / 100.0
|
||||
rl.draw_rectangle_rounded(bar, 0.5, 10, rl.WHITE)
|
||||
else:
|
||||
text_size = rl.measure_text_ex(gui_app.font(), user_input, FONT_SIZE, 1.0)
|
||||
rl.draw_text_ex(gui_app.font(), user_input,
|
||||
rl.Vector2(center.x - text_size.x / 2, y_pos), FONT_SIZE, 1.0, rl.WHITE)
|
||||
|
||||
rl.end_drawing()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+67
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
from openpilot.system.ui.lib.button import gui_button
|
||||
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
|
||||
MARGIN = 50
|
||||
SPACING = 50
|
||||
FONT_SIZE = 60
|
||||
LINE_HEIGHT = 64
|
||||
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.""" * 20
|
||||
|
||||
def wrap_text(text, font_size, max_width):
|
||||
lines = []
|
||||
current_line = ""
|
||||
font = gui_app.font()
|
||||
|
||||
for word in text.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 + " "
|
||||
if current_line:
|
||||
lines.append(current_line)
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
def main():
|
||||
gui_app.init_window("Text")
|
||||
|
||||
text_content = sys.argv[1] if len(sys.argv) > 1 else DEMO_TEXT
|
||||
|
||||
textarea_rect = rl.Rectangle(MARGIN, MARGIN, gui_app.width - MARGIN * 2, gui_app.height - MARGIN * 2 - BUTTON_SIZE.y - SPACING)
|
||||
wrapped_lines = wrap_text(text_content, FONT_SIZE, textarea_rect.width - 20)
|
||||
content_rect = rl.Rectangle(0, 0, textarea_rect.width - 20, len(wrapped_lines) * LINE_HEIGHT)
|
||||
scroll_panel = GuiScrollPanel(textarea_rect, content_rect, show_vertical_scroll_bar=True)
|
||||
|
||||
while not rl.window_should_close():
|
||||
rl.begin_drawing()
|
||||
rl.clear_background(rl.BLACK)
|
||||
|
||||
scroll = scroll_panel.handle_scroll()
|
||||
|
||||
rl.begin_scissor_mode(int(textarea_rect.x), int(textarea_rect.y), int(textarea_rect.width), int(textarea_rect.height))
|
||||
for i, line in enumerate(wrapped_lines):
|
||||
position = rl.Vector2(textarea_rect.x + scroll.x, textarea_rect.y + scroll.y + i * LINE_HEIGHT)
|
||||
rl.draw_text_ex(gui_app.font(), line.strip(), 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)
|
||||
if gui_button(button_bounds, "Reboot"):
|
||||
HARDWARE.reboot()
|
||||
|
||||
rl.end_drawing()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user