diff --git a/selfdrive/ui/widgets/offroad_alerts.py b/selfdrive/ui/widgets/offroad_alerts.py index 586d90dc4b..8ff8f27bed 100644 --- a/selfdrive/ui/widgets/offroad_alerts.py +++ b/selfdrive/ui/widgets/offroad_alerts.py @@ -1,10 +1,11 @@ import pyray as rl +from enum import IntEnum 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.application import gui_app, FontWeight, MousePos +from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.wrap_text import wrap_text @@ -17,15 +18,16 @@ class AlertColors: LOW_SEVERITY = rl.Color(41, 41, 41, 255) BACKGROUND = rl.Color(57, 57, 57, 255) BUTTON = rl.WHITE + BUTTON_PRESSED = rl.Color(200, 200, 200, 255) BUTTON_TEXT = rl.BLACK SNOOZE_BG = rl.Color(79, 79, 79, 255) + SNOOZE_BG_PRESSED = rl.Color(100, 100, 100, 255) TEXT = rl.WHITE class AlertConstants: - BUTTON_SIZE = (400, 125) - SNOOZE_BUTTON_SIZE = (550, 125) - REBOOT_BUTTON_SIZE = (600, 125) + MIN_BUTTON_WIDTH = 400 + BUTTON_HEIGHT = 125 MARGIN = 50 SPACING = 30 FONT_SIZE = 48 @@ -43,6 +45,41 @@ class AlertData: visible: bool = False +class ButtonStyle(IntEnum): + LIGHT = 0 + DARK = 1 + + +class ActionButton(Widget): + def __init__(self, text: str, style: ButtonStyle = ButtonStyle.LIGHT, + min_width: int = AlertConstants.MIN_BUTTON_WIDTH): + super().__init__() + self._style = style + self._min_width = min_width + self._font = gui_app.font(FontWeight.MEDIUM) + self.set_text(text) + + def set_text(self, text: str): + self._text = text + self._text_width = measure_text_cached(gui_app.font(FontWeight.MEDIUM), self._text, AlertConstants.FONT_SIZE).x + self._rect.width = max(self._text_width + 60 * 2, self._min_width) + self._rect.height = AlertConstants.BUTTON_HEIGHT + + def _render(self, _): + roundness = AlertConstants.BORDER_RADIUS / self._rect.height + bg_color = AlertColors.BUTTON if self._style == ButtonStyle.LIGHT else AlertColors.SNOOZE_BG + if self.is_pressed: + bg_color = AlertColors.BUTTON_PRESSED if self._style == ButtonStyle.LIGHT else AlertColors.SNOOZE_BG_PRESSED + + rl.draw_rectangle_rounded(self._rect, roundness, 10, bg_color) + + # center text + color = rl.WHITE if self._style == ButtonStyle.DARK else rl.BLACK + text_x = int(self._rect.x + (self._rect.width - self._text_width) // 2) + text_y = int(self._rect.y + (self._rect.height - AlertConstants.FONT_SIZE) // 2) + rl.draw_text_ex(self._font, self._text, rl.Vector2(text_x, text_y), AlertConstants.FONT_SIZE, 0, color) + + class AbstractAlert(Widget, ABC): def __init__(self, has_reboot_btn: bool = False): super().__init__() @@ -50,17 +87,35 @@ class AbstractAlert(Widget, ABC): 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) + def snooze_callback(): + self.params.put_bool("SnoozeUpdate", True) + if self.dismiss_callback: + self.dismiss_callback() + + def excessive_actuation_callback(): + self.params.remove("Offroad_ExcessiveActuation") + if self.dismiss_callback: + self.dismiss_callback() + + self.dismiss_btn = ActionButton("Close") + + self.snooze_btn = ActionButton("Snooze Update", style=ButtonStyle.DARK) + self.snooze_btn.set_click_callback(snooze_callback) + + self.excessive_actuation_btn = ActionButton("Acknowledge Excessive Actuation", style=ButtonStyle.DARK, min_width=800) + self.excessive_actuation_btn.set_click_callback(excessive_actuation_callback) - self.snooze_visible = False + self.reboot_btn = ActionButton("Reboot and Update", min_width=600) + self.reboot_btn.set_click_callback(lambda: HARDWARE.reboot()) + + # TODO: just use a Scroller? 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 + self.dismiss_btn.set_click_callback(self.dismiss_callback) @abstractmethod def refresh(self) -> bool: @@ -70,28 +125,10 @@ class AbstractAlert(Widget, ABC): def get_content_height(self) -> float: pass - def _handle_mouse_release(self, mouse_pos: MousePos): - super()._handle_mouse_release(mouse_pos) - - if not self.scroll_panel.is_touch_valid(): - return - - if rl.check_collision_point_rec(mouse_pos, self.dismiss_btn_rect): - if self.dismiss_callback: - self.dismiss_callback() - - elif 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() - - elif self.has_reboot_btn and rl.check_collision_point_rec(mouse_pos, self.reboot_btn_rect): - HARDWARE.reboot() - def _render(self, rect: rl.Rectangle): rl.draw_rectangle_rounded(rect, AlertConstants.BORDER_RADIUS / rect.height, 10, AlertColors.BACKGROUND) - footer_height = AlertConstants.BUTTON_SIZE[1] + AlertConstants.SPACING + footer_height = AlertConstants.BUTTON_HEIGHT + AlertConstants.SPACING content_height = rect.height - 2 * AlertConstants.MARGIN - footer_height self.content_rect = rl.Rectangle( @@ -134,47 +171,26 @@ class AbstractAlert(Widget, ABC): 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 - roundness = AlertConstants.BORDER_RADIUS / self.dismiss_btn_rect.height - rl.draw_rectangle_rounded(self.dismiss_btn_rect, roundness, 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 - ) + footer_y = rect.y + rect.height - AlertConstants.MARGIN - AlertConstants.BUTTON_HEIGHT - 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 - roundness = AlertConstants.BORDER_RADIUS / self.snooze_btn_rect.height - rl.draw_rectangle_rounded(self.snooze_btn_rect, roundness, 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 - roundness = AlertConstants.BORDER_RADIUS / self.reboot_btn_rect.height - rl.draw_rectangle_rounded(self.reboot_btn_rect, roundness, 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 - ) + dismiss_x = rect.x + AlertConstants.MARGIN + self.dismiss_btn.set_position(dismiss_x, footer_y) + self.dismiss_btn.render() + + if self.has_reboot_btn: + reboot_x = rect.x + rect.width - AlertConstants.MARGIN - self.reboot_btn.rect.width + self.reboot_btn.set_position(reboot_x, footer_y) + self.reboot_btn.render() + + elif self.excessive_actuation_btn.is_visible: + actuation_x = rect.x + rect.width - AlertConstants.MARGIN - self.excessive_actuation_btn.rect.width + self.excessive_actuation_btn.set_position(actuation_x, footer_y) + self.excessive_actuation_btn.render() + + elif self.snooze_btn.is_visible: + snooze_x = rect.x + rect.width - AlertConstants.MARGIN - self.snooze_btn.rect.width + self.snooze_btn.set_position(snooze_x, footer_y) + self.snooze_btn.render() class OffroadAlert(AbstractAlert): @@ -188,6 +204,7 @@ class OffroadAlert(AbstractAlert): active_count = 0 connectivity_needed = False + excessive_actuation = False for alert_data in self.sorted_alerts: text = "" @@ -205,7 +222,11 @@ class OffroadAlert(AbstractAlert): if alert_data.key == "Offroad_ConnectivityNeeded" and alert_data.visible: connectivity_needed = True - self.snooze_visible = connectivity_needed + if alert_data.key == "Offroad_ExcessiveActuation" and alert_data.visible: + excessive_actuation = True + + self.excessive_actuation_btn.set_visible(excessive_actuation) + self.snooze_btn.set_visible(connectivity_needed and not excessive_actuation) return active_count def get_content_height(self) -> float: