mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-09-27 09:53:45 +08:00
e7554170b8
* commaai/openpilot:d05cb31e2e916fba41ba8167030945f427fd811b * bump opendbc * bump opendbc * bump opendbc * bump opendbc * bump opendbc * sunnypilot: remove Qt * cabana: revert to stock Qt * commaai/openpilot:5198b1b079c37742c1050f02ce0aa6dd42b038b9 * commaai/openpilot:954b567b9ba0f3d1ae57d6aa7797fa86dd92ec6e * commaai/openpilot:7534b2a160faa683412c04c1254440e338931c5e * sum more * bump opendbc * not yet * should've been symlink'ed * raylib says wut * quiet mode back * more fixes * no more * too extra red diff on the side * need to bring this back * too extra * let's update docs here * Revert "let's update docs here" This reverts commit 51fe03cd5121e6fdf14657b2c33852c34922b851. * param to control stock vs sp ui * init styles * SP Toggles * Lint * optimizations * multi-button * Lint * param to control stock vs sp ui * init styles * SP Toggles * Lint * optimizations * sp raylib preview * fix callback * fix ui preview * better padding * this * support for next line multi-button * uhh * disabled colors * listitem -> listitemsp * listitem -> listitemsp * add show_description method * remove padding from line separator. like, WHY? 😩😩 * ui: `GuiApplicationExt` * simple button * simple button * add to readme * use gui_app.sunnypilot_ui() * i've got something to confessa * sync * revert * Fix SimpleButtonActionSP not respecting enabled state * some more * ui: `ButtonSP` * slight cleanup * fixes * fix * unused * try this --------- Co-authored-by: Jason Wen <haibin.wen3@gmail.com> Co-authored-by: DevTekVE <devtekve@gmail.com> Co-authored-by: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Co-authored-by: discountchubbs <alexgrant990@gmail.com>
315 lines
15 KiB
Python
315 lines
15 KiB
Python
"""
|
|
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
|
|
|
This file is part of sunnypilot and is licensed under the MIT License.
|
|
See the LICENSE.md file in the root directory for more details.
|
|
"""
|
|
from collections.abc import Callable
|
|
|
|
import pyray as rl
|
|
from openpilot.common.params import Params
|
|
from openpilot.system.ui.lib.application import gui_app, MousePos, FontWeight
|
|
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
|
from openpilot.system.ui.sunnypilot.widgets.toggle import ToggleSP
|
|
from openpilot.system.ui.widgets.button import Button, ButtonStyle
|
|
from openpilot.system.ui.widgets.label import gui_label
|
|
from openpilot.system.ui.widgets.list_view import ListItem, ToggleAction, ItemAction, MultipleButtonAction, ButtonAction, \
|
|
_resolve_value, BUTTON_WIDTH, BUTTON_HEIGHT, TEXT_PADDING
|
|
from openpilot.system.ui.sunnypilot.lib.styles import style
|
|
from openpilot.system.ui.sunnypilot.widgets.option_control import OptionControlSP, LABEL_WIDTH
|
|
|
|
|
|
class ToggleActionSP(ToggleAction):
|
|
def __init__(self, initial_state: bool = False, width: int = style.TOGGLE_WIDTH, enabled: bool | Callable[[], bool] = True,
|
|
callback: Callable[[bool], None] | None = None, param: str | None = None):
|
|
ToggleAction.__init__(self, initial_state, width, enabled, callback)
|
|
self.toggle = ToggleSP(initial_state=initial_state, callback=callback, param=param)
|
|
|
|
|
|
class ButtonSP(Button):
|
|
def _update_state(self):
|
|
super()._update_state()
|
|
if self.enabled:
|
|
if self.is_pressed:
|
|
self._background_color = style.BUTTON_OFF_PRESSED
|
|
else:
|
|
self._background_color = style.BUTTON_ENABLED_OFF
|
|
else:
|
|
self._background_color = style.BUTTON_DISABLED
|
|
self._label.set_text_color(style.BUTTON_TEXT_DISABLED)
|
|
|
|
|
|
class SimpleButtonActionSP(ItemAction):
|
|
def __init__(self, button_text: str | Callable[[], str], callback: Callable = None,
|
|
enabled: bool | Callable[[], bool] = True, button_width: int = style.SIMPLE_BUTTON_WIDTH):
|
|
super().__init__(width=button_width, enabled=enabled)
|
|
self.button_action = ButtonSP(button_text, click_callback=callback, button_style=ButtonStyle.NORMAL,
|
|
border_radius=20)
|
|
|
|
def set_touch_valid_callback(self, touch_callback: Callable[[], bool]) -> None:
|
|
super().set_touch_valid_callback(touch_callback)
|
|
self.button_action.set_touch_valid_callback(touch_callback)
|
|
|
|
def _render(self, rect: rl.Rectangle) -> bool | int | None:
|
|
self.button_action.set_enabled(self.enabled)
|
|
return self.button_action.render(rect)
|
|
|
|
|
|
class ButtonActionSP(ButtonAction):
|
|
def __init__(self, text: str | Callable[[], str], width: int = style.BUTTON_ACTION_WIDTH, enabled: bool | Callable[[], bool] = True):
|
|
super().__init__(text=text, width=width, enabled=enabled)
|
|
self._value_color: rl.Color = style.ITEM_TEXT_VALUE_COLOR
|
|
|
|
def set_value(self, value: str | Callable[[], str], color: rl.Color = style.ITEM_TEXT_VALUE_COLOR):
|
|
self._value_source = value
|
|
self._value_color = color
|
|
|
|
def _render(self, rect: rl.Rectangle) -> bool:
|
|
"""Duplicate of ButtonAction._render, with additional value rendering"""
|
|
self._button.set_text(self.text)
|
|
self._button.set_enabled(_resolve_value(self.enabled))
|
|
button_rect = rl.Rectangle(rect.x + rect.width - BUTTON_WIDTH, rect.y + (rect.height - BUTTON_HEIGHT) / 2, BUTTON_WIDTH, BUTTON_HEIGHT)
|
|
self._button.render(button_rect)
|
|
|
|
value_text = self.value
|
|
if value_text:
|
|
value_rect = rl.Rectangle(rect.x, rect.y, rect.width - BUTTON_WIDTH - TEXT_PADDING, rect.height)
|
|
gui_label(value_rect, value_text, font_size=style.ITEM_TEXT_FONT_SIZE, color=self._value_color,
|
|
font_weight=FontWeight.NORMAL, alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
|
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
|
|
|
pressed = self._pressed
|
|
self._pressed = False
|
|
return pressed
|
|
|
|
|
|
class MultipleButtonActionSP(MultipleButtonAction):
|
|
def __init__(self, buttons: list[str | Callable[[], str]], button_width: int, selected_index: int = 0, callback: Callable = None,
|
|
param: str | None = None):
|
|
MultipleButtonAction.__init__(self, buttons, button_width, selected_index, callback)
|
|
self.param_key = param
|
|
self.params = Params()
|
|
if self.param_key:
|
|
self.selected_button = int(self.params.get(self.param_key, return_default=True))
|
|
self._anim_x: float | None = None
|
|
|
|
def _render(self, rect: rl.Rectangle):
|
|
|
|
button_y = rect.y + (rect.height - style.BUTTON_HEIGHT) / 2
|
|
|
|
total_width = len(self.buttons) * self.button_width
|
|
track_rect = rl.Rectangle(rect.x, button_y, total_width, style.BUTTON_HEIGHT)
|
|
|
|
bg_color = style.MBC_TRANSPARENT
|
|
text_color = style.ITEM_TEXT_COLOR if self.enabled else style.MBC_DISABLED
|
|
highlight_color = style.MBC_BG_CHECKED_ENABLED if self.enabled else style.MBC_DISABLED
|
|
|
|
# background
|
|
rl.draw_rectangle_rounded(track_rect, 0.2, 20, bg_color)
|
|
|
|
# border
|
|
border_color = style.MBC_BG_CHECKED_ENABLED if self.enabled else style.MBC_DISABLED
|
|
rl.draw_rectangle_rounded_lines_ex(track_rect, 0.2, 20, 2, border_color)
|
|
|
|
# highlight with animation
|
|
target_x = rect.x + self.selected_button * self.button_width
|
|
if not self._anim_x:
|
|
self._anim_x = target_x
|
|
self._anim_x += (target_x - self._anim_x) * 0.2
|
|
|
|
highlight_rect = rl.Rectangle(self._anim_x, button_y, self.button_width, style.BUTTON_HEIGHT)
|
|
rl.draw_rectangle_rounded(highlight_rect, 0.2, 20, highlight_color)
|
|
|
|
# text
|
|
for i, _text in enumerate(self.buttons):
|
|
button_x = rect.x + i * self.button_width
|
|
|
|
text = _resolve_value(_text, "")
|
|
text_size = measure_text_cached(self._font, text, 40)
|
|
text_x = button_x + (self.button_width - text_size.x) / 2
|
|
text_y = button_y + (style.BUTTON_HEIGHT - text_size.y) / 2
|
|
|
|
rl.draw_text_ex(self._font, text, rl.Vector2(text_x, text_y), 40, 0, text_color)
|
|
|
|
def _handle_mouse_release(self, mouse_pos: MousePos):
|
|
MultipleButtonAction._handle_mouse_release(self, mouse_pos)
|
|
if self.param_key:
|
|
self.params.put(self.param_key, self.selected_button)
|
|
|
|
|
|
class ListItemSP(ListItem):
|
|
def __init__(self, title: str | Callable[[], str] = "", icon: str | None = None, description: str | Callable[[], str] | None = None,
|
|
description_visible: bool = False, callback: Callable | None = None,
|
|
action_item: ItemAction | None = None, inline: bool = True, title_color: rl.Color = style.ITEM_TEXT_COLOR):
|
|
ListItem.__init__(self, title, icon, description, description_visible, callback, action_item)
|
|
self.title_color = title_color
|
|
self.inline = inline
|
|
if not self.inline:
|
|
self._rect.height += style.ITEM_BASE_HEIGHT/1.75
|
|
self._right_value_source: str | Callable[[], str] | None = None
|
|
self._right_value_font = gui_app.font(FontWeight.NORMAL)
|
|
self._right_value_color: rl.Color = style.ITEM_TEXT_VALUE_COLOR
|
|
|
|
def set_right_value(self, value: str | Callable[[], str], color: rl.Color = style.ITEM_TEXT_VALUE_COLOR):
|
|
self._right_value_source = value
|
|
self._right_value_color = color
|
|
|
|
@property
|
|
def right_value(self) -> str:
|
|
if self._right_value_source is None:
|
|
return ""
|
|
return str(_resolve_value(self._right_value_source, ""))
|
|
|
|
def get_item_height(self, font: rl.Font, max_width: int) -> float:
|
|
height = super().get_item_height(font, max_width)
|
|
|
|
if self.description_visible:
|
|
height += style.ITEM_PADDING * 1.5
|
|
|
|
if not self.inline:
|
|
height += style.ITEM_BASE_HEIGHT / 1.75
|
|
|
|
return height
|
|
|
|
def show_description(self, show: bool):
|
|
self._set_description_visible(show)
|
|
|
|
def get_right_item_rect(self, item_rect: rl.Rectangle) -> rl.Rectangle:
|
|
if not self.action_item:
|
|
return rl.Rectangle(0, 0, 0, 0)
|
|
|
|
if not self.inline:
|
|
has_description = bool(self.description) and self.description_visible
|
|
|
|
if has_description:
|
|
action_y = item_rect.y + self._text_size.y + style.ITEM_PADDING * 3
|
|
else:
|
|
action_y = item_rect.y + item_rect.height - style.BUTTON_HEIGHT - style.ITEM_PADDING * 1.5
|
|
|
|
return rl.Rectangle(item_rect.x + style.ITEM_PADDING, action_y, item_rect.width - (style.ITEM_PADDING * 2), style.BUTTON_HEIGHT)
|
|
|
|
right_width = self.action_item.get_width_hint()
|
|
if right_width == 0:
|
|
return rl.Rectangle(item_rect.x + style.ITEM_PADDING, item_rect.y, item_rect.width - (style.ITEM_PADDING * 2), style.ITEM_BASE_HEIGHT)
|
|
|
|
content_width = item_rect.width - (style.ITEM_PADDING * 2)
|
|
title_width = measure_text_cached(self._font, self.title, style.ITEM_TEXT_FONT_SIZE).x
|
|
right_width = min(content_width - title_width, right_width)
|
|
if isinstance(self.action_item, ToggleAction) or isinstance(self.action_item, SimpleButtonActionSP):
|
|
action_x = item_rect.x
|
|
else:
|
|
action_x = item_rect.x + item_rect.width - right_width
|
|
action_y = item_rect.y
|
|
return rl.Rectangle(action_x, action_y, right_width, style.ITEM_BASE_HEIGHT)
|
|
|
|
def _render(self, _):
|
|
if not self.is_visible:
|
|
return
|
|
|
|
# Don't draw items that are not in parent's viewport
|
|
if (self._rect.y + self.rect.height) <= self._parent_rect.y or self._rect.y >= (self._parent_rect.y + self._parent_rect.height):
|
|
return
|
|
|
|
content_x = self._rect.x + style.ITEM_PADDING
|
|
text_x = content_x
|
|
left_action_item = isinstance(self.action_item, ToggleAction) or isinstance(self.action_item, SimpleButtonActionSP)
|
|
|
|
if left_action_item:
|
|
item_height = style.SIMPLE_BUTTON_HEIGHT if isinstance(self.action_item, SimpleButtonActionSP) else style.TOGGLE_HEIGHT
|
|
left_rect = rl.Rectangle(
|
|
content_x,
|
|
self._rect.y + (style.ITEM_BASE_HEIGHT - item_height) // 2,
|
|
self.action_item.rect.width,
|
|
item_height
|
|
)
|
|
text_x = left_rect.x + left_rect.width + style.ITEM_PADDING * 1.5
|
|
|
|
# Draw title
|
|
if self.title:
|
|
self._text_size = measure_text_cached(self._font, self.title, style.ITEM_TEXT_FONT_SIZE)
|
|
item_y = self._rect.y + (style.ITEM_BASE_HEIGHT - self._text_size.y) // 2
|
|
rl.draw_text_ex(self._font, self.title, rl.Vector2(text_x, item_y), style.ITEM_TEXT_FONT_SIZE, 0, self.title_color)
|
|
|
|
value_text = self.right_value
|
|
if value_text:
|
|
# area from after the title to the right edge of the row
|
|
value_rect = rl.Rectangle(
|
|
text_x, # start at the beginning of the text area
|
|
self._rect.y,
|
|
self._rect.width - (text_x - self._rect.x) - style.ITEM_PADDING,
|
|
style.ITEM_BASE_HEIGHT,
|
|
)
|
|
if value_rect.width > 0:
|
|
gui_label(value_rect, value_text, font_size=style.ITEM_TEXT_FONT_SIZE, color=self._right_value_color, font_weight=FontWeight.NORMAL,
|
|
alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
|
|
|
# Render toggle and handle callback
|
|
if self.action_item.render(left_rect) and self.action_item.enabled:
|
|
if self.callback:
|
|
self.callback()
|
|
|
|
else:
|
|
if self.title:
|
|
# Draw main text
|
|
self._text_size = measure_text_cached(self._font, self.title, style.ITEM_TEXT_FONT_SIZE)
|
|
item_y = self._rect.y + (style.ITEM_BASE_HEIGHT - self._text_size.y) // 2 if self.inline else self._rect.y + style.ITEM_PADDING * 1.5
|
|
rl.draw_text_ex(self._font, self.title, rl.Vector2(text_x, item_y), style.ITEM_TEXT_FONT_SIZE, 0, self.title_color)
|
|
|
|
# Draw right item if present
|
|
if self.action_item:
|
|
right_rect = self.get_right_item_rect(self._rect)
|
|
if self.action_item.render(right_rect) and self.action_item.enabled:
|
|
# Right item was clicked/activated
|
|
if self.callback:
|
|
self.callback()
|
|
|
|
# Draw description if visible
|
|
if self.description_visible:
|
|
content_width = int(self._rect.width - style.ITEM_PADDING * 2)
|
|
description_height = self._html_renderer.get_total_height(content_width)
|
|
|
|
desc_y = self._rect.y + style.ITEM_DESC_V_OFFSET
|
|
if not self.inline and self.action_item:
|
|
desc_y = self.action_item.rect.y + style.ITEM_DESC_V_OFFSET - style.ITEM_PADDING * 0.5
|
|
|
|
description_rect = rl.Rectangle(self._rect.x + style.ITEM_PADDING, desc_y, content_width, description_height)
|
|
self._html_renderer.render(description_rect)
|
|
|
|
|
|
def simple_button_item_sp(button_text: str | Callable[[], str], callback: Callable | None = None,
|
|
enabled: bool | Callable[[], bool] = True, button_width: int = style.SIMPLE_BUTTON_WIDTH) -> ListItemSP:
|
|
action = SimpleButtonActionSP(button_text=button_text, enabled=enabled, callback=callback, button_width=button_width)
|
|
return ListItemSP(title="", callback=callback, description="", action_item=action)
|
|
|
|
|
|
def toggle_item_sp(title: str | Callable[[], str], description: str | Callable[[], str] | None = None, initial_state: bool = False,
|
|
callback: Callable | None = None, icon: str = "", enabled: bool | Callable[[], bool] = True, param: str | None = None) -> ListItemSP:
|
|
action = ToggleActionSP(initial_state=initial_state, enabled=enabled, callback=callback, param=param)
|
|
return ListItemSP(title=title, description=description, action_item=action, icon=icon, callback=callback)
|
|
|
|
|
|
def multiple_button_item_sp(title: str | Callable[[], str], description: str | Callable[[], str], buttons: list[str | Callable[[], str]],
|
|
selected_index: int = 0, button_width: int = style.BUTTON_ACTION_WIDTH, callback: Callable = None,
|
|
icon: str = "", param: str | None = None, inline: bool = False) -> ListItemSP:
|
|
action = MultipleButtonActionSP(buttons, button_width, selected_index, callback=callback, param=param)
|
|
return ListItemSP(title=title, description=description, icon=icon, action_item=action, inline=inline)
|
|
|
|
|
|
def option_item_sp(title: str | Callable[[], str], param: str,
|
|
min_value: int, max_value: int, description: str | Callable[[], str] | None = None,
|
|
value_change_step: int = 1, on_value_changed: Callable[[int], None] | None = None,
|
|
enabled: bool | Callable[[], bool] = True,
|
|
icon: str = "", label_width: int = LABEL_WIDTH, value_map: dict[int, int] | None = None,
|
|
use_float_scaling: bool = False, label_callback: Callable[[int], str] | None = None) -> ListItemSP:
|
|
action = OptionControlSP(
|
|
param, min_value, max_value, value_change_step,
|
|
enabled, on_value_changed, value_map, label_width, use_float_scaling, label_callback
|
|
)
|
|
return ListItemSP(title=title, description=description, action_item=action, icon=icon)
|
|
|
|
|
|
def button_item_sp(title: str | Callable[[], str], button_text: str | Callable[[], str], description: str | Callable[[], str] | None = None,
|
|
callback: Callable | None = None, enabled: bool | Callable[[], bool] = True) -> ListItemSP:
|
|
action = ButtonActionSP(text=button_text, enabled=enabled)
|
|
return ListItemSP(title=title, description=description, action_item=action, callback=callback)
|