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