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 |
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: |
class HomeLayout: |
||||||
def __init__(self): |
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): |
def render(self, rect: rl.Rectangle): |
||||||
gui_text_box( |
self._update_layout_rects(rect) |
||||||
rect, |
|
||||||
"Demo Home Layout", |
current_time = time.time() |
||||||
font_size=170, |
if current_time - self.last_refresh >= REFRESH_INTERVAL: |
||||||
color=rl.WHITE, |
self._refresh() |
||||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, |
self.last_refresh = current_time |
||||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE, |
|
||||||
|
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