raylib: remove gui_button (#36229)

* vibing can be good

* and listview

* rm that

* html render

* text.py

* ssh keys

* updater w/ Auto

* wow gpt5 actually is better

* well this is better

* huh wifi still doesn't work

* lfg

* lint

* manager waits for exit

* wait a minute this changes nothing

* this will work

* whoops

* clean up html

* actually useless

* clean up option

* typing

* bump
pull/36232/head
Shane Smiskol 2 days ago committed by GitHub
parent 9493f2a0eb
commit eadab06f59
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      selfdrive/ui/layouts/settings/device.py
  2. 26
      selfdrive/ui/widgets/ssh_key.py
  3. 19
      system/ui/text.py
  4. 30
      system/ui/updater.py
  5. 116
      system/ui/widgets/button.py
  6. 10
      system/ui/widgets/html_render.py
  7. 26
      system/ui/widgets/list_view.py
  8. 50
      system/ui/widgets/option_dialog.py

@ -141,7 +141,7 @@ class DeviceLayout(Widget):
def _on_regulatory(self): def _on_regulatory(self):
if not self._fcc_dialog: if not self._fcc_dialog:
self._fcc_dialog = HtmlRenderer(os.path.join(BASEDIR, "selfdrive/assets/offroad/fcc.html")) self._fcc_dialog = HtmlRenderer(os.path.join(BASEDIR, "selfdrive/assets/offroad/fcc.html"))
gui_app.set_modal_overlay(self._fcc_dialog, callback=lambda result: setattr(self, '_fcc_dialog', None)) gui_app.set_modal_overlay(self._fcc_dialog)
def _on_review_training_guide(self): def _on_review_training_guide(self):
if not self._training_guide: if not self._training_guide:

@ -2,13 +2,14 @@ import pyray as rl
import requests import requests
import threading import threading
import copy import copy
from collections.abc import Callable
from enum import Enum from enum import Enum
from openpilot.common.params import Params from openpilot.common.params import Params
from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.widgets import DialogResult from openpilot.system.ui.widgets import DialogResult
from openpilot.system.ui.widgets.button import gui_button, ButtonStyle from openpilot.system.ui.widgets.button import Button, ButtonStyle
from openpilot.system.ui.widgets.confirm_dialog import alert_dialog from openpilot.system.ui.widgets.confirm_dialog import alert_dialog
from openpilot.system.ui.widgets.keyboard import Keyboard from openpilot.system.ui.widgets.keyboard import Keyboard
from openpilot.system.ui.widgets.list_view import ( from openpilot.system.ui.widgets.list_view import (
@ -38,9 +39,15 @@ class SshKeyAction(ItemAction):
self._params = Params() self._params = Params()
self._error_message: str = "" self._error_message: str = ""
self._text_font = gui_app.font(FontWeight.MEDIUM) self._text_font = gui_app.font(FontWeight.MEDIUM)
self._button = Button("", click_callback=self._handle_button_click, button_style=ButtonStyle.LIST_ACTION,
border_radius=BUTTON_BORDER_RADIUS, font_size=BUTTON_FONT_SIZE)
self._refresh_state() self._refresh_state()
def set_touch_valid_callback(self, touch_callback: Callable[[], bool]) -> None:
super().set_touch_valid_callback(touch_callback)
self._button.set_touch_valid_callback(touch_callback)
def _refresh_state(self): def _refresh_state(self):
self._username = self._params.get("GithubUsername") self._username = self._params.get("GithubUsername")
self._state = SshKeyActionState.REMOVE if self._params.get("GithubSshKeys") else SshKeyActionState.ADD self._state = SshKeyActionState.REMOVE if self._params.get("GithubSshKeys") else SshKeyActionState.ADD
@ -66,18 +73,11 @@ class SshKeyAction(ItemAction):
) )
# Draw button # Draw button
if gui_button( button_rect = rl.Rectangle(rect.x + rect.width - BUTTON_WIDTH, rect.y + (rect.height - BUTTON_HEIGHT) / 2, BUTTON_WIDTH, BUTTON_HEIGHT)
rl.Rectangle( self._button.set_rect(button_rect)
rect.x + rect.width - BUTTON_WIDTH, rect.y + (rect.height - BUTTON_HEIGHT) / 2, BUTTON_WIDTH, BUTTON_HEIGHT self._button.set_text(self._state.value)
), self._button.set_enabled(self._state != SshKeyActionState.LOADING)
self._state.value, self._button.render(button_rect)
is_enabled=self._state != SshKeyActionState.LOADING,
border_radius=BUTTON_BORDER_RADIUS,
font_size=BUTTON_FONT_SIZE,
button_style=ButtonStyle.LIST_ACTION,
):
self._handle_button_click()
return True
return False return False
def _handle_button_click(self): def _handle_button_click(self):

@ -7,7 +7,7 @@ from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel 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.text_measure import measure_text_cached
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.button import gui_button, ButtonStyle from openpilot.system.ui.widgets.button import Button, ButtonStyle
MARGIN = 50 MARGIN = 50
SPACING = 40 SPACING = 40
@ -56,6 +56,15 @@ class TextWindow(Widget):
self._scroll_panel = GuiScrollPanel() self._scroll_panel = GuiScrollPanel()
self._scroll_panel._offset_filter_y.x = -max(self._content_rect.height - self._textarea_rect.height, 0) self._scroll_panel._offset_filter_y.x = -max(self._content_rect.height - self._textarea_rect.height, 0)
button_text = "Exit" if PC else "Reboot"
self._button = Button(button_text, click_callback=self._on_button_clicked, button_style=ButtonStyle.TRANSPARENT_WHITE_BORDER)
@staticmethod
def _on_button_clicked():
gui_app.request_close()
if not PC:
HARDWARE.reboot()
def _render(self, rect: rl.Rectangle): def _render(self, rect: rl.Rectangle):
scroll = self._scroll_panel.update(self._textarea_rect, self._content_rect) scroll = self._scroll_panel.update(self._textarea_rect, self._content_rect)
rl.begin_scissor_mode(int(self._textarea_rect.x), int(self._textarea_rect.y), int(self._textarea_rect.width), int(self._textarea_rect.height)) rl.begin_scissor_mode(int(self._textarea_rect.x), int(self._textarea_rect.y), int(self._textarea_rect.width), int(self._textarea_rect.height))
@ -67,13 +76,7 @@ class TextWindow(Widget):
rl.end_scissor_mode() rl.end_scissor_mode()
button_bounds = rl.Rectangle(rect.width - MARGIN - BUTTON_SIZE.x - SPACING, rect.height - MARGIN - BUTTON_SIZE.y, BUTTON_SIZE.x, BUTTON_SIZE.y) button_bounds = rl.Rectangle(rect.width - MARGIN - BUTTON_SIZE.x - SPACING, rect.height - MARGIN - BUTTON_SIZE.y, BUTTON_SIZE.x, BUTTON_SIZE.y)
ret = gui_button(button_bounds, "Exit" if PC else "Reboot", button_style=ButtonStyle.TRANSPARENT) self._button.render(button_bounds)
if ret:
if PC:
gui_app.request_close()
else:
HARDWARE.reboot()
return ret
if __name__ == "__main__": if __name__ == "__main__":

@ -9,7 +9,7 @@ from openpilot.system.hardware import HARDWARE
from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.wifi_manager import WifiManager from openpilot.system.ui.lib.wifi_manager import WifiManager
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.button import gui_button, ButtonStyle from openpilot.system.ui.widgets.button import Button, ButtonStyle
from openpilot.system.ui.widgets.label import gui_text_box, gui_label from openpilot.system.ui.widgets.label import gui_text_box, gui_label
from openpilot.system.ui.widgets.network import WifiManagerUI from openpilot.system.ui.widgets.network import WifiManagerUI
@ -45,8 +45,17 @@ class Updater(Widget):
self.update_thread = None self.update_thread = None
self.wifi_manager_ui = WifiManagerUI(WifiManager()) self.wifi_manager_ui = WifiManagerUI(WifiManager())
# Buttons
self._wifi_button = Button("Connect to Wi-Fi", click_callback=lambda: self.set_current_screen(Screen.WIFI))
self._install_button = Button("Install", click_callback=self.install_update, button_style=ButtonStyle.PRIMARY)
self._back_button = Button("Back", click_callback=lambda: self.set_current_screen(Screen.PROMPT))
self._reboot_button = Button("Reboot", click_callback=lambda: HARDWARE.reboot())
def set_current_screen(self, screen: Screen):
self.current_screen = screen
def install_update(self): def install_update(self):
self.current_screen = Screen.PROGRESS self.set_current_screen(Screen.PROGRESS)
self.progress_value = 0 self.progress_value = 0
self.progress_text = "Downloading..." self.progress_text = "Downloading..."
self.show_reboot_button = False self.show_reboot_button = False
@ -96,15 +105,11 @@ class Updater(Widget):
# WiFi button # WiFi button
wifi_button_rect = rl.Rectangle(MARGIN, button_y, button_width, BUTTON_HEIGHT) wifi_button_rect = rl.Rectangle(MARGIN, button_y, button_width, BUTTON_HEIGHT)
if gui_button(wifi_button_rect, "Connect to Wi-Fi"): self._wifi_button.render(wifi_button_rect)
self.current_screen = Screen.WIFI
return # Return to avoid processing other buttons after screen change
# Install button # Install button
install_button_rect = rl.Rectangle(MARGIN * 2 + button_width, button_y, button_width, BUTTON_HEIGHT) install_button_rect = rl.Rectangle(MARGIN * 2 + button_width, button_y, button_width, BUTTON_HEIGHT)
if gui_button(install_button_rect, "Install", button_style=ButtonStyle.PRIMARY): self._install_button.render(install_button_rect)
self.install_update()
return # Return to avoid further processing after action
def render_wifi_screen(self, rect: rl.Rectangle): def render_wifi_screen(self, rect: rl.Rectangle):
# Draw the Wi-Fi manager UI # Draw the Wi-Fi manager UI
@ -112,9 +117,7 @@ class Updater(Widget):
self.wifi_manager_ui.render(wifi_rect) self.wifi_manager_ui.render(wifi_rect)
back_button_rect = rl.Rectangle(MARGIN, rect.height - MARGIN - BUTTON_HEIGHT, BUTTON_WIDTH, BUTTON_HEIGHT) back_button_rect = rl.Rectangle(MARGIN, rect.height - MARGIN - BUTTON_HEIGHT, BUTTON_WIDTH, BUTTON_HEIGHT)
if gui_button(back_button_rect, "Back"): self._back_button.render(back_button_rect)
self.current_screen = Screen.PROMPT
return # Return to avoid processing other interactions after screen change
def render_progress_screen(self, rect: rl.Rectangle): def render_progress_screen(self, rect: rl.Rectangle):
title_rect = rl.Rectangle(MARGIN + 100, 330, rect.width - MARGIN * 2 - 200, 100) title_rect = rl.Rectangle(MARGIN + 100, 330, rect.width - MARGIN * 2 - 200, 100)
@ -133,10 +136,7 @@ class Updater(Widget):
# Show reboot button if needed # Show reboot button if needed
if self.show_reboot_button: if self.show_reboot_button:
reboot_rect = rl.Rectangle(MARGIN + 100, rect.height - MARGIN - BUTTON_HEIGHT, BUTTON_WIDTH, BUTTON_HEIGHT) reboot_rect = rl.Rectangle(MARGIN + 100, rect.height - MARGIN - BUTTON_HEIGHT, BUTTON_WIDTH, BUTTON_HEIGHT)
if gui_button(reboot_rect, "Reboot"): self._reboot_button.render(reboot_rect)
# Return True to signal main loop to exit before rebooting
HARDWARE.reboot()
return
def _render(self, rect: rl.Rectangle): def _render(self, rect: rl.Rectangle):
if self.current_screen == Screen.PROMPT: if self.current_screen == Screen.PROMPT:

@ -3,8 +3,7 @@ from enum import IntEnum
import pyray as rl import pyray as rl
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos from openpilot.system.ui.lib.application import FontWeight, MousePos
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.label import TextAlignment, Label from openpilot.system.ui.widgets.label import TextAlignment, Label
@ -14,7 +13,8 @@ class ButtonStyle(IntEnum):
PRIMARY = 1 # For main actions PRIMARY = 1 # For main actions
DANGER = 2 # For critical actions, like reboot or delete DANGER = 2 # For critical actions, like reboot or delete
TRANSPARENT = 3 # For buttons with transparent background and border TRANSPARENT = 3 # For buttons with transparent background and border
TRANSPARENT_WHITE_TEXT = 3 # For buttons with transparent background and border and white text TRANSPARENT_WHITE_TEXT = 9 # For buttons with transparent background and border and white text
TRANSPARENT_WHITE_BORDER = 10 # For buttons with transparent background and white border and text
ACTION = 4 ACTION = 4
LIST_ACTION = 5 # For list items with action buttons LIST_ACTION = 5 # For list items with action buttons
NO_EFFECT = 6 NO_EFFECT = 6
@ -32,6 +32,7 @@ BUTTON_TEXT_COLOR = {
ButtonStyle.DANGER: rl.Color(228, 228, 228, 255), ButtonStyle.DANGER: rl.Color(228, 228, 228, 255),
ButtonStyle.TRANSPARENT: rl.BLACK, ButtonStyle.TRANSPARENT: rl.BLACK,
ButtonStyle.TRANSPARENT_WHITE_TEXT: rl.WHITE, ButtonStyle.TRANSPARENT_WHITE_TEXT: rl.WHITE,
ButtonStyle.TRANSPARENT_WHITE_BORDER: rl.Color(228, 228, 228, 255),
ButtonStyle.ACTION: rl.BLACK, ButtonStyle.ACTION: rl.BLACK,
ButtonStyle.LIST_ACTION: rl.Color(228, 228, 228, 255), ButtonStyle.LIST_ACTION: rl.Color(228, 228, 228, 255),
ButtonStyle.NO_EFFECT: rl.Color(228, 228, 228, 255), ButtonStyle.NO_EFFECT: rl.Color(228, 228, 228, 255),
@ -49,6 +50,7 @@ BUTTON_BACKGROUND_COLORS = {
ButtonStyle.DANGER: rl.Color(255, 36, 36, 255), ButtonStyle.DANGER: rl.Color(255, 36, 36, 255),
ButtonStyle.TRANSPARENT: rl.BLACK, ButtonStyle.TRANSPARENT: rl.BLACK,
ButtonStyle.TRANSPARENT_WHITE_TEXT: rl.BLANK, ButtonStyle.TRANSPARENT_WHITE_TEXT: rl.BLANK,
ButtonStyle.TRANSPARENT_WHITE_BORDER: rl.BLACK,
ButtonStyle.ACTION: rl.Color(189, 189, 189, 255), ButtonStyle.ACTION: rl.Color(189, 189, 189, 255),
ButtonStyle.LIST_ACTION: rl.Color(57, 57, 57, 255), ButtonStyle.LIST_ACTION: rl.Color(57, 57, 57, 255),
ButtonStyle.NO_EFFECT: rl.Color(51, 51, 51, 255), ButtonStyle.NO_EFFECT: rl.Color(51, 51, 51, 255),
@ -62,6 +64,7 @@ BUTTON_PRESSED_BACKGROUND_COLORS = {
ButtonStyle.DANGER: rl.Color(255, 36, 36, 255), ButtonStyle.DANGER: rl.Color(255, 36, 36, 255),
ButtonStyle.TRANSPARENT: rl.BLACK, ButtonStyle.TRANSPARENT: rl.BLACK,
ButtonStyle.TRANSPARENT_WHITE_TEXT: rl.BLANK, ButtonStyle.TRANSPARENT_WHITE_TEXT: rl.BLANK,
ButtonStyle.TRANSPARENT_WHITE_BORDER: rl.BLANK,
ButtonStyle.ACTION: rl.Color(130, 130, 130, 255), ButtonStyle.ACTION: rl.Color(130, 130, 130, 255),
ButtonStyle.LIST_ACTION: rl.Color(74, 74, 74, 74), ButtonStyle.LIST_ACTION: rl.Color(74, 74, 74, 74),
ButtonStyle.NO_EFFECT: rl.Color(51, 51, 51, 255), ButtonStyle.NO_EFFECT: rl.Color(51, 51, 51, 255),
@ -73,104 +76,6 @@ BUTTON_DISABLED_BACKGROUND_COLORS = {
ButtonStyle.TRANSPARENT_WHITE_TEXT: rl.BLANK, ButtonStyle.TRANSPARENT_WHITE_TEXT: rl.BLANK,
} }
_pressed_buttons: set[str] = set() # Track mouse press state globally
# TODO: This should be a Widget class
def gui_button(
rect: rl.Rectangle,
text: str,
font_size: int = DEFAULT_BUTTON_FONT_SIZE,
font_weight: FontWeight = FontWeight.MEDIUM,
button_style: ButtonStyle = ButtonStyle.NORMAL,
is_enabled: bool = True,
border_radius: int = 10, # Corner rounding in pixels
text_alignment: TextAlignment = TextAlignment.CENTER,
text_padding: int = 20, # Padding for left/right alignment
icon=None,
) -> int:
button_id = f"{rect.x}_{rect.y}_{rect.width}_{rect.height}"
result = 0
if button_style in (ButtonStyle.PRIMARY, ButtonStyle.DANGER) and not is_enabled:
button_style = ButtonStyle.NORMAL
if button_style == ButtonStyle.ACTION and font_size == DEFAULT_BUTTON_FONT_SIZE:
font_size = ACTION_BUTTON_FONT_SIZE
# Set background color based on button type
bg_color = BUTTON_BACKGROUND_COLORS[button_style]
mouse_over = is_enabled and rl.check_collision_point_rec(rl.get_mouse_position(), rect)
is_pressed = button_id in _pressed_buttons
if mouse_over:
if rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT):
# Only this button enters pressed state
_pressed_buttons.add(button_id)
is_pressed = True
# Use pressed color when mouse is down over this button
if is_pressed and rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT):
bg_color = BUTTON_PRESSED_BACKGROUND_COLORS[button_style]
# Handle button click
if rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT) and is_pressed:
result = 1
_pressed_buttons.remove(button_id)
# Clean up pressed state if mouse is released anywhere
if rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT) and button_id in _pressed_buttons:
_pressed_buttons.remove(button_id)
# Draw the button with rounded corners
roundness = border_radius / (min(rect.width, rect.height) / 2)
if button_style != ButtonStyle.TRANSPARENT:
rl.draw_rectangle_rounded(rect, roundness, 20, bg_color)
else:
rl.draw_rectangle_rounded(rect, roundness, 20, rl.BLACK)
rl.draw_rectangle_rounded_lines_ex(rect, roundness, 20, 2, rl.WHITE)
# Handle icon and text positioning
font = gui_app.font(font_weight)
text_size = measure_text_cached(font, text, font_size)
text_pos = rl.Vector2(0, rect.y + (rect.height - text_size.y) // 2) # Vertical centering
# Draw icon if provided
if icon:
icon_y = rect.y + (rect.height - icon.height) / 2
if text:
if text_alignment == TextAlignment.LEFT:
icon_x = rect.x + text_padding
text_pos.x = icon_x + icon.width + ICON_PADDING
elif text_alignment == TextAlignment.CENTER:
total_width = icon.width + ICON_PADDING + text_size.x
icon_x = rect.x + (rect.width - total_width) / 2
text_pos.x = icon_x + icon.width + ICON_PADDING
else: # RIGHT
text_pos.x = rect.x + rect.width - text_size.x - text_padding
icon_x = text_pos.x - ICON_PADDING - icon.width
else:
# Center icon when no text
icon_x = rect.x + (rect.width - icon.width) / 2
rl.draw_texture_v(icon, rl.Vector2(icon_x, icon_y), rl.WHITE if is_enabled else rl.Color(255, 255, 255, 100))
else:
# No icon, position text normally
if text_alignment == TextAlignment.LEFT:
text_pos.x = rect.x + text_padding
elif text_alignment == TextAlignment.CENTER:
text_pos.x = rect.x + (rect.width - text_size.x) // 2
elif text_alignment == TextAlignment.RIGHT:
text_pos.x = rect.x + rect.width - text_size.x - text_padding
# Draw the button text if any
if text:
color = BUTTON_TEXT_COLOR[button_style] if is_enabled else BUTTON_DISABLED_TEXT_COLORS.get(button_style, rl.Color(228, 228, 228, 51))
rl.draw_text_ex(font, text, text_pos, font_size, 0, color)
return result
class Button(Widget): class Button(Widget):
def __init__(self, def __init__(self,
@ -200,6 +105,11 @@ class Button(Widget):
def set_text(self, text): def set_text(self, text):
self._label.set_text(text) self._label.set_text(text)
def set_button_style(self, button_style: ButtonStyle):
self._button_style = button_style
self._background_color = BUTTON_BACKGROUND_COLORS[self._button_style]
self._label.set_text_color(BUTTON_TEXT_COLOR[self._button_style])
def _update_state(self): def _update_state(self):
if self.enabled: if self.enabled:
self._label.set_text_color(BUTTON_TEXT_COLOR[self._button_style]) self._label.set_text_color(BUTTON_TEXT_COLOR[self._button_style])
@ -213,6 +123,10 @@ class Button(Widget):
def _render(self, _): def _render(self, _):
roundness = self._border_radius / (min(self._rect.width, self._rect.height) / 2) roundness = self._border_radius / (min(self._rect.width, self._rect.height) / 2)
if self._button_style == ButtonStyle.TRANSPARENT_WHITE_BORDER:
rl.draw_rectangle_rounded(self._rect, roundness, 10, rl.BLACK)
rl.draw_rectangle_rounded_lines_ex(self._rect, roundness, 10, 2, rl.WHITE)
else:
rl.draw_rectangle_rounded(self._rect, roundness, 10, self._background_color) rl.draw_rectangle_rounded(self._rect, roundness, 10, self._background_color)
self._label.render(self._rect) self._label.render(self._rect)

@ -6,8 +6,8 @@ from typing import Any
from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
from openpilot.system.ui.lib.wrap_text import wrap_text from openpilot.system.ui.lib.wrap_text import wrap_text
from openpilot.system.ui.widgets import Widget, DialogResult from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.button import gui_button, ButtonStyle from openpilot.system.ui.widgets.button import Button, ButtonStyle
class ElementType(Enum): class ElementType(Enum):
@ -40,6 +40,7 @@ class HtmlRenderer(Widget):
self._normal_font = gui_app.font(FontWeight.NORMAL) self._normal_font = gui_app.font(FontWeight.NORMAL)
self._bold_font = gui_app.font(FontWeight.BOLD) self._bold_font = gui_app.font(FontWeight.BOLD)
self._scroll_panel = GuiScrollPanel() self._scroll_panel = GuiScrollPanel()
self._ok_button = Button("OK", click_callback=lambda: gui_app.set_modal_overlay(None), button_style=ButtonStyle.PRIMARY)
self.styles: dict[ElementType, dict[str, Any]] = { self.styles: dict[ElementType, dict[str, Any]] = {
ElementType.H1: {"size": 68, "weight": FontWeight.BOLD, "color": rl.BLACK, "margin_top": 20, "margin_bottom": 16}, ElementType.H1: {"size": 68, "weight": FontWeight.BOLD, "color": rl.BLACK, "margin_top": 20, "margin_bottom": 16},
@ -126,10 +127,9 @@ class HtmlRenderer(Widget):
button_x = content_rect.x + content_rect.width - button_width button_x = content_rect.x + content_rect.width - button_width
button_y = content_rect.y + content_rect.height - button_height button_y = content_rect.y + content_rect.height - button_height
button_rect = rl.Rectangle(button_x, button_y, button_width, button_height) button_rect = rl.Rectangle(button_x, button_y, button_width, button_height)
if gui_button(button_rect, "OK", button_style=ButtonStyle.PRIMARY) == 1: self._ok_button.render(button_rect)
return DialogResult.CONFIRM
return DialogResult.NO_ACTION return -1
def _render_content(self, rect: rl.Rectangle, scroll_offset: float = 0) -> float: def _render_content(self, rect: rl.Rectangle, scroll_offset: float = 0) -> float:
current_y = rect.y + scroll_offset current_y = rect.y + scroll_offset

@ -6,7 +6,7 @@ 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.text_measure import measure_text_cached
from openpilot.system.ui.lib.wrap_text import wrap_text from openpilot.system.ui.lib.wrap_text import wrap_text
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.button import Button, gui_button, ButtonStyle 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.toggle import Toggle, WIDTH as TOGGLE_WIDTH, HEIGHT as TOGGLE_HEIGHT
ITEM_BASE_WIDTH = 600 ITEM_BASE_WIDTH = 600
@ -149,9 +149,16 @@ class DualButtonAction(ItemAction):
right_callback: Callable = None, enabled: bool | Callable[[], bool] = True): right_callback: Callable = None, enabled: bool | Callable[[], bool] = True):
super().__init__(width=0, enabled=enabled) # Width 0 means use full width super().__init__(width=0, enabled=enabled) # Width 0 means use full width
self.left_text, self.right_text = left_text, right_text self.left_text, self.right_text = left_text, right_text
self.left_callback, self.right_callback = left_callback, right_callback
def _render(self, rect: rl.Rectangle) -> bool: self.left_button = Button(left_text, click_callback=left_callback, button_style=ButtonStyle.LIST_ACTION)
self.right_button = Button(right_text, click_callback=right_callback, button_style=ButtonStyle.DANGER)
def set_touch_valid_callback(self, touch_callback: Callable[[], bool]) -> None:
super().set_touch_valid_callback(touch_callback)
self.left_button.set_touch_valid_callback(touch_callback)
self.right_button.set_touch_valid_callback(touch_callback)
def _render(self, rect: rl.Rectangle):
button_spacing = 30 button_spacing = 30
button_height = 120 button_height = 120
button_width = (rect.width - button_spacing) / 2 button_width = (rect.width - button_spacing) / 2
@ -160,16 +167,9 @@ class DualButtonAction(ItemAction):
left_rect = rl.Rectangle(rect.x, button_y, button_width, button_height) left_rect = rl.Rectangle(rect.x, button_y, button_width, button_height)
right_rect = rl.Rectangle(rect.x + button_width + button_spacing, button_y, button_width, button_height) right_rect = rl.Rectangle(rect.x + button_width + button_spacing, button_y, button_width, button_height)
left_clicked = gui_button(left_rect, self.left_text, button_style=ButtonStyle.LIST_ACTION) == 1 # Render buttons
right_clicked = gui_button(right_rect, self.right_text, button_style=ButtonStyle.DANGER) == 1 self.left_button.render(left_rect)
self.right_button.render(right_rect)
if left_clicked and self.left_callback:
self.left_callback()
return True
if right_clicked and self.right_callback:
self.right_callback()
return True
return False
class MultipleButtonAction(ItemAction): class MultipleButtonAction(ItemAction):

@ -1,9 +1,9 @@
import pyray as rl import pyray as rl
from openpilot.system.ui.lib.application import FontWeight from openpilot.system.ui.lib.application import FontWeight, gui_app
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.button import gui_button, ButtonStyle, TextAlignment from openpilot.system.ui.widgets.button import Button, ButtonStyle, TextAlignment
from openpilot.system.ui.widgets.label import gui_label from openpilot.system.ui.widgets.label import gui_label
from openpilot.system.ui.widgets.scroller import Scroller
# Constants # Constants
MARGIN = 50 MARGIN = 50
@ -22,7 +22,17 @@ class MultiOptionDialog(Widget):
self.options = options self.options = options
self.current = current self.current = current
self.selection = current self.selection = current
self.scroll = GuiScrollPanel()
# Create scroller with option buttons
self.option_buttons = [Button(option, click_callback=lambda opt=option: self._on_option_clicked(opt),
text_alignment=TextAlignment.LEFT, button_style=ButtonStyle.NORMAL) for option in options]
self.scroller = Scroller(self.option_buttons, spacing=LIST_ITEM_SPACING)
self.cancel_button = Button("Cancel", click_callback=lambda: gui_app.set_modal_overlay(None))
self.select_button = Button("Select", click_callback=lambda: gui_app.set_modal_overlay(None), button_style=ButtonStyle.PRIMARY)
def _on_option_clicked(self, option):
self.selection = option
def _render(self, rect): def _render(self, rect):
dialog_rect = rl.Rectangle(rect.x + MARGIN, rect.y + MARGIN, rect.width - 2 * MARGIN, rect.height - 2 * MARGIN) dialog_rect = rl.Rectangle(rect.x + MARGIN, rect.y + MARGIN, rect.width - 2 * MARGIN, rect.height - 2 * MARGIN)
@ -36,36 +46,26 @@ class MultiOptionDialog(Widget):
# Options area # Options area
options_y = content_rect.y + TITLE_FONT_SIZE + ITEM_SPACING options_y = content_rect.y + TITLE_FONT_SIZE + ITEM_SPACING
options_h = content_rect.height - TITLE_FONT_SIZE - BUTTON_HEIGHT - 2 * ITEM_SPACING options_h = content_rect.height - TITLE_FONT_SIZE - BUTTON_HEIGHT - 2 * ITEM_SPACING
view_rect = rl.Rectangle(content_rect.x, options_y, content_rect.width, options_h) options_rect = rl.Rectangle(content_rect.x, options_y, content_rect.width, options_h)
content_h = len(self.options) * (ITEM_HEIGHT + LIST_ITEM_SPACING)
list_content_rect = rl.Rectangle(content_rect.x, options_y, content_rect.width, content_h)
# Scroll and render options # Update button styles and set width based on selection
offset = self.scroll.update(view_rect, list_content_rect)
valid_click = self.scroll.is_touch_valid() and rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT)
rl.begin_scissor_mode(int(view_rect.x), int(options_y), int(view_rect.width), int(options_h))
for i, option in enumerate(self.options): for i, option in enumerate(self.options):
item_y = options_y + i * (ITEM_HEIGHT + LIST_ITEM_SPACING) + offset
item_rect = rl.Rectangle(view_rect.x, item_y, view_rect.width, ITEM_HEIGHT)
if rl.check_collision_recs(item_rect, view_rect):
selected = option == self.selection selected = option == self.selection
style = ButtonStyle.PRIMARY if selected else ButtonStyle.NORMAL button = self.option_buttons[i]
button.set_button_style(ButtonStyle.PRIMARY if selected else ButtonStyle.NORMAL)
button.set_rect(rl.Rectangle(0, 0, options_rect.width, ITEM_HEIGHT))
if gui_button(item_rect, option, button_style=style, text_alignment=TextAlignment.LEFT) and valid_click: self.scroller.render(options_rect)
self.selection = option
rl.end_scissor_mode()
# Buttons # Buttons
button_y = content_rect.y + content_rect.height - BUTTON_HEIGHT button_y = content_rect.y + content_rect.height - BUTTON_HEIGHT
button_w = (content_rect.width - BUTTON_SPACING) / 2 button_w = (content_rect.width - BUTTON_SPACING) / 2
if gui_button(rl.Rectangle(content_rect.x, button_y, button_w, BUTTON_HEIGHT), "Cancel"): cancel_rect = rl.Rectangle(content_rect.x, button_y, button_w, BUTTON_HEIGHT)
return 0 self.cancel_button.render(cancel_rect)
if gui_button(rl.Rectangle(content_rect.x + button_w + BUTTON_SPACING, button_y, button_w, BUTTON_HEIGHT), select_rect = rl.Rectangle(content_rect.x + button_w + BUTTON_SPACING, button_y, button_w, BUTTON_HEIGHT)
"Select", is_enabled=self.selection != self.current, button_style=ButtonStyle.PRIMARY): self.select_button.set_enabled(self.selection != self.current)
return 1 self.select_button.render(select_rect)
return -1 return -1

Loading…
Cancel
Save