raylib: font sizes from QT should match (#36306)

* pt 2

* fix line height

* fixup html renderer

* fix sidebar

* fix label line height

* firehose fixups

* fix ssh value font styling

* fixup inputbot

* do experimental mode

* pairing dialog numbers

* fix radius for prime user

* add emoji to firehose mode

* full screen registration

* fix registration btn size

* fix update and alerts

* debugging

* Revert "debugging"

This reverts commit 0095372e94.

* firehose styling

* fix offroad alerts missing bottom spacing expansion

* huge oof

* huge oof
pull/36305/head^2
Shane Smiskol 3 days ago committed by GitHub
parent fdcf8b592e
commit b6dbb0fd8d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 13
      selfdrive/ui/layouts/home.py
  2. 1
      selfdrive/ui/layouts/main.py
  3. 77
      selfdrive/ui/layouts/settings/firehose.py
  4. 4
      selfdrive/ui/layouts/sidebar.py
  5. 11
      selfdrive/ui/widgets/exp_mode_button.py
  6. 20
      selfdrive/ui/widgets/offroad_alerts.py
  7. 4
      selfdrive/ui/widgets/pairing_dialog.py
  8. 2
      selfdrive/ui/widgets/prime.py
  9. 19
      selfdrive/ui/widgets/setup.py
  10. 10
      selfdrive/ui/widgets/ssh_key.py
  11. 15
      system/ui/lib/application.py
  12. 4
      system/ui/lib/scroll_panel.py
  13. 3
      system/ui/lib/text_measure.py
  14. 32
      system/ui/widgets/html_render.py
  15. 6
      system/ui/widgets/inputbox.py
  16. 6
      system/ui/widgets/label.py

@ -68,6 +68,7 @@ class HomeLayout(Widget):
def _setup_callbacks(self): def _setup_callbacks(self):
self.update_alert.set_dismiss_callback(lambda: self._set_state(HomeLayoutState.HOME)) self.update_alert.set_dismiss_callback(lambda: self._set_state(HomeLayoutState.HOME))
self.offroad_alert.set_dismiss_callback(lambda: self._set_state(HomeLayoutState.HOME)) self.offroad_alert.set_dismiss_callback(lambda: self._set_state(HomeLayoutState.HOME))
self._exp_mode_button.set_click_callback(lambda: self.settings_callback() if self.settings_callback else None)
def set_settings_callback(self, callback: Callable): def set_settings_callback(self, callback: Callable):
self.settings_callback = callback self.settings_callback = callback
@ -147,9 +148,9 @@ class HomeLayout(Widget):
rl.draw_rectangle_rounded(self.update_notif_rect, 0.3, 10, highlight_color) rl.draw_rectangle_rounded(self.update_notif_rect, 0.3, 10, highlight_color)
text = "UPDATE" text = "UPDATE"
text_width = measure_text_cached(font, text, HEAD_BUTTON_FONT_SIZE).x text_size = measure_text_cached(font, text, HEAD_BUTTON_FONT_SIZE)
text_x = self.update_notif_rect.x + (self.update_notif_rect.width - text_width) // 2 text_x = self.update_notif_rect.x + (self.update_notif_rect.width - text_size.x) // 2
text_y = self.update_notif_rect.y + (self.update_notif_rect.height - HEAD_BUTTON_FONT_SIZE) // 2 text_y = self.update_notif_rect.y + (self.update_notif_rect.height - text_size.y) // 2
rl.draw_text_ex(font, text, rl.Vector2(int(text_x), int(text_y)), HEAD_BUTTON_FONT_SIZE, 0, rl.WHITE) rl.draw_text_ex(font, text, rl.Vector2(int(text_x), int(text_y)), HEAD_BUTTON_FONT_SIZE, 0, rl.WHITE)
# Alert notification button # Alert notification button
@ -161,9 +162,9 @@ class HomeLayout(Widget):
rl.draw_rectangle_rounded(self.alert_notif_rect, 0.3, 10, highlight_color) rl.draw_rectangle_rounded(self.alert_notif_rect, 0.3, 10, highlight_color)
alert_text = f"{self.alert_count} ALERT{'S' if self.alert_count > 1 else ''}" alert_text = f"{self.alert_count} ALERT{'S' if self.alert_count > 1 else ''}"
text_width = measure_text_cached(font, alert_text, HEAD_BUTTON_FONT_SIZE).x text_size = measure_text_cached(font, alert_text, HEAD_BUTTON_FONT_SIZE)
text_x = self.alert_notif_rect.x + (self.alert_notif_rect.width - text_width) // 2 text_x = self.alert_notif_rect.x + (self.alert_notif_rect.width - text_size.x) // 2
text_y = self.alert_notif_rect.y + (self.alert_notif_rect.height - HEAD_BUTTON_FONT_SIZE) // 2 text_y = self.alert_notif_rect.y + (self.alert_notif_rect.height - text_size.y) // 2
rl.draw_text_ex(font, alert_text, rl.Vector2(int(text_x), int(text_y)), HEAD_BUTTON_FONT_SIZE, 0, rl.WHITE) rl.draw_text_ex(font, alert_text, rl.Vector2(int(text_x), int(text_y)), HEAD_BUTTON_FONT_SIZE, 0, rl.WHITE)
# Version text (right aligned) # Version text (right aligned)

@ -50,6 +50,7 @@ class MainLayout(Widget):
on_flag=self._on_bookmark_clicked, on_flag=self._on_bookmark_clicked,
open_settings=lambda: self.open_settings(PanelType.TOGGLES)) open_settings=lambda: self.open_settings(PanelType.TOGGLES))
self._layouts[MainState.HOME]._setup_widget.set_open_settings_callback(lambda: self.open_settings(PanelType.FIREHOSE)) self._layouts[MainState.HOME]._setup_widget.set_open_settings_callback(lambda: self.open_settings(PanelType.FIREHOSE))
self._layouts[MainState.HOME].set_settings_callback(lambda: self.open_settings(PanelType.TOGGLES))
self._layouts[MainState.SETTINGS].set_callbacks(on_close=self._set_mode_for_state) self._layouts[MainState.SETTINGS].set_callbacks(on_close=self._set_mode_for_state)
self._layouts[MainState.ONROAD].set_click_callback(self._on_onroad_clicked) self._layouts[MainState.ONROAD].set_click_callback(self._on_onroad_clicked)
device.add_interactive_timeout_callback(self._set_mode_for_state) device.add_interactive_timeout_callback(self._set_mode_for_state)

