raylib: base widget class (#35484)

* use some widgets

* consistent name draw -> render

* more

* rest
pull/35487/head
Shane Smiskol 6 days ago committed by GitHub
parent a1ee5f5ba8
commit 3ce87d0ac9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      selfdrive/ui/layouts/home.py
  2. 3
      selfdrive/ui/layouts/main.py
  3. 3
      selfdrive/ui/layouts/network.py
  4. 3
      selfdrive/ui/layouts/settings/developer.py
  5. 3
      selfdrive/ui/layouts/settings/device.py
  6. 4
      selfdrive/ui/layouts/settings/settings.py
  7. 3
      selfdrive/ui/layouts/settings/software.py
  8. 3
      selfdrive/ui/layouts/settings/toggles.py
  9. 4
      selfdrive/ui/layouts/sidebar.py
  10. 8
      selfdrive/ui/onroad/alert_renderer.py
  11. 8
      selfdrive/ui/onroad/augmented_road_view.py
  12. 4
      selfdrive/ui/onroad/cameraview.py
  13. 13
      selfdrive/ui/onroad/driver_camera_view.py
  14. 10
      selfdrive/ui/onroad/driver_state.py
  15. 8
      selfdrive/ui/onroad/hud_renderer.py
  16. 8
      selfdrive/ui/onroad/model_renderer.py
  17. 4
      selfdrive/ui/widgets/offroad_alerts.py
  18. 7
      system/ui/lib/application.py
  19. 4
      system/ui/lib/inputbox.py
  20. 19
      system/ui/lib/list_view.py
  21. 3
      system/ui/lib/toggle.py
  22. 4
      system/ui/reset.py
  23. 4
      system/ui/setup.py
  24. 6
      system/ui/spinner.py
  25. 6
      system/ui/text.py
  26. 6
      system/ui/updater.py
  27. 4
      system/ui/widgets/keyboard.py
  28. 4
      system/ui/widgets/network.py
  29. 3
      system/ui/widgets/option_dialog.py

@ -6,7 +6,7 @@ from openpilot.common.params import Params
from openpilot.selfdrive.ui.widgets.offroad_alerts import UpdateAlert, OffroadAlert from openpilot.selfdrive.ui.widgets.offroad_alerts import UpdateAlert, OffroadAlert
from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.label import gui_label from openpilot.system.ui.lib.label import gui_label
from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_COLOR from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_COLOR, Widget
HEADER_HEIGHT = 80 HEADER_HEIGHT = 80
HEAD_BUTTON_FONT_SIZE = 40 HEAD_BUTTON_FONT_SIZE = 40
@ -24,7 +24,7 @@ class HomeLayoutState(IntEnum):
ALERTS = 2 ALERTS = 2
class HomeLayout: class HomeLayout(Widget):
def __init__(self): def __init__(self):
self.params = Params() self.params = Params()

@ -5,6 +5,7 @@ from openpilot.selfdrive.ui.layouts.home import HomeLayout
from openpilot.selfdrive.ui.layouts.settings.settings import SettingsLayout from openpilot.selfdrive.ui.layouts.settings.settings import SettingsLayout
from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.selfdrive.ui.onroad.augmented_road_view import AugmentedRoadView from openpilot.selfdrive.ui.onroad.augmented_road_view import AugmentedRoadView
from openpilot.system.ui.lib.application import Widget
class MainState(IntEnum): class MainState(IntEnum):
@ -13,7 +14,7 @@ class MainState(IntEnum):
ONROAD = 2 ONROAD = 2
class MainLayout: class MainLayout(Widget):
def __init__(self): def __init__(self):
self._sidebar = Sidebar() self._sidebar = Sidebar()
self._sidebar_visible = True self._sidebar_visible = True

@ -1,9 +1,10 @@
import pyray as rl import pyray as rl
from openpilot.system.ui.lib.application import Widget
from openpilot.system.ui.lib.wifi_manager import WifiManagerWrapper from openpilot.system.ui.lib.wifi_manager import WifiManagerWrapper
from openpilot.system.ui.widgets.network import WifiManagerUI from openpilot.system.ui.widgets.network import WifiManagerUI
class NetworkLayout: class NetworkLayout(Widget):
def __init__(self): def __init__(self):
self.wifi_manager = WifiManagerWrapper() self.wifi_manager = WifiManagerWrapper()
self.wifi_ui = WifiManagerUI(self.wifi_manager) self.wifi_ui = WifiManagerUI(self.wifi_manager)

@ -1,3 +1,4 @@
from openpilot.system.ui.lib.application import Widget
from openpilot.system.ui.lib.list_view import ListView, toggle_item from openpilot.system.ui.lib.list_view import ListView, toggle_item
from openpilot.common.params import Params from openpilot.common.params import Params
@ -11,7 +12,7 @@ DESCRIPTIONS = {
} }
class DeveloperLayout: class DeveloperLayout(Widget):
def __init__(self): def __init__(self):
self._params = Params() self._params = Params()
items = [ items = [

@ -1,3 +1,4 @@
from openpilot.system.ui.lib.application import Widget
from openpilot.system.ui.lib.list_view import ListView, text_item, button_item from openpilot.system.ui.lib.list_view import ListView, text_item, button_item
from openpilot.common.params import Params from openpilot.common.params import Params
from openpilot.system.hardware import TICI from openpilot.system.hardware import TICI
@ -14,7 +15,7 @@ DESCRIPTIONS = {
} }
class DeviceLayout: class DeviceLayout(Widget):
def __init__(self): def __init__(self):
params = Params() params = Params()
dongle_id = params.get("DongleId", encoding="utf-8") or "N/A" dongle_id = params.get("DongleId", encoding="utf-8") or "N/A"

@ -7,7 +7,7 @@ from openpilot.selfdrive.ui.layouts.settings.developer import DeveloperLayout
from openpilot.selfdrive.ui.layouts.settings.device import DeviceLayout from openpilot.selfdrive.ui.layouts.settings.device import DeviceLayout
from openpilot.selfdrive.ui.layouts.settings.software import SoftwareLayout from openpilot.selfdrive.ui.layouts.settings.software import SoftwareLayout
from openpilot.selfdrive.ui.layouts.settings.toggles import TogglesLayout from openpilot.selfdrive.ui.layouts.settings.toggles import TogglesLayout
from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.application import gui_app, FontWeight, Widget
from openpilot.system.ui.lib.label import gui_text_box from openpilot.system.ui.lib.label import gui_text_box
from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.selfdrive.ui.layouts.network import NetworkLayout from openpilot.selfdrive.ui.layouts.network import NetworkLayout
@ -48,7 +48,7 @@ class PanelInfo:
button_rect: rl.Rectangle button_rect: rl.Rectangle
class SettingsLayout: class SettingsLayout(Widget):
def __init__(self): def __init__(self):
self._params = Params() self._params = Params()
self._current_panel = PanelType.DEVICE self._current_panel = PanelType.DEVICE

@ -1,7 +1,8 @@
from openpilot.system.ui.lib.application import Widget
from openpilot.system.ui.lib.list_view import ListView, button_item, text_item from openpilot.system.ui.lib.list_view import ListView, button_item, text_item
class SoftwareLayout: class SoftwareLayout(Widget):
def __init__(self): def __init__(self):
items = [ items = [
text_item("Current Version", ""), text_item("Current Version", ""),

@ -1,3 +1,4 @@
from openpilot.system.ui.lib.application import Widget
from openpilot.system.ui.lib.list_view import ListView, toggle_item from openpilot.system.ui.lib.list_view import ListView, toggle_item
from openpilot.common.params import Params from openpilot.common.params import Params
@ -18,7 +19,7 @@ DESCRIPTIONS = {
} }
class TogglesLayout: class TogglesLayout(Widget):
def __init__(self): def __init__(self):
self._params = Params() self._params = Params()
items = [ items = [

@ -4,7 +4,7 @@ from dataclasses import dataclass
from collections.abc import Callable from collections.abc import Callable
from cereal import log from cereal import log
from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.application import gui_app, FontWeight, Widget
from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.text_measure import measure_text_cached
SIDEBAR_WIDTH = 300 SIDEBAR_WIDTH = 300
@ -60,7 +60,7 @@ class MetricData:
self.color = color self.color = color
class Sidebar: class Sidebar(Widget):
def __init__(self): def __init__(self):
self._net_type = NETWORK_TYPES.get(NetworkType.none) self._net_type = NETWORK_TYPES.get(NetworkType.none)
self._net_strength = 0 self._net_strength = 0

@ -3,7 +3,7 @@ import pyray as rl
from dataclasses import dataclass from dataclasses import dataclass
from cereal import messaging, log from cereal import messaging, log
from openpilot.system.hardware import TICI from openpilot.system.hardware import TICI
from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_FPS from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_FPS, Widget
from openpilot.system.ui.lib.label import gui_text_box from openpilot.system.ui.lib.label import gui_text_box
from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.ui_state import ui_state
@ -59,7 +59,7 @@ ALERT_CRITICAL_REBOOT = Alert(
) )
class AlertRenderer: class AlertRenderer(Widget):
def __init__(self): def __init__(self):
self.font_regular: rl.Font = gui_app.font(FontWeight.NORMAL) self.font_regular: rl.Font = gui_app.font(FontWeight.NORMAL)
self.font_bold: rl.Font = gui_app.font(FontWeight.BOLD) self.font_bold: rl.Font = gui_app.font(FontWeight.BOLD)
@ -91,8 +91,8 @@ class AlertRenderer:
# Return current alert # Return current alert
return Alert(text1=ss.alertText1, text2=ss.alertText2, size=ss.alertSize, status=ss.alertStatus) return Alert(text1=ss.alertText1, text2=ss.alertText2, size=ss.alertSize, status=ss.alertStatus)
def draw(self, rect: rl.Rectangle, sm: messaging.SubMaster) -> None: def render(self, rect: rl.Rectangle) -> None:
alert = self.get_alert(sm) alert = self.get_alert(ui_state.sm)
if not alert: if not alert:
return return

@ -78,10 +78,10 @@ class AugmentedRoadView(CameraView):
super().render(rect) super().render(rect)
# Draw all UI overlays # Draw all UI overlays
self.model_renderer.draw(self._content_rect, ui_state.sm) self.model_renderer.render(self._content_rect)
self._hud_renderer.draw(self._content_rect, ui_state.sm) self._hud_renderer.render(self._content_rect)
self.alert_renderer.draw(self._content_rect, ui_state.sm) self.alert_renderer.render(self._content_rect)
self.driver_state_renderer.draw(self._content_rect, ui_state.sm) self.driver_state_renderer.render(self._content_rect, ui_state.sm)
# Custom UI extension point - add custom overlays here # Custom UI extension point - add custom overlays here
# Use self._content_rect for positioning within camera bounds # Use self._content_rect for positioning within camera bounds

@ -4,7 +4,7 @@ import pyray as rl
from openpilot.system.hardware import TICI from openpilot.system.hardware import TICI
from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf
from openpilot.common.swaglog import cloudlog from openpilot.common.swaglog import cloudlog
from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.application import gui_app, Widget
from openpilot.system.ui.lib.egl import init_egl, create_egl_image, destroy_egl_image, bind_egl_image_to_texture, EGLImage from openpilot.system.ui.lib.egl import init_egl, create_egl_image, destroy_egl_image, bind_egl_image_to_texture, EGLImage
CONNECTION_RETRY_INTERVAL = 0.2 # seconds between connection attempts CONNECTION_RETRY_INTERVAL = 0.2 # seconds between connection attempts
@ -55,7 +55,7 @@ else:
""" """
class CameraView: class CameraView(Widget):
def __init__(self, name: str, stream_type: VisionStreamType): def __init__(self, name: str, stream_type: VisionStreamType):
self._name = name self._name = name
# Primary stream # Primary stream

@ -1,9 +1,9 @@
import numpy as np import numpy as np
import pyray as rl import pyray as rl
from cereal import messaging
from msgq.visionipc import VisionStreamType from msgq.visionipc import VisionStreamType
from openpilot.selfdrive.ui.onroad.cameraview import CameraView from openpilot.selfdrive.ui.onroad.cameraview import CameraView
from openpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer from openpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.label import gui_label from openpilot.system.ui.lib.label import gui_label
@ -13,7 +13,7 @@ class DriverCameraView(CameraView):
super().__init__("camerad", stream_type) super().__init__("camerad", stream_type)
self.driver_state_renderer = DriverStateRenderer() self.driver_state_renderer = DriverStateRenderer()
def render(self, rect, sm): def render(self, rect):
super().render(rect) super().render(rect)
if not self.frame: if not self.frame:
@ -26,8 +26,8 @@ class DriverCameraView(CameraView):
) )
return return
self._draw_face_detection(rect, sm) self._draw_face_detection(rect, ui_state.sm)
self.driver_state_renderer.draw(rect, sm) self.driver_state_renderer.draw(rect, ui_state.sm)
def _draw_face_detection(self, rect: rl.Rectangle, sm) -> None: def _draw_face_detection(self, rect: rl.Rectangle, sm) -> None:
driver_state = sm["driverStateV2"] driver_state = sm["driverStateV2"]
@ -83,12 +83,11 @@ class DriverCameraView(CameraView):
if __name__ == "__main__": if __name__ == "__main__":
gui_app.init_window("Driver Camera View") gui_app.init_window("Driver Camera View")
sm = messaging.SubMaster(["selfdriveState", "driverStateV2", "driverMonitoringState"])
driver_camera_view = DriverCameraView(VisionStreamType.VISION_STREAM_DRIVER) driver_camera_view = DriverCameraView(VisionStreamType.VISION_STREAM_DRIVER)
try: try:
for _ in gui_app.render(): for _ in gui_app.render():
sm.update() ui_state.update()
driver_camera_view.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height), sm) driver_camera_view.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
finally: finally:
driver_camera_view.close() driver_camera_view.close()

