ui: implement home layout with fully functional offroad alerts (#35468)
implement home layout with offroad alertspull/35474/head
parent
cb22be6079
commit
6b59f67ab5
2 changed files with 532 additions and 9 deletions
@ -1,17 +1,212 @@ |
||||
import time |
||||
import pyray as rl |
||||
from openpilot.system.ui.lib.label import gui_text_box |
||||
from collections.abc import Callable |
||||
from enum import IntEnum |
||||
from openpilot.common.params import Params |
||||
from openpilot.selfdrive.ui.widgets.offroad_alerts import UpdateAlert, OffroadAlert |
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached |
||||
from openpilot.system.ui.lib.label import gui_label |
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_COLOR |
||||
|
||||
|
||||
HEADER_HEIGHT = 80 |
||||
HEAD_BUTTON_FONT_SIZE = 40 |
||||
CONTENT_MARGIN = 40 |
||||
SPACING = 25 |
||||
RIGHT_COLUMN_WIDTH = 750 |
||||
REFRESH_INTERVAL = 10.0 |
||||
|
||||
PRIME_BG_COLOR = rl.Color(51, 51, 51, 255) |
||||
|
||||
|
||||
class HomeLayoutState(IntEnum): |
||||
HOME = 0 |
||||
UPDATE = 1 |
||||
ALERTS = 2 |
||||
|
||||
|
||||
class HomeLayout: |
||||
def __init__(self): |
||||
pass |
||||
self.params = Params() |
||||
|
||||
self.update_alert = UpdateAlert() |
||||
self.offroad_alert = OffroadAlert() |
||||
|
||||
self.current_state = HomeLayoutState.HOME |
||||
self.last_refresh = 0 |
||||
self.settings_callback: callable | None = None |
||||
|
||||
self.update_available = False |
||||
self.alert_count = 0 |
||||
|
||||
self.header_rect = rl.Rectangle(0, 0, 0, 0) |
||||
self.content_rect = rl.Rectangle(0, 0, 0, 0) |
||||
self.left_column_rect = rl.Rectangle(0, 0, 0, 0) |
||||
self.right_column_rect = rl.Rectangle(0, 0, 0, 0) |
||||
|
||||
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._setup_callbacks() |
||||
|
||||
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)) |
||||
|
||||
def set_settings_callback(self, callback: Callable): |
||||
self.settings_callback = callback |
||||
|
||||
def _set_state(self, state: HomeLayoutState): |
||||
self.current_state = state |
||||
|
||||
def render(self, rect: rl.Rectangle): |
||||
gui_text_box( |
||||
rect, |
||||
"Demo Home Layout", |
||||
font_size=170, |
||||
color=rl.WHITE, |
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, |
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE, |
||||
self._update_layout_rects(rect) |
||||
|
||||
current_time = time.time() |
||||
if current_time - self.last_refresh >= REFRESH_INTERVAL: |
||||
self._refresh() |
||||
self.last_refresh = current_time |
||||
|
||||
self._handle_input() |
||||
self._render_header() |
||||
|
||||
# Render content based on current state |
||||
if self.current_state == HomeLayoutState.HOME: |
||||
self._render_home_content() |
||||
elif self.current_state == HomeLayoutState.UPDATE: |
||||
self._render_update_view() |
||||
elif self.current_state == HomeLayoutState.ALERTS: |
||||
self._render_alerts_view() |
||||
|
||||
def _update_layout_rects(self, rect: rl.Rectangle): |
||||
self.header_rect = rl.Rectangle( |
||||
rect.x + CONTENT_MARGIN, rect.y + CONTENT_MARGIN, rect.width - 2 * CONTENT_MARGIN, HEADER_HEIGHT |
||||
) |
||||
|
||||
content_y = rect.y + CONTENT_MARGIN + HEADER_HEIGHT + SPACING |
||||
content_height = rect.height - CONTENT_MARGIN - HEADER_HEIGHT - SPACING - CONTENT_MARGIN |
||||
|
||||
self.content_rect = rl.Rectangle( |
||||
rect.x + CONTENT_MARGIN, content_y, rect.width - 2 * CONTENT_MARGIN, content_height |
||||
) |
||||
|
||||
left_width = self.content_rect.width - RIGHT_COLUMN_WIDTH - SPACING |
||||
|
||||
self.left_column_rect = rl.Rectangle(self.content_rect.x, self.content_rect.y, left_width, self.content_rect.height) |
||||
|
||||
self.right_column_rect = rl.Rectangle( |
||||
self.content_rect.x + left_width + SPACING, self.content_rect.y, RIGHT_COLUMN_WIDTH, self.content_rect.height |
||||
) |
||||
|
||||
self.update_notif_rect.x = self.header_rect.x |
||||
self.update_notif_rect.y = self.header_rect.y + (self.header_rect.height - 60) // 2 |
||||
|
||||
notif_x = self.header_rect.x + (220 if self.update_available else 0) |
||||
self.alert_notif_rect.x = notif_x |
||||
self.alert_notif_rect.y = self.header_rect.y + (self.header_rect.height - 60) // 2 |
||||
|
||||
def _handle_input(self): |
||||
if not rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT): |
||||
return |
||||
|
||||
mouse_pos = rl.get_mouse_position() |
||||
|
||||
if self.update_available and rl.check_collision_point_rec(mouse_pos, self.update_notif_rect): |
||||
self._set_state(HomeLayoutState.UPDATE) |
||||
return |
||||
|
||||
if self.alert_count > 0 and rl.check_collision_point_rec(mouse_pos, self.alert_notif_rect): |
||||
self._set_state(HomeLayoutState.ALERTS) |
||||
return |
||||
|
||||
# Content area input handling |
||||
if self.current_state == HomeLayoutState.UPDATE: |
||||
self.update_alert.handle_input(mouse_pos, True) |
||||
elif self.current_state == HomeLayoutState.ALERTS: |
||||
self.offroad_alert.handle_input(mouse_pos, True) |
||||
|
||||
def _render_header(self): |
||||
font = gui_app.font(FontWeight.MEDIUM) |
||||
|
||||
# Update notification button |
||||
if self.update_available: |
||||
# Highlight if currently viewing updates |
||||
highlight_color = rl.Color(255, 140, 40, 255) if self.current_state == HomeLayoutState.UPDATE else rl.Color(255, 102, 0, 255) |
||||
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 |
||||
rl.draw_text_ex(font, text, rl.Vector2(int(text_x), int(text_y)), HEAD_BUTTON_FONT_SIZE, 0, rl.WHITE) |
||||
|
||||
# Alert notification button |
||||
if self.alert_count > 0: |
||||
# 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) |
||||
|
||||
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 |
||||
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) |
||||
|
||||
def _render_home_content(self): |
||||
self._render_left_column() |
||||
self._render_right_column() |
||||
|
||||
def _render_update_view(self): |
||||
self.update_alert.render(self.content_rect) |
||||
|
||||
def _render_alerts_view(self): |
||||
self.offroad_alert.render(self.content_rect) |
||||
|
||||
def _render_left_column(self): |
||||
rl.draw_rectangle_rounded(self.left_column_rect, 0.02, 10, PRIME_BG_COLOR) |
||||
gui_label(self.left_column_rect, "Prime Widget", 48, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) |
||||
|
||||
def _render_right_column(self): |
||||
widget_height = (self.right_column_rect.height - SPACING) // 2 |
||||
|
||||
exp_rect = rl.Rectangle( |
||||
self.right_column_rect.x, self.right_column_rect.y, self.right_column_rect.width, widget_height |
||||
) |
||||
rl.draw_rectangle_rounded(exp_rect, 0.02, 10, PRIME_BG_COLOR) |
||||
gui_label(exp_rect, "Experimental Mode", 36, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) |
||||
|
||||
setup_rect = rl.Rectangle( |
||||
self.right_column_rect.x, |
||||
self.right_column_rect.y + widget_height + SPACING, |
||||
self.right_column_rect.width, |
||||
widget_height, |
||||
) |
||||
rl.draw_rectangle_rounded(setup_rect, 0.02, 10, PRIME_BG_COLOR) |
||||
gui_label(setup_rect, "Setup", 36, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) |
||||
|
||||
def _refresh(self): |
||||
self.update_available = self.update_alert.refresh() |
||||
self.alert_count = self.offroad_alert.refresh() |
||||
self._update_state_priority(self.update_available, self.alert_count > 0) |
||||
|
||||
def _update_state_priority(self, update_available: bool, alerts_present: bool): |
||||
current_state = self.current_state |
||||
|
||||
if not update_available and not alerts_present: |
||||
self.current_state = HomeLayoutState.HOME |
||||
elif update_available and (current_state == HomeLayoutState.HOME or (not alerts_present and current_state == HomeLayoutState.ALERTS)): |
||||
self.current_state = HomeLayoutState.UPDATE |
||||
elif alerts_present and (current_state == HomeLayoutState.HOME or (not update_available and current_state == HomeLayoutState.UPDATE)): |
||||
self.current_state = HomeLayoutState.ALERTS |
||||
|
||||
def _get_version_text(self) -> str: |
||||
brand = "openpilot" |
||||
description = self.params.get("UpdaterCurrentDescription", encoding='utf-8') |
||||
return f"{brand} {description}" if description else brand |
||||
|
@ -0,0 +1,328 @@ |
||||
import json |
||||
import pyray as rl |
||||
from abc import ABC, abstractmethod |
||||
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.scroll_panel import GuiScrollPanel |
||||
from openpilot.system.ui.lib.wrap_text import wrap_text |
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached |
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight |
||||
|
||||
class AlertColors: |
||||
HIGH_SEVERITY = rl.Color(226, 44, 44, 255) |
||||
LOW_SEVERITY = rl.Color(41, 41, 41, 255) |
||||
BACKGROUND = rl.Color(57, 57, 57, 255) |
||||
BUTTON = rl.WHITE |
||||
BUTTON_TEXT = rl.BLACK |
||||
SNOOZE_BG = rl.Color(79, 79, 79, 255) |
||||
TEXT = rl.WHITE |
||||
|
||||
|
||||
class AlertConstants: |
||||
BUTTON_SIZE = (400, 125) |
||||
SNOOZE_BUTTON_SIZE = (550, 125) |
||||
REBOOT_BUTTON_SIZE = (600, 125) |
||||
MARGIN = 50 |
||||
SPACING = 30 |
||||
FONT_SIZE = 48 |
||||
BORDER_RADIUS = 30 |
||||
ALERT_HEIGHT = 120 |
||||
ALERT_SPACING = 20 |
||||
|
||||
|
||||
@dataclass |
||||
class AlertData: |
||||
key: str |
||||
text: str |
||||
severity: int |
||||
visible: bool = False |
||||
|
||||
|
||||
class AbstractAlert(ABC): |
||||
def __init__(self, has_reboot_btn: bool = False): |
||||
self.params = Params() |
||||
self.has_reboot_btn = has_reboot_btn |
||||
self.dismiss_callback: Callable | None = None |
||||
|
||||
self.dismiss_btn_rect = rl.Rectangle(0, 0, *AlertConstants.BUTTON_SIZE) |
||||
self.snooze_btn_rect = rl.Rectangle(0, 0, *AlertConstants.SNOOZE_BUTTON_SIZE) |
||||
self.reboot_btn_rect = rl.Rectangle(0, 0, *AlertConstants.REBOOT_BUTTON_SIZE) |
||||
|
||||
self.snooze_visible = False |
||||
self.content_rect = rl.Rectangle(0, 0, 0, 0) |
||||
self.scroll_panel_rect = rl.Rectangle(0, 0, 0, 0) |
||||
self.scroll_panel = GuiScrollPanel() |
||||
|
||||
def set_dismiss_callback(self, callback: Callable): |
||||
self.dismiss_callback = callback |
||||
|
||||
@abstractmethod |
||||
def refresh(self) -> bool: |
||||
pass |
||||
|
||||
@abstractmethod |
||||
def get_content_height(self) -> float: |
||||
pass |
||||
|
||||
def handle_input(self, mouse_pos: rl.Vector2, mouse_clicked: bool) -> bool: |
||||
# TODO: fix scroll_panel.is_click_valid() |
||||
if not mouse_clicked: |
||||
return False |
||||
|
||||
if rl.check_collision_point_rec(mouse_pos, self.dismiss_btn_rect): |
||||
if self.dismiss_callback: |
||||
self.dismiss_callback() |
||||
return True |
||||
|
||||
if self.snooze_visible and rl.check_collision_point_rec(mouse_pos, self.snooze_btn_rect): |
||||
self.params.put_bool("SnoozeUpdate", True) |
||||
if self.dismiss_callback: |
||||
self.dismiss_callback() |
||||
return True |
||||
|
||||
if self.has_reboot_btn and rl.check_collision_point_rec(mouse_pos, self.reboot_btn_rect): |
||||
HARDWARE.reboot() |
||||
return True |
||||
|
||||
return False |
||||
|
||||
def render(self, rect: rl.Rectangle): |
||||
rl.draw_rectangle_rounded(rect, AlertConstants.BORDER_RADIUS / rect.width, 10, AlertColors.BACKGROUND) |
||||
|
||||
footer_height = AlertConstants.BUTTON_SIZE[1] + AlertConstants.SPACING |
||||
content_height = rect.height - 2 * AlertConstants.MARGIN - footer_height |
||||
|
||||
self.content_rect = rl.Rectangle( |
||||
rect.x + AlertConstants.MARGIN, |
||||
rect.y + AlertConstants.MARGIN, |
||||
rect.width - 2 * AlertConstants.MARGIN, |
||||
content_height, |
||||
) |
||||
self.scroll_panel_rect = rl.Rectangle( |
||||
self.content_rect.x, self.content_rect.y, self.content_rect.width, self.content_rect.height |
||||
) |
||||
|
||||
self._render_scrollable_content() |
||||
self._render_footer(rect) |
||||
|
||||
def _render_scrollable_content(self): |
||||
content_total_height = self.get_content_height() |
||||
content_bounds = rl.Rectangle(0, 0, self.scroll_panel_rect.width, content_total_height) |
||||
scroll_offset = self.scroll_panel.handle_scroll(self.scroll_panel_rect, content_bounds) |
||||
|
||||
rl.begin_scissor_mode( |
||||
int(self.scroll_panel_rect.x), |
||||
int(self.scroll_panel_rect.y), |
||||
int(self.scroll_panel_rect.width), |
||||
int(self.scroll_panel_rect.height), |
||||
) |
||||
|
||||
content_rect_with_scroll = rl.Rectangle( |
||||
self.scroll_panel_rect.x, |
||||
self.scroll_panel_rect.y + scroll_offset.y, |
||||
self.scroll_panel_rect.width, |
||||
content_total_height, |
||||
) |
||||
|
||||
self._render_content(content_rect_with_scroll) |
||||
rl.end_scissor_mode() |
||||
|
||||
@abstractmethod |
||||
def _render_content(self, content_rect: rl.Rectangle): |
||||
pass |
||||
|
||||
def _render_footer(self, rect: rl.Rectangle): |
||||
footer_y = rect.y + rect.height - AlertConstants.MARGIN - AlertConstants.BUTTON_SIZE[1] |
||||
font = gui_app.font(FontWeight.MEDIUM) |
||||
|
||||
self.dismiss_btn_rect.x = rect.x + AlertConstants.MARGIN |
||||
self.dismiss_btn_rect.y = footer_y |
||||
rl.draw_rectangle_rounded(self.dismiss_btn_rect, 0.3, 10, AlertColors.BUTTON) |
||||
|
||||
text = "Close" |
||||
text_width = measure_text_cached(font, text, AlertConstants.FONT_SIZE).x |
||||
text_x = self.dismiss_btn_rect.x + (AlertConstants.BUTTON_SIZE[0] - text_width) // 2 |
||||
text_y = self.dismiss_btn_rect.y + (AlertConstants.BUTTON_SIZE[1] - AlertConstants.FONT_SIZE) // 2 |
||||
rl.draw_text_ex( |
||||
font, text, rl.Vector2(int(text_x), int(text_y)), AlertConstants.FONT_SIZE, 0, AlertColors.BUTTON_TEXT |
||||
) |
||||
|
||||
if self.snooze_visible: |
||||
self.snooze_btn_rect.x = rect.x + rect.width - AlertConstants.MARGIN - AlertConstants.SNOOZE_BUTTON_SIZE[0] |
||||
self.snooze_btn_rect.y = footer_y |
||||
rl.draw_rectangle_rounded(self.snooze_btn_rect, 0.3, 10, AlertColors.SNOOZE_BG) |
||||
|
||||
text = "Snooze Update" |
||||
text_width = measure_text_cached(font, text, AlertConstants.FONT_SIZE).x |
||||
text_x = self.snooze_btn_rect.x + (AlertConstants.SNOOZE_BUTTON_SIZE[0] - text_width) // 2 |
||||
text_y = self.snooze_btn_rect.y + (AlertConstants.SNOOZE_BUTTON_SIZE[1] - AlertConstants.FONT_SIZE) // 2 |
||||
rl.draw_text_ex(font, text, rl.Vector2(int(text_x), int(text_y)), AlertConstants.FONT_SIZE, 0, AlertColors.TEXT) |
||||
|
||||
elif self.has_reboot_btn: |
||||
self.reboot_btn_rect.x = rect.x + rect.width - AlertConstants.MARGIN - AlertConstants.REBOOT_BUTTON_SIZE[0] |
||||
self.reboot_btn_rect.y = footer_y |
||||
rl.draw_rectangle_rounded(self.reboot_btn_rect, 0.3, 10, AlertColors.BUTTON) |
||||
|
||||
text = "Reboot and Update" |
||||
text_width = measure_text_cached(font, text, AlertConstants.FONT_SIZE).x |
||||
text_x = self.reboot_btn_rect.x + (AlertConstants.REBOOT_BUTTON_SIZE[0] - text_width) // 2 |
||||
text_y = self.reboot_btn_rect.y + (AlertConstants.REBOOT_BUTTON_SIZE[1] - AlertConstants.FONT_SIZE) // 2 |
||||
rl.draw_text_ex( |
||||
font, text, rl.Vector2(int(text_x), int(text_y)), AlertConstants.FONT_SIZE, 0, AlertColors.BUTTON_TEXT |
||||
) |
||||
|
||||
|
||||
class OffroadAlert(AbstractAlert): |
||||
def __init__(self): |
||||
super().__init__(has_reboot_btn=False) |
||||
self.sorted_alerts: list[AlertData] = [] |
||||
|
||||
def refresh(self): |
||||
if not self.sorted_alerts: |
||||
self._build_alerts() |
||||
|
||||
active_count = 0 |
||||
connectivity_needed = False |
||||
|
||||
for alert_data in self.sorted_alerts: |
||||
text = "" |
||||
bytes_data = self.params.get(alert_data.key) |
||||
|
||||
if bytes_data: |
||||
try: |
||||
alert_json = json.loads(bytes_data) |
||||
text = alert_json.get("text", "").replace("{}", alert_json.get("extra", "")) |
||||
except json.JSONDecodeError: |
||||
text = "" |
||||
|
||||
alert_data.text = text |
||||
alert_data.visible = bool(text) |
||||
|
||||
if alert_data.visible: |
||||
active_count += 1 |
||||
|
||||
if alert_data.key == "Offroad_ConnectivityNeeded" and alert_data.visible: |
||||
connectivity_needed = True |
||||
|
||||
self.snooze_visible = connectivity_needed |
||||
return active_count |
||||
|
||||
def get_content_height(self) -> float: |
||||
if not self.sorted_alerts: |
||||
return 0 |
||||
|
||||
total_height = 20 |
||||
font = gui_app.font(FontWeight.NORMAL) |
||||
|
||||
for alert_data in self.sorted_alerts: |
||||
if not alert_data.visible: |
||||
continue |
||||
|
||||
text_width = int(self.content_rect.width - 90) |
||||
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) |
||||
alert_item_height = max(text_height + 40, AlertConstants.ALERT_HEIGHT) |
||||
total_height += alert_item_height + AlertConstants.ALERT_SPACING |
||||
|
||||
if total_height > 20: |
||||
total_height = total_height - AlertConstants.ALERT_SPACING + 20 |
||||
|
||||
return total_height |
||||
|
||||
def _build_alerts(self): |
||||
self.sorted_alerts = [] |
||||
try: |
||||
with open("../selfdrived/alerts_offroad.json", "rb") as f: |
||||
alerts_config = json.load(f) |
||||
for key, config in sorted(alerts_config.items(), key=lambda x: x[1].get("severity", 0), reverse=True): |
||||
severity = config.get("severity", 0) |
||||
alert_data = AlertData(key=key, text="", severity=severity) |
||||
self.sorted_alerts.append(alert_data) |
||||
except (FileNotFoundError, json.JSONDecodeError): |
||||
pass |
||||
|
||||
def _render_content(self, content_rect: rl.Rectangle): |
||||
y_offset = 20 |
||||
font = gui_app.font(FontWeight.NORMAL) |
||||
|
||||
for alert_data in self.sorted_alerts: |
||||
if not alert_data.visible: |
||||
continue |
||||
|
||||
bg_color = AlertColors.HIGH_SEVERITY if alert_data.severity > 0 else AlertColors.LOW_SEVERITY |
||||
text_width = int(content_rect.width - 90) |
||||
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) |
||||
alert_item_height = max(text_height + 40, AlertConstants.ALERT_HEIGHT) |
||||
|
||||
alert_rect = rl.Rectangle( |
||||
content_rect.x + 10, |
||||
content_rect.y + y_offset, |
||||
content_rect.width - 30, |
||||
alert_item_height, |
||||
) |
||||
|
||||
rl.draw_rectangle_rounded(alert_rect, 0.2, 10, bg_color) |
||||
|
||||
text_x = alert_rect.x + 30 |
||||
text_y = alert_rect.y + 20 |
||||
|
||||
for i, line in enumerate(wrapped_lines): |
||||
rl.draw_text_ex( |
||||
font, |
||||
line, |
||||
rl.Vector2(text_x, text_y + i * (AlertConstants.FONT_SIZE + 5)), |
||||
AlertConstants.FONT_SIZE, |
||||
0, |
||||
AlertColors.TEXT, |
||||
) |
||||
|
||||
y_offset += alert_item_height + AlertConstants.ALERT_SPACING |
||||
|
||||
|
||||
class UpdateAlert(AbstractAlert): |
||||
def __init__(self): |
||||
super().__init__(has_reboot_btn=True) |
||||
self.release_notes = "" |
||||
self._wrapped_release_notes = "" |
||||
self._cached_content_height: float = 0.0 |
||||
|
||||
def refresh(self) -> bool: |
||||
update_available: bool = self.params.get_bool("UpdateAvailable") |
||||
if update_available: |
||||
self.release_notes = self.params.get("UpdaterNewReleaseNotes", encoding='utf-8') |
||||
self._cached_content_height = 0 |
||||
|
||||
return update_available |
||||
|
||||
def get_content_height(self) -> float: |
||||
if not self.release_notes: |
||||
return 100 |
||||
|
||||
if self._cached_content_height == 0: |
||||
self._wrapped_release_notes = self.release_notes |
||||
size = measure_text_cached(gui_app.font(FontWeight.NORMAL), self._wrapped_release_notes, AlertConstants.FONT_SIZE) |
||||
self._cached_content_height = max(size.y + 60, 100) |
||||
|
||||
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(no_notes_text, int(text_x), int(text_y), AlertConstants.FONT_SIZE, AlertColors.TEXT) |
Loading…
Reference in new issue