@ -7,7 +7,8 @@ from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog from openpilot.common.swaglog import cloudlog
from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID
from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE
from openpilot.system.ui.lib.text_measure import measure_text_cached
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.widgets import Widget from openpilot.system.ui.widgets import Widget
@ -21,7 +22,7 @@ DESCRIPTION = (
) )
INSTRUCTIONS = ( INSTRUCTIONS = (
"For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.\n\n" "For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.\n\n"
+ "Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.\n\n" + "Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.\n\n\n"
+ "Frequently Asked Questions\n\n" + "Frequently Asked Questions\n\n"
+ "Does it matter how or where I drive? Nope, just drive as you normally would.\n\n" + "Does it matter how or where I drive? Nope, just drive as you normally would.\n\n"
+ "Do all of my segments get pulled in Firehose Mode? No, we selectively pull a subset of your segments.\n\n" + "Do all of my segments get pulled in Firehose Mode? No, we selectively pull a subset of your segments.\n\n"
@ -43,6 +44,7 @@ class FirehoseLayout(Widget):
self.params = Params() self.params = Params()
self.segment_count = self._get_segment_count() self.segment_count = self._get_segment_count()
self.scroll_panel = GuiScrollPanel() self.scroll_panel = GuiScrollPanel()
self._content_height = 0
self.running = True self.running = True
self.update_thread = threading.Thread(target=self._update_loop, daemon=True) self.update_thread = threading.Thread(target=self._update_loop, daemon=True)
@ -69,88 +71,61 @@ class FirehoseLayout(Widget):
def _render(self, rect: rl.Rectangle): def _render(self, rect: rl.Rectangle):
# Calculate content dimensions # Calculate content dimensions
content_width = rect.width - 80 content_rect = rl.Rectangle(rect.x, rect.y, rect.width, self._content_height)
content_height = self._calculate_content_height(int(content_width))
content_rect = rl.Rectangle(rect.x, rect.y, rect.width, content_height)
# Handle scrolling and render with clipping # Handle scrolling and render with clipping
scroll_offset = self.scroll_panel.update(rect, content_rect) scroll_offset = self.scroll_panel.update(rect, content_rect)
rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height)) rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height))
self._render_content(rect, scroll_offset) self._content_height = self._render_content(rect, scroll_offset)
rl.end_scissor_mode() rl.end_scissor_mode()
def _calculate_content_height(self, content_width: int) -> int: def _render_content(self, rect: rl.Rectangle, scroll_offset: float) -> int:
height = 80 # Top margin
# Title
height += 100 + 40
# Description
desc_font = gui_app.font(FontWeight.NORMAL)
desc_lines = wrap_text(desc_font, DESCRIPTION, 45, content_width)
height += len(desc_lines) * 45 + 40
# Status section
height += 32 # Separator
status_text, _ = self._get_status()
status_lines = wrap_text(gui_app.font(FontWeight.BOLD), status_text, 60, content_width)
height += len(status_lines) * 60 + 20
# Contribution count (if available)
if self.segment_count > 0:
contrib_text = f"{self.segment_count} segment(s) of your driving is in the training dataset so far."
contrib_lines = wrap_text(gui_app.font(FontWeight.BOLD), contrib_text, 52, content_width)
height += len(contrib_lines) * 52 + 20
# Instructions section
height += 32 # Separator
inst_lines = wrap_text(gui_app.font(FontWeight.NORMAL), INSTRUCTIONS, 40, content_width)
height += len(inst_lines) * 40 + 40 # Bottom margin
return height
def _render_content(self, rect: rl.Rectangle, scroll_offset: float):
x = int(rect.x + 40) x = int(rect.x + 40)
y = int(rect.y + 40 + scroll_offset) y = int(rect.y + 40 + scroll_offset)
w = int(rect.width - 80) w = int(rect.width - 80)
# Title # Title (centered)
title_font = gui_app.font(FontWeight.MEDIUM) title_font = gui_app.font(FontWeight.MEDIUM)
rl.draw_text_ex(title_font, TITLE, rl.Vector2(x, y), 100, 0, rl.WHITE) text_width = measure_text_cached(title_font, TITLE, 100).x
y += 140 title_x = rect.x + (rect.width - text_width) / 2
rl.draw_text_ex(title_font, TITLE, rl.Vector2(title_x, y), 100, 0, rl.WHITE)
y += 200
# Description # Description
y = self._draw_wrapped_text(x, y, w, DESCRIPTION, gui_app.font(FontWeight.NORMAL), 45, rl.WHITE) y = self._draw_wrapped_text(x, y, w, DESCRIPTION, gui_app.font(FontWeight.NORMAL), 45, rl.WHITE)
y += 40 y += 40 + 20
# Separator # Separator
rl.draw_rectangle(x, y, w, 2, self.GRAY) rl.draw_rectangle(x, y, w, 2, self.GRAY)
y += 30 y += 30 + 20
# Status # Status
status_text, status_color = self._get_status() status_text, status_color = self._get_status()
y = self._draw_wrapped_text(x, y, w, status_text, gui_app.font(FontWeight.BOLD), 60, status_color) y = self._draw_wrapped_text(x, y, w, status_text, gui_app.font(FontWeight.BOLD), 60, status_color)
y += 20 y += 20 + 20
# Contribution count (if available) # Contribution count (if available)
if self.segment_count > 0: if self.segment_count > 0:
contrib_text = f"{self.segment_count} segment(s) of your driving is in the training dataset so far." contrib_text = f"{self.segment_count} segment(s) of your driving is in the training dataset so far."
y = self._draw_wrapped_text(x, y, w, contrib_text, gui_app.font(FontWeight.BOLD), 52, rl.WHITE) y = self._draw_wrapped_text(x, y, w, contrib_text, gui_app.font(FontWeight.BOLD), 52, rl.WHITE)
y += 20 y += 20 + 20
# Separator # Separator
rl.draw_rectangle(x, y, w, 2, self.GRAY) rl.draw_rectangle(x, y, w, 2, self.GRAY)
y += 30 y += 30 + 20
# Instructions # Instructions
self._draw_wrapped_text(x, y, w, INSTRUCTIONS, gui_app.font(FontWeight.NORMAL), 40, self.LIGHT_GRAY) y = self._draw_wrapped_text(x, y, w, INSTRUCTIONS, gui_app.font(FontWeight.NORMAL), 40, self.LIGHT_GRAY)
# bottom margin + remove effect of scroll offset
return int(round(y - self.scroll_panel.offset + 40))
def _draw_wrapped_text(self, x, y, width, text, font, size, color): def _draw_wrapped_text(self, x, y, width, text, font, font_size, color):
wrapped = wrap_text(font, text, size, width) wrapped = wrap_text(font, text, font_size, width)
for line in wrapped: for line in wrapped:
rl.draw_text_ex(font, line, rl.Vector2(x, y), size, 0, color) rl.draw_text_ex(font, line, rl.Vector2(x, y), font_size, 0, color)
y += size y += font_size * FONT_SCALE
return y return round(y)
def _get_status(self) -> tuple[str, rl.Color]: def _get_status(self) -> tuple[str, rl.Color]:
network_type = ui_state.sm["deviceState"].networkType network_type = ui_state.sm["deviceState"].networkType

