This commit is contained in:
firestar5683
2026-04-11 21:56:46 -05:00
parent d2e5f06395
commit d43b7d0d3f
187 changed files with 5499 additions and 6222 deletions
+172 -140
View File
@@ -1,11 +1,11 @@
import math
import pyray as rl
from typing import Union
from enum import Enum
from collections.abc import Callable
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.label import MiciLabel
from openpilot.system.ui.widgets.label import UnifiedLabel
from openpilot.system.ui.widgets.scroller import DO_ZOOM
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
from openpilot.common.filter_simple import BounceFilter
@@ -16,8 +16,7 @@ except ImportError:
SCROLLING_SPEED_PX_S = 50
COMPLICATION_SIZE = 36
LABEL_COLOR = rl.WHITE
LABEL_HORIZONTAL_PADDING = 40
LABEL_COLOR = rl.Color(255, 255, 255, int(255 * 0.9))
COMPLICATION_GREY = rl.Color(0xAA, 0xAA, 0xAA, 255)
PRESSED_SCALE = 1.15 if DO_ZOOM else 1.07
@@ -29,50 +28,51 @@ class ScrollState(Enum):
class BigCircleButton(Widget):
def __init__(self, icon: str, red: bool = False):
def __init__(self, icon: rl.Texture, red: bool = False, icon_offset: tuple[int, int] = (0, 0)):
super().__init__()
self._red = red
self._icon_offset = icon_offset
# State
self.set_rect(rl.Rectangle(0, 0, 180, 180))
self._press_state_enabled = True
self._scale_filter = BounceFilter(1.0, 0.1, 1 / gui_app.target_fps)
self._click_delay = 0.075
# Icons
self._txt_icon = gui_app.texture(icon, 64, 53)
self._txt_icon = icon
self._txt_btn_disabled_bg = gui_app.texture("icons_mici/buttons/button_circle_disabled.png", 180, 180)
self._txt_btn_bg = gui_app.texture("icons_mici/buttons/button_circle.png", 180, 180)
self._txt_btn_pressed_bg = gui_app.texture("icons_mici/buttons/button_circle_hover.png", 180, 180)
self._txt_btn_pressed_bg = gui_app.texture("icons_mici/buttons/button_circle_pressed.png", 180, 180)
self._txt_btn_red_bg = gui_app.texture("icons_mici/buttons/button_circle_red.png", 180, 180)
self._txt_btn_red_pressed_bg = gui_app.texture("icons_mici/buttons/button_circle_red_hover.png", 180, 180)
self._txt_btn_red_pressed_bg = gui_app.texture("icons_mici/buttons/button_circle_red_pressed.png", 180, 180)
def set_enable_pressed_state(self, pressed: bool):
self._press_state_enabled = pressed
def _draw_content(self, btn_y: float):
# draw icon
icon_color = rl.Color(255, 255, 255, int(255 * 0.9)) if self.enabled else rl.Color(255, 255, 255, int(255 * 0.35))
rl.draw_texture_ex(self._txt_icon, (self._rect.x + (self._rect.width - self._txt_icon.width) / 2 + self._icon_offset[0],
btn_y + (self._rect.height - self._txt_icon.height) / 2 + self._icon_offset[1]), 0, 1.0, icon_color)
def _render(self, _):
# draw background
txt_bg = self._txt_btn_bg if not self._red else self._txt_btn_red_bg
if not self.enabled:
txt_bg = self._txt_btn_disabled_bg
elif self.is_pressed and self._press_state_enabled:
elif self.is_pressed:
txt_bg = self._txt_btn_pressed_bg if not self._red else self._txt_btn_red_pressed_bg
scale = self._scale_filter.update(PRESSED_SCALE if self.is_pressed and self._press_state_enabled else 1.0)
scale = self._scale_filter.update(PRESSED_SCALE if self.is_pressed else 1.0)
btn_x = self._rect.x + (self._rect.width * (1 - scale)) / 2
btn_y = self._rect.y + (self._rect.height * (1 - scale)) / 2
rl.draw_texture_ex(txt_bg, (btn_x, btn_y), 0, scale, rl.WHITE)
# draw icon
icon_color = rl.WHITE if self.enabled else rl.Color(255, 255, 255, int(255 * 0.35))
rl.draw_texture(self._txt_icon, int(self._rect.x + (self._rect.width - self._txt_icon.width) / 2),
int(self._rect.y + (self._rect.height - self._txt_icon.height) / 2), icon_color)
self._draw_content(btn_y)
class BigCircleToggle(BigCircleButton):
def __init__(self, icon: str, toggle_callback: Callable = None):
super().__init__(icon, False)
def __init__(self, icon: rl.Texture, toggle_callback: Callable | None = None, icon_offset: tuple[int, int] = (0, 0)):
super().__init__(icon, False, icon_offset=icon_offset)
self._toggle_callback = toggle_callback
# State
@@ -80,7 +80,7 @@ class BigCircleToggle(BigCircleButton):
# Icons
self._txt_toggle_enabled = gui_app.texture("icons_mici/buttons/toggle_dot_enabled.png", 66, 66)
self._txt_toggle_disabled = gui_app.texture("icons_mici/buttons/toggle_dot_disabled.png", 70, 70) # TODO: why discrepancy?
self._txt_toggle_disabled = gui_app.texture("icons_mici/buttons/toggle_dot_disabled.png", 66, 66)
def set_checked(self, checked: bool):
self._checked = checked
@@ -92,62 +92,47 @@ class BigCircleToggle(BigCircleButton):
if self._toggle_callback:
self._toggle_callback(self._checked)
def _render(self, _):
super()._render(_)
def _draw_content(self, btn_y: float):
super()._draw_content(btn_y)
# draw status icon
rl.draw_texture(self._txt_toggle_enabled if self._checked else self._txt_toggle_disabled,
int(self._rect.x + (self._rect.width - self._txt_toggle_enabled.width) / 2),
int(self._rect.y + 5), rl.WHITE)
rl.draw_texture_ex(self._txt_toggle_enabled if self._checked else self._txt_toggle_disabled,
(self._rect.x + (self._rect.width - self._txt_toggle_enabled.width) / 2, btn_y + 5),
0, 1.0, rl.WHITE)
class BigButton(Widget):
LABEL_HORIZONTAL_PADDING = 40
LABEL_VERTICAL_PADDING = 23 # visually matches 30 in figma
"""A lightweight stand-in for the Qt BigButton, drawn & updated each frame."""
def __init__(self, text: str, value: str = "", icon: Union[str, rl.Texture] = ""):
def __init__(self, text: str, value: str = "", icon: Union[rl.Texture, None] = None, scroll: bool = False):
super().__init__()
self.set_rect(rl.Rectangle(0, 0, 402, 180))
self.text = text
self.value = value
self.set_icon(icon)
self._label_font_size_override: int | None = None
self._txt_icon = icon
self._scroll = scroll
self._scale_filter = BounceFilter(1.0, 0.1, 1 / gui_app.target_fps)
self._click_delay = 0.075
self._shake_start: float | None = None
self._grow_animation_until: float | None = None
self._rotate_icon_t: float | None = None
self._label_font = gui_app.font(FontWeight.DISPLAY)
self._value_font = gui_app.font(FontWeight.ROMAN)
self._label = MiciLabel(text, font_size=self._get_label_font_size(), width=int(self._rect.width - LABEL_HORIZONTAL_PADDING * 2),
font_weight=FontWeight.DISPLAY, color=LABEL_COLOR,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM, wrap_text=True)
self._sub_label = MiciLabel(value, font_size=COMPLICATION_SIZE, width=int(self._rect.width - LABEL_HORIZONTAL_PADDING * 2),
font_weight=FontWeight.ROMAN, color=COMPLICATION_GREY,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM, wrap_text=True)
self._label = UnifiedLabel(text, font_size=self._get_label_font_size(), font_weight=FontWeight.BOLD,
text_color=LABEL_COLOR, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM, scroll=scroll,
line_height=0.9)
self._sub_label = UnifiedLabel(value, font_size=COMPLICATION_SIZE, font_weight=FontWeight.ROMAN,
text_color=COMPLICATION_GREY, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM)
self._update_label_layout()
self._load_images()
# internal state
self._scroll_offset = 0 # in pixels
self._needs_scroll = measure_text_cached(self._label_font, text, self._get_label_font_size()).x + 25 > self._rect.width
self._scroll_timer = 0
self._scroll_state = ScrollState.PRE_SCROLL
def set_icon(self, icon: Union[str, rl.Texture]):
self._txt_icon = gui_app.texture(icon, 64, 64) if isinstance(icon, str) and len(icon) else icon
def _refresh_label_metrics(self):
font_size = self._label_font_size_override if self._label_font_size_override is not None else self._get_label_font_size()
self._label.set_font_size(font_size)
self._needs_scroll = measure_text_cached(self._label_font, self.text, font_size).x + 25 > self._rect.width
self._scroll_offset = 0
self._scroll_timer = 0
self._scroll_state = ScrollState.PRE_SCROLL
def _set_label_font_size_override(self, font_size: int | None):
self._label_font_size_override = font_size
self._refresh_label_metrics()
def set_icon(self, icon: Union[rl.Texture, None]):
self._txt_icon = icon
def set_rotate_icon(self, rotate: bool):
if rotate and self._rotate_icon_t is not None:
@@ -158,32 +143,37 @@ class BigButton(Widget):
self._txt_default_bg = gui_app.texture("icons_mici/buttons/button_rectangle.png", 402, 180)
self._txt_pressed_bg = gui_app.texture("icons_mici/buttons/button_rectangle_pressed.png", 402, 180)
self._txt_disabled_bg = gui_app.texture("icons_mici/buttons/button_rectangle_disabled.png", 402, 180)
self._txt_hover_bg = gui_app.texture("icons_mici/buttons/button_rectangle_hover.png", 402, 180)
def set_touch_valid_callback(self, touch_callback: Callable[[], bool]) -> None:
super().set_touch_valid_callback(lambda: touch_callback() and self._grow_animation_until is None)
def _width_hint(self) -> int:
# Single line if scrolling, so hide behind icon if exists
icon_size = self._txt_icon.width if self._txt_icon and self._scroll and self.value else 0
return int(self._rect.width - self.LABEL_HORIZONTAL_PADDING * 2 - icon_size)
def _get_label_font_size(self):
if len(self.text) < 12:
font_size = 64
elif len(self.text) < 17:
font_size = 48
elif len(self.text) < 20:
font_size = 42
if len(self.text) <= 18:
return 48
else:
font_size = 36
return 42
def _update_label_layout(self):
self._label.set_font_size(self._get_label_font_size())
if self.value:
font_size -= 20
return font_size
self._label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP)
else:
self._label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM)
def set_text(self, text: str):
self.text = text
self._label.set_text(text)
self._refresh_label_metrics()
self._update_label_layout()
def set_value(self, value: str):
self.value = value
self._sub_label.set_text(value)
self._refresh_label_metrics()
self._update_label_layout()
def get_value(self) -> str:
return self.value
@@ -191,64 +181,60 @@ class BigButton(Widget):
def get_text(self):
return self.text
def _update_state(self):
# hold on text for a bit, scroll, hold again, reset
if self._needs_scroll:
"""`dt` should be seconds since last frame (rl.get_frame_time())."""
# TODO: this comment is generated by GPT, prob wrong and misused
dt = rl.get_frame_time()
def trigger_shake(self):
self._shake_start = rl.get_time()
self._scroll_timer += dt
if self._scroll_state == ScrollState.PRE_SCROLL:
if self._scroll_timer < 0.5:
return
self._scroll_state = ScrollState.SCROLLING
self._scroll_timer = 0
def trigger_grow_animation(self, duration: float = 0.65):
self._grow_animation_until = rl.get_time() + duration
elif self._scroll_state == ScrollState.SCROLLING:
self._scroll_offset -= SCROLLING_SPEED_PX_S * dt
# reset when text has completely left the button + 50 px gap
# TODO: use global constant for 30+30 px gap
# TODO: add std Widget padding option integrated into the self._rect
full_len = measure_text_cached(self._label_font, self.text, self._get_label_font_size()).x + 30 + 30
if self._scroll_offset < (self._rect.width - full_len):
self._scroll_state = ScrollState.POST_SCROLL
self._scroll_timer = 0
@property
def _shake_offset(self) -> float:
SHAKE_DURATION = 0.5
SHAKE_AMPLITUDE = 24.0
SHAKE_FREQUENCY = 32.0
if self._shake_start is None:
return 0.0
t = rl.get_time() - self._shake_start
if t > SHAKE_DURATION:
return 0.0
decay = 1.0 - t / SHAKE_DURATION
return decay * SHAKE_AMPLITUDE * math.sin(t * SHAKE_FREQUENCY)
elif self._scroll_state == ScrollState.POST_SCROLL:
# wait for a bit before starting to scroll again
if self._scroll_timer < 0.75:
return
self._scroll_state = ScrollState.PRE_SCROLL
self._scroll_timer = 0
self._scroll_offset = 0
def set_position(self, x: float, y: float) -> None:
super().set_position(x + self._shake_offset, y)
def _handle_background(self) -> tuple[rl.Texture, float, float, float]:
if self._grow_animation_until is not None:
if rl.get_time() >= self._grow_animation_until:
self._grow_animation_until = None
def _render(self, _):
# draw _txt_default_bg
txt_bg = self._txt_default_bg
if not self.enabled:
txt_bg = self._txt_disabled_bg
elif self.is_pressed:
txt_bg = self._txt_hover_bg
txt_bg = self._txt_pressed_bg
scale = self._scale_filter.update(PRESSED_SCALE if self.is_pressed else 1.0)
scale = self._scale_filter.update(PRESSED_SCALE if self.is_pressed or self._grow_animation_until is not None else 1.0)
btn_x = self._rect.x + (self._rect.width * (1 - scale)) / 2
btn_y = self._rect.y + (self._rect.height * (1 - scale)) / 2
rl.draw_texture_ex(txt_bg, (btn_x, btn_y), 0, scale, rl.WHITE)
return txt_bg, btn_x, btn_y, scale
def _draw_content(self, btn_y: float):
# LABEL ------------------------------------------------------------------
lx = self._rect.x + LABEL_HORIZONTAL_PADDING
ly = btn_y + self._rect.height - 33 # - 40# - self._get_label_font_size() / 2
if self.value:
self._sub_label.set_position(lx, ly)
ly -= self._sub_label.font_size + 9
self._sub_label.render()
label_x = self._rect.x + self.LABEL_HORIZONTAL_PADDING
label_color = LABEL_COLOR if self.enabled else rl.Color(255, 255, 255, int(255 * 0.35))
self._label.set_color(label_color)
self._label.set_position(lx, ly)
self._label.render()
label_rect = rl.Rectangle(label_x, btn_y + self.LABEL_VERTICAL_PADDING, self._width_hint(),
self._rect.height - self.LABEL_VERTICAL_PADDING * 2)
self._label.render(label_rect)
if self.value:
label_y = btn_y + self.LABEL_VERTICAL_PADDING + self._label.get_content_height(self._width_hint())
sub_label_height = btn_y + self._rect.height - self.LABEL_VERTICAL_PADDING - label_y
sub_label_rect = rl.Rectangle(label_x, label_y, self._width_hint(), sub_label_height)
self._sub_label.render(sub_label_rect)
# ICON -------------------------------------------------------------------
if self._txt_icon:
@@ -256,23 +242,35 @@ class BigButton(Widget):
if self._rotate_icon_t is not None:
rotation = (rl.get_time() - self._rotate_icon_t) * 180
# drop top right with 30px padding
# draw top right with 30px padding
x = self._rect.x + self._rect.width - 30 - self._txt_icon.width / 2
y = self._rect.y + 30 + self._txt_icon.height / 2
y = btn_y + 30 + self._txt_icon.height / 2
source_rec = rl.Rectangle(0, 0, self._txt_icon.width, self._txt_icon.height)
dest_rec = rl.Rectangle(int(x), int(y), self._txt_icon.width, self._txt_icon.height)
dest_rec = rl.Rectangle(x, y, self._txt_icon.width, self._txt_icon.height)
origin = rl.Vector2(self._txt_icon.width / 2, self._txt_icon.height / 2)
rl.draw_texture_pro(self._txt_icon, source_rec, dest_rec, origin, rotation, rl.WHITE)
rl.draw_texture_pro(self._txt_icon, source_rec, dest_rec, origin, rotation, rl.Color(255, 255, 255, int(255 * 0.9)))
def _render(self, _):
txt_bg, btn_x, btn_y, scale = self._handle_background()
if self._scroll:
# draw black background since images are transparent
scaled_rect = rl.Rectangle(btn_x, btn_y, self._rect.width * scale, self._rect.height * scale)
rl.draw_rectangle_rounded(scaled_rect, 0.4, 7, rl.Color(0, 0, 0, int(255 * 0.5)))
self._draw_content(btn_y)
rl.draw_texture_ex(txt_bg, (btn_x, btn_y), 0, scale, rl.WHITE)
else:
rl.draw_texture_ex(txt_bg, (btn_x, btn_y), 0, scale, rl.WHITE)
self._draw_content(btn_y)
class BigToggle(BigButton):
def __init__(self, text: str, value: str = "", initial_state: bool = False, toggle_callback: Callable = None):
def __init__(self, text: str, value: str = "", initial_state: bool = False, toggle_callback: Callable | None = None):
super().__init__(text, value, "")
self._checked = initial_state
self._toggle_callback = toggle_callback
self._set_label_font_size_override(48)
def _load_images(self):
super()._load_images()
self._txt_enabled_toggle = gui_app.texture("icons_mici/buttons/toggle_pill_enabled.png", 84, 66)
@@ -290,35 +288,30 @@ class BigToggle(BigButton):
def _draw_pill(self, x: float, y: float, checked: bool):
# draw toggle icon top right
if checked:
rl.draw_texture(self._txt_enabled_toggle, int(x), int(y), rl.WHITE)
rl.draw_texture_ex(self._txt_enabled_toggle, (x, y), 0, 1.0, rl.WHITE)
else:
rl.draw_texture(self._txt_disabled_toggle, int(x), int(y), rl.WHITE)
rl.draw_texture_ex(self._txt_disabled_toggle, (x, y), 0, 1.0, rl.WHITE)
def _render(self, _):
super()._render(_)
def _draw_content(self, btn_y: float):
super()._draw_content(btn_y)
x = self._rect.x + self._rect.width - self._txt_enabled_toggle.width
y = self._rect.y
y = btn_y
self._draw_pill(x, y, self._checked)
class BigMultiToggle(BigToggle):
def __init__(self, text: str, options: list[str], toggle_callback: Callable = None,
select_callback: Callable = None):
def __init__(self, text: str, options: list[str], toggle_callback: Callable | None = None,
select_callback: Callable | None = None):
super().__init__(text, "", toggle_callback=toggle_callback)
assert len(options) > 0
self._options = options
self._select_callback = select_callback
self._label.set_width(int(self._rect.width - LABEL_HORIZONTAL_PADDING * 2 - self._txt_enabled_toggle.width))
# Keep the title size stable when the selected option changes.
self._set_label_font_size_override(self._get_label_font_size())
self.set_value(self._options[0])
def _get_label_font_size(self):
font_size = super()._get_label_font_size()
return font_size - 6
def _width_hint(self) -> int:
return int(self._rect.width - self.LABEL_HORIZONTAL_PADDING * 2 - self._txt_enabled_toggle.width)
def _handle_mouse_release(self, mouse_pos: MousePos):
super()._handle_mouse_release(mouse_pos)
@@ -328,22 +321,60 @@ class BigMultiToggle(BigToggle):
if self._select_callback:
self._select_callback(self.value)
def _render(self, _):
BigButton._render(self, _)
def _draw_content(self, btn_y: float):
# don't draw pill from BigToggle
BigButton._draw_content(self, btn_y)
checked_idx = self._options.index(self.value)
x = self._rect.x + self._rect.width - self._txt_enabled_toggle.width
y = self._rect.y
y = btn_y
for i in range(len(self._options)):
self._draw_pill(x, y, checked_idx == i)
y += 35
class GreyBigButton(BigButton):
"""Users should manage newlines with this class themselves"""
LABEL_HORIZONTAL_PADDING = 30
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_touch_valid_callback(lambda: False)
self._rect.width = 476
self._label.set_font_size(36)
self._label.set_font_weight(FontWeight.BOLD)
self._label.set_line_height(1.0)
self._sub_label.set_font_size(36)
self._sub_label.set_text_color(rl.Color(255, 255, 255, int(255 * 0.9)))
self._sub_label.set_font_weight(FontWeight.DISPLAY_REGULAR)
self._sub_label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE if not self._label.text else
rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM)
self._sub_label.set_line_height(0.95)
@property
def LABEL_VERTICAL_PADDING(self):
return BigButton.LABEL_VERTICAL_PADDING if self._label.text else 18
def _width_hint(self) -> int:
return int(self._rect.width - self.LABEL_HORIZONTAL_PADDING * 2)
def _get_label_font_size(self):
return 36
def _render(self, _):
rl.draw_rectangle_rounded(self._rect, 0.4, 10, rl.Color(255, 255, 255, int(255 * 0.15)))
self._draw_content(self._rect.y)
class BigMultiParamToggle(BigMultiToggle):
def __init__(self, text: str, param: str, options: list[str], toggle_callback: Callable = None,
select_callback: Callable = None):
def __init__(self, text: str, param: str, options: list[str], toggle_callback: Callable | None = None,
select_callback: Callable | None = None):
super().__init__(text, options, toggle_callback, select_callback)
self._param = param
@@ -360,7 +391,7 @@ class BigMultiParamToggle(BigMultiToggle):
class BigParamControl(BigToggle):
def __init__(self, text: str, param: str, toggle_callback: Callable = None):
def __init__(self, text: str, param: str, toggle_callback: Callable | None = None):
super().__init__(text, "", toggle_callback=toggle_callback)
self.param = param
self.params = Params()
@@ -376,8 +407,9 @@ class BigParamControl(BigToggle):
# TODO: param control base class
class BigCircleParamControl(BigCircleToggle):
def __init__(self, icon: str, param: str, toggle_callback: Callable = None):
super().__init__(icon, toggle_callback)
def __init__(self, icon: rl.Texture, param: str, toggle_callback: Callable | None = None,
icon_offset: tuple[int, int] = (0, 0)):
super().__init__(icon, toggle_callback, icon_offset=icon_offset)
self._param = param
self.params = Params()
self.set_checked(self.params.get_bool(self._param, False))
+146 -212
View File
@@ -1,19 +1,18 @@
import abc
import math
import pyray as rl
from typing import Union
from typing import Union, cast
from collections.abc import Callable
from typing import cast
from openpilot.system.ui.widgets import Widget, NavWidget, DialogResult
from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.nav_widget import NavWidget
from openpilot.system.ui.widgets.label import UnifiedLabel
from openpilot.system.ui.widgets.mici_keyboard import MiciKeyboard
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.application import gui_app, FontWeight, MousePos, MouseEvent
from openpilot.system.ui.widgets.scroller import Scroller
from openpilot.system.ui.widgets.slider import RedBigSlider, BigSlider
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.selfdrive.ui.mici.widgets.button import BigButton
from openpilot.selfdrive.ui.mici.widgets.button import BigCircleButton, BigButton, GreyBigButton
from openpilot.selfdrive.ui.mici.widgets.side_button import SideButton
DEBUG = False
@@ -22,162 +21,80 @@ PADDING = 20
class BigDialogBase(NavWidget, abc.ABC):
def __init__(self, right_btn: str | None = None, right_btn_callback: Callable | None = None):
def __init__(self):
super().__init__()
self._ret = DialogResult.NO_ACTION
self.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
self.set_back_callback(lambda: setattr(self, '_ret', DialogResult.CANCEL))
self._right_btn = None
if right_btn:
def right_btn_callback_wrapper():
gui_app.set_modal_overlay(None)
if right_btn_callback:
right_btn_callback()
self._right_btn = SideButton(right_btn)
self._right_btn.set_click_callback(right_btn_callback_wrapper)
# move to right side
self._right_btn._rect.x = self._rect.x + self._rect.width - self._right_btn._rect.width
def _render(self, _) -> DialogResult:
"""
Allows `gui_app.set_modal_overlay(BigDialog(...))`.
The overlay runner keeps calling until result != NO_ACTION.
"""
if self._right_btn:
self._right_btn.set_position(self._right_btn._rect.x, self._rect.y)
self._right_btn.render()
return self._ret
class BigDialog(BigDialogBase):
def __init__(self,
title: str,
description: str,
right_btn: str | None = None,
right_btn_callback: Callable | None = None):
super().__init__(right_btn, right_btn_callback)
self._title = title
self._description = description
def __init__(self, title: str, description: str, icon: Union[rl.Texture, None] = None):
super().__init__()
self._card = GreyBigButton(title, description, icon)
def _render(self, _) -> DialogResult:
super()._render(_)
# draw title
# TODO: we desperately need layouts
# TODO: coming up with these numbers manually is a pain and not scalable
# TODO: no clue what any of these numbers mean. VBox and HBox would remove all of this shite
max_width = self._rect.width - PADDING * 2
if self._right_btn:
max_width -= self._right_btn._rect.width
title_font_size = 50
desc_font_size = 30
title_lines = wrap_text(gui_app.font(FontWeight.BOLD), self._title, title_font_size, int(max_width))
if not title_lines:
title_lines = [""]
title_line_height = max(int(title_font_size * 1.2), int(measure_text_cached(gui_app.font(FontWeight.BOLD), "Ag", title_font_size).y))
text_x_offset = 0
title_x = int(self._rect.x + text_x_offset + PADDING)
title_y = int(self._rect.y + PADDING)
for i, line in enumerate(title_lines):
line_rect = rl.Rectangle(
title_x,
title_y + i * title_line_height,
int(max_width),
int(title_line_height),
)
gui_label(line_rect, line, title_font_size, font_weight=FontWeight.BOLD,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP)
# draw description
desc_lines = wrap_text(gui_app.font(FontWeight.MEDIUM), self._description, desc_font_size, int(max_width))
if not desc_lines:
desc_lines = [""]
desc_line_height = max(int(desc_font_size * 1.25), int(measure_text_cached(gui_app.font(FontWeight.MEDIUM), "Ag", desc_font_size).y))
desc_y = max(
int(self._rect.y + self._rect.height / 3),
title_y + title_line_height * len(title_lines) + 22,
)
for i, line in enumerate(desc_lines):
line_rect = rl.Rectangle(
title_x,
desc_y + i * desc_line_height,
int(max_width),
int(desc_line_height),
)
gui_label(line_rect, line, desc_font_size, font_weight=FontWeight.MEDIUM,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP)
return self._ret
def _render(self, _):
self._card.render(rl.Rectangle(
self._rect.x + self._rect.width / 2 - self._card.rect.width / 2,
self._rect.y + self._rect.height / 2 - self._card.rect.height / 2,
self._card.rect.width,
self._card.rect.height,
))
class BigConfirmationDialogV2(BigDialogBase):
def __init__(self, title: str, icon: str, red: bool = False,
exit_on_confirm: bool = True,
confirm_callback: Callable | None = None):
class BigConfirmationDialog(BigDialogBase):
def __init__(self, title: str, icon: rl.Texture, confirm_callback: Callable[[], None],
exit_on_confirm: bool = True, red: bool = False):
super().__init__()
self._confirm_callback = confirm_callback
self._exit_on_confirm = exit_on_confirm
icon_txt = gui_app.texture(icon, 64, 53)
self._slider: BigSlider | RedBigSlider
if red:
self._slider = RedBigSlider(title, icon_txt, confirm_callback=self._on_confirm)
self._slider = self._child(RedBigSlider(title, icon, confirm_callback=self._on_confirm))
else:
self._slider = BigSlider(title, icon_txt, confirm_callback=self._on_confirm)
self._slider.set_enabled(lambda: not self._swiping_away)
def show_event(self):
super().show_event()
self._slider.show_event()
def hide_event(self):
super().hide_event()
self._slider.hide_event()
self._slider = self._child(BigSlider(title, icon, confirm_callback=self._on_confirm))
self._slider.set_enabled(lambda: self.enabled and not self.is_dismissing) # for nav stack + NavWidget
def _on_confirm(self):
if self._confirm_callback:
self._confirm_callback()
if self._exit_on_confirm:
self._ret = DialogResult.CONFIRM
self.dismiss(self._confirm_callback)
elif self._confirm_callback:
self._confirm_callback()
def _update_state(self):
super()._update_state()
if self._swiping_away and not self._slider.confirmed:
self._slider.reset(reset_shimmer=False)
if self.is_dismissing and not self._slider.confirmed:
self._slider.reset()
def _render(self, _) -> DialogResult:
def _render(self, _):
self._slider.render(self._rect)
return self._ret
class BigInputDialog(BigDialogBase):
BACK_TOUCH_AREA_PERCENTAGE = 0.2
BACKSPACE_RATE = 25 # hz
TEXT_INPUT_SIZE = 35
def __init__(self,
hint: str,
default_text: str = "",
minimum_length: int = 1,
confirm_callback: Callable[[str], None] = None):
super().__init__(None, None)
confirm_callback: Callable[[str], None] | None = None,
auto_return_to_letters: str = ""):
super().__init__()
self._hint_label = UnifiedLabel(hint, font_size=35, text_color=rl.Color(255, 255, 255, int(255 * 0.35)),
font_weight=FontWeight.MEDIUM)
self._keyboard = MiciKeyboard()
self._keyboard = MiciKeyboard(auto_return_to_letters=auto_return_to_letters)
self._keyboard.set_text(default_text)
self._keyboard.set_enabled(lambda: self.enabled and not self.is_dismissing) # for nav stack + NavWidget
self._minimum_length = minimum_length
self._backspace_held_time: float | None = None
self._backspace_img = gui_app.texture("icons_mici/settings/keyboard/backspace.png", 44, 44)
self._backspace_img = gui_app.texture("icons_mici/settings/keyboard/backspace.png", 42, 36)
self._backspace_img_alpha = FirstOrderFilter(0, 0.05, 1 / gui_app.target_fps)
self._enter_img = gui_app.texture("icons_mici/settings/keyboard/confirm.png", 44, 44)
self._enter_img = gui_app.texture("icons_mici/settings/keyboard/enter.png", 76, 62)
self._enter_disabled_img = gui_app.texture("icons_mici/settings/keyboard/enter_disabled.png", 76, 62)
self._enter_img_alpha = FirstOrderFilter(0, 0.05, 1 / gui_app.target_fps)
# rects for top buttons
@@ -185,14 +102,17 @@ class BigInputDialog(BigDialogBase):
self._top_right_button_rect = rl.Rectangle(0, 0, 0, 0)
def confirm_callback_wrapper():
self._ret = DialogResult.CONFIRM
if confirm_callback:
confirm_callback(self._keyboard.text())
text = self._keyboard.text()
self.dismiss((lambda: confirm_callback(text)) if confirm_callback else None)
self._confirm_callback = confirm_callback_wrapper
def _update_state(self):
super()._update_state()
if self.is_dismissing:
self._backspace_held_time = None
return
last_mouse_event = gui_app.last_mouse_event
if last_mouse_event.left_down and rl.check_collision_point_rec(last_mouse_event.pos, self._top_right_button_rect) and self._backspace_img_alpha.x > 1:
if self._backspace_held_time is None:
@@ -206,64 +126,60 @@ class BigInputDialog(BigDialogBase):
self._backspace_held_time = None
def _render(self, _):
text_input_size = 35
# draw current text so far below everything. text floats left but always stays in view
text = self._keyboard.text()
candidate_char = self._keyboard.get_candidate_character()
text_size = measure_text_cached(gui_app.font(FontWeight.ROMAN), text + candidate_char or self._hint_label.text, text_input_size)
text_x = PADDING * 2 + self._enter_img.width
text_size = measure_text_cached(gui_app.font(FontWeight.ROMAN), text + candidate_char or self._hint_label.text, self.TEXT_INPUT_SIZE)
# text needs to move left if we're at the end where right button is
text_rect = rl.Rectangle(text_x,
int(self._rect.y + PADDING),
# clip width to right button when in view
int(self._rect.width - text_x - PADDING * 2 - self._enter_img.width + 5), # TODO: why 5?
int(text_size.y))
# draw rounded background for text input
bg_block_margin = 5
text_field_rect = rl.Rectangle(text_rect.x - bg_block_margin, text_rect.y - bg_block_margin,
text_rect.width + bg_block_margin * 2, text_input_size + bg_block_margin * 2)
text_x = PADDING / 2 + self._enter_img.width + PADDING
text_field_rect = rl.Rectangle(text_x, self._rect.y + PADDING - bg_block_margin,
self._rect.width - text_x * 2,
text_size.y)
# draw text input
# push text left with a gradient on left side if too long
if text_size.x > text_rect.width:
text_x -= text_size.x - text_rect.width
if text_size.x > text_field_rect.width:
text_x -= text_size.x - text_field_rect.width
rl.begin_scissor_mode(int(text_rect.x), int(text_rect.y), int(text_rect.width), int(text_rect.height))
rl.draw_text_ex(gui_app.font(FontWeight.ROMAN), text, rl.Vector2(text_x, text_rect.y), text_input_size, 0, rl.WHITE)
rl.begin_scissor_mode(int(text_field_rect.x), int(text_field_rect.y), int(text_field_rect.width), int(text_field_rect.height))
rl.draw_text_ex(gui_app.font(FontWeight.ROMAN), text, rl.Vector2(text_x, text_field_rect.y), self.TEXT_INPUT_SIZE, 0, rl.WHITE)
# draw grayed out character user is hovering over
if candidate_char:
candidate_char_size = measure_text_cached(gui_app.font(FontWeight.ROMAN), candidate_char, text_input_size)
candidate_char_size = measure_text_cached(gui_app.font(FontWeight.ROMAN), candidate_char, self.TEXT_INPUT_SIZE)
rl.draw_text_ex(gui_app.font(FontWeight.ROMAN), candidate_char,
rl.Vector2(min(text_x + text_size.x, text_rect.x + text_rect.width) - candidate_char_size.x, text_rect.y),
text_input_size, 0, rl.Color(255, 255, 255, 128))
rl.Vector2(min(text_x + text_size.x, text_field_rect.x + text_field_rect.width) - candidate_char_size.x, text_field_rect.y),
self.TEXT_INPUT_SIZE, 0, rl.Color(255, 255, 255, 128))
rl.end_scissor_mode()
# draw gradient on left side to indicate more text
if text_size.x > text_rect.width:
rl.draw_rectangle_gradient_h(int(text_rect.x), int(text_rect.y), 80, int(text_rect.height),
rl.BLACK, rl.BLANK)
if text_size.x > text_field_rect.width:
rl.draw_rectangle_gradient_ex(rl.Rectangle(text_field_rect.x, text_field_rect.y, 80, text_field_rect.height),
rl.BLACK, rl.BLANK, rl.BLANK, rl.BLACK)
# draw cursor
blink_alpha = (math.sin(rl.get_time() * 6) + 1) / 2
if text:
blink_alpha = (math.sin(rl.get_time() * 6) + 1) / 2
cursor_x = min(text_x + text_size.x + 3, text_rect.x + text_rect.width)
rl.draw_rectangle_rounded(rl.Rectangle(int(cursor_x), int(text_rect.y), 4, int(text_size.y)),
1, 4, rl.Color(255, 255, 255, int(255 * blink_alpha)))
cursor_x = min(text_x + text_size.x + 3, text_field_rect.x + text_field_rect.width)
else:
cursor_x = text_field_rect.x - 6
rl.draw_rectangle_rounded(rl.Rectangle(cursor_x, text_field_rect.y, 4, text_size.y),
1, 4, rl.Color(255, 255, 255, int(255 * blink_alpha)))
# draw backspace icon with nice fade
self._backspace_img_alpha.update(255 * bool(text))
if self._backspace_img_alpha.x > 1:
color = rl.Color(255, 255, 255, int(self._backspace_img_alpha.x))
rl.draw_texture(self._backspace_img, int(self._rect.width - self._enter_img.width - 15), int(text_field_rect.y), color)
rl.draw_texture_ex(self._backspace_img, rl.Vector2(self._rect.width - self._backspace_img.width - 27, self._rect.y + 14), 0.0, 1.0, color)
if not text and self._hint_label.text and not candidate_char:
# draw description if no text entered yet and not drawing candidate char
self._hint_label.render(text_field_rect)
hint_rect = rl.Rectangle(text_field_rect.x, text_field_rect.y,
self._rect.width - text_field_rect.x - PADDING,
text_field_rect.height)
self._hint_label.render(hint_rect)
# TODO: move to update state
# make rect take up entire area so it's easier to click
@@ -271,10 +187,12 @@ class BigInputDialog(BigDialogBase):
self._top_right_button_rect = rl.Rectangle(text_field_rect.x + text_field_rect.width, self._rect.y,
self._rect.width - (text_field_rect.x + text_field_rect.width), self._top_left_button_rect.height)
self._enter_img_alpha.update(255 if (len(text) >= self._minimum_length) else 255 * 0.35)
if self._enter_img_alpha.x > 1:
color = rl.Color(255, 255, 255, int(self._enter_img_alpha.x))
rl.draw_texture(self._enter_img, int(self._rect.x + 15), int(text_field_rect.y), color)
# draw enter button
self._enter_img_alpha.update(255 if len(text) >= self._minimum_length else 0)
color = rl.Color(255, 255, 255, int(self._enter_img_alpha.x))
rl.draw_texture_ex(self._enter_img, rl.Vector2(self._rect.x + PADDING / 2, self._rect.y), 0.0, 1.0, color)
color = rl.Color(255, 255, 255, 255 - int(self._enter_img_alpha.x))
rl.draw_texture_ex(self._enter_disabled_img, rl.Vector2(self._rect.x + PADDING / 2, self._rect.y), 0.0, 1.0, color)
# keyboard goes over everything
self._keyboard.render(self._rect)
@@ -282,16 +200,17 @@ class BigInputDialog(BigDialogBase):
# draw debugging rect bounds
if DEBUG:
rl.draw_rectangle_lines_ex(text_field_rect, 1, rl.Color(100, 100, 100, 255))
rl.draw_rectangle_lines_ex(text_rect, 1, rl.Color(0, 255, 0, 255))
rl.draw_rectangle_lines_ex(self._top_right_button_rect, 1, rl.Color(0, 255, 0, 255))
rl.draw_rectangle_lines_ex(self._top_left_button_rect, 1, rl.Color(0, 255, 0, 255))
return self._ret
def _handle_mouse_press(self, mouse_pos: MousePos):
super()._handle_mouse_press(mouse_pos)
# TODO: need to track where press was so enter and back can activate on release rather than press
# or turn into icon widgets :eyes_open:
if self.is_dismissing:
return
# handle backspace icon click
if rl.check_collision_point_rec(mouse_pos, self._top_right_button_rect) and self._backspace_img_alpha.x > 254:
self._keyboard.backspace()
@@ -300,6 +219,30 @@ class BigInputDialog(BigDialogBase):
self._confirm_callback()
class BigDialogButton(BigButton):
def __init__(self, text: str, value: str = "", icon: Union[str, rl.Texture] = "", description: str = ""):
super().__init__(text, value, icon)
self._description = description
def _handle_mouse_release(self, mouse_pos: MousePos):
super()._handle_mouse_release(mouse_pos)
dlg = BigDialog(self.text, self._description)
gui_app.push_widget(dlg)
class BigConfirmationCircleButton(BigCircleButton):
def __init__(self, title: str, icon: rl.Texture, confirm_callback: Callable[[], None], exit_on_confirm: bool = True,
red: bool = False, icon_offset: tuple[int, int] = (0, 0)):
super().__init__(icon, red, icon_offset)
def show_confirm_dialog():
gui_app.push_widget(BigConfirmationDialog(title, icon, confirm_callback,
exit_on_confirm=exit_on_confirm, red=red))
self.set_click_callback(show_confirm_dialog)
class BigDialogOptionButton(Widget):
HEIGHT = 64
SELECTED_HEIGHT = 74
@@ -308,12 +251,15 @@ class BigDialogOptionButton(Widget):
super().__init__()
self.option = option
self.set_rect(rl.Rectangle(0, 0, int(gui_app.width / 2 + 220), self.HEIGHT))
self._selected = False
self._label = UnifiedLabel(option, font_size=70, text_color=rl.Color(255, 255, 255, int(255 * 0.58)),
font_weight=FontWeight.DISPLAY_REGULAR, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE,
scroll=True)
self._label = UnifiedLabel(
option,
font_size=70,
text_color=rl.Color(255, 255, 255, int(255 * 0.58)),
font_weight=FontWeight.DISPLAY_REGULAR,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE,
scroll=True,
)
def show_event(self):
super().show_event()
@@ -324,10 +270,6 @@ class BigDialogOptionButton(Widget):
self._rect.height = self.SELECTED_HEIGHT if selected else self.HEIGHT
def _render(self, _):
if DEBUG:
rl.draw_rectangle_lines_ex(self._rect, 1, rl.Color(0, 255, 0, 255))
# FIXME: offset x by -45 because scroller centers horizontally
if self._selected:
self._label.set_font_size(self.SELECTED_HEIGHT)
self._label.set_color(rl.Color(255, 255, 255, int(255 * 0.9)))
@@ -340,56 +282,63 @@ class BigDialogOptionButton(Widget):
self._label.render(self._rect)
class BigMultiOptionDialog(BigDialogBase):
BACK_TOUCH_AREA_PERCENTAGE = 0.1
class BigMultiOptionDialog(NavWidget):
BACK_TOUCH_AREA_PERCENTAGE = 0.25
def __init__(self, options: list[str], default: str | None,
right_btn: str | None = 'check', right_btn_callback: Callable[[], None] = None):
super().__init__(right_btn, right_btn_callback=right_btn_callback)
right_btn: str | None = "check", right_btn_callback: Callable[[], None] | None = None):
super().__init__()
self._options = options
if default is not None:
assert default in options
if default not in options:
default = options[0] if options else None
self._right_btn_callback = right_btn_callback
self._default_option: str | None = default
self._selected_option: str = self._default_option or (options[0] if len(options) > 0 else "")
self._selected_option: str = self._default_option or (options[0] if options else "")
self._last_selected_option: str = self._selected_option
# Widget doesn't differentiate between click and drag
self._can_click = True
self._scroller = Scroller([], horizontal=False, pad_start=100, pad_end=100, spacing=0, snap_items=True)
self._scroller = self._child(Scroller(horizontal=False, pad=100, spacing=0, snap_items=True,
scroll_indicator=False, edge_shadows=False))
self._scroll_inner = self._scroller._scroller
self._right_btn = self._child(SideButton(right_btn or "check")) if right_btn is not None else None
if self._right_btn is not None:
self._scroller.set_enabled(lambda: not cast(Widget, self._right_btn).is_pressed)
self._right_btn.set_click_callback(self._confirm_selection)
self._scroller.set_enabled(lambda: self.enabled and not self.is_dismissing and not self._right_btn.is_pressed)
else:
self._scroller.set_enabled(lambda: self.enabled and not self.is_dismissing)
for option in options:
self._scroller.add_widget(BigDialogOptionButton(option))
self._scroll_inner.add_widget(BigDialogOptionButton(option))
def show_event(self):
super().show_event()
self._scroller.show_event()
if self._default_option is not None:
self._on_option_selected(self._default_option)
def get_selected_option(self) -> str:
return self._selected_option
def _confirm_selection(self):
self.dismiss(self._right_btn_callback)
def _on_option_selected(self, option: str):
y_pos = 0.0
for btn in self._scroller._items:
for btn in self._scroll_inner.items:
btn = cast(BigDialogOptionButton, btn)
if btn.option == option:
rect_center_y = self._rect.y + self._rect.height / 2
if btn._selected:
height = btn.rect.height
else:
# when selecting an option under current, account for changing heights
btn_center_y = btn.rect.y + btn.rect.height / 2 # not accurate, just to determine direction
btn_center_y = btn.rect.y + btn.rect.height / 2
height_offset = BigDialogOptionButton.SELECTED_HEIGHT - BigDialogOptionButton.HEIGHT
height = (BigDialogOptionButton.HEIGHT - height_offset) if rect_center_y < btn_center_y else BigDialogOptionButton.SELECTED_HEIGHT
y_pos = rect_center_y - (btn.rect.y + height / 2)
break
self._scroller.scroll_to(-y_pos)
self._scroll_inner.scroll_to(-y_pos)
def _selected_option_changed(self):
pass
@@ -400,9 +349,7 @@ class BigMultiOptionDialog(BigDialogBase):
def _handle_mouse_event(self, mouse_event: MouseEvent) -> None:
super()._handle_mouse_event(mouse_event)
# # TODO: add generic _handle_mouse_click handler to Widget
if not self._scroller.scroll_panel.is_touch_valid():
if not self._scroll_inner.scroll_panel.is_touch_valid():
self._can_click = False
def _handle_mouse_release(self, mouse_pos: MousePos):
@@ -411,8 +358,7 @@ class BigMultiOptionDialog(BigDialogBase):
if not self._can_click:
return
# select current option
for btn in self._scroller._items:
for btn in self._scroll_inner.items:
btn = cast(BigDialogOptionButton, btn)
if btn.option == self._selected_option:
self._on_option_selected(btn.option)
@@ -420,39 +366,27 @@ class BigMultiOptionDialog(BigDialogBase):
def _update_state(self):
super()._update_state()
if not self.is_dismissing:
self._nav_bar.set_alpha(1.0)
# get selection by whichever button is closest to center
center_y = self._rect.y + self._rect.height / 2
closest_btn = (None, float('inf'))
for btn in self._scroller._items:
closest_btn = (None, float("inf"))
for btn in self._scroll_inner.items:
dist_y = abs((btn.rect.y + btn.rect.height / 2) - center_y)
if dist_y < closest_btn[1]:
closest_btn = (btn, dist_y)
if closest_btn[0]:
for btn in self._scroller._items:
if closest_btn[0] is not None:
for btn in self._scroll_inner.items:
btn.set_selected(btn.option == closest_btn[0].option)
self._selected_option = closest_btn[0].option
# Signal to subclasses if selection changed
if self._selected_option != self._last_selected_option:
self._selected_option_changed()
self._last_selected_option = self._selected_option
def _render(self, _):
super()._render(_)
if self._right_btn is not None:
self._right_btn.set_position(self._rect.x + self._rect.width - self._right_btn.rect.width, self._rect.y)
self._right_btn.render()
self._scroller.render(self._rect)
return self._ret
class BigDialogButton(BigButton):
def __init__(self, text: str, value: str = "", icon: Union[str, rl.Texture] = "", description: str = ""):
super().__init__(text, value, icon)
self._description = description
def _handle_mouse_release(self, mouse_pos: MousePos):
super()._handle_mouse_release(mouse_pos)
dlg = BigDialog(self.text, self._description)
gui_app.set_modal_overlay(dlg)
+11 -16
View File
@@ -7,9 +7,9 @@ from openpilot.common.api import Api
from openpilot.common.swaglog import cloudlog
from openpilot.common.params import Params
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.widgets import NavWidget
from openpilot.system.ui.widgets.nav_widget import NavWidget
from openpilot.system.ui.lib.application import FontWeight, gui_app
from openpilot.system.ui.widgets.label import MiciLabel
from openpilot.system.ui.widgets.label import UnifiedLabel
class PairingDialog(NavWidget):
@@ -19,14 +19,12 @@ class PairingDialog(NavWidget):
def __init__(self):
super().__init__()
self.set_back_callback(lambda: gui_app.set_modal_overlay(None))
self._params = Params()
self._qr_texture: rl.Texture | None = None
self._last_qr_generation = float("-inf")
self._txt_pair = gui_app.texture("icons_mici/settings/device/pair.png", 84, 64)
self._pair_label = MiciLabel("pair with comma connect", 48, font_weight=FontWeight.BOLD,
color=rl.Color(255, 255, 255, int(255 * 0.9)), line_height=40, wrap_text=True)
self._txt_pair = gui_app.texture("icons_mici/settings/device/pair.png", 33, 60)
self._pair_label = UnifiedLabel("pair with comma connect", font_size=48, font_weight=FontWeight.BOLD, line_height=0.8)
def _get_pairing_url(self) -> str:
try:
@@ -69,24 +67,22 @@ class PairingDialog(NavWidget):
def _update_state(self):
super()._update_state()
if ui_state.prime_state.is_paired():
self._playing_dismiss_animation = True
if ui_state.prime_state.is_paired() and not self.is_dismissing:
self.dismiss()
def _render(self, rect: rl.Rectangle) -> int:
def _render(self, rect: rl.Rectangle):
self._check_qr_refresh()
self._render_qr_code()
label_x = self._rect.x + 8 + self._rect.height + 24
self._pair_label.set_width(int(self._rect.width - label_x))
self._pair_label.set_max_width(int(self._rect.width - label_x))
self._pair_label.set_position(label_x, self._rect.y + 16)
self._pair_label.render()
rl.draw_texture_ex(self._txt_pair, rl.Vector2(label_x, self._rect.y + self._rect.height - self._txt_pair.height - 16),
0.0, 1.0, rl.Color(255, 255, 255, int(255 * 0.35)))
return -1
def _render_qr_code(self) -> None:
if not self._qr_texture:
error_font = gui_app.font(FontWeight.BOLD)
@@ -96,7 +92,7 @@ class PairingDialog(NavWidget):
return
scale = self._rect.height / self._qr_texture.height
pos = rl.Vector2(self._rect.x + 8, self._rect.y)
pos = rl.Vector2(round(self._rect.x + 8), round(self._rect.y))
rl.draw_texture_ex(self._qr_texture, pos, 0.0, scale, rl.WHITE)
def __del__(self):
@@ -107,10 +103,9 @@ class PairingDialog(NavWidget):
if __name__ == "__main__":
gui_app.init_window("pairing device")
pairing = PairingDialog()
gui_app.push_widget(pairing)
try:
for _ in gui_app.render():
result = pairing.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
if result != -1:
break
pass
finally:
del pairing
+5 -10
View File
@@ -1,11 +1,8 @@
import pyray as rl
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.lib.application import gui_app
# ---------------------------------------------------------------------------
# Constants extracted from the original Qt style
# ---------------------------------------------------------------------------
# TODO: this should be corrected, but Scroller relies on this being incorrect :/
WIDTH, HEIGHT = 112, 240
@@ -15,17 +12,15 @@ class SideButton(Widget):
self.type = btn_type
self.set_rect(rl.Rectangle(0, 0, WIDTH, HEIGHT))
# load pre-rendered button images
if btn_type not in ("check", "back"):
btn_type = "back"
btn_img_path = f"icons_mici/buttons/button_side_{btn_type}.png"
btn_img_pressed_path = f"icons_mici/buttons/button_side_{btn_type}_pressed.png"
self._txt_btn, self._txt_btn_back = gui_app.texture(btn_img_path, 100, 224), gui_app.texture(btn_img_pressed_path, 100, 224)
self._txt_btn = gui_app.texture(btn_img_path, 100, 224)
self._txt_btn_back = gui_app.texture(btn_img_pressed_path, 100, 224)
def _render(self, _) -> bool:
def _render(self, _):
x = int(self._rect.x + 12)
y = int(self._rect.y + (self._rect.height - self._txt_btn.height) / 2)
rl.draw_texture(self._txt_btn if not self.is_pressed else self._txt_btn_back,
x, y, rl.WHITE)
rl.draw_texture(self._txt_btn if not self.is_pressed else self._txt_btn_back, x, y, rl.WHITE)
return False