mirror of
https://github.com/ajouatom/openpilot.git
synced 2026-06-08 11:04:57 +08:00
241 lines
10 KiB
Python
241 lines
10 KiB
Python
import abc
|
|
import math
|
|
import pyray as rl
|
|
from typing import Union
|
|
from collections.abc import Callable
|
|
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.application import gui_app, FontWeight, MousePos
|
|
from openpilot.system.ui.widgets.slider import RedBigSlider, BigSlider
|
|
from openpilot.common.filter_simple import FirstOrderFilter
|
|
from openpilot.selfdrive.ui.mici.widgets.button import BigCircleButton, BigButton, GreyBigButton
|
|
|
|
DEBUG = False
|
|
|
|
PADDING = 20
|
|
|
|
|
|
class BigDialogBase(NavWidget, abc.ABC):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
|
|
|
|
|
|
class BigDialog(BigDialogBase):
|
|
def __init__(self, title: str, description: str, icon: Union[rl.Texture, None] = None):
|
|
super().__init__()
|
|
self._card = GreyBigButton(title, description, icon)
|
|
|
|
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 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
|
|
|
|
self._slider: BigSlider | RedBigSlider
|
|
if red:
|
|
self._slider = self._child(RedBigSlider(title, icon, confirm_callback=self._on_confirm))
|
|
else:
|
|
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._exit_on_confirm:
|
|
self.dismiss(self._confirm_callback)
|
|
elif self._confirm_callback:
|
|
self._confirm_callback()
|
|
|
|
def _update_state(self):
|
|
super()._update_state()
|
|
if self.is_dismissing and not self._slider.confirmed:
|
|
self._slider.reset()
|
|
|
|
def _render(self, _):
|
|
self._slider.render(self._rect)
|
|
|
|
|
|
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 = 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(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", 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/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
|
|
self._top_left_button_rect = rl.Rectangle(0, 0, 0, 0)
|
|
self._top_right_button_rect = rl.Rectangle(0, 0, 0, 0)
|
|
|
|
def confirm_callback_wrapper():
|
|
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:
|
|
self._backspace_held_time = rl.get_time()
|
|
|
|
if rl.get_time() - self._backspace_held_time > 0.5:
|
|
if gui_app.frame % round(gui_app.target_fps / self.BACKSPACE_RATE) == 0:
|
|
self._keyboard.backspace()
|
|
|
|
else:
|
|
self._backspace_held_time = None
|
|
|
|
def _render(self, _):
|
|
# 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, self.TEXT_INPUT_SIZE)
|
|
|
|
bg_block_margin = 5
|
|
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_field_rect.width:
|
|
text_x -= text_size.x - text_field_rect.width
|
|
|
|
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, self.TEXT_INPUT_SIZE)
|
|
rl.draw_text_ex(gui_app.font(FontWeight.ROMAN), candidate_char,
|
|
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_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:
|
|
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_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
|
|
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
|
|
self._top_left_button_rect = rl.Rectangle(self._rect.x, self._rect.y, text_field_rect.x, self._rect.height - self._keyboard.get_keyboard_height())
|
|
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)
|
|
|
|
# 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)
|
|
|
|
# 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(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))
|
|
|
|
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()
|
|
elif rl.check_collision_point_rec(mouse_pos, self._top_left_button_rect) and self._enter_img_alpha.x > 254:
|
|
# handle enter icon click
|
|
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)
|