@ -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, MousePos from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, FONT_SCALE
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.widgets import Widget from openpilot.system.ui.widgets import Widget
@ -218,7 +218,7 @@ class Sidebar(Widget):
# Draw label and value # Draw label and value
labels = [metric.label, metric.value] labels = [metric.label, metric.value]
text_y = metric_rect.y + (metric_rect.height / 2 - len(labels) * FONT_SIZE) text_y = metric_rect.y + (metric_rect.height / 2 - len(labels) * FONT_SIZE * FONT_SCALE)
for text in labels: for text in labels:
text_size = measure_text_cached(self._font_bold, text, FONT_SIZE) text_size = measure_text_cached(self._font_bold, text, FONT_SIZE)
text_y += text_size.y text_y += text_size.y

@ -1,6 +1,6 @@
import pyray as rl import pyray as rl
from openpilot.common.params import Params from openpilot.common.params import Params
from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
@ -9,7 +9,7 @@ class ExperimentalModeButton(Widget):
super().__init__() super().__init__()
self.img_width = 80 self.img_width = 80
self.horizontal_padding = 50 self.horizontal_padding = 25
self.button_height = 125 self.button_height = 125
self.params = Params() self.params = Params()
@ -31,11 +31,6 @@ class ExperimentalModeButton(Widget):
rl.draw_rectangle_gradient_h(int(rect.x), int(rect.y), int(rect.width), int(rect.height), rl.draw_rectangle_gradient_h(int(rect.x), int(rect.y), int(rect.width), int(rect.height),
start_color, end_color) start_color, end_color)
def _handle_mouse_release(self, mouse_pos):
self.experimental_mode = not self.experimental_mode
# TODO: Opening settings for ExperimentalMode
self.params.put_bool("ExperimentalMode", self.experimental_mode)
def _render(self, rect): def _render(self, rect):
rl.draw_rectangle_rounded(rect, 0.08, 20, rl.WHITE) rl.draw_rectangle_rounded(rect, 0.08, 20, rl.WHITE)
@ -51,7 +46,7 @@ class ExperimentalModeButton(Widget):
# Draw text label (left aligned) # Draw text label (left aligned)
text = "EXPERIMENTAL MODE ON" if self.experimental_mode else "CHILL MODE ON" text = "EXPERIMENTAL MODE ON" if self.experimental_mode else "CHILL MODE ON"
text_x = rect.x + self.horizontal_padding text_x = rect.x + self.horizontal_padding
text_y = rect.y + rect.height / 2 - 45 // 2 # Center vertically text_y = rect.y + rect.height / 2 - 45 * FONT_SCALE // 2 # Center vertically
rl.draw_text_ex(gui_app.font(FontWeight.NORMAL), text, rl.Vector2(int(text_x), int(text_y)), 45, 0, rl.BLACK) rl.draw_text_ex(gui_app.font(FontWeight.NORMAL), text, rl.Vector2(int(text_x), int(text_y)), 45, 0, rl.BLACK)