@ -2,7 +2,7 @@ import numpy as np
import pyray as rl import pyray as rl
from dataclasses import dataclass from dataclasses import dataclass
from openpilot.selfdrive.ui.ui_state import ui_state, UI_BORDER_SIZE from openpilot.selfdrive.ui.ui_state import ui_state, UI_BORDER_SIZE
from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.application import gui_app, Widget
# Default 3D coordinates for face keypoints as a NumPy array # Default 3D coordinates for face keypoints as a NumPy array
DEFAULT_FACE_KPTS_3D = np.array([ DEFAULT_FACE_KPTS_3D = np.array([
@ -41,7 +41,7 @@ class ArcData:
thickness: float thickness: float
class DriverStateRenderer: class DriverStateRenderer(Widget):
def __init__(self): def __init__(self):
# Initial state with NumPy arrays # Initial state with NumPy arrays
self.face_kpts_draw = DEFAULT_FACE_KPTS_3D.copy() self.face_kpts_draw = DEFAULT_FACE_KPTS_3D.copy()
@ -74,11 +74,11 @@ class DriverStateRenderer:
self.engaged_color = rl.Color(26, 242, 66, 255) self.engaged_color = rl.Color(26, 242, 66, 255)
self.disengaged_color = rl.Color(139, 139, 139, 255) self.disengaged_color = rl.Color(139, 139, 139, 255)
def draw(self, rect, sm): def render(self, rect):
if not self._is_visible(sm): if not self._is_visible(ui_state.sm):
return return
self._update_state(sm, rect) self._update_state(ui_state.sm, rect)
if not self.state_updated: if not self.state_updated:
return return

@ -2,7 +2,7 @@ import pyray as rl
from dataclasses import dataclass from dataclasses import dataclass
from cereal.messaging import SubMaster from cereal.messaging import SubMaster
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.application import gui_app, FontWeight, Widget
from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.common.conversions import Conversions as CV from openpilot.common.conversions import Conversions as CV
@ -54,7 +54,7 @@ FONT_SIZES = FontSizes()
COLORS = Colors() COLORS = Colors()
class HudRenderer: class HudRenderer(Widget):
def __init__(self): def __init__(self):
"""Initialize the HUD renderer.""" """Initialize the HUD renderer."""
self.is_cruise_set: bool = False self.is_cruise_set: bool = False
@ -94,9 +94,9 @@ class HudRenderer:
speed_conversion = CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH speed_conversion = CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH
self.speed = max(0.0, v_ego * speed_conversion) self.speed = max(0.0, v_ego * speed_conversion)
def draw(self, rect: rl.Rectangle, sm: SubMaster) -> None: def render(self, rect: rl.Rectangle) -> None:
"""Render HUD elements to the screen.""" """Render HUD elements to the screen."""
self._update_state(sm) self._update_state(ui_state.sm)
rl.draw_rectangle_gradient_v( rl.draw_rectangle_gradient_v(
int(rect.x), int(rect.x),
int(rect.y), int(rect.y),

@ -5,7 +5,7 @@ from cereal import messaging, car
from dataclasses import dataclass, field from dataclasses import dataclass, field
from openpilot.common.params import Params from openpilot.common.params import Params
from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.application import DEFAULT_FPS from openpilot.system.ui.lib.application import DEFAULT_FPS, Widget
from openpilot.system.ui.lib.shader_polygon import draw_polygon from openpilot.system.ui.lib.shader_polygon import draw_polygon
from openpilot.selfdrive.locationd.calibrationd import HEIGHT_INIT from openpilot.selfdrive.locationd.calibrationd import HEIGHT_INIT
@ -43,7 +43,7 @@ class LeadVehicle:
fill_alpha: int = 0 fill_alpha: int = 0
class ModelRenderer: class ModelRenderer(Widget):
def __init__(self): def __init__(self):
self._longitudinal_control = False self._longitudinal_control = False
self._experimental_mode = False self._experimental_mode = False
@ -86,7 +86,9 @@ class ModelRenderer:
self._car_space_transform = transform.astype(np.float32) self._car_space_transform = transform.astype(np.float32)
self._transform_dirty = True self._transform_dirty = True
def draw(self, rect: rl.Rectangle, sm: messaging.SubMaster): def render(self, rect: rl.Rectangle):
sm = ui_state.sm
# Check if data is up-to-date # Check if data is up-to-date
if (sm.recv_frame["liveCalibration"] < ui_state.started_frame or if (sm.recv_frame["liveCalibration"] < ui_state.started_frame or
sm.recv_frame["modelV2"] < ui_state.started_frame): sm.recv_frame["modelV2"] < ui_state.started_frame):

@ -8,7 +8,7 @@ from openpilot.system.hardware import HARDWARE
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
from openpilot.system.ui.lib.wrap_text import wrap_text from openpilot.system.ui.lib.wrap_text import wrap_text
from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.application import gui_app, FontWeight, Widget
class AlertColors: class AlertColors:
@ -41,7 +41,7 @@ class AlertData:
visible: bool = False visible: bool = False
class AbstractAlert(ABC): class AbstractAlert(Widget, ABC):
def __init__(self, has_reboot_btn: bool = False): def __init__(self, has_reboot_btn: bool = False):
self.params = Params() self.params = Params()
self.has_reboot_btn = has_reboot_btn self.has_reboot_btn = has_reboot_btn

@ -1,3 +1,4 @@
import abc
import atexit import atexit
import os import os
import time import time
@ -26,6 +27,12 @@ ASSETS_DIR = files("openpilot.selfdrive").joinpath("assets")
FONT_DIR = ASSETS_DIR.joinpath("fonts") FONT_DIR = ASSETS_DIR.joinpath("fonts")
class Widget(abc.ABC):
@abc.abstractmethod
def render(self, rect: rl.Rectangle) -> bool | None:
"""Render the widget within the given rectangle."""
class FontWeight(IntEnum): class FontWeight(IntEnum):
THIN = 0 THIN = 0
EXTRA_LIGHT = 1 EXTRA_LIGHT = 1

@ -1,13 +1,13 @@
import pyray as rl import pyray as rl
import time import time
from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.application import gui_app, Widget
from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.text_measure import measure_text_cached
PASSWORD_MASK_CHAR = "" PASSWORD_MASK_CHAR = ""
PASSWORD_MASK_DELAY = 1.5 # Seconds to show character before masking PASSWORD_MASK_DELAY = 1.5 # Seconds to show character before masking
class InputBox: class InputBox(Widget):
def __init__(self, max_text_size=255, password_mode=False): def __init__(self, max_text_size=255, password_mode=False):
self._max_text_size = max_text_size self._max_text_size = max_text_size
self._input_text = "" self._input_text = ""

@ -4,12 +4,11 @@ from dataclasses import dataclass
from collections.abc import Callable from collections.abc import Callable
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.application import gui_app, FontWeight, Widget
from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.wrap_text import wrap_text from openpilot.system.ui.lib.wrap_text import wrap_text
from openpilot.system.ui.lib.button import gui_button from openpilot.system.ui.lib.button import gui_button
from openpilot.system.ui.lib.toggle import Toggle from openpilot.system.ui.lib.toggle import Toggle, WIDTH as TOGGLE_WIDTH, HEIGHT as TOGGLE_HEIGHT
from openpilot.system.ui.lib.toggle import WIDTH as TOGGLE_WIDTH, HEIGHT as TOGGLE_HEIGHT
LINE_PADDING = 40 LINE_PADDING = 40
LINE_COLOR = rl.GRAY LINE_COLOR = rl.GRAY
@ -31,15 +30,11 @@ BUTTON_FONT_WEIGHT = FontWeight.MEDIUM
# Abstract base class for right-side items # Abstract base class for right-side items
class RightItem(ABC): class RightItem(Widget, ABC):
def __init__(self, width: int = 100): def __init__(self, width: int = 100):
self.width = width self.width = width
self.enabled = True self.enabled = True
@abstractmethod
def draw(self, rect: rl.Rectangle) -> bool:
pass
@abstractmethod @abstractmethod
def get_width(self) -> int: def get_width(self) -> int:
pass pass
@ -52,7 +47,7 @@ class ToggleRightItem(RightItem):
self.state = initial_state self.state = initial_state
self.enabled = True self.enabled = True
def draw(self, rect: rl.Rectangle) -> bool: def render(self, rect: rl.Rectangle) -> bool:
if self.toggle.render(rl.Rectangle(rect.x, rect.y + (rect.height - TOGGLE_HEIGHT) / 2, self.width, TOGGLE_HEIGHT)): if self.toggle.render(rl.Rectangle(rect.x, rect.y + (rect.height - TOGGLE_HEIGHT) / 2, self.width, TOGGLE_HEIGHT)):
self.state = not self.state self.state = not self.state
return True return True
@ -78,7 +73,7 @@ class ButtonRightItem(RightItem):
self.text = text self.text = text
self.enabled = True self.enabled = True
def draw(self, rect: rl.Rectangle) -> bool: def render(self, rect: rl.Rectangle) -> bool:
return ( return (
gui_button( gui_button(
rl.Rectangle(rect.x, rect.y + (rect.height - BUTTON_HEIGHT) / 2, BUTTON_WIDTH, BUTTON_HEIGHT), rl.Rectangle(rect.x, rect.y + (rect.height - BUTTON_HEIGHT) / 2, BUTTON_WIDTH, BUTTON_HEIGHT),
@ -108,7 +103,7 @@ class TextRightItem(RightItem):
text_width = measure_text_cached(font, text, font_size).x text_width = measure_text_cached(font, text, font_size).x
super().__init__(int(text_width + 20)) super().__init__(int(text_width + 20))
def draw(self, rect: rl.Rectangle) -> bool: def render(self, rect: rl.Rectangle) -> bool:
font = gui_app.font(FontWeight.NORMAL) font = gui_app.font(FontWeight.NORMAL)
text_size = measure_text_cached(font, self.text, self.font_size) text_size = measure_text_cached(font, self.text, self.font_size)
@ -269,7 +264,7 @@ class ListView:
right_rect = item.get_right_item_rect(rect) right_rect = item.get_right_item_rect(rect)
# Adjust for scroll offset # Adjust for scroll offset
right_rect.y = right_rect.y right_rect.y = right_rect.y
if item.right_item.draw(right_rect): if item.right_item.render(right_rect):
# Right item was clicked/activated # Right item was clicked/activated
if item.callback: if item.callback:
item.callback() item.callback()

@ -1,4 +1,5 @@
import pyray as rl import pyray as rl
from openpilot.system.ui.lib.application import Widget
ON_COLOR = rl.Color(51, 171, 76, 255) ON_COLOR = rl.Color(51, 171, 76, 255)
OFF_COLOR = rl.Color(0x39, 0x39, 0x39, 255) OFF_COLOR = rl.Color(0x39, 0x39, 0x39, 255)
@ -11,7 +12,7 @@ BG_HEIGHT = 60
ANIMATION_SPEED = 8.0 ANIMATION_SPEED = 8.0
class Toggle: class Toggle(Widget):
def __init__(self, initial_state=False): def __init__(self, initial_state=False):
self._state = initial_state self._state = initial_state
self._enabled = True self._enabled = True

@ -6,7 +6,7 @@ import threading
from enum import IntEnum from enum import IntEnum
from openpilot.system.hardware import PC from openpilot.system.hardware import PC
from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.application import gui_app, FontWeight, Widget
from openpilot.system.ui.lib.button import gui_button, ButtonStyle from openpilot.system.ui.lib.button import gui_button, ButtonStyle
from openpilot.system.ui.lib.label import gui_label, gui_text_box from openpilot.system.ui.lib.label import gui_label, gui_text_box
@ -27,7 +27,7 @@ class ResetState(IntEnum):
FAILED = 3 FAILED = 3
class Reset: class Reset(Widget):
def __init__(self, mode): def __init__(self, mode):
self.mode = mode self.mode = mode
self.reset_state = ResetState.NONE self.reset_state = ResetState.NONE

@ -9,7 +9,7 @@ import pyray as rl
from cereal import log from cereal import log
from openpilot.system.hardware import HARDWARE from openpilot.system.hardware import HARDWARE
from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.application import gui_app, FontWeight, Widget
from openpilot.system.ui.lib.button import gui_button, ButtonStyle from openpilot.system.ui.lib.button import gui_button, ButtonStyle
from openpilot.system.ui.lib.label import gui_label, gui_text_box from openpilot.system.ui.lib.label import gui_label, gui_text_box
from openpilot.system.ui.widgets.network import WifiManagerUI, WifiManagerWrapper from openpilot.system.ui.widgets.network import WifiManagerUI, WifiManagerWrapper
@ -39,7 +39,7 @@ class SetupState(IntEnum):
DOWNLOAD_FAILED = 6 DOWNLOAD_FAILED = 6
class Setup: class Setup(Widget):
def __init__(self): def __init__(self):
self.state = SetupState.GETTING_STARTED self.state = SetupState.GETTING_STARTED
self.network_check_thread = None self.network_check_thread = None

@ -3,7 +3,7 @@ import pyray as rl
import select import select
import sys import sys
from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.application import gui_app, Widget
from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.text import wrap_text from openpilot.system.ui.text import wrap_text
@ -22,7 +22,7 @@ def clamp(value, min_value, max_value):
return max(min(value, max_value), min_value) return max(min(value, max_value), min_value)
class Spinner: class Spinner(Widget):
def __init__(self): def __init__(self):
self._comma_texture = gui_app.texture("images/spinner_comma.png", TEXTURE_SIZE, TEXTURE_SIZE) self._comma_texture = gui_app.texture("images/spinner_comma.png", TEXTURE_SIZE, TEXTURE_SIZE)
self._spinner_texture = gui_app.texture("images/spinner_track.png", TEXTURE_SIZE, TEXTURE_SIZE, alpha_premultiply=True) self._spinner_texture = gui_app.texture("images/spinner_track.png", TEXTURE_SIZE, TEXTURE_SIZE, alpha_premultiply=True)
@ -38,7 +38,7 @@ class Spinner:
self._progress = None self._progress = None
self._wrapped_lines = wrap_text(text, FONT_SIZE, gui_app.width - MARGIN_H) self._wrapped_lines = wrap_text(text, FONT_SIZE, gui_app.width - MARGIN_H)
def render(self): def render(self, _: rl.Rectangle):
if self._wrapped_lines: if self._wrapped_lines:
# Calculate total height required for spinner and text # Calculate total height required for spinner and text
spacing = 50 spacing = 50

@ -6,7 +6,7 @@ from openpilot.system.hardware import HARDWARE, PC
from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.button import gui_button, ButtonStyle from openpilot.system.ui.lib.button import gui_button, ButtonStyle
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.application import gui_app, Widget
MARGIN = 50 MARGIN = 50
SPACING = 40 SPACING = 40
@ -46,7 +46,7 @@ def wrap_text(text, font_size, max_width):
return lines return lines
class TextWindow: class TextWindow(Widget):
def __init__(self, text: str): def __init__(self, text: str):
self._textarea_rect = rl.Rectangle(MARGIN, MARGIN, gui_app.width - MARGIN * 2, gui_app.height - MARGIN * 2) 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._wrapped_lines = wrap_text(text, FONT_SIZE, self._textarea_rect.width - 20)
@ -54,7 +54,7 @@ class TextWindow:
self._scroll_panel = GuiScrollPanel(show_vertical_scroll_bar=True) self._scroll_panel = GuiScrollPanel(show_vertical_scroll_bar=True)
self._scroll_panel._offset.y = -max(self._content_rect.height - self._textarea_rect.height, 0) self._scroll_panel._offset.y = -max(self._content_rect.height - self._textarea_rect.height, 0)
def render(self): def render(self, _: rl.Rectangle = None):
scroll = self._scroll_panel.handle_scroll(self._textarea_rect, self._content_rect) 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)) 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): for i, line in enumerate(self._wrapped_lines):

@ -6,7 +6,7 @@ import pyray as rl
from enum import IntEnum from enum import IntEnum
from openpilot.system.hardware import HARDWARE from openpilot.system.hardware import HARDWARE
from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.application import gui_app, FontWeight, Widget
from openpilot.system.ui.lib.button import gui_button, ButtonStyle from openpilot.system.ui.lib.button import gui_button, ButtonStyle
from openpilot.system.ui.lib.label import gui_text_box, gui_label from openpilot.system.ui.lib.label import gui_text_box, gui_label
from openpilot.system.ui.lib.wifi_manager import WifiManagerWrapper from openpilot.system.ui.lib.wifi_manager import WifiManagerWrapper
@ -30,7 +30,7 @@ class Screen(IntEnum):
PROGRESS = 2 PROGRESS = 2
class Updater: class Updater(Widget):
def __init__(self, updater_path, manifest_path): def __init__(self, updater_path, manifest_path):
self.updater = updater_path self.updater = updater_path
self.manifest = manifest_path self.manifest = manifest_path
@ -137,7 +137,7 @@ class Updater:
HARDWARE.reboot() HARDWARE.reboot()
return return
def render(self): def render(self, _: rl.Rectangle = None):
if self.current_screen == Screen.PROMPT: if self.current_screen == Screen.PROMPT:
self.render_prompt_screen() self.render_prompt_screen()
elif self.current_screen == Screen.WIFI: elif self.current_screen == Screen.WIFI:

@ -1,7 +1,7 @@
import time import time
from typing import Literal from typing import Literal
import pyray as rl import pyray as rl
from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.application import gui_app, FontWeight, Widget
from openpilot.system.ui.lib.button import ButtonStyle, gui_button from openpilot.system.ui.lib.button import ButtonStyle, gui_button
from openpilot.system.ui.lib.inputbox import InputBox from openpilot.system.ui.lib.inputbox import InputBox
from openpilot.system.ui.lib.label import gui_label from openpilot.system.ui.lib.label import gui_label
@ -52,7 +52,7 @@ KEYBOARD_LAYOUTS = {
} }
class Keyboard: class Keyboard(Widget):
def __init__(self, max_text_size: int = 255, min_text_size: int = 0, password_mode: bool = False, show_password_toggle: bool = False): def __init__(self, max_text_size: int = 255, min_text_size: int = 0, password_mode: bool = False, show_password_toggle: bool = False):
self._layout_name: Literal["lowercase", "uppercase", "numbers", "specials"] = "lowercase" self._layout_name: Literal["lowercase", "uppercase", "numbers", "specials"] = "lowercase"
self._caps_lock = False self._caps_lock = False

@ -3,7 +3,7 @@ from threading import Lock
from typing import Literal from typing import Literal
import pyray as rl import pyray as rl
from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.application import gui_app, Widget
from openpilot.system.ui.lib.button import ButtonStyle, gui_button from openpilot.system.ui.lib.button import ButtonStyle, gui_button
from openpilot.system.ui.lib.label import gui_label from openpilot.system.ui.lib.label import gui_label
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
@ -57,7 +57,7 @@ class StateForgetting:
UIState = StateIdle | StateConnecting | StateNeedsAuth | StateShowForgetConfirm | StateForgetting UIState = StateIdle | StateConnecting | StateNeedsAuth | StateShowForgetConfirm | StateForgetting
class WifiManagerUI: class WifiManagerUI(Widget):
def __init__(self, wifi_manager: WifiManagerWrapper): def __init__(self, wifi_manager: WifiManagerWrapper):
self.state: UIState = StateIdle() self.state: UIState = StateIdle()
self.btn_width: int = 200 self.btn_width: int = 200

@ -1,11 +1,12 @@
import pyray as rl import pyray as rl
from openpilot.system.ui.lib.application import Widget
from openpilot.system.ui.lib.button import gui_button, ButtonStyle, TextAlignment from openpilot.system.ui.lib.button import gui_button, ButtonStyle, TextAlignment
from openpilot.system.ui.lib.label import gui_label from openpilot.system.ui.lib.label import gui_label
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
class MultiOptionDialog: class MultiOptionDialog(Widget):
def __init__(self, title, options, current=""): def __init__(self, title, options, current=""):
self._title = title self._title = title
self._options = options self._options = options

Loading…
Cancel
Save