raylib: release notes are drawn with HTML renderer (#36245)

* stash

* ok chatter is useful for once

* draw text outside tags

* hmm

* undo that shit

* i don't like this chatgpt

* Revert "i don't like this chatgpt"

This reverts commit 5b511911d8.

* more robust parsing (works with missing tags, markdown.py actually had bug) + add indent level

* the html looks weird but is correct - the old parser didn't handle it

* clean up

* some

* move out

* clean up

* oh this was wrong

* draft

* rm that

* fix

* fix indentation for new driving model

* clean up

* some clean up

* more clean up

* more clean up

* and this

* cmt

* ok this is egregious mypy
pull/36260/head
Shane Smiskol 3 days ago committed by GitHub
parent bd9888a439
commit 2337704602
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      selfdrive/ui/layouts/settings/software.py
  2. 27
      selfdrive/ui/widgets/offroad_alerts.py
  3. 2
      system/ui/lib/application.py
  4. 8
      system/ui/widgets/html_render.py
  5. 75
      system/ui/widgets/list_view.py

@ -80,7 +80,7 @@ class SoftwareLayout(Widget):
current_desc = ui_state.params.get("UpdaterCurrentDescription") or ""
current_release_notes = (ui_state.params.get("UpdaterCurrentReleaseNotes") or b"").decode("utf-8", "replace")
self._version_item.action_item.set_text(current_desc)
self._version_item.description = current_release_notes
self._version_item.set_description(current_release_notes)
# Update download button visibility and state
self._download_btn.set_visible(ui_state.is_offroad())
@ -125,7 +125,7 @@ class SoftwareLayout(Widget):
new_release_notes = (ui_state.params.get("UpdaterNewReleaseNotes") or b"").decode("utf-8", "replace")
self._install_btn.action_item.set_text("INSTALL")
self._install_btn.action_item.set_value(new_desc)
self._install_btn.description = new_release_notes
self._install_btn.set_description(new_release_notes)
# Enable install button for testing (like Qt showEvent)
self._install_btn.action_item.set_enabled(True)
else:

@ -14,6 +14,9 @@ from openpilot.system.ui.widgets.html_render import HtmlRenderer
from openpilot.selfdrive.selfdrived.alertmanager import OFFROAD_ALERTS
NO_RELEASE_NOTES = "<h2>No release notes available.</h2>"
class AlertColors:
HIGH_SEVERITY = rl.Color(226, 44, 44, 255)
LOW_SEVERITY = rl.Color(41, 41, 41, 255)
@ -307,13 +310,16 @@ class UpdateAlert(AbstractAlert):
self.release_notes = ""
self._wrapped_release_notes = ""
self._cached_content_height: float = 0.0
self._html_renderer: HtmlRenderer | None = None
self._html_renderer = HtmlRenderer(text="")
def refresh(self) -> bool:
update_available: bool = self.params.get_bool("UpdateAvailable")
if update_available:
self.release_notes = self.params.get("UpdaterNewReleaseNotes")
self.release_notes = (self.params.get("UpdaterNewReleaseNotes") or b"").decode("utf8").strip()
self._html_renderer.parse_html_content(self.release_notes or NO_RELEASE_NOTES)
self._cached_content_height = 0
else:
self._html_renderer.parse_html_content(NO_RELEASE_NOTES)
return update_available
@ -329,18 +335,5 @@ class UpdateAlert(AbstractAlert):
return self._cached_content_height
def _render_content(self, content_rect: rl.Rectangle):
if self.release_notes:
rl.draw_text_ex(
gui_app.font(FontWeight.NORMAL),
self._wrapped_release_notes,
rl.Vector2(content_rect.x + 30, content_rect.y + 30),
AlertConstants.FONT_SIZE,
0.0,
AlertColors.TEXT,
)
else:
no_notes_text = "No release notes available."
text_width = rl.measure_text(no_notes_text, AlertConstants.FONT_SIZE)
text_x = content_rect.x + (content_rect.width - text_width) // 2
text_y = content_rect.y + 50
rl.draw_text_ex(gui_app.font(FontWeight.NORMAL), no_notes_text, (int(text_x), int(text_y)), AlertConstants.FONT_SIZE, 0, AlertColors.TEXT)
notes_rect = rl.Rectangle(content_rect.x + 30, content_rect.y + 30, content_rect.width - 60, content_rect.height - 60)
self._html_renderer.render(notes_rect)

@ -331,7 +331,7 @@ class GuiApplication:
for layout in KEYBOARD_LAYOUTS.values():
all_chars.update(key for row in layout for key in row)
all_chars = "".join(all_chars)
all_chars += "–✓×°§"
all_chars += "–✓×°§"
codepoint_count = rl.ffi.new("int *", 1)
codepoints = rl.load_codepoints(all_chars, codepoint_count)

@ -70,6 +70,7 @@ class HtmlRenderer(Widget):
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.BR: {"size": 0, "weight": FontWeight.NORMAL, "margin_top": 0, "margin_bottom": 12},
}
@ -122,9 +123,6 @@ class HtmlRenderer(Widget):
if tag == ElementType.BR:
self._add_element(ElementType.BR, "")
elif tag == ElementType.UL:
self._indent_level = self._indent_level + 1 if is_start_tag else max(0, self._indent_level - 1)
elif is_start_tag or is_end_tag:
# Always add content regardless of opening or closing tag
close_tag()
@ -133,6 +131,10 @@ class HtmlRenderer(Widget):
if is_start_tag:
current_tag = tag
# increment after we add the content for the current tag
if tag == ElementType.UL:
self._indent_level = self._indent_level + 1 if is_start_tag else max(0, self._indent_level - 1)
else:
current_content.append(token)

@ -4,10 +4,10 @@ from collections.abc import Callable
from abc import ABC
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
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.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.html_render import HtmlRenderer, ElementType
ITEM_BASE_WIDTH = 600
ITEM_BASE_HEIGHT = 170
@ -258,7 +258,7 @@ class ListItem(Widget):
super().__init__()
self.title = title
self.icon = icon
self.description = description
self._description = description
self.description_visible = description_visible
self.callback = callback
self.action_item = action_item
@ -267,11 +267,12 @@ class ListItem(Widget):
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._html_renderer = HtmlRenderer(text="", text_size={ElementType.P: ITEM_DESC_FONT_SIZE},
text_color=ITEM_DESC_TEXT_COLOR)
self.set_description(self.description)
# Cached properties for performance
self._prev_max_width: int = 0
self._wrapped_description: str | None = None
self._prev_description: str | None = None
self._description_height: float = 0
self._prev_description: str | None = self.description
def set_touch_valid_callback(self, touch_callback: Callable[[], bool]) -> None:
super().set_touch_valid_callback(touch_callback)
@ -295,9 +296,15 @@ class ListItem(Widget):
if self.description:
self.description_visible = not self.description_visible
content_width = self.get_content_width(int(self._rect.width - ITEM_PADDING * 2))
content_width = int(self._rect.width - ITEM_PADDING * 2)
self._rect.height = self.get_item_height(self._font, content_width)
def _update_state(self):
# Detect changes if description is callback
new_description = self.description
if new_description != self._prev_description:
self.set_description(new_description)
def _render(self, _):
if not self.is_visible:
return
@ -323,16 +330,16 @@ class ListItem(Widget):
rl.draw_text_ex(self._font, self.title, rl.Vector2(text_x, item_y), ITEM_TEXT_FONT_SIZE, 0, ITEM_TEXT_COLOR)
# Draw description if visible
current_description = self.get_description()
if self.description_visible and current_description and self._wrapped_description:
rl.draw_text_ex(
self._font,
self._wrapped_description,
rl.Vector2(text_x, self._rect.y + ITEM_DESC_V_OFFSET),
ITEM_DESC_FONT_SIZE,
0,
ITEM_DESC_TEXT_COLOR,
if self.description_visible:
content_width = int(self._rect.width - ITEM_PADDING * 2)
description_height = self._html_renderer.get_total_height(content_width)
description_rect = rl.Rectangle(
self._rect.x + ITEM_PADDING,
self._rect.y + ITEM_DESC_V_OFFSET,
content_width,
description_height
)
self._html_renderer.render(description_rect)
# Draw right item if present
if self.action_item:
@ -343,33 +350,25 @@ class ListItem(Widget):
if self.callback:
self.callback()
def get_description(self):
return _resolve_value(self.description, None)
def set_description(self, description: str | Callable[[], str] | None):
self._description = description
new_desc = self.description
self._html_renderer.parse_html_content(new_desc)
self._prev_description = new_desc
@property
def description(self):
return _resolve_value(self._description, "")
def get_item_height(self, font: rl.Font, max_width: int) -> float:
if not self.is_visible:
return 0
current_description = self.get_description()
if self.description_visible and current_description:
if (
not self._wrapped_description
or current_description != self._prev_description
or max_width != self._prev_max_width
):
self._prev_max_width = max_width
self._prev_description = current_description
wrapped_lines = wrap_text(font, current_description, ITEM_DESC_FONT_SIZE, max_width)
self._wrapped_description = "\n".join(wrapped_lines)
self._description_height = len(wrapped_lines) * ITEM_DESC_FONT_SIZE + 10
return ITEM_BASE_HEIGHT + self._description_height - (ITEM_BASE_HEIGHT - ITEM_DESC_V_OFFSET) + ITEM_PADDING
return ITEM_BASE_HEIGHT
def get_content_width(self, total_width: int) -> int:
if self.action_item and self.action_item.rect.width > 0:
return total_width - int(self.action_item.rect.width) - RIGHT_ITEM_PADDING
return total_width
height = float(ITEM_BASE_HEIGHT)
if self.description_visible:
description_height = self._html_renderer.get_total_height(max_width)
height += description_height - (ITEM_BASE_HEIGHT - ITEM_DESC_V_OFFSET) + ITEM_PADDING
return height
def get_right_item_rect(self, item_rect: rl.Rectangle) -> rl.Rectangle:
if not self.action_item:

Loading…
Cancel
Save