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_apppull/34590/head
parent
958c8d1ce3
commit
ce7ff5c0e6
16 changed files with 327 additions and 192 deletions
@ -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; |
|
@ -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() |
@ -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() |
Loading…
Reference in new issue