From cf39eefd219a260e08a7dc17929d04b974156842 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 2 Oct 2025 02:44:07 -0700 Subject: [PATCH 01/21] debug --- selfdrive/ui/layouts/settings/software.py | 25 ++++++++++- selfdrive/ui/qt/offroad/settings.h | 1 + selfdrive/ui/qt/offroad/software_settings.cc | 45 ++++++++++++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) diff --git a/selfdrive/ui/layouts/settings/software.py b/selfdrive/ui/layouts/settings/software.py index 0349070010670c..ce5d1be9a45b9d 100644 --- a/selfdrive/ui/layouts/settings/software.py +++ b/selfdrive/ui/layouts/settings/software.py @@ -1,5 +1,8 @@ from openpilot.common.params import Params -from openpilot.system.ui.lib.application import gui_app +import os +import pyray as rl +from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.widgets import Widget, DialogResult from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog from openpilot.system.ui.widgets.list_view import button_item, text_item @@ -26,11 +29,31 @@ def _init_items(self): def _render(self, rect): self._scroller.render(rect) + if os.getenv("DEBUG_TEXT_COMPARE") == "1": + self._draw_text_compare() def _on_download_update(self): pass def _on_install_update(self): pass def _on_select_branch(self): pass + def _draw_text_compare(self): + try: + font = gui_app.font(FontWeight.NORMAL) + samples = [(50, "Hg"), (40, "Hg"), (35, "Hg")] + left_x = 40 + top_y = 40 + row_h = 120 + + for i, (size, text) in enumerate(samples): + y = top_y + i * row_h + rl.draw_line_ex(rl.Vector2(left_x, y), rl.Vector2(left_x, y + size), 2, rl.RED) + text_pos = rl.Vector2(left_x + 10, y) + rl.draw_text_ex(font, text, text_pos, size, 0, rl.WHITE) + ts = measure_text_cached(font, text, size) + rl.draw_rectangle_lines(int(text_pos.x), int(text_pos.y), int(ts.x), int(ts.y), rl.GREEN) + except Exception: + pass + def _on_uninstall(self): def handle_uninstall_confirmation(result): if result == DialogResult.CONFIRM: diff --git a/selfdrive/ui/qt/offroad/settings.h b/selfdrive/ui/qt/offroad/settings.h index d52cf16bb7ebb9..d364b1512ad430 100644 --- a/selfdrive/ui/qt/offroad/settings.h +++ b/selfdrive/ui/qt/offroad/settings.h @@ -86,6 +86,7 @@ class SoftwarePanel : public ListWidget { explicit SoftwarePanel(QWidget* parent = nullptr); private: + void paintEvent(QPaintEvent *event) override; void showEvent(QShowEvent *event) override; void updateLabels(); void checkForUpdates(); diff --git a/selfdrive/ui/qt/offroad/software_settings.cc b/selfdrive/ui/qt/offroad/software_settings.cc index 9bc3fad3c9b67a..2ca9e557b9ed38 100644 --- a/selfdrive/ui/qt/offroad/software_settings.cc +++ b/selfdrive/ui/qt/offroad/software_settings.cc @@ -96,6 +96,51 @@ SoftwarePanel::SoftwarePanel(QWidget* parent) : ListWidget(parent) { updateLabels(); } +void SoftwarePanel::paintEvent(QPaintEvent *event) { + QWidget::paintEvent(event); + if (qEnvironmentVariableIsSet("DEBUG_TEXT_COMPARE") && qgetenv("DEBUG_TEXT_COMPARE") == "1") { + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing); + + struct Sample { int size; const char *text; }; + const Sample samples[] = {{50, "Hg"}, {40, "Hg"}, {35, "Hg"}}; + + int left_x = 8; // far left to avoid overlapping content + int top_y = 40; + int row_h = 120; + + for (int i = 0; i < 3; ++i) { + int sz = samples[i].size; + QString text = samples[i].text; + int y = top_y + i * row_h; + + QFont f = font(); + f.setPixelSize(sz); + p.setFont(f); + QFontMetrics fm(f); + QRect br = fm.boundingRect(text); + + int tx = left_x + 12; + int ty = y + fm.ascent(); // baseline + p.setPen(Qt::white); + p.drawText(tx, ty, text); + + // Green: full font metrics box (top at ascent, height = fm.height()) + p.setPen(QPen(Qt::green, 1)); + QRect r(tx, ty - fm.ascent(), br.width(), fm.height()); + p.drawRect(r); + + // Tight bounding rect of the actual glyph + QRect tr = fm.tightBoundingRect(text); + QRect trp = tr.translated(tx, ty); // translate from baseline-origin to scene coords + + // Red: from top of green box to bottom of actual glyph box + p.setPen(QPen(Qt::red, 2)); + p.drawLine(QPoint(left_x, r.top()), QPoint(left_x, trp.bottom())); + } + } +} + void SoftwarePanel::showEvent(QShowEvent *event) { // nice for testing on PC installBtn->setEnabled(true); From cc17be0ee3e8be1b81d9d4d0e7e8a072860f7047 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 2 Oct 2025 03:25:50 -0700 Subject: [PATCH 02/21] hacks everywhere but kind of works --- selfdrive/ui/layouts/settings/software.py | 19 +++++- selfdrive/ui/main.cc | 11 ++++ selfdrive/ui/qt/offroad/software_settings.cc | 45 +++++++++++--- selfdrive/ui/ui.cc | 2 +- selfdrive/ui/ui_state.py | 2 +- system/ui/lib/application.py | 63 ++++++++++++++++++++ 6 files changed, 130 insertions(+), 12 deletions(-) diff --git a/selfdrive/ui/layouts/settings/software.py b/selfdrive/ui/layouts/settings/software.py index ce5d1be9a45b9d..9123b49c468ba3 100644 --- a/selfdrive/ui/layouts/settings/software.py +++ b/selfdrive/ui/layouts/settings/software.py @@ -39,9 +39,9 @@ def _on_select_branch(self): pass def _draw_text_compare(self): try: font = gui_app.font(FontWeight.NORMAL) - samples = [(50, "Hg"), (40, "Hg"), (35, "Hg")] - left_x = 40 - top_y = 40 + samples = [(100, "HH"), (80, "HH"), (70, "HH")] + left_x = 10 + top_y = int(gui_app.height / 2 + 40) row_h = 120 for i, (size, text) in enumerate(samples): @@ -51,6 +51,19 @@ def _draw_text_compare(self): rl.draw_text_ex(font, text, text_pos, size, 0, rl.WHITE) ts = measure_text_cached(font, text, size) rl.draw_rectangle_lines(int(text_pos.x), int(text_pos.y), int(ts.x), int(ts.y), rl.GREEN) + + # Metrics readout + info_y = top_y + len(samples) * row_h + 40 + info_lines = [ + f"SCALE={os.getenv('SCALE','')} target_fps={gui_app.target_fps}", + ] + for size, text in samples: + ts = measure_text_cached(font, text, size) + info_lines.append(f"sz={size} meas.h={ts.y} baseSize=200") + info_font = gui_app.font(FontWeight.NORMAL) + info_size = 20 + for i, line in enumerate(info_lines): + rl.draw_text_ex(info_font, line, rl.Vector2(left_x, info_y + i * 24), info_size, 0, rl.WHITE) except Exception: pass diff --git a/selfdrive/ui/main.cc b/selfdrive/ui/main.cc index 4903a3db3dcc07..95325fba25e702 100644 --- a/selfdrive/ui/main.cc +++ b/selfdrive/ui/main.cc @@ -2,6 +2,8 @@ #include #include +#include +#include #include "system/hardware/hw.h" #include "selfdrive/ui/qt/qt_window.h" @@ -23,6 +25,15 @@ int main(int argc, char *argv[]) { QApplication a(argc, argv); a.installTranslator(&translator); + // Debug DPI/scaling info + const char *sf = qgetenv("QT_SCALE_FACTOR").constData(); + auto scr = a.primaryScreen(); + qDebug() << "QT_SCALE_FACTOR=" << (sf ? sf : "") + << " devicePixelRatio=" << (scr ? scr->devicePixelRatio() : 0) + << " devicePixelRatioF=" << (scr ? scr->devicePixelRatio() : 0) + << " logicalDpi=" << (scr ? scr->logicalDotsPerInch() : 0) + << " physicalDpi=" << (scr ? scr->physicalDotsPerInch() : 0); + MainWindow w; setMainWindow(&w); a.installEventFilter(&w); diff --git a/selfdrive/ui/qt/offroad/software_settings.cc b/selfdrive/ui/qt/offroad/software_settings.cc index 2ca9e557b9ed38..eb5b06eed30f74 100644 --- a/selfdrive/ui/qt/offroad/software_settings.cc +++ b/selfdrive/ui/qt/offroad/software_settings.cc @@ -6,6 +6,8 @@ #include #include +#include +#include #include "common/params.h" #include "common/util.h" @@ -103,10 +105,10 @@ void SoftwarePanel::paintEvent(QPaintEvent *event) { p.setRenderHint(QPainter::Antialiasing); struct Sample { int size; const char *text; }; - const Sample samples[] = {{50, "Hg"}, {40, "Hg"}, {35, "Hg"}}; + const Sample samples[] = {{100, "HH"}, {80, "HH"}, {70, "HH"}}; int left_x = 8; // far left to avoid overlapping content - int top_y = 40; + int top_y = height() / 2 + 40; // bottom half of screen int row_h = 120; for (int i = 0; i < 3; ++i) { @@ -130,13 +132,42 @@ void SoftwarePanel::paintEvent(QPaintEvent *event) { QRect r(tx, ty - fm.ascent(), br.width(), fm.height()); p.drawRect(r); - // Tight bounding rect of the actual glyph - QRect tr = fm.tightBoundingRect(text); - QRect trp = tr.translated(tx, ty); // translate from baseline-origin to scene coords + // Tight bounding rect of the actual glyph (optional for reference) - // Red: from top of green box to bottom of actual glyph box + // Red: exactly font-size pixels tall, starting at top of the green box p.setPen(QPen(Qt::red, 2)); - p.drawLine(QPoint(left_x, r.top()), QPoint(left_x, trp.bottom())); + p.drawLine(QPoint(left_x, r.top()), QPoint(left_x, r.top() + sz)); + } + + // Metrics readout + QFont mf = font(); + mf.setPixelSize(18); + p.setFont(mf); + auto scr = QApplication::primaryScreen(); + QString scale_line = QString("QT_SCALE_FACTOR=%1 DPR=%2 logicalDPI=%3 physicalDPI=%4") + .arg(QString::fromUtf8(qgetenv("QT_SCALE_FACTOR"))) + .arg(scr ? scr->devicePixelRatio() : 0.0) + .arg(scr ? scr->logicalDotsPerInch() : 0.0) + .arg(scr ? scr->physicalDotsPerInch() : 0.0); + p.setPen(Qt::white); + p.drawText(left_x, top_y + 3 * row_h + 40, scale_line); + + // Per-size metrics (height and tight height) + int metrics_y = top_y + 3 * row_h + 70; + const int line_step = 22; + for (int i = 0; i < 3; ++i) { + int sz = samples[i].size; + QFont f = font(); + f.setPixelSize(sz); + QFontMetrics fm(f); + QRect tr = fm.tightBoundingRect("HH"); + QString mline = QString("sz=%1 fm.height=%2 tight.h=%3 ascent=%4 descent=%5") + .arg(sz) + .arg(fm.height()) + .arg(tr.height()) + .arg(fm.ascent()) + .arg(fm.descent()); + p.drawText(left_x, metrics_y + i * line_step, mline); } } } diff --git a/selfdrive/ui/ui.cc b/selfdrive/ui/ui.cc index 9ec61b9b8143f0..9fceb5246e2f36 100644 --- a/selfdrive/ui/ui.cc +++ b/selfdrive/ui/ui.cc @@ -145,7 +145,7 @@ void Device::setAwake(bool on) { void Device::resetInteractiveTimeout(int timeout) { if (timeout == -1) { - timeout = (ignition_on ? 10 : 30); + timeout = (ignition_on ? 10 : 300); } interactive_timeout = timeout * UI_FREQ; } diff --git a/selfdrive/ui/ui_state.py b/selfdrive/ui/ui_state.py index 9c45519b4bab40..06a24ef80f57c8 100644 --- a/selfdrive/ui/ui_state.py +++ b/selfdrive/ui/ui_state.py @@ -158,7 +158,7 @@ def __init__(self): def reset_interactive_timeout(self, timeout: int = -1) -> None: if timeout == -1: - timeout = 10 if ui_state.ignition else 30 + timeout = 10 if ui_state.ignition else 300 self._interaction_time = time.monotonic() + timeout def add_interactive_timeout_callback(self, callback: Callable): diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index a16f0b0bdaafad..7df1a439f523ff 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -141,11 +141,16 @@ def __init__(self, width: int, height: int): # Debug variables self._mouse_history: deque[MousePos] = deque(maxlen=MOUSE_THREAD_RATE) + self._text_scale: float = float(os.getenv("TEXT_SIZE_SCALE", "1.22")) @property def target_fps(self): return self._target_fps + @property + def text_scale(self) -> float: + return self._text_scale + def request_close(self): self._window_close_requested = True @@ -173,6 +178,7 @@ def init_window(self, title: str, fps: int = _DEFAULT_FPS): self._target_fps = fps self._set_styles() self._load_fonts() + self._monkey_patch_text_functions() if not PC: self._mouse.start() @@ -345,6 +351,41 @@ def _load_fonts(self): rl.unload_codepoints(codepoints) rl.gui_set_font(self._fonts[FontWeight.NORMAL]) + try: + font = self._fonts[FontWeight.NORMAL] + base = float(font.baseSize) if getattr(font, 'baseSize', 0) else 200.0 + + # Estimate line height from glyph extents: min(top), max(bottom) across a set with ascenders/descenders + codepoints = [ord(c) for c in "HXYZAMNQgjpqy"] + min_top = 1e9 + max_bottom = -1e9 + for cp in codepoints: + idx = rl.get_glyph_index(font, cp) + # Some glyphs may be missing; skip if index invalid + try: + gi = font.glyphs[idx] + rec = font.recs[idx] + top = float(getattr(gi, 'offsetY', 0.0)) + bottom = top + float(getattr(rec, 'height', 0.0)) + if top < min_top: + min_top = top + if bottom > max_bottom: + max_bottom = bottom + except Exception: + continue + + line_px = max(1.0, max_bottom - min_top) + # Compute scale purely from font: map baseSize to the tight glyph line box height + # so requesting N px roughly yields a line box of N px, matching typical UI sizing. + # scale = baseSize / linePx (usually > 1.0 for Inter on Linux) + denom = line_px if line_px > 0.01 else 1.0 + self._text_scale = base / denom + + cloudlog.info( + f"raylib font metrics: baseSize={base:.1f} linePx={line_px:.1f} textScale={self._text_scale:.3f}") + except Exception as e: + cloudlog.exception(f"font metrics computation failed: {e}") + def _set_styles(self): rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiControlProperty.BORDER_WIDTH, 0) rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_SIZE, DEFAULT_TEXT_SIZE) @@ -352,6 +393,28 @@ def _set_styles(self): 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 _monkey_patch_text_functions(self): + # Wrap pyray text APIs to apply a global text size scale so our px sizes match Qt + try: + if not hasattr(rl, "_orig_draw_text_ex"): + rl._orig_draw_text_ex = rl.draw_text_ex + if not hasattr(rl, "_orig_measure_text_ex"): + rl._orig_measure_text_ex = rl.measure_text_ex + + scale = self._text_scale + + def _draw_text_ex_scaled(font, text, position, font_size, spacing, tint): + return rl._orig_draw_text_ex(font, text, position, float(font_size) * scale, spacing, tint) + + def _measure_text_ex_scaled(font, text, font_size, spacing): + return rl._orig_measure_text_ex(font, text, float(font_size) * scale, spacing) + + rl.draw_text_ex = _draw_text_ex_scaled + rl.measure_text_ex = _measure_text_ex_scaled + cloudlog.info(f"raylib text functions wrapped with scale={scale}") + except Exception as e: + cloudlog.exception(f"failed to wrap text functions: {e}") + def _set_log_callback(self): ffi_libc = cffi.FFI() ffi_libc.cdef(""" From 76bad30e67f9a4bb12c6443e47308941acf0efe2 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 2 Oct 2025 03:31:30 -0700 Subject: [PATCH 03/21] by font --- system/ui/lib/application.py | 76 +++++++++++++++++------------------- 1 file changed, 35 insertions(+), 41 deletions(-) diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index 7df1a439f523ff..09a0ef0963deaa 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -141,16 +141,12 @@ def __init__(self, width: int, height: int): # Debug variables self._mouse_history: deque[MousePos] = deque(maxlen=MOUSE_THREAD_RATE) - self._text_scale: float = float(os.getenv("TEXT_SIZE_SCALE", "1.22")) + self._font_scale_by_id: dict[int, float] = {} @property def target_fps(self): return self._target_fps - @property - def text_scale(self) -> float: - return self._text_scale - def request_close(self): self._window_close_requested = True @@ -350,39 +346,30 @@ def _load_fonts(self): rl.unload_codepoints(codepoints) rl.gui_set_font(self._fonts[FontWeight.NORMAL]) - + # Compute per-font scale: map baseSize to tight glyph line height for each loaded weight try: - font = self._fonts[FontWeight.NORMAL] - base = float(font.baseSize) if getattr(font, 'baseSize', 0) else 200.0 - - # Estimate line height from glyph extents: min(top), max(bottom) across a set with ascenders/descenders - codepoints = [ord(c) for c in "HXYZAMNQgjpqy"] - min_top = 1e9 - max_bottom = -1e9 - for cp in codepoints: - idx = rl.get_glyph_index(font, cp) - # Some glyphs may be missing; skip if index invalid - try: - gi = font.glyphs[idx] - rec = font.recs[idx] - top = float(getattr(gi, 'offsetY', 0.0)) - bottom = top + float(getattr(rec, 'height', 0.0)) - if top < min_top: - min_top = top - if bottom > max_bottom: - max_bottom = bottom - except Exception: - continue - - line_px = max(1.0, max_bottom - min_top) - # Compute scale purely from font: map baseSize to the tight glyph line box height - # so requesting N px roughly yields a line box of N px, matching typical UI sizing. - # scale = baseSize / linePx (usually > 1.0 for Inter on Linux) - denom = line_px if line_px > 0.01 else 1.0 - self._text_scale = base / denom - - cloudlog.info( - f"raylib font metrics: baseSize={base:.1f} linePx={line_px:.1f} textScale={self._text_scale:.3f}") + for weight, font in self._fonts.items(): + base = float(getattr(font, 'baseSize', 200.0)) + min_top = 1e9 + max_bottom = -1e9 + for cp in [ord(c) for c in "HXYZAMNQgjpqy"]: + try: + idx = rl.get_glyph_index(font, cp) + gi = font.glyphs[idx] + rec = font.recs[idx] + top = float(getattr(gi, 'offsetY', 0.0)) + bottom = top + float(getattr(rec, 'height', 0.0)) + if top < min_top: + min_top = top + if bottom > max_bottom: + max_bottom = bottom + except Exception: + continue + line_px = max(1.0, max_bottom - min_top) + scale = (base / line_px) if line_px > 0.01 else 1.0 + tex_id = int(font.texture.id) if hasattr(font, 'texture') and hasattr(font.texture, 'id') else 0 + self._font_scale_by_id[tex_id] = scale + cloudlog.info(f"font scale computed: weight={weight} baseSize={base:.1f} linePx={line_px:.1f} scale={scale:.3f}") except Exception as e: cloudlog.exception(f"font metrics computation failed: {e}") @@ -400,18 +387,25 @@ def _monkey_patch_text_functions(self): rl._orig_draw_text_ex = rl.draw_text_ex if not hasattr(rl, "_orig_measure_text_ex"): rl._orig_measure_text_ex = rl.measure_text_ex - - scale = self._text_scale - def _draw_text_ex_scaled(font, text, position, font_size, spacing, tint): + try: + tex_id = int(font.texture.id) + scale = self._font_scale_by_id.get(tex_id, 1.0) + except Exception: + scale = 1.0 return rl._orig_draw_text_ex(font, text, position, float(font_size) * scale, spacing, tint) def _measure_text_ex_scaled(font, text, font_size, spacing): + try: + tex_id = int(font.texture.id) + scale = self._font_scale_by_id.get(tex_id, 1.0) + except Exception: + scale = 1.0 return rl._orig_measure_text_ex(font, text, float(font_size) * scale, spacing) rl.draw_text_ex = _draw_text_ex_scaled rl.measure_text_ex = _measure_text_ex_scaled - cloudlog.info(f"raylib text functions wrapped with scale={scale}") + cloudlog.info("raylib text functions wrapped with per-font scale") except Exception as e: cloudlog.exception(f"failed to wrap text functions: {e}") From d855c0bd433dee4d3a30fa29285bfb2a478ba4af Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 3 Oct 2025 16:00:52 -0700 Subject: [PATCH 04/21] fix sidebar --- selfdrive/ui/layouts/sidebar.py | 29 ++++++++++++++++++----------- system/ui/lib/application.py | 21 +++++++++------------ system/ui/lib/text_measure.py | 13 ++++++++++--- 3 files changed, 37 insertions(+), 26 deletions(-) diff --git a/selfdrive/ui/layouts/sidebar.py b/selfdrive/ui/layouts/sidebar.py index 5987d062ff60c9..34354ecbab0568 100644 --- a/selfdrive/ui/layouts/sidebar.py +++ b/selfdrive/ui/layouts/sidebar.py @@ -216,14 +216,21 @@ def _draw_metric(self, rect: rl.Rectangle, metric: MetricData, y: float): # Draw border rl.draw_rectangle_rounded_lines_ex(metric_rect, 0.3, 10, 2, Colors.METRIC_BORDER) - # Draw label and value - labels = [metric.label, metric.value] - text_y = metric_rect.y + (metric_rect.height / 2 - len(labels) * FONT_SIZE) - for text in labels: - text_size = measure_text_cached(self._font_bold, text, FONT_SIZE) - text_y += text_size.y - text_pos = rl.Vector2( - metric_rect.x + 22 + (metric_rect.width - 22 - text_size.x) / 2, - text_y - ) - rl.draw_text_ex(self._font_bold, text, text_pos, FONT_SIZE, 0, Colors.WHITE) + label_size = measure_text_cached(self._font_bold, metric.label, FONT_SIZE) + value_size = measure_text_cached(self._font_bold, metric.value, FONT_SIZE) + text_height = label_size.y + value_size.y + + label_y = metric_rect.y + (metric_rect.height - text_height) / 2 + value_y = label_y + label_size.y + + # label + rl.draw_text_ex(self._font_bold, metric.label, rl.Vector2( + metric_rect.x + 22 + (metric_rect.width - 22 - label_size.x) / 2, + label_y + ), FONT_SIZE, 0, Colors.WHITE) + + # value + rl.draw_text_ex(self._font_bold, metric.value, rl.Vector2( + metric_rect.x + 22 + (metric_rect.width - 22 - value_size.x) / 2, + value_y + ), FONT_SIZE, 0, Colors.WHITE) diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index 09a0ef0963deaa..2f38a2551cc29f 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -317,6 +317,14 @@ def render(self): def font(self, font_weight: FontWeight = FontWeight.NORMAL): return self._fonts[font_weight] + def get_font_scale(self, font: rl.Font) -> float: + """Return per-font scale factor computed from glyph metrics.""" + try: + tex_id = int(font.texture.id) + return self._font_scale_by_id.get(tex_id, 1.0) + except Exception: + return 1.0 + @property def width(self): return self._width @@ -385,8 +393,6 @@ def _monkey_patch_text_functions(self): try: if not hasattr(rl, "_orig_draw_text_ex"): rl._orig_draw_text_ex = rl.draw_text_ex - if not hasattr(rl, "_orig_measure_text_ex"): - rl._orig_measure_text_ex = rl.measure_text_ex def _draw_text_ex_scaled(font, text, position, font_size, spacing, tint): try: tex_id = int(font.texture.id) @@ -395,17 +401,8 @@ def _draw_text_ex_scaled(font, text, position, font_size, spacing, tint): scale = 1.0 return rl._orig_draw_text_ex(font, text, position, float(font_size) * scale, spacing, tint) - def _measure_text_ex_scaled(font, text, font_size, spacing): - try: - tex_id = int(font.texture.id) - scale = self._font_scale_by_id.get(tex_id, 1.0) - except Exception: - scale = 1.0 - return rl._orig_measure_text_ex(font, text, float(font_size) * scale, spacing) - rl.draw_text_ex = _draw_text_ex_scaled - rl.measure_text_ex = _measure_text_ex_scaled - cloudlog.info("raylib text functions wrapped with per-font scale") + cloudlog.info("raylib draw_text_ex wrapped with per-font scale") except Exception as e: cloudlog.exception(f"failed to wrap text functions: {e}") diff --git a/system/ui/lib/text_measure.py b/system/ui/lib/text_measure.py index c172f942512f40..a04056dee12eb5 100644 --- a/system/ui/lib/text_measure.py +++ b/system/ui/lib/text_measure.py @@ -1,14 +1,21 @@ import pyray as rl +from openpilot.system.ui.lib.application import gui_app _cache: dict[int, rl.Vector2] = {} def measure_text_cached(font: rl.Font, text: str, font_size: int, spacing: int = 0) -> rl.Vector2: - """Caches text measurements to avoid redundant calculations.""" - key = hash((font.texture.id, text, font_size, spacing)) + """Caches text measurements to avoid redundant calculations. + + Applies the same per-font scaling used by draw/measure wrappers so + returned sizes match Qt across all call sites. + """ + scale = gui_app.get_font_scale(font) + key = hash((int(getattr(font.texture, 'id', 0)), text, font_size, spacing, scale)) if key in _cache: return _cache[key] - result = rl.measure_text_ex(font, text, font_size, spacing) # noqa: TID251 + # Call Raylib's original measure function directly with our per-font scaling + result = rl.measure_text_ex(font, text, float(font_size) * scale, spacing) # noqa: TID251 _cache[key] = result return result From 5da64658d1952a6031e2fd3947eaba97bcfa31dc Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 3 Oct 2025 16:19:57 -0700 Subject: [PATCH 05/21] stash --- selfdrive/ui/layouts/home.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/selfdrive/ui/layouts/home.py b/selfdrive/ui/layouts/home.py index 39b39d33d99e02..2369561bbc6499 100644 --- a/selfdrive/ui/layouts/home.py +++ b/selfdrive/ui/layouts/home.py @@ -8,6 +8,7 @@ from openpilot.selfdrive.ui.widgets.prime import PrimeWidget from openpilot.selfdrive.ui.widgets.setup import SetupWidget from openpilot.system.ui.lib.text_measure import measure_text_cached +from openpilot.system.ui.widgets.label import Label from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, DEFAULT_TEXT_COLOR from openpilot.system.ui.widgets import Widget @@ -52,6 +53,8 @@ def __init__(self): self.update_notif_rect = rl.Rectangle(0, 0, 200, HEADER_HEIGHT - 10) self.alert_notif_rect = rl.Rectangle(0, 0, 220, HEADER_HEIGHT - 10) + self._version_label = Label("", 48, FontWeight.NORMAL, text_color=DEFAULT_TEXT_COLOR) + self._prime_widget = PrimeWidget() self._setup_widget = SetupWidget() From 99c8840604b58d6dedbcc17a33e0bef21cd5b9ac Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 3 Oct 2025 17:40:58 -0700 Subject: [PATCH 06/21] test update From be644e7a22f0ed110d2fca210d8e911b2b60dd78 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 3 Oct 2025 21:56:24 -0700 Subject: [PATCH 07/21] just use a const --- system/ui/lib/application.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index 97e782edf60a44..531fac8ef34900 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -30,6 +30,10 @@ 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 ranges from 1.212 to 1.266 +FONT_SCALE = 1.242 + ASSETS_DIR = files("openpilot.selfdrive").joinpath("assets") FONT_DIR = ASSETS_DIR.joinpath("fonts") @@ -380,6 +384,7 @@ def _load_fonts(self): cloudlog.info(f"font scale computed: weight={weight} baseSize={base:.1f} linePx={line_px:.1f} scale={scale:.3f}") except Exception as e: cloudlog.exception(f"font metrics computation failed: {e}") + print(self._font_scale_by_id) def _set_styles(self): rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiControlProperty.BORDER_WIDTH, 0) From 7e73a5b842d6d13864a6afb340b4b259591cb038 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 3 Oct 2025 22:01:59 -0700 Subject: [PATCH 08/21] just use a const --- system/ui/lib/application.py | 62 +++++------------------------------ system/ui/lib/text_measure.py | 14 +++----- 2 files changed, 13 insertions(+), 63 deletions(-) diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index 531fac8ef34900..42d5749347b535 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -145,7 +145,6 @@ def __init__(self, width: int, height: int): # Debug variables self._mouse_history: deque[MousePos] = deque(maxlen=MOUSE_THREAD_RATE) - self._font_scale_by_id: dict[int, float] = {} @property def target_fps(self): @@ -178,7 +177,7 @@ def init_window(self, title: str, fps: int = _DEFAULT_FPS): self._target_fps = fps self._set_styles() self._load_fonts() - self._monkey_patch_text_functions() + self._patch_text_functions() if not PC: self._mouse.start() @@ -321,14 +320,6 @@ def render(self): def font(self, font_weight: FontWeight = FontWeight.NORMAL): return self._fonts[font_weight] - def get_font_scale(self, font: rl.Font) -> float: - """Return per-font scale factor computed from glyph metrics.""" - try: - tex_id = int(font.texture.id) - return self._font_scale_by_id.get(tex_id, 1.0) - except Exception: - return 1.0 - @property def width(self): return self._width @@ -358,33 +349,6 @@ def _load_fonts(self): rl.unload_codepoints(codepoints) rl.gui_set_font(self._fonts[FontWeight.NORMAL]) - # Compute per-font scale: map baseSize to tight glyph line height for each loaded weight - try: - for weight, font in self._fonts.items(): - base = float(getattr(font, 'baseSize', 200.0)) - min_top = 1e9 - max_bottom = -1e9 - for cp in [ord(c) for c in "HXYZAMNQgjpqy"]: - try: - idx = rl.get_glyph_index(font, cp) - gi = font.glyphs[idx] - rec = font.recs[idx] - top = float(getattr(gi, 'offsetY', 0.0)) - bottom = top + float(getattr(rec, 'height', 0.0)) - if top < min_top: - min_top = top - if bottom > max_bottom: - max_bottom = bottom - except Exception: - continue - line_px = max(1.0, max_bottom - min_top) - scale = (base / line_px) if line_px > 0.01 else 1.0 - tex_id = int(font.texture.id) if hasattr(font, 'texture') and hasattr(font.texture, 'id') else 0 - self._font_scale_by_id[tex_id] = scale - cloudlog.info(f"font scale computed: weight={weight} baseSize={base:.1f} linePx={line_px:.1f} scale={scale:.3f}") - except Exception as e: - cloudlog.exception(f"font metrics computation failed: {e}") - print(self._font_scale_by_id) def _set_styles(self): rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiControlProperty.BORDER_WIDTH, 0) @@ -393,23 +357,15 @@ def _set_styles(self): 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 _monkey_patch_text_functions(self): + def _patch_text_functions(self): # Wrap pyray text APIs to apply a global text size scale so our px sizes match Qt - try: - 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): - try: - tex_id = int(font.texture.id) - scale = self._font_scale_by_id.get(tex_id, 1.0) - except Exception: - scale = 1.0 - return rl._orig_draw_text_ex(font, text, position, float(font_size) * scale, spacing, tint) - - rl.draw_text_ex = _draw_text_ex_scaled - cloudlog.info("raylib draw_text_ex wrapped with per-font scale") - except Exception as e: - cloudlog.exception(f"failed to wrap text functions: {e}") + 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() diff --git a/system/ui/lib/text_measure.py b/system/ui/lib/text_measure.py index a04056dee12eb5..fcb7b25ccd72b3 100644 --- a/system/ui/lib/text_measure.py +++ b/system/ui/lib/text_measure.py @@ -1,21 +1,15 @@ import pyray as rl -from openpilot.system.ui.lib.application import gui_app +from openpilot.system.ui.lib.application import FONT_SCALE _cache: dict[int, rl.Vector2] = {} def measure_text_cached(font: rl.Font, text: str, font_size: int, spacing: int = 0) -> rl.Vector2: - """Caches text measurements to avoid redundant calculations. - - Applies the same per-font scaling used by draw/measure wrappers so - returned sizes match Qt across all call sites. - """ - scale = gui_app.get_font_scale(font) - key = hash((int(getattr(font.texture, 'id', 0)), text, font_size, spacing, scale)) + """Caches text measurements to avoid redundant calculations.""" + key = hash((font.texture.id, text, font_size, spacing)) if key in _cache: return _cache[key] - # Call Raylib's original measure function directly with our per-font scaling - result = rl.measure_text_ex(font, text, float(font_size) * scale, spacing) # noqa: TID251 + result = rl.measure_text_ex(font, text, font_size * FONT_SCALE, spacing) # noqa: TID251 _cache[key] = result return result From 12e4ce0918ae932ea0d496ee32005013f110b032 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 3 Oct 2025 22:13:45 -0700 Subject: [PATCH 09/21] better --- system/ui/lib/application.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index 42d5749347b535..17872a5f3756cc 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -31,7 +31,7 @@ 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 ranges from 1.212 to 1.266 +# 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") From ce6304c63bfe31d362a538d26bbdb95648837b57 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 3 Oct 2025 22:15:38 -0700 Subject: [PATCH 10/21] clean up --- selfdrive/ui/layouts/settings/software.py | 37 ---------- selfdrive/ui/main.cc | 11 --- selfdrive/ui/qt/offroad/settings.h | 1 - selfdrive/ui/qt/offroad/software_settings.cc | 76 -------------------- 4 files changed, 125 deletions(-) diff --git a/selfdrive/ui/layouts/settings/software.py b/selfdrive/ui/layouts/settings/software.py index f9740665407797..7aebc609f87ce7 100644 --- a/selfdrive/ui/layouts/settings/software.py +++ b/selfdrive/ui/layouts/settings/software.py @@ -1,7 +1,3 @@ -from openpilot.common.params import Params -import pyray as rl -from openpilot.system.ui.lib.application import gui_app, FontWeight -from openpilot.system.ui.lib.text_measure import measure_text_cached import os import time import datetime @@ -75,8 +71,6 @@ def _init_items(self): def _render(self, rect): self._scroller.render(rect) - if os.getenv("DEBUG_TEXT_COMPARE") == "1": - self._draw_text_compare() def _update_state(self): # Show/hide onroad warning @@ -151,37 +145,6 @@ def _on_download_update(self): self._waiting_start_ts = time.monotonic() os.system("pkill -SIGHUP -f system.updated.updated") - def _draw_text_compare(self): - try: - font = gui_app.font(FontWeight.NORMAL) - samples = [(100, "HH"), (80, "HH"), (70, "HH")] - left_x = 10 - top_y = int(gui_app.height / 2 + 40) - row_h = 120 - - for i, (size, text) in enumerate(samples): - y = top_y + i * row_h - rl.draw_line_ex(rl.Vector2(left_x, y), rl.Vector2(left_x, y + size), 2, rl.RED) - text_pos = rl.Vector2(left_x + 10, y) - rl.draw_text_ex(font, text, text_pos, size, 0, rl.WHITE) - ts = measure_text_cached(font, text, size) - rl.draw_rectangle_lines(int(text_pos.x), int(text_pos.y), int(ts.x), int(ts.y), rl.GREEN) - - # Metrics readout - info_y = top_y + len(samples) * row_h + 40 - info_lines = [ - f"SCALE={os.getenv('SCALE','')} target_fps={gui_app.target_fps}", - ] - for size, text in samples: - ts = measure_text_cached(font, text, size) - info_lines.append(f"sz={size} meas.h={ts.y} baseSize=200") - info_font = gui_app.font(FontWeight.NORMAL) - info_size = 20 - for i, line in enumerate(info_lines): - rl.draw_text_ex(info_font, line, rl.Vector2(left_x, info_y + i * 24), info_size, 0, rl.WHITE) - except Exception: - pass - def _on_uninstall(self): def handle_uninstall_confirmation(result): if result == DialogResult.CONFIRM: diff --git a/selfdrive/ui/main.cc b/selfdrive/ui/main.cc index 95325fba25e702..4903a3db3dcc07 100644 --- a/selfdrive/ui/main.cc +++ b/selfdrive/ui/main.cc @@ -2,8 +2,6 @@ #include #include -#include -#include #include "system/hardware/hw.h" #include "selfdrive/ui/qt/qt_window.h" @@ -25,15 +23,6 @@ int main(int argc, char *argv[]) { QApplication a(argc, argv); a.installTranslator(&translator); - // Debug DPI/scaling info - const char *sf = qgetenv("QT_SCALE_FACTOR").constData(); - auto scr = a.primaryScreen(); - qDebug() << "QT_SCALE_FACTOR=" << (sf ? sf : "") - << " devicePixelRatio=" << (scr ? scr->devicePixelRatio() : 0) - << " devicePixelRatioF=" << (scr ? scr->devicePixelRatio() : 0) - << " logicalDpi=" << (scr ? scr->logicalDotsPerInch() : 0) - << " physicalDpi=" << (scr ? scr->physicalDotsPerInch() : 0); - MainWindow w; setMainWindow(&w); a.installEventFilter(&w); diff --git a/selfdrive/ui/qt/offroad/settings.h b/selfdrive/ui/qt/offroad/settings.h index d364b1512ad430..d52cf16bb7ebb9 100644 --- a/selfdrive/ui/qt/offroad/settings.h +++ b/selfdrive/ui/qt/offroad/settings.h @@ -86,7 +86,6 @@ class SoftwarePanel : public ListWidget { explicit SoftwarePanel(QWidget* parent = nullptr); private: - void paintEvent(QPaintEvent *event) override; void showEvent(QShowEvent *event) override; void updateLabels(); void checkForUpdates(); diff --git a/selfdrive/ui/qt/offroad/software_settings.cc b/selfdrive/ui/qt/offroad/software_settings.cc index eb5b06eed30f74..9bc3fad3c9b67a 100644 --- a/selfdrive/ui/qt/offroad/software_settings.cc +++ b/selfdrive/ui/qt/offroad/software_settings.cc @@ -6,8 +6,6 @@ #include #include -#include -#include #include "common/params.h" #include "common/util.h" @@ -98,80 +96,6 @@ SoftwarePanel::SoftwarePanel(QWidget* parent) : ListWidget(parent) { updateLabels(); } -void SoftwarePanel::paintEvent(QPaintEvent *event) { - QWidget::paintEvent(event); - if (qEnvironmentVariableIsSet("DEBUG_TEXT_COMPARE") && qgetenv("DEBUG_TEXT_COMPARE") == "1") { - QPainter p(this); - p.setRenderHint(QPainter::Antialiasing); - - struct Sample { int size; const char *text; }; - const Sample samples[] = {{100, "HH"}, {80, "HH"}, {70, "HH"}}; - - int left_x = 8; // far left to avoid overlapping content - int top_y = height() / 2 + 40; // bottom half of screen - int row_h = 120; - - for (int i = 0; i < 3; ++i) { - int sz = samples[i].size; - QString text = samples[i].text; - int y = top_y + i * row_h; - - QFont f = font(); - f.setPixelSize(sz); - p.setFont(f); - QFontMetrics fm(f); - QRect br = fm.boundingRect(text); - - int tx = left_x + 12; - int ty = y + fm.ascent(); // baseline - p.setPen(Qt::white); - p.drawText(tx, ty, text); - - // Green: full font metrics box (top at ascent, height = fm.height()) - p.setPen(QPen(Qt::green, 1)); - QRect r(tx, ty - fm.ascent(), br.width(), fm.height()); - p.drawRect(r); - - // Tight bounding rect of the actual glyph (optional for reference) - - // Red: exactly font-size pixels tall, starting at top of the green box - p.setPen(QPen(Qt::red, 2)); - p.drawLine(QPoint(left_x, r.top()), QPoint(left_x, r.top() + sz)); - } - - // Metrics readout - QFont mf = font(); - mf.setPixelSize(18); - p.setFont(mf); - auto scr = QApplication::primaryScreen(); - QString scale_line = QString("QT_SCALE_FACTOR=%1 DPR=%2 logicalDPI=%3 physicalDPI=%4") - .arg(QString::fromUtf8(qgetenv("QT_SCALE_FACTOR"))) - .arg(scr ? scr->devicePixelRatio() : 0.0) - .arg(scr ? scr->logicalDotsPerInch() : 0.0) - .arg(scr ? scr->physicalDotsPerInch() : 0.0); - p.setPen(Qt::white); - p.drawText(left_x, top_y + 3 * row_h + 40, scale_line); - - // Per-size metrics (height and tight height) - int metrics_y = top_y + 3 * row_h + 70; - const int line_step = 22; - for (int i = 0; i < 3; ++i) { - int sz = samples[i].size; - QFont f = font(); - f.setPixelSize(sz); - QFontMetrics fm(f); - QRect tr = fm.tightBoundingRect("HH"); - QString mline = QString("sz=%1 fm.height=%2 tight.h=%3 ascent=%4 descent=%5") - .arg(sz) - .arg(fm.height()) - .arg(tr.height()) - .arg(fm.ascent()) - .arg(fm.descent()); - p.drawText(left_x, metrics_y + i * line_step, mline); - } - } -} - void SoftwarePanel::showEvent(QShowEvent *event) { // nice for testing on PC installBtn->setEnabled(true); From 46aa5b166c7430b651c714244004796e09971437 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 3 Oct 2025 22:23:43 -0700 Subject: [PATCH 11/21] fix label --- selfdrive/ui/layouts/home.py | 30 ++++++++++++++++++++---------- selfdrive/ui/ui.cc | 2 +- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/selfdrive/ui/layouts/home.py b/selfdrive/ui/layouts/home.py index 2369561bbc6499..fcd77758e01a66 100644 --- a/selfdrive/ui/layouts/home.py +++ b/selfdrive/ui/layouts/home.py @@ -8,8 +8,8 @@ from openpilot.selfdrive.ui.widgets.prime import PrimeWidget from openpilot.selfdrive.ui.widgets.setup import SetupWidget from openpilot.system.ui.lib.text_measure import measure_text_cached -from openpilot.system.ui.widgets.label import Label -from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, DEFAULT_TEXT_COLOR +from openpilot.system.ui.widgets.label import gui_label +from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos from openpilot.system.ui.widgets import Widget HEADER_HEIGHT = 80 @@ -53,8 +53,6 @@ def __init__(self): self.update_notif_rect = rl.Rectangle(0, 0, 200, HEADER_HEIGHT - 10) self.alert_notif_rect = rl.Rectangle(0, 0, 220, HEADER_HEIGHT - 10) - self._version_label = Label("", 48, FontWeight.NORMAL, text_color=DEFAULT_TEXT_COLOR) - self._prime_widget = PrimeWidget() self._setup_widget = SetupWidget() @@ -129,8 +127,12 @@ def _handle_mouse_release(self, mouse_pos: MousePos): def _render_header(self): font = gui_app.font(FontWeight.MEDIUM) + version_text_width = self.header_rect.width + # Update notification button if self.update_available: + version_text_width -= self.update_notif_rect.width + # Highlight if currently viewing updates highlight_color = rl.Color(75, 95, 255, 255) if self.current_state == HomeLayoutState.UPDATE else rl.Color(54, 77, 239, 255) rl.draw_rectangle_rounded(self.update_notif_rect, 0.3, 10, highlight_color) @@ -143,6 +145,8 @@ def _render_header(self): # Alert notification button if self.alert_count > 0: + version_text_width -= self.alert_notif_rect.width + # Highlight if currently viewing alerts highlight_color = rl.Color(255, 70, 70, 255) if self.current_state == HomeLayoutState.ALERTS else rl.Color(226, 44, 44, 255) rl.draw_rectangle_rounded(self.alert_notif_rect, 0.3, 10, highlight_color) @@ -153,12 +157,18 @@ def _render_header(self): text_y = self.alert_notif_rect.y + (self.alert_notif_rect.height - HEAD_BUTTON_FONT_SIZE) // 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) - version_text = self._get_version_text() - text_width = measure_text_cached(gui_app.font(FontWeight.NORMAL), version_text, 48).x - version_x = self.header_rect.x + self.header_rect.width - text_width - version_y = self.header_rect.y + (self.header_rect.height - 48) // 2 - rl.draw_text_ex(gui_app.font(FontWeight.NORMAL), version_text, rl.Vector2(int(version_x), int(version_y)), 48, 0, DEFAULT_TEXT_COLOR) + # Draw version text (right-aligned) + if self.update_available or self.alert_count > 0: + version_text_width -= SPACING * 1.5 + + version_rect = rl.Rectangle( + self.header_rect.x + self.header_rect.width - version_text_width, + self.header_rect.y, + version_text_width, + self.header_rect.height + ) + + gui_label(version_rect, self._get_version_text(), 48, rl.WHITE, alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT) def _render_home_content(self): self._render_left_column() diff --git a/selfdrive/ui/ui.cc b/selfdrive/ui/ui.cc index 9fceb5246e2f36..9ec61b9b8143f0 100644 --- a/selfdrive/ui/ui.cc +++ b/selfdrive/ui/ui.cc @@ -145,7 +145,7 @@ void Device::setAwake(bool on) { void Device::resetInteractiveTimeout(int timeout) { if (timeout == -1) { - timeout = (ignition_on ? 10 : 300); + timeout = (ignition_on ? 10 : 30); } interactive_timeout = timeout * UI_FREQ; } From 2015e8d8f016c75b63f862980c8edbe8dc8cce46 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 3 Oct 2025 22:43:39 -0700 Subject: [PATCH 12/21] simplify --- selfdrive/ui/layouts/home.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/selfdrive/ui/layouts/home.py b/selfdrive/ui/layouts/home.py index fcd77758e01a66..0e0989015e02c2 100644 --- a/selfdrive/ui/layouts/home.py +++ b/selfdrive/ui/layouts/home.py @@ -161,12 +161,8 @@ def _render_header(self): if self.update_available or self.alert_count > 0: version_text_width -= SPACING * 1.5 - version_rect = rl.Rectangle( - self.header_rect.x + self.header_rect.width - version_text_width, - self.header_rect.y, - version_text_width, - self.header_rect.height - ) + version_rect = rl.Rectangle(self.header_rect.x + self.header_rect.width - version_text_width, self.header_rect.y, + version_text_width, self.header_rect.height) gui_label(version_rect, self._get_version_text(), 48, rl.WHITE, alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT) From 258df57dbff4e9c0f9c767d0555d09450e6c9093 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 3 Oct 2025 22:45:40 -0700 Subject: [PATCH 13/21] gpt5 is yet again garbage --- system/ui/widgets/list_view.py | 50 ++++++++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/system/ui/widgets/list_view.py b/system/ui/widgets/list_view.py index 056aa2d90bcdff..ec248aef828bed 100644 --- a/system/ui/widgets/list_view.py +++ b/system/ui/widgets/list_view.py @@ -7,6 +7,7 @@ from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.button import Button, ButtonStyle from openpilot.system.ui.widgets.toggle import Toggle, WIDTH as TOGGLE_WIDTH, HEIGHT as TOGGLE_HEIGHT +from openpilot.system.ui.widgets.label import gui_label from openpilot.system.ui.widgets.html_render import HtmlRenderer, ElementType ITEM_BASE_WIDTH = 600 @@ -42,6 +43,9 @@ def __init__(self, width: int = BUTTON_HEIGHT, enabled: bool | Callable[[], bool self.set_rect(rl.Rectangle(0, 0, width, 0)) self._enabled_source = enabled + def get_width_hint(self) -> float: + return self._rect.width + def set_enabled(self, enabled: bool | Callable[[], bool]): self._enabled_source = enabled @@ -147,17 +151,28 @@ def __init__(self, text: str | Callable[[], str], color: rl.Color = ITEM_TEXT_CO def text(self): return _resolve_value(self._text_source, "Error") - def _update_state(self): + def get_width_hint(self) -> float: text_width = measure_text_cached(self._font, self.text, ITEM_TEXT_FONT_SIZE).x - self._rect.width = int(text_width + TEXT_PADDING) + return text_width + TEXT_PADDING def _render(self, rect: rl.Rectangle) -> bool: - current_text = self.text - text_size = measure_text_cached(self._font, current_text, ITEM_TEXT_FONT_SIZE) - text_x = rect.x + (rect.width - text_size.x) / 2 - text_y = rect.y + (rect.height - text_size.y) / 2 - rl.draw_text_ex(self._font, current_text, rl.Vector2(text_x, text_y), ITEM_TEXT_FONT_SIZE, 0, self.color) + """ + def _render(self, rect: rl.Rectangle) -> bool: + current_text = self.text + text_size = measure_text_cached(self._font, current_text, ITEM_TEXT_FONT_SIZE) + + text_x = rect.x + (rect.width - text_size.x) / 2 + text_y = rect.y + (rect.height - text_size.y) / 2 + rl.draw_text_ex(self._font, current_text, rl.Vector2(text_x, text_y), ITEM_TEXT_FONT_SIZE, 0, self.color) + return False + """ + + rl.draw_rectangle_lines_ex(rect, 1, rl.RED) + + gui_label(self._rect, self.text, font_size=ITEM_TEXT_FONT_SIZE, color=self.color, + font_weight=FontWeight.NORMAL, alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT, + alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) return False def set_text(self, text: str | Callable[[], str]): @@ -266,6 +281,7 @@ def __init__(self, title: str = "", icon: str | None = None, description: str | self.set_rect(rl.Rectangle(0, 0, ITEM_BASE_WIDTH, ITEM_BASE_HEIGHT)) self._font = gui_app.font(FontWeight.NORMAL) self._icon_texture = gui_app.texture(os.path.join("icons", self.icon), ICON_SIZE, ICON_SIZE) if self.icon else None + self._title_width = 0 self._html_renderer = HtmlRenderer(text="", text_size={ElementType.P: ITEM_DESC_FONT_SIZE}, text_color=ITEM_DESC_TEXT_COLOR) @@ -328,6 +344,7 @@ def _render(self, _): text_size = measure_text_cached(self._font, self.title, ITEM_TEXT_FONT_SIZE) item_y = self._rect.y + (ITEM_BASE_HEIGHT - text_size.y) // 2 rl.draw_text_ex(self._font, self.title, rl.Vector2(text_x, item_y), ITEM_TEXT_FONT_SIZE, 0, ITEM_TEXT_COLOR) + self._title_width = text_size.x # Draw description if visible if self.description_visible: @@ -374,15 +391,32 @@ def get_right_item_rect(self, item_rect: rl.Rectangle) -> rl.Rectangle: if not self.action_item: return rl.Rectangle(0, 0, 0, 0) - right_width = self.action_item.rect.width + # right_width = self.action_item.rect.width + right_width = self.action_item.get_width_hint() if right_width == 0: # Full width action (like DualButtonAction) return rl.Rectangle(item_rect.x + ITEM_PADDING, item_rect.y, item_rect.width - (ITEM_PADDING * 2), ITEM_BASE_HEIGHT) + content_width = item_rect.width - (ITEM_PADDING * 2) + right_width = min(content_width - self._title_width, right_width) + right_x = item_rect.x + item_rect.width - right_width right_y = item_rect.y return rl.Rectangle(right_x, right_y, right_width, ITEM_BASE_HEIGHT) + # # Compute the leftmost x we allow the right item to start at, so it never covers the title + # content_x = item_rect.x + ITEM_PADDING + # text_x = content_x + # if self.icon: + # text_x += ICON_SIZE + ITEM_PADDING + # title_width = measure_text_cached(self._font, self.title, ITEM_TEXT_FONT_SIZE).x if self.title else 0 + # min_right_x = text_x + title_width + RIGHT_ITEM_PADDING + # + # # Place the action so it doesn't overlap the title + # right_x = max(min_right_x, item_rect.x + item_rect.width - right_width) + # right_y = item_rect.y + # return rl.Rectangle(right_x, right_y, right_width, ITEM_BASE_HEIGHT) + # Factory functions def simple_item(title: str, callback: Callable | None = None) -> ListItem: From 6af6892c0cf51863cd861254dcb17016ffdb49f9 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 3 Oct 2025 22:45:46 -0700 Subject: [PATCH 14/21] rm that --- system/ui/widgets/list_view.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/system/ui/widgets/list_view.py b/system/ui/widgets/list_view.py index ec248aef828bed..c72b32dca164eb 100644 --- a/system/ui/widgets/list_view.py +++ b/system/ui/widgets/list_view.py @@ -404,19 +404,6 @@ def get_right_item_rect(self, item_rect: rl.Rectangle) -> rl.Rectangle: right_y = item_rect.y return rl.Rectangle(right_x, right_y, right_width, ITEM_BASE_HEIGHT) - # # Compute the leftmost x we allow the right item to start at, so it never covers the title - # content_x = item_rect.x + ITEM_PADDING - # text_x = content_x - # if self.icon: - # text_x += ICON_SIZE + ITEM_PADDING - # title_width = measure_text_cached(self._font, self.title, ITEM_TEXT_FONT_SIZE).x if self.title else 0 - # min_right_x = text_x + title_width + RIGHT_ITEM_PADDING - # - # # Place the action so it doesn't overlap the title - # right_x = max(min_right_x, item_rect.x + item_rect.width - right_width) - # right_y = item_rect.y - # return rl.Rectangle(right_x, right_y, right_width, ITEM_BASE_HEIGHT) - # Factory functions def simple_item(title: str, callback: Callable | None = None) -> ListItem: From 9bc6f5718443a67afc6839c11bf630377e86d8f9 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 3 Oct 2025 22:47:25 -0700 Subject: [PATCH 15/21] clean up --- system/ui/widgets/list_view.py | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/system/ui/widgets/list_view.py b/system/ui/widgets/list_view.py index c72b32dca164eb..60fbab2f399b59 100644 --- a/system/ui/widgets/list_view.py +++ b/system/ui/widgets/list_view.py @@ -156,20 +156,6 @@ def get_width_hint(self) -> float: return text_width + TEXT_PADDING def _render(self, rect: rl.Rectangle) -> bool: - - """ - def _render(self, rect: rl.Rectangle) -> bool: - current_text = self.text - text_size = measure_text_cached(self._font, current_text, ITEM_TEXT_FONT_SIZE) - - text_x = rect.x + (rect.width - text_size.x) / 2 - text_y = rect.y + (rect.height - text_size.y) / 2 - rl.draw_text_ex(self._font, current_text, rl.Vector2(text_x, text_y), ITEM_TEXT_FONT_SIZE, 0, self.color) - return False - """ - - rl.draw_rectangle_lines_ex(rect, 1, rl.RED) - gui_label(self._rect, self.text, font_size=ITEM_TEXT_FONT_SIZE, color=self.color, font_weight=FontWeight.NORMAL, alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) @@ -344,7 +330,6 @@ def _render(self, _): text_size = measure_text_cached(self._font, self.title, ITEM_TEXT_FONT_SIZE) item_y = self._rect.y + (ITEM_BASE_HEIGHT - text_size.y) // 2 rl.draw_text_ex(self._font, self.title, rl.Vector2(text_x, item_y), ITEM_TEXT_FONT_SIZE, 0, ITEM_TEXT_COLOR) - self._title_width = text_size.x # Draw description if visible if self.description_visible: @@ -398,7 +383,8 @@ def get_right_item_rect(self, item_rect: rl.Rectangle) -> rl.Rectangle: item_rect.width - (ITEM_PADDING * 2), ITEM_BASE_HEIGHT) content_width = item_rect.width - (ITEM_PADDING * 2) - right_width = min(content_width - self._title_width, right_width) + title_width = measure_text_cached(self._font, self.title, ITEM_TEXT_FONT_SIZE).x + right_width = min(content_width - title_width, right_width) right_x = item_rect.x + item_rect.width - right_width right_y = item_rect.y From 369b368db8b4c69e173d0703f2a2219dcffd96ae Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 3 Oct 2025 23:19:26 -0700 Subject: [PATCH 16/21] rm --- selfdrive/ui/layouts/home.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/selfdrive/ui/layouts/home.py b/selfdrive/ui/layouts/home.py index 6f5b18aab44658..f7f57c680d3c79 100644 --- a/selfdrive/ui/layouts/home.py +++ b/selfdrive/ui/layouts/home.py @@ -8,8 +8,6 @@ from openpilot.selfdrive.ui.widgets.prime import PrimeWidget from openpilot.selfdrive.ui.widgets.setup import SetupWidget from openpilot.system.ui.lib.text_measure import measure_text_cached -from openpilot.system.ui.widgets.label import gui_label -from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos from openpilot.system.ui.widgets.label import gui_label from openpilot.system.ui.widgets import Widget From 88c126b296839a930ce81ec642d5f7ffef31f9f3 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 3 Oct 2025 23:24:50 -0700 Subject: [PATCH 17/21] blank From c01e106a2c9d11b6241e9bafaff1cd6e53bb1f15 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 3 Oct 2025 23:25:19 -0700 Subject: [PATCH 18/21] clean up --- selfdrive/ui/ui_state.py | 2 +- system/ui/widgets/list_view.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/selfdrive/ui/ui_state.py b/selfdrive/ui/ui_state.py index 06a24ef80f57c8..9c45519b4bab40 100644 --- a/selfdrive/ui/ui_state.py +++ b/selfdrive/ui/ui_state.py @@ -158,7 +158,7 @@ def __init__(self): def reset_interactive_timeout(self, timeout: int = -1) -> None: if timeout == -1: - timeout = 10 if ui_state.ignition else 300 + timeout = 10 if ui_state.ignition else 30 self._interaction_time = time.monotonic() + timeout def add_interactive_timeout_callback(self, callback: Callable): diff --git a/system/ui/widgets/list_view.py b/system/ui/widgets/list_view.py index aa3f4711193f2c..509c49be35a51c 100644 --- a/system/ui/widgets/list_view.py +++ b/system/ui/widgets/list_view.py @@ -268,7 +268,6 @@ def __init__(self, title: str = "", icon: str | None = None, description: str | self.set_rect(rl.Rectangle(0, 0, ITEM_BASE_WIDTH, ITEM_BASE_HEIGHT)) self._font = gui_app.font(FontWeight.NORMAL) self._icon_texture = gui_app.texture(os.path.join("icons", self.icon), ICON_SIZE, ICON_SIZE) if self.icon else None - self._title_width = 0 self._html_renderer = HtmlRenderer(text="", text_size={ElementType.P: ITEM_DESC_FONT_SIZE}, text_color=ITEM_DESC_TEXT_COLOR) From 952a928387a5d25282332e423db364966800a1e9 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 4 Oct 2025 00:18:50 -0700 Subject: [PATCH 19/21] I really don't like this but shrug --- system/ui/widgets/html_render.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/system/ui/widgets/html_render.py b/system/ui/widgets/html_render.py index 2c3eeb37931153..aae0672d8bbe13 100644 --- a/system/ui/widgets/html_render.py +++ b/system/ui/widgets/html_render.py @@ -3,7 +3,7 @@ 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 @@ -176,8 +176,8 @@ def _render(self, rect: rl.Rectangle): 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 + 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: @@ -186,7 +186,7 @@ def _render(self, rect: rl.Rectangle): 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 @@ -210,7 +210,7 @@ def get_total_height(self, content_width: int) -> float: 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 * 1.2 * element.line_height total_height += element.margin_bottom From e242994f132761ef3cc8ad0bd2b581f5cace157f Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 4 Oct 2025 00:20:13 -0700 Subject: [PATCH 20/21] fix --- system/ui/widgets/html_render.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/ui/widgets/html_render.py b/system/ui/widgets/html_render.py index aae0672d8bbe13..91a1ccbbc432db 100644 --- a/system/ui/widgets/html_render.py +++ b/system/ui/widgets/html_render.py @@ -210,7 +210,7 @@ def get_total_height(self, content_width: int) -> float: wrapped_lines = wrap_text(font, element.content, element.font_size, int(usable_width)) for _ in wrapped_lines: - total_height += element.font_size * 1.2 * element.line_height + total_height += element.font_size * FONT_SCALE * element.line_height total_height += element.margin_bottom From 4dfe9b7d3ea745ea62e9149a36a0116a20f27e88 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 4 Oct 2025 00:27:23 -0700 Subject: [PATCH 21/21] fix experimental text --- selfdrive/ui/widgets/exp_mode_button.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/selfdrive/ui/widgets/exp_mode_button.py b/selfdrive/ui/widgets/exp_mode_button.py index 9618768957febb..6fe7b6843c603b 100644 --- a/selfdrive/ui/widgets/exp_mode_button.py +++ b/selfdrive/ui/widgets/exp_mode_button.py @@ -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 @@ def __init__(self): super().__init__() self.img_width = 80 - self.horizontal_padding = 50 + self.horizontal_padding = 30 self.button_height = 125 self.params = Params() @@ -51,7 +51,7 @@ def _render(self, rect): # 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)