@ -5,7 +5,7 @@ from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from openpilot.common.params import Params from openpilot.common.params import Params
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, FONT_SCALE
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
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
@ -65,8 +65,8 @@ class ActionButton(Widget):
def set_text(self, text: str): def set_text(self, text: str):
self._text = text self._text = text
self._text_width = measure_text_cached(gui_app.font(FontWeight.MEDIUM), self._text, AlertConstants.FONT_SIZE).x self._text_size = measure_text_cached(gui_app.font(FontWeight.MEDIUM), self._text, AlertConstants.FONT_SIZE)
self._rect.width = max(self._text_width + 60 * 2, self._min_width) self._rect.width = max(self._text_size.x + 60 * 2, self._min_width)
self._rect.height = AlertConstants.BUTTON_HEIGHT self._rect.height = AlertConstants.BUTTON_HEIGHT
def _render(self, _): def _render(self, _):
@ -79,8 +79,8 @@ class ActionButton(Widget):
# center text # center text
color = rl.WHITE if self._style == ButtonStyle.DARK else rl.BLACK color = rl.WHITE if self._style == ButtonStyle.DARK else rl.BLACK
text_x = int(self._rect.x + (self._rect.width - self._text_width) // 2) text_x = int(self._rect.x + (self._rect.width - self._text_size.x) // 2)
text_y = int(self._rect.y + (self._rect.height - AlertConstants.FONT_SIZE) // 2) text_y = int(self._rect.y + (self._rect.height - self._text_size.y) // 2)
rl.draw_text_ex(self._font, self._text, rl.Vector2(text_x, text_y), AlertConstants.FONT_SIZE, 0, color) rl.draw_text_ex(self._font, self._text, rl.Vector2(text_x, text_y), AlertConstants.FONT_SIZE, 0, color)
@ -250,9 +250,9 @@ class OffroadAlert(AbstractAlert):
text_width = int(self.content_rect.width - (AlertConstants.ALERT_INSET * 2)) text_width = int(self.content_rect.width - (AlertConstants.ALERT_INSET * 2))
wrapped_lines = wrap_text(font, alert_data.text, AlertConstants.FONT_SIZE, text_width) wrapped_lines = wrap_text(font, alert_data.text, AlertConstants.FONT_SIZE, text_width)
line_count = len(wrapped_lines) line_count = len(wrapped_lines)
text_height = line_count * (AlertConstants.FONT_SIZE + 5) text_height = line_count * (AlertConstants.FONT_SIZE * FONT_SCALE)
alert_item_height = max(text_height + (AlertConstants.ALERT_INSET * 2), AlertConstants.ALERT_HEIGHT) alert_item_height = max(text_height + (AlertConstants.ALERT_INSET * 2), AlertConstants.ALERT_HEIGHT)
total_height += alert_item_height + AlertConstants.ALERT_SPACING total_height += round(alert_item_height + AlertConstants.ALERT_SPACING)
if total_height > 20: if total_height > 20:
total_height = total_height - AlertConstants.ALERT_SPACING + 20 total_height = total_height - AlertConstants.ALERT_SPACING + 20
@ -278,7 +278,7 @@ class OffroadAlert(AbstractAlert):
text_width = int(content_rect.width - (AlertConstants.ALERT_INSET * 2)) text_width = int(content_rect.width - (AlertConstants.ALERT_INSET * 2))
wrapped_lines = wrap_text(font, alert_data.text, AlertConstants.FONT_SIZE, text_width) wrapped_lines = wrap_text(font, alert_data.text, AlertConstants.FONT_SIZE, text_width)
line_count = len(wrapped_lines) line_count = len(wrapped_lines)
text_height = line_count * (AlertConstants.FONT_SIZE + 5) text_height = line_count * (AlertConstants.FONT_SIZE * FONT_SCALE)
alert_item_height = max(text_height + (AlertConstants.ALERT_INSET * 2), AlertConstants.ALERT_HEIGHT) alert_item_height = max(text_height + (AlertConstants.ALERT_INSET * 2), AlertConstants.ALERT_HEIGHT)
alert_rect = rl.Rectangle( alert_rect = rl.Rectangle(
@ -298,13 +298,13 @@ class OffroadAlert(AbstractAlert):
rl.draw_text_ex( rl.draw_text_ex(
font, font,
line, line,
rl.Vector2(text_x, text_y + i * (AlertConstants.FONT_SIZE + 5)), rl.Vector2(text_x, text_y + i * AlertConstants.FONT_SIZE * FONT_SCALE),
AlertConstants.FONT_SIZE, AlertConstants.FONT_SIZE,
0, 0,
AlertColors.TEXT, AlertColors.TEXT,
) )
y_offset += alert_item_height + AlertConstants.ALERT_SPACING y_offset += round(alert_item_height + AlertConstants.ALERT_SPACING)
class UpdateAlert(AbstractAlert): class UpdateAlert(AbstractAlert):

@ -145,8 +145,8 @@ class PairingDialog(Widget):
# Circle and number # Circle and number
rl.draw_circle(int(circle_x), int(circle_y), circle_radius, rl.Color(70, 70, 70, 255)) rl.draw_circle(int(circle_x), int(circle_y), circle_radius, rl.Color(70, 70, 70, 255))
number = str(i + 1) number = str(i + 1)
number_width = measure_text_cached(font, number, 30).x number_size = measure_text_cached(font, number, 30)
rl.draw_text_ex(font, number, (int(circle_x - number_width // 2), int(circle_y - 15)), 30, 0, rl.WHITE) rl.draw_text_ex(font, number, (int(circle_x - number_size.x // 2), int(circle_y - number_size.y // 2)), 30, 0, rl.WHITE)
# Text # Text
rl.draw_text_ex(font, "\n".join(wrapped), rl.Vector2(text_x, y), 47, 0.0, rl.BLACK) rl.draw_text_ex(font, "\n".join(wrapped), rl.Vector2(text_x, y), 47, 0.0, rl.BLACK)

@ -52,7 +52,7 @@ class PrimeWidget(Widget):
def _render_for_prime_user(self, rect: rl.Rectangle): def _render_for_prime_user(self, rect: rl.Rectangle):
"""Renders the prime user widget with subscription status.""" """Renders the prime user widget with subscription status."""
rl.draw_rectangle_rounded(rl.Rectangle(rect.x, rect.y, rect.width, 230), 0.02, 10, self.PRIME_BG_COLOR) rl.draw_rectangle_rounded(rl.Rectangle(rect.x, rect.y, rect.width, 230), 0.05, 10, self.PRIME_BG_COLOR)
x = rect.x + 56 x = rect.x + 56
y = rect.y + 40 y = rect.y + 40

@ -1,10 +1,11 @@
import pyray as rl import pyray as rl
from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog
from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE
from openpilot.system.ui.lib.wrap_text import wrap_text from openpilot.system.ui.lib.wrap_text import wrap_text
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.button import Button, ButtonStyle from openpilot.system.ui.widgets.button import Button, ButtonStyle
from openpilot.system.ui.widgets.label import Label
class SetupWidget(Widget): class SetupWidget(Widget):
@ -15,6 +16,7 @@ class SetupWidget(Widget):
self._pair_device_btn = Button("Pair device", self._show_pairing, button_style=ButtonStyle.PRIMARY) self._pair_device_btn = Button("Pair device", self._show_pairing, button_style=ButtonStyle.PRIMARY)
self._open_settings_btn = Button("Open", lambda: self._open_settings_callback() if self._open_settings_callback else None, self._open_settings_btn = Button("Open", lambda: self._open_settings_callback() if self._open_settings_callback else None,
button_style=ButtonStyle.PRIMARY) button_style=ButtonStyle.PRIMARY)
self._firehose_label = Label("🔥Firehose Mode 🔥", font_weight=FontWeight.MEDIUM, font_size=64)
def set_open_settings_callback(self, callback): def set_open_settings_callback(self, callback):
self._open_settings_callback = callback self._open_settings_callback = callback
@ -28,7 +30,7 @@ class SetupWidget(Widget):
def _render_registration(self, rect: rl.Rectangle): def _render_registration(self, rect: rl.Rectangle):
"""Render registration prompt.""" """Render registration prompt."""
rl.draw_rectangle_rounded(rl.Rectangle(rect.x, rect.y, rect.width, 630), 0.02, 20, rl.Color(51, 51, 51, 255)) rl.draw_rectangle_rounded(rl.Rectangle(rect.x, rect.y, rect.width, rect.height), 0.02, 20, rl.Color(51, 51, 51, 255))
x = rect.x + 64 x = rect.x + 64
y = rect.y + 48 y = rect.y + 48
@ -45,15 +47,15 @@ class SetupWidget(Widget):
wrapped = wrap_text(light_font, desc, 50, int(w)) wrapped = wrap_text(light_font, desc, 50, int(w))
for line in wrapped: for line in wrapped:
rl.draw_text_ex(light_font, line, rl.Vector2(x, y), 50, 0, rl.WHITE) rl.draw_text_ex(light_font, line, rl.Vector2(x, y), 50, 0, rl.WHITE)
y += 50 y += 50 * FONT_SCALE
button_rect = rl.Rectangle(x, y + 50, w, 128) button_rect = rl.Rectangle(x, y + 30, w, 200)
self._pair_device_btn.render(button_rect) self._pair_device_btn.render(button_rect)
def _render_firehose_prompt(self, rect: rl.Rectangle): def _render_firehose_prompt(self, rect: rl.Rectangle):
"""Render firehose prompt widget.""" """Render firehose prompt widget."""
rl.draw_rectangle_rounded(rl.Rectangle(rect.x, rect.y, rect.width, 450), 0.02, 20, rl.Color(51, 51, 51, 255)) rl.draw_rectangle_rounded(rl.Rectangle(rect.x, rect.y, rect.width, 500), 0.02, 20, rl.Color(51, 51, 51, 255))
# Content margins (56, 40, 56, 40) # Content margins (56, 40, 56, 40)
x = rect.x + 56 x = rect.x + 56
@ -62,9 +64,8 @@ class SetupWidget(Widget):
spacing = 42 spacing = 42
# Title with fire emojis # Title with fire emojis
title_font = gui_app.font(FontWeight.MEDIUM) # TODO: fix Label centering with emojis
title_text = "Firehose Mode" self._firehose_label.render(rl.Rectangle(x - 40, y, w, 64))
rl.draw_text_ex(title_font, title_text, rl.Vector2(x, y), 64, 0, rl.WHITE)
y += 64 + spacing y += 64 + spacing
# Description # Description
@ -74,7 +75,7 @@ class SetupWidget(Widget):
for line in wrapped_desc: for line in wrapped_desc:
rl.draw_text_ex(desc_font, line, rl.Vector2(x, y), 40, 0, rl.WHITE) rl.draw_text_ex(desc_font, line, rl.Vector2(x, y), 40, 0, rl.WHITE)
y += 40 y += 40 * FONT_SCALE
y += spacing y += spacing

@ -21,6 +21,8 @@ from openpilot.system.ui.widgets.list_view import (
BUTTON_WIDTH, BUTTON_WIDTH,
) )
VALUE_FONT_SIZE = 48
class SshKeyActionState(Enum): class SshKeyActionState(Enum):
LOADING = "LOADING" LOADING = "LOADING"
@ -38,7 +40,7 @@ class SshKeyAction(ItemAction):
self._keyboard = Keyboard(min_text_size=1) self._keyboard = Keyboard(min_text_size=1)
self._params = Params() self._params = Params()
self._error_message: str = "" self._error_message: str = ""
self._text_font = gui_app.font(FontWeight.MEDIUM) self._text_font = gui_app.font(FontWeight.NORMAL)
self._button = Button("", click_callback=self._handle_button_click, button_style=ButtonStyle.LIST_ACTION, self._button = Button("", click_callback=self._handle_button_click, button_style=ButtonStyle.LIST_ACTION,
border_radius=BUTTON_BORDER_RADIUS, font_size=BUTTON_FONT_SIZE) border_radius=BUTTON_BORDER_RADIUS, font_size=BUTTON_FONT_SIZE)
@ -62,14 +64,14 @@ class SshKeyAction(ItemAction):
# Draw username if exists # Draw username if exists
if self._username: if self._username:
text_size = measure_text_cached(self._text_font, self._username, BUTTON_FONT_SIZE) text_size = measure_text_cached(self._text_font, self._username, VALUE_FONT_SIZE)
rl.draw_text_ex( rl.draw_text_ex(
self._text_font, self._text_font,
self._username, self._username,
(rect.x + rect.width - BUTTON_WIDTH - text_size.x - 30, rect.y + (rect.height - text_size.y) / 2), (rect.x + rect.width - BUTTON_WIDTH - text_size.x - 30, rect.y + (rect.height - text_size.y) / 2),
BUTTON_FONT_SIZE, VALUE_FONT_SIZE,
1.0, 1.0,
rl.WHITE, rl.Color(170, 170, 170, 255),
) )
# Draw button # Draw button

@ -30,6 +30,10 @@ SCALE = float(os.getenv("SCALE", "1.0"))
DEFAULT_TEXT_SIZE = 60 DEFAULT_TEXT_SIZE = 60
DEFAULT_TEXT_COLOR = rl.WHITE DEFAULT_TEXT_COLOR = rl.WHITE
# Qt draws fonts accounting for ascent/descent differently, so compensate to match old styles
# The real scales for the fonts below range from 1.212 to 1.266
FONT_SCALE = 1.242
ASSETS_DIR = files("openpilot.selfdrive").joinpath("assets") ASSETS_DIR = files("openpilot.selfdrive").joinpath("assets")
FONT_DIR = ASSETS_DIR.joinpath("fonts") FONT_DIR = ASSETS_DIR.joinpath("fonts")
@ -173,6 +177,7 @@ class GuiApplication:
self._target_fps = fps self._target_fps = fps
self._set_styles() self._set_styles()
self._load_fonts() self._load_fonts()
self._patch_text_functions()
if not PC: if not PC:
self._mouse.start() self._mouse.start()
@ -356,6 +361,16 @@ class GuiApplication:
rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiControlProperty.TEXT_COLOR_NORMAL, rl.color_to_int(DEFAULT_TEXT_COLOR)) rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiControlProperty.TEXT_COLOR_NORMAL, rl.color_to_int(DEFAULT_TEXT_COLOR))
rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiControlProperty.BASE_COLOR_NORMAL, rl.color_to_int(rl.Color(50, 50, 50, 255))) rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiControlProperty.BASE_COLOR_NORMAL, rl.color_to_int(rl.Color(50, 50, 50, 255)))
def _patch_text_functions(self):
# Wrap pyray text APIs to apply a global text size scale so our px sizes match Qt
if not hasattr(rl, "_orig_draw_text_ex"):
rl._orig_draw_text_ex = rl.draw_text_ex
def _draw_text_ex_scaled(font, text, position, font_size, spacing, tint):
return rl._orig_draw_text_ex(font, text, position, font_size * FONT_SCALE, spacing, tint)
rl.draw_text_ex = _draw_text_ex_scaled
def _set_log_callback(self): def _set_log_callback(self):
ffi_libc = cffi.FFI() ffi_libc = cffi.FFI()
ffi_libc.cdef(""" ffi_libc.cdef("""

@ -128,3 +128,7 @@ class GuiScrollPanel:
self._offset_filter_y.x = position self._offset_filter_y.x = position
self._velocity_filter_y.x = 0.0 self._velocity_filter_y.x = 0.0
self._scroll_state = ScrollState.IDLE self._scroll_state = ScrollState.IDLE
@property
def offset(self) -> float:
return float(self._offset_filter_y.x)

@ -1,4 +1,5 @@
import pyray as rl import pyray as rl
from openpilot.system.ui.lib.application import FONT_SCALE
_cache: dict[int, rl.Vector2] = {} _cache: dict[int, rl.Vector2] = {}
@ -9,6 +10,6 @@ def measure_text_cached(font: rl.Font, text: str, font_size: int, spacing: int =
if key in _cache: if key in _cache:
return _cache[key] return _cache[key]
result = rl.measure_text_ex(font, text, font_size, spacing) # noqa: TID251 result = rl.measure_text_ex(font, text, font_size * FONT_SCALE, spacing) # noqa: TID251
_cache[key] = result _cache[key] = result
return result return result

@ -3,7 +3,7 @@ import pyray as rl
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum from enum import Enum
from typing import Any from typing import Any
from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE
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.widgets import Widget from openpilot.system.ui.widgets import Widget
@ -45,7 +45,7 @@ class HtmlElement:
font_weight: FontWeight font_weight: FontWeight
margin_top: int margin_top: int
margin_bottom: int margin_bottom: int
line_height: float = 1.2 line_height: float = 0.9 # matches Qt visually, unsure why not default 1.2
indent_level: int = 0 indent_level: int = 0
@ -61,16 +61,19 @@ class HtmlRenderer(Widget):
if text_size is None: if text_size is None:
text_size = {} text_size = {}
# Base paragraph size (Qt stylesheet default is 48px in offroad alerts)
base_p_size = int(text_size.get(ElementType.P, 48))
# Untagged text defaults to <p> # Untagged text defaults to <p>
self.styles: dict[ElementType, dict[str, Any]] = { self.styles: dict[ElementType, dict[str, Any]] = {
ElementType.H1: {"size": 68, "weight": FontWeight.BOLD, "margin_top": 20, "margin_bottom": 16}, ElementType.H1: {"size": round(base_p_size * 2), "weight": FontWeight.BOLD, "margin_top": 20, "margin_bottom": 16},
ElementType.H2: {"size": 60, "weight": FontWeight.BOLD, "margin_top": 24, "margin_bottom": 12}, ElementType.H2: {"size": round(base_p_size * 1.50), "weight": FontWeight.BOLD, "margin_top": 24, "margin_bottom": 12},
ElementType.H3: {"size": 52, "weight": FontWeight.BOLD, "margin_top": 20, "margin_bottom": 10}, ElementType.H3: {"size": round(base_p_size * 1.17), "weight": FontWeight.BOLD, "margin_top": 20, "margin_bottom": 10},
ElementType.H4: {"size": 48, "weight": FontWeight.BOLD, "margin_top": 16, "margin_bottom": 8}, ElementType.H4: {"size": round(base_p_size * 1.00), "weight": FontWeight.BOLD, "margin_top": 16, "margin_bottom": 8},
ElementType.H5: {"size": 44, "weight": FontWeight.BOLD, "margin_top": 12, "margin_bottom": 6}, ElementType.H5: {"size": round(base_p_size * 0.83), "weight": FontWeight.BOLD, "margin_top": 12, "margin_bottom": 6},
ElementType.H6: {"size": 40, "weight": FontWeight.BOLD, "margin_top": 10, "margin_bottom": 4}, ElementType.H6: {"size": round(base_p_size * 0.67), "weight": FontWeight.BOLD, "margin_top": 10, "margin_bottom": 4},
ElementType.P: {"size": text_size.get(ElementType.P, 38), "weight": FontWeight.NORMAL, "margin_top": 8, "margin_bottom": 12}, ElementType.P: {"size": base_p_size, "weight": FontWeight.NORMAL, "margin_top": 8, "margin_bottom": 12},
ElementType.LI: {"size": 38, "weight": FontWeight.NORMAL, "color": rl.Color(40, 40, 40, 255), "margin_top": 6, "margin_bottom": 6}, ElementType.LI: {"size": base_p_size, "weight": FontWeight.NORMAL, "color": rl.Color(40, 40, 40, 255), "margin_top": 6, "margin_bottom": 6},
ElementType.BR: {"size": 0, "weight": FontWeight.NORMAL, "margin_top": 0, "margin_bottom": 12}, ElementType.BR: {"size": 0, "weight": FontWeight.NORMAL, "margin_top": 0, "margin_bottom": 12},
} }
@ -179,8 +182,9 @@ class HtmlRenderer(Widget):
wrapped_lines = wrap_text(font, element.content, element.font_size, int(content_width)) wrapped_lines = wrap_text(font, element.content, element.font_size, int(content_width))
for line in wrapped_lines: for line in wrapped_lines:
if current_y < rect.y - element.font_size: # Use FONT_SCALE from wrapped raylib text functions to match what is drawn
current_y += element.font_size * element.line_height if current_y < rect.y - element.font_size * FONT_SCALE:
current_y += element.font_size * FONT_SCALE * element.line_height
continue continue
if current_y > rect.y + rect.height: if current_y > rect.y + rect.height:
@ -189,7 +193,7 @@ class HtmlRenderer(Widget):
text_x = rect.x + (max(element.indent_level - 1, 0) * LIST_INDENT_PX) text_x = rect.x + (max(element.indent_level - 1, 0) * LIST_INDENT_PX)
rl.draw_text_ex(font, line, rl.Vector2(text_x + padding, current_y), element.font_size, 0, self._text_color) rl.draw_text_ex(font, line, rl.Vector2(text_x + padding, current_y), element.font_size, 0, self._text_color)
current_y += element.font_size * element.line_height current_y += element.font_size * FONT_SCALE * element.line_height
# Apply bottom margin # Apply bottom margin
current_y += element.margin_bottom current_y += element.margin_bottom
@ -213,7 +217,7 @@ class HtmlRenderer(Widget):
wrapped_lines = wrap_text(font, element.content, element.font_size, int(usable_width)) wrapped_lines = wrap_text(font, element.content, element.font_size, int(usable_width))
for _ in wrapped_lines: for _ in wrapped_lines:
total_height += element.font_size * element.line_height total_height += element.font_size * FONT_SCALE * element.line_height
total_height += element.margin_bottom total_height += element.margin_bottom

@ -1,6 +1,6 @@
import pyray as rl import pyray as rl
import time import time
from openpilot.system.ui.lib.application import gui_app, MousePos from openpilot.system.ui.lib.application import gui_app, MousePos, FONT_SCALE
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.widgets import Widget from openpilot.system.ui.widgets import Widget
@ -130,7 +130,7 @@ class InputBox(Widget):
rl.draw_text_ex( rl.draw_text_ex(
font, font,
display_text, display_text,
rl.Vector2(int(rect.x + padding - self._text_offset), int(rect.y + rect.height / 2 - font_size / 2)), rl.Vector2(int(rect.x + padding - self._text_offset), int(rect.y + rect.height / 2 - font_size * FONT_SCALE / 2)),
font_size, font_size,
0, 0,
text_color, text_color,
@ -145,7 +145,7 @@ class InputBox(Widget):
# Apply text offset to cursor position # Apply text offset to cursor position
cursor_x -= self._text_offset cursor_x -= self._text_offset
cursor_height = font_size + 4 cursor_height = font_size * FONT_SCALE + 4
cursor_y = rect.y + rect.height / 2 - cursor_height / 2 cursor_y = rect.y + rect.height / 2 - cursor_height / 2
rl.draw_line(int(cursor_x), int(cursor_y), int(cursor_x), int(cursor_y + cursor_height), rl.WHITE) rl.draw_line(int(cursor_x), int(cursor_y), int(cursor_x), int(cursor_y + cursor_height), rl.WHITE)

@ -3,7 +3,7 @@ from itertools import zip_longest
import pyray as rl import pyray as rl
from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_SIZE, DEFAULT_TEXT_COLOR from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_SIZE, DEFAULT_TEXT_COLOR, FONT_SCALE
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.utils import GuiStyleContext from openpilot.system.ui.lib.utils import GuiStyleContext
from openpilot.system.ui.lib.emoji import find_emoji, emoji_tex from openpilot.system.ui.lib.emoji import find_emoji, emoji_tex
@ -171,7 +171,7 @@ class Label(Widget):
tex = emoji_tex(emoji) tex = emoji_tex(emoji)
rl.draw_texture_ex(tex, line_pos, 0.0, self._font_size / tex.height, self._text_color) rl.draw_texture_ex(tex, line_pos, 0.0, self._font_size / tex.height, self._text_color)
line_pos.x += self._font_size line_pos.x += self._font_size * FONT_SCALE
prev_index = end prev_index = end
rl.draw_text_ex(self._font, text[prev_index:], line_pos, self._font_size, 0, self._text_color) rl.draw_text_ex(self._font, text[prev_index:], line_pos, self._font_size, 0, self._text_color)
text_pos.y += text_size.y or self._font_size text_pos.y += text_size.y or self._font_size * FONT_SCALE

Loading…
Cancel
Save