mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-07-21 01:02:48 +08:00
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 5b511911d81242457bfb5fc808a9b9f35fe9f7a2. * 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
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user