mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-08-22 00:33:44 +08:00
Keeb
This commit is contained in:
@@ -225,6 +225,7 @@ class GuiApplication:
|
||||
self._modal_overlay = ModalOverlay()
|
||||
self._modal_overlay_shown = False
|
||||
self._modal_overlay_tick: Callable[[], None] | None = None
|
||||
self._nav_stack: list = []
|
||||
|
||||
self._mouse = MouseState(self._scale)
|
||||
self._mouse_events: list[MouseEvent] = []
|
||||
@@ -369,6 +370,41 @@ class GuiApplication:
|
||||
def set_modal_overlay_tick(self, tick_function: Callable | None):
|
||||
self._modal_overlay_tick = tick_function
|
||||
|
||||
def push_widget(self, widget):
|
||||
if widget in self._nav_stack:
|
||||
return
|
||||
if self._nav_stack:
|
||||
prev = self._nav_stack[-1]
|
||||
if hasattr(prev, 'set_enabled'):
|
||||
prev.set_enabled(False)
|
||||
self._nav_stack.append(widget)
|
||||
if hasattr(widget, 'show_event'):
|
||||
widget.show_event()
|
||||
if hasattr(widget, 'set_enabled'):
|
||||
widget.set_enabled(True)
|
||||
|
||||
def pop_widget(self, idx: int | None = None):
|
||||
if len(self._nav_stack) < 2:
|
||||
return
|
||||
idx_to_pop = len(self._nav_stack) - 1 if idx is None else idx
|
||||
if idx_to_pop <= 0 or idx_to_pop >= len(self._nav_stack):
|
||||
return
|
||||
if idx_to_pop == len(self._nav_stack) - 1:
|
||||
prev = self._nav_stack[idx_to_pop - 1]
|
||||
if hasattr(prev, 'set_enabled'):
|
||||
prev.set_enabled(True)
|
||||
widget = self._nav_stack.pop(idx_to_pop)
|
||||
if hasattr(widget, 'hide_event'):
|
||||
widget.hide_event()
|
||||
|
||||
def _render_nav_stack(self) -> bool:
|
||||
if not self._nav_stack:
|
||||
return False
|
||||
widget = self._nav_stack[-1]
|
||||
if hasattr(widget, 'render'):
|
||||
widget.render(rl.Rectangle(0, 0, self.width, self.height))
|
||||
return True
|
||||
|
||||
def set_should_render(self, should_render: bool):
|
||||
self._should_render = should_render
|
||||
|
||||
@@ -523,7 +559,9 @@ class GuiApplication:
|
||||
rl.clear_background(rl.BLACK)
|
||||
|
||||
# Handle modal overlay rendering and input processing
|
||||
if self._handle_modal_overlay():
|
||||
if self._render_nav_stack():
|
||||
yield False
|
||||
elif self._handle_modal_overlay():
|
||||
# Allow a Widget to still run a function while overlay is shown
|
||||
if self._modal_overlay_tick is not None:
|
||||
self._modal_overlay_tick()
|
||||
|
||||
@@ -34,6 +34,7 @@ class Widget(abc.ABC):
|
||||
self._click_callback: Callable[[], None] | None = None
|
||||
self._multi_touch = False
|
||||
self.__was_awake = True
|
||||
self._children: list = []
|
||||
|
||||
@property
|
||||
def rect(self) -> rl.Rectangle:
|
||||
@@ -180,9 +181,25 @@ class Widget(abc.ABC):
|
||||
|
||||
def show_event(self):
|
||||
"""Optionally handle show event. Parent must manually call this"""
|
||||
for child in self._children:
|
||||
child.show_event()
|
||||
|
||||
def hide_event(self):
|
||||
"""Optionally handle hide event. Parent must manually call this"""
|
||||
for child in self._children:
|
||||
child.hide_event()
|
||||
|
||||
def _child(self, widget):
|
||||
"""Register a child widget for lifecycle propagation."""
|
||||
assert widget not in self._children, f"{type(widget).__name__} already a child of {type(self).__name__}"
|
||||
self._children.append(widget)
|
||||
return widget
|
||||
|
||||
def dismiss(self, callback: Callable[[], None] | None = None):
|
||||
"""Dismiss this widget from the nav stack."""
|
||||
gui_app.pop_widget()
|
||||
if callback:
|
||||
callback()
|
||||
|
||||
|
||||
SWIPE_AWAY_THRESHOLD = 80 # px to dismiss after releasing
|
||||
|
||||
@@ -1,118 +1,43 @@
|
||||
import pyray as rl
|
||||
from collections.abc import Callable
|
||||
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.widgets import Widget, DialogResult
|
||||
from openpilot.system.ui.widgets.button import Button, ButtonStyle
|
||||
from openpilot.system.ui.widgets.label import Label, FontWeight
|
||||
from openpilot.system.ui.widgets.keyboard import Keyboard, KeyboardLayout
|
||||
from openpilot.system.ui.widgets.keyboard import Keyboard
|
||||
|
||||
MARGIN = 50
|
||||
BUTTON_HEIGHT = 160
|
||||
OUTER_MARGIN_X = 200
|
||||
OUTER_MARGIN_Y = 150
|
||||
BACKGROUND_COLOR = rl.Color(27, 27, 27, 255)
|
||||
|
||||
class InputDialog(Widget):
|
||||
def __init__(self, title: str, default_text: str = "", hint_text: str = "", on_close: Callable[[DialogResult, str], None] | None = None):
|
||||
super().__init__()
|
||||
self._title = title
|
||||
self._text = default_text
|
||||
self._hint = hint_text
|
||||
self._default_text = default_text
|
||||
self._on_close = on_close
|
||||
|
||||
self._dialog_result = DialogResult.NO_ACTION
|
||||
|
||||
self._title_label = Label(title, 70, FontWeight.BOLD, text_color=rl.Color(201, 201, 201, 255))
|
||||
self._cancel_button = Button("Cancel", self._cancel_button_callback)
|
||||
self._confirm_button = Button("Confirm", self._confirm_button_callback, button_style=ButtonStyle.PRIMARY)
|
||||
|
||||
self._keyboard = Keyboard(self._on_key_pressed, self._on_keyboard_done, layout=KeyboardLayout.QWERTY)
|
||||
|
||||
self._font = gui_app.font(FontWeight.MEDIUM)
|
||||
|
||||
def _on_key_pressed(self, key: str):
|
||||
if key == "\b":
|
||||
self._text = self._text[:-1]
|
||||
else:
|
||||
self._text += key
|
||||
self._keyboard = Keyboard(callback=self._on_keyboard_result)
|
||||
self._keyboard.set_title(title)
|
||||
self._keyboard.set_text(default_text)
|
||||
|
||||
def _on_keyboard_done(self):
|
||||
self._confirm_button_callback()
|
||||
|
||||
def _cancel_button_callback(self):
|
||||
self._dialog_result = DialogResult.CANCEL
|
||||
def _on_keyboard_result(self, result: DialogResult):
|
||||
if self._dialog_result != DialogResult.NO_ACTION:
|
||||
return
|
||||
self._dialog_result = result
|
||||
if self._on_close:
|
||||
self._on_close(self._dialog_result, self._text)
|
||||
|
||||
def _confirm_button_callback(self):
|
||||
self._dialog_result = DialogResult.CONFIRM
|
||||
if self._on_close:
|
||||
self._on_close(self._dialog_result, self._text)
|
||||
self._on_close(result, self._keyboard.text)
|
||||
|
||||
@property
|
||||
def result(self) -> DialogResult:
|
||||
return self._dialog_result
|
||||
|
||||
|
||||
@property
|
||||
def text(self) -> str:
|
||||
return self._text
|
||||
return self._keyboard.text
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._dialog_result = DialogResult.NO_ACTION
|
||||
self._keyboard.show_event()
|
||||
self._keyboard.clear()
|
||||
if self._default_text:
|
||||
self._keyboard.set_text(self._default_text)
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
# Dim background
|
||||
rl.draw_rectangle(0, 0, int(rect.width), int(rect.height), rl.Color(0, 0, 0, 200))
|
||||
|
||||
# Dialog Box
|
||||
dialog_rect = rl.Rectangle(
|
||||
rect.x + OUTER_MARGIN_X,
|
||||
rect.y + OUTER_MARGIN_Y,
|
||||
rect.width - 2 * OUTER_MARGIN_X,
|
||||
rect.height - 2 * OUTER_MARGIN_Y,
|
||||
)
|
||||
rl.draw_rectangle_rounded(dialog_rect, 0.05, 10, BACKGROUND_COLOR)
|
||||
|
||||
# Title
|
||||
title_rect = rl.Rectangle(dialog_rect.x + MARGIN, dialog_rect.y + MARGIN, dialog_rect.width - 2 * MARGIN, 100)
|
||||
self._title_label.render(title_rect)
|
||||
|
||||
# Text Input Field
|
||||
input_rect = rl.Rectangle(dialog_rect.x + MARGIN, title_rect.y + title_rect.height + 40, dialog_rect.width - 2 * MARGIN, 120)
|
||||
rl.draw_rectangle_rounded(input_rect, 0.1, 10, rl.Color(40, 40, 40, 255))
|
||||
|
||||
display_text = self._text
|
||||
text_color = rl.WHITE
|
||||
if not display_text:
|
||||
display_text = self._hint
|
||||
text_color = rl.Color(128, 128, 128, 255)
|
||||
|
||||
text_size = rl.measure_text_ex(self._font, display_text, 50, 0)
|
||||
text_pos = rl.Vector2(input_rect.x + 40, input_rect.y + (input_rect.height - text_size.y) / 2)
|
||||
rl.draw_text_ex(self._font, display_text, text_pos, 50, 0, text_color)
|
||||
|
||||
# Blinking cursor
|
||||
if (rl.get_time() % 1.0) < 0.5:
|
||||
cursor_x = text_pos.x + (text_size.x if self._text else 0) + 5
|
||||
rl.draw_rectangle(int(cursor_x), int(text_pos.y), 4, 50, rl.WHITE)
|
||||
|
||||
# Keyboard
|
||||
keyboard_rect = rl.Rectangle(
|
||||
dialog_rect.x + MARGIN,
|
||||
input_rect.y + input_rect.height + 40,
|
||||
dialog_rect.width - 2 * MARGIN,
|
||||
400
|
||||
)
|
||||
self._keyboard.render(keyboard_rect)
|
||||
|
||||
# Buttons
|
||||
btn_y = dialog_rect.y + dialog_rect.height - BUTTON_HEIGHT - MARGIN
|
||||
btn_width = (dialog_rect.width - 3 * MARGIN) / 2
|
||||
|
||||
cancel_rect = rl.Rectangle(dialog_rect.x + MARGIN, btn_y, btn_width, BUTTON_HEIGHT)
|
||||
confirm_rect = rl.Rectangle(dialog_rect.x + 2 * MARGIN + btn_width, btn_y, btn_width, BUTTON_HEIGHT)
|
||||
|
||||
self._cancel_button.render(cancel_rect)
|
||||
self._confirm_button.render(confirm_rect)
|
||||
def _render(self, rect):
|
||||
self._keyboard.render(rect)
|
||||
return self._dialog_result
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
from functools import partial
|
||||
import time
|
||||
from typing import Literal
|
||||
from collections.abc import Callable
|
||||
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets import Widget, DialogResult
|
||||
from openpilot.system.ui.widgets.button import ButtonStyle, Button
|
||||
from openpilot.system.ui.widgets.inputbox import InputBox
|
||||
from openpilot.system.ui.widgets.label import Label
|
||||
@@ -58,7 +59,14 @@ KEYBOARD_LAYOUTS = {
|
||||
|
||||
|
||||
class Keyboard(Widget):
|
||||
def __init__(self, max_text_size: int = 255, min_text_size: int = 0, password_mode: bool = False, show_password_toggle: bool = False):
|
||||
def __init__(
|
||||
self,
|
||||
max_text_size: int = 255,
|
||||
min_text_size: int = 0,
|
||||
password_mode: bool = False,
|
||||
show_password_toggle: bool = False,
|
||||
callback: Callable[[DialogResult], None] | None = None,
|
||||
):
|
||||
super().__init__()
|
||||
self._layout_name: Literal["lowercase", "uppercase", "numbers", "specials"] = "lowercase"
|
||||
self._caps_lock = False
|
||||
@@ -71,6 +79,7 @@ class Keyboard(Widget):
|
||||
self._input_box = InputBox(max_text_size)
|
||||
self._password_mode = password_mode
|
||||
self._show_password_toggle = show_password_toggle
|
||||
self._callback = callback
|
||||
|
||||
# Backspace key repeat tracking
|
||||
self._backspace_pressed: bool = False
|
||||
@@ -78,6 +87,8 @@ class Keyboard(Widget):
|
||||
self._backspace_last_repeat: float = 0.0
|
||||
|
||||
self._render_return_status = -1
|
||||
self._first_render = False
|
||||
self._skip_input = False
|
||||
self._cancel_button = Button(lambda: tr("Cancel"), self._cancel_button_callback)
|
||||
|
||||
self._eye_button = Button("", self._eye_button_callback, button_style=ButtonStyle.TRANSPARENT)
|
||||
@@ -98,12 +109,18 @@ class Keyboard(Widget):
|
||||
for _, key in enumerate(keys):
|
||||
if key in self._key_icons:
|
||||
texture = self._key_icons[key]
|
||||
self._all_keys[key] = Button("", partial(self._key_callback, key), icon=texture,
|
||||
button_style=ButtonStyle.PRIMARY if key == ENTER_KEY else ButtonStyle.KEYBOARD, multi_touch=True)
|
||||
self._all_keys[key] = Button(
|
||||
"",
|
||||
partial(self._key_callback, key),
|
||||
icon=texture,
|
||||
button_style=ButtonStyle.PRIMARY if key == ENTER_KEY else ButtonStyle.KEYBOARD,
|
||||
multi_touch=True,
|
||||
)
|
||||
else:
|
||||
self._all_keys[key] = Button(key, partial(self._key_callback, key), button_style=ButtonStyle.KEYBOARD, font_size=85, multi_touch=True)
|
||||
self._all_keys[CAPS_LOCK_KEY] = Button("", partial(self._key_callback, CAPS_LOCK_KEY), icon=self._key_icons[CAPS_LOCK_KEY],
|
||||
button_style=ButtonStyle.KEYBOARD, multi_touch=True)
|
||||
self._all_keys[CAPS_LOCK_KEY] = Button(
|
||||
"", partial(self._key_callback, CAPS_LOCK_KEY), icon=self._key_icons[CAPS_LOCK_KEY], button_style=ButtonStyle.KEYBOARD, multi_touch=True
|
||||
)
|
||||
|
||||
def set_text(self, text: str):
|
||||
self._input_box.text = text
|
||||
@@ -122,20 +139,42 @@ class Keyboard(Widget):
|
||||
self._title.set_text(title)
|
||||
self._sub_title.set_text(sub_title)
|
||||
|
||||
def _eye_button_callback(self):
|
||||
self._password_mode = not self._password_mode
|
||||
def set_callback(self, callback: Callable[[DialogResult], None] | None):
|
||||
self._callback = callback
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._skip_input = True
|
||||
|
||||
def _process_mouse_events(self):
|
||||
if not self._skip_input:
|
||||
super()._process_mouse_events()
|
||||
|
||||
def _cancel_button_callback(self):
|
||||
self.clear()
|
||||
self._render_return_status = 0
|
||||
if self in gui_app._nav_stack:
|
||||
gui_app.pop_widget()
|
||||
else:
|
||||
self._render_return_status = 0
|
||||
if self._callback:
|
||||
self._callback(DialogResult.CANCEL)
|
||||
|
||||
def _eye_button_callback(self):
|
||||
self._password_mode = not self._password_mode
|
||||
|
||||
def _key_callback(self, k):
|
||||
if k == ENTER_KEY:
|
||||
self._render_return_status = 1
|
||||
if self in gui_app._nav_stack:
|
||||
gui_app.pop_widget()
|
||||
else:
|
||||
self._render_return_status = 1
|
||||
if self._callback:
|
||||
self._callback(DialogResult.CONFIRM)
|
||||
else:
|
||||
self.handle_key_press(k)
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
self._skip_input = False
|
||||
rect = rl.Rectangle(rect.x + CONTENT_MARGIN, rect.y + CONTENT_MARGIN, rect.width - 2 * CONTENT_MARGIN, rect.height - 2 * CONTENT_MARGIN)
|
||||
self._title.render(rl.Rectangle(rect.x, rect.y, rect.width, 95))
|
||||
self._sub_title.render(rl.Rectangle(rect.x, rect.y + 95, rect.width, 60))
|
||||
|
||||
Reference in New Issue
Block a user