Skip to content

Commit

Permalink
ui: Initial UI rewrite using pyray (spinner and text window) (#34583)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
deanlee authored Feb 15, 2025
1 parent 958c8d1 commit ce7ff5c
Show file tree
Hide file tree
Showing 16 changed files with 327 additions and 192 deletions.
1 change: 0 additions & 1 deletion SConstruct
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,6 @@ SConscript(['rednose/SConscript'])

# Build system services
SConscript([
'system/ui/SConscript',
'system/proclogd/SConscript',
'system/ubloxd/SConscript',
'system/loggerd/SConscript',
Expand Down
1 change: 0 additions & 1 deletion scripts/lint/lint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion system/ui/.gitignore

This file was deleted.

20 changes: 0 additions & 20 deletions system/ui/SConscript

This file was deleted.

Empty file added system/ui/lib/__init__.py
Empty file.
100 changes: 100 additions & 0 deletions system/ui/lib/application.py
Original file line number Diff line number Diff line change
@@ -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)
14 changes: 14 additions & 0 deletions system/ui/lib/button.py
Original file line number Diff line number Diff line change
@@ -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)
13 changes: 13 additions & 0 deletions system/ui/lib/label.py
Original file line number Diff line number Diff line change
@@ -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)
40 changes: 40 additions & 0 deletions system/ui/lib/scroll_panel.py
Original file line number Diff line number Diff line change
@@ -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
17 changes: 17 additions & 0 deletions system/ui/lib/utils.py
Original file line number Diff line number Diff line change
@@ -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)
5 changes: 0 additions & 5 deletions system/ui/raylib/raylib.h

This file was deleted.

66 changes: 0 additions & 66 deletions system/ui/raylib/spinner.cc

This file was deleted.

65 changes: 0 additions & 65 deletions system/ui/raylib/util.cc

This file was deleted.

Loading

0 comments on commit ce7ff5c

Please sign in to comment.