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):
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._exp_mode_button.set_click_callback(lambda: self.settings_callback() if self.settings_callback else None)
def set_settings_callback(self, callback: Callable):
self.settings_callback = callback
@ -147,9 +148,9 @@ class HomeLayout(Widget):
rl.draw_rectangle_rounded(self.update_notif_rect, 0.3, 10, highlight_color)
text = "UPDATE"
text_width = measure_text_cached(font, text, HEAD_BUTTON_FONT_SIZE).x
text_x = self.update_notif_rect.x + (self.update_notif_rect.width - text_width) // 2
text_y = self.update_notif_rect.y + (self.update_notif_rect.height - HEAD_BUTTON_FONT_SIZE) // 2
text_size = measure_text_cached(font, text, HEAD_BUTTON_FONT_SIZE)
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 - 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)
# Alert notification button
@ -161,9 +162,9 @@ class HomeLayout(Widget):
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 ''}"
text_width = measure_text_cached(font, alert_text, HEAD_BUTTON_FONT_SIZE).x
text_x = self.alert_notif_rect.x + (self.alert_notif_rect.width - text_width) // 2
text_y = self.alert_notif_rect.y + (self.alert_notif_rect.height - HEAD_BUTTON_FONT_SIZE) // 2
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_size.x) // 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)
# Version text (right aligned)

@ -50,6 +50,7 @@ class MainLayout(Widget):
on_flag=self._on_bookmark_clicked,
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].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.ONROAD].set_click_callback(self._on_onroad_clicked)
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.selfdrive.ui.ui_state import ui_state
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.wrap_text import wrap_text
from openpilot.system.ui.widgets import Widget
@ -21,7 +22,7 @@ DESCRIPTION = (
)
INSTRUCTIONS = (
"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"
+ "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"
@ -43,6 +44,7 @@ class FirehoseLayout(Widget):
self.params = Params()
self.segment_count = self._get_segment_count()
self.scroll_panel = GuiScrollPanel()
self._content_height = 0
self.running = 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):
# Calculate content dimensions
content_width = rect.width - 80
content_height = self._calculate_content_height(int(content_width))
content_rect = rl.Rectangle(rect.x, rect.y, rect.width, content_height)
content_rect = rl.Rectangle(rect.x, rect.y, rect.width, self._content_height)
# Handle scrolling and render with clipping
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))
self._render_content(rect, scroll_offset)
self._content_height = self._render_content(rect, scroll_offset)
rl.end_scissor_mode()
def _calculate_content_height(self, content_width: int) -> 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):
def _render_content(self, rect: rl.Rectangle, scroll_offset: float) -> int:
x = int(rect.x + 40)
y = int(rect.y + 40 + scroll_offset)
w = int(rect.width - 80)
# Title
# Title (centered)
title_font = gui_app.font(FontWeight.MEDIUM)
rl.draw_text_ex(title_font, TITLE, rl.Vector2(x, y), 100, 0, rl.WHITE)
y += 140
text_width = measure_text_cached(title_font, TITLE, 100).x
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
y = self._draw_wrapped_text(x, y, w, DESCRIPTION, gui_app.font(FontWeight.NORMAL), 45, rl.WHITE)
y += 40
y += 40 + 20
# Separator
rl.draw_rectangle(x, y, w, 2, self.GRAY)
y += 30
y += 30 + 20
# 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 += 20
y += 20 + 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."
y = self._draw_wrapped_text(x, y, w, contrib_text, gui_app.font(FontWeight.BOLD), 52, rl.WHITE)
y += 20
y += 20 + 20
# Separator
rl.draw_rectangle(x, y, w, 2, self.GRAY)
y += 30
y += 30 + 20
# 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):
wrapped = wrap_text(font, text, size, width)
def _draw_wrapped_text(self, x, y, width, text, font, font_size, color):
wrapped = wrap_text(font, text, font_size, width)
for line in wrapped:
rl.draw_text_ex(font, line, rl.Vector2(x, y), size, 0, color)
y += size
return y
rl.draw_text_ex(font, line, rl.Vector2(x, y), font_size, 0, color)
y += font_size * FONT_SCALE
return round(y)
def _get_status(self) -> tuple[str, rl.Color]:
network_type = ui_state.sm["deviceState"].networkType

@ -4,7 +4,7 @@ from dataclasses import dataclass
from collections.abc import Callable
from cereal import log
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.widgets import Widget
@ -218,7 +218,7 @@ class Sidebar(Widget):
# Draw label and 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:
text_size = measure_text_cached(self._font_bold, text, FONT_SIZE)
text_y += text_size.y

@ -1,6 +1,6 @@
import pyray as rl
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
@ -9,7 +9,7 @@ class ExperimentalModeButton(Widget):
super().__init__()
self.img_width = 80
self.horizontal_padding = 50
self.horizontal_padding = 25
self.button_height = 125
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),
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):
rl.draw_rectangle_rounded(rect, 0.08, 20, rl.WHITE)
@ -51,7 +46,7 @@ class ExperimentalModeButton(Widget):
# Draw text label (left aligned)
text = "EXPERIMENTAL MODE ON" if self.experimental_mode else "CHILL MODE ON"
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)

@ -5,7 +5,7 @@ from collections.abc import Callable
from dataclasses import dataclass
from openpilot.common.params import Params
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.text_measure import measure_text_cached
from openpilot.system.ui.lib.wrap_text import wrap_text
@ -65,8 +65,8 @@ class ActionButton(Widget):
def set_text(self, text: str):
self._text = text
self._text_width = measure_text_cached(gui_app.font(FontWeight.MEDIUM), self._text, AlertConstants.FONT_SIZE).x
self._rect.width = max(self._text_width + 60 * 2, self._min_width)
self._text_size = measure_text_cached(gui_app.font(FontWeight.MEDIUM), self._text, AlertConstants.FONT_SIZE)
self._rect.width = max(self._text_size.x + 60 * 2, self._min_width)
self._rect.height = AlertConstants.BUTTON_HEIGHT
def _render(self, _):
@ -79,8 +79,8 @@ class ActionButton(Widget):
# center text
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_y = int(self._rect.y + (self._rect.height - AlertConstants.FONT_SIZE) // 2)
text_x = int(self._rect.x + (self._rect.width - self._text_size.x) // 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)
@ -250,9 +250,9 @@ class OffroadAlert(AbstractAlert):
text_width = int(self.content_rect.width - (AlertConstants.ALERT_INSET * 2))
wrapped_lines = wrap_text(font, alert_data.text, AlertConstants.FONT_SIZE, text_width)
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)
total_height += alert_item_height + AlertConstants.ALERT_SPACING
total_height += round(alert_item_height + AlertConstants.ALERT_SPACING)
if total_height > 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))
wrapped_lines = wrap_text(font, alert_data.text, AlertConstants.FONT_SIZE, text_width)
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_rect = rl.Rectangle(
@ -298,13 +298,13 @@ class OffroadAlert(AbstractAlert):
rl.draw_text_ex(
font,
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,
0,
AlertColors.TEXT,
)
y_offset += alert_item_height + AlertConstants.ALERT_SPACING
y_offset += round(alert_item_height + AlertConstants.ALERT_SPACING)
class UpdateAlert(AbstractAlert):

@ -145,8 +145,8 @@ class PairingDialog(Widget):
# Circle and number
rl.draw_circle(int(circle_x), int(circle_y), circle_radius, rl.Color(70, 70, 70, 255))
number = str(i + 1)
number_width = measure_text_cached(font, number, 30).x
rl.draw_text_ex(font, number, (int(circle_x - number_width // 2), int(circle_y - 15)), 30, 0, rl.WHITE)
number_size = measure_text_cached(font, number, 30)
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
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):
"""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
y = rect.y + 40

@ -1,10 +1,11 @@
import pyray as rl
from openpilot.selfdrive.ui.ui_state import ui_state
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.widgets import Widget
from openpilot.system.ui.widgets.button import Button, ButtonStyle
from openpilot.system.ui.widgets.label import Label
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._open_settings_btn = Button("Open", lambda: self._open_settings_callback() if self._open_settings_callback else None,
button_style=ButtonStyle.PRIMARY)
self._firehose_label = Label("🔥Firehose Mode 🔥", font_weight=FontWeight.MEDIUM, font_size=64)
def set_open_settings_callback(self, callback):
self._open_settings_callback = callback
@ -28,7 +30,7 @@ class SetupWidget(Widget):
def _render_registration(self, rect: rl.Rectangle):
"""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
y = rect.y + 48
@ -45,15 +47,15 @@ class SetupWidget(Widget):
wrapped = wrap_text(light_font, desc, 50, int(w))
for line in wrapped:
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)
def _render_firehose_prompt(self, rect: rl.Rectangle):
"""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)
x = rect.x + 56
@ -62,9 +64,8 @@ class SetupWidget(Widget):
spacing = 42
# Title with fire emojis
title_font = gui_app.font(FontWeight.MEDIUM)
title_text = "Firehose Mode"
rl.draw_text_ex(title_font, title_text, rl.Vector2(x, y), 64, 0, rl.WHITE)
# TODO: fix Label centering with emojis
self._firehose_label.render(rl.Rectangle(x - 40, y, w, 64))
y += 64 + spacing
# Description
@ -74,7 +75,7 @@ class SetupWidget(Widget):
for line in wrapped_desc:
rl.draw_text_ex(desc_font, line, rl.Vector2(x, y), 40, 0, rl.WHITE)
y += 40
y += 40 * FONT_SCALE
y += spacing

@ -21,6 +21,8 @@ from openpilot.system.ui.widgets.list_view import (
BUTTON_WIDTH,
)
VALUE_FONT_SIZE = 48
class SshKeyActionState(Enum):
LOADING = "LOADING"
@ -38,7 +40,7 @@ class SshKeyAction(ItemAction):
self._keyboard = Keyboard(min_text_size=1)
self._params = Params()
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,
border_radius=BUTTON_BORDER_RADIUS, font_size=BUTTON_FONT_SIZE)
@ -62,14 +64,14 @@ class SshKeyAction(ItemAction):
# Draw username if exists
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(
self._text_font,
self._username,
(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,
rl.WHITE,
rl.Color(170, 170, 170, 255),
)
# Draw button

@ -30,6 +30,10 @@ SCALE = float(os.getenv("SCALE", "1.0"))
DEFAULT_TEXT_SIZE = 60
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")
FONT_DIR = ASSETS_DIR.joinpath("fonts")
@ -173,6 +177,7 @@ class GuiApplication:
self._target_fps = fps
self._set_styles()
self._load_fonts()
self._patch_text_functions()
if not PC:
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.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):
ffi_libc = cffi.FFI()
ffi_libc.cdef("""

@ -128,3 +128,7 @@ class GuiScrollPanel:
self._offset_filter_y.x = position
self._velocity_filter_y.x = 0.0
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
from openpilot.system.ui.lib.application import FONT_SCALE
_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:
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
return result

@ -3,7 +3,7 @@ import pyray as rl
from dataclasses import dataclass
from enum import Enum
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.wrap_text import wrap_text
from openpilot.system.ui.widgets import Widget
@ -45,7 +45,7 @@ class HtmlElement:
font_weight: FontWeight
margin_top: 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
@ -61,16 +61,19 @@ class HtmlRenderer(Widget):
if text_size is None:
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>
self.styles: dict[ElementType, dict[str, Any]] = {
ElementType.H1: {"size": 68, "weight": FontWeight.BOLD, "margin_top": 20, "margin_bottom": 16},
ElementType.H2: {"size": 60, "weight": FontWeight.BOLD, "margin_top": 24, "margin_bottom": 12},
ElementType.H3: {"size": 52, "weight": FontWeight.BOLD, "margin_top": 20, "margin_bottom": 10},
ElementType.H4: {"size": 48, "weight": FontWeight.BOLD, "margin_top": 16, "margin_bottom": 8},
ElementType.H5: {"size": 44, "weight": FontWeight.BOLD, "margin_top": 12, "margin_bottom": 6},
ElementType.H6: {"size": 40, "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.LI: {"size": 38, "weight": FontWeight.NORMAL, "color": rl.Color(40, 40, 40, 255), "margin_top": 6, "margin_bottom": 6},
ElementType.H1: {"size": round(base_p_size * 2), "weight": FontWeight.BOLD, "margin_top": 20, "margin_bottom": 16},
ElementType.H2: {"size": round(base_p_size * 1.50), "weight": FontWeight.BOLD, "margin_top": 24, "margin_bottom": 12},
ElementType.H3: {"size": round(base_p_size * 1.17), "weight": FontWeight.BOLD, "margin_top": 20, "margin_bottom": 10},
ElementType.H4: {"size": round(base_p_size * 1.00), "weight": FontWeight.BOLD, "margin_top": 16, "margin_bottom": 8},
ElementType.H5: {"size": round(base_p_size * 0.83), "weight": FontWeight.BOLD, "margin_top": 12, "margin_bottom": 6},
ElementType.H6: {"size": round(base_p_size * 0.67), "weight": FontWeight.BOLD, "margin_top": 10, "margin_bottom": 4},
ElementType.P: {"size": base_p_size, "weight": FontWeight.NORMAL, "margin_top": 8, "margin_bottom": 12},
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},
}
@ -179,8 +182,9 @@ class HtmlRenderer(Widget):
wrapped_lines = wrap_text(font, element.content, element.font_size, int(content_width))
for line in wrapped_lines:
if current_y < rect.y - element.font_size:
current_y += element.font_size * element.line_height
# Use FONT_SCALE from wrapped raylib text functions to match what is drawn
if current_y < rect.y - element.font_size * FONT_SCALE:
current_y += element.font_size * FONT_SCALE * element.line_height
continue
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)
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
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))
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

@ -1,6 +1,6 @@
import pyray as rl
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.widgets import Widget
@ -130,7 +130,7 @@ class InputBox(Widget):
rl.draw_text_ex(
font,
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,
0,
text_color,
@ -145,7 +145,7 @@ class InputBox(Widget):
# Apply text offset to cursor position
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
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
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.utils import GuiStyleContext
from openpilot.system.ui.lib.emoji import find_emoji, emoji_tex
@ -171,7 +171,7 @@ class Label(Widget):
tex = emoji_tex(emoji)
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
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