Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions selfdrive/ui/bp/mici/layouts/home_bp.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
place; a top progress strip fills as you hold. Tap on body opens settings.

Public API mirrors MiciHomeLayout so main.py wiring works unchanged:
- set_callbacks(on_settings: Callable | None = None)
- set_callbacks(on_settings, on_alerts, alert_count_callback, max_severity_callback)
"""
import time
import pyray as rl
Expand All @@ -17,7 +17,7 @@
from openpilot.system.ui.widgets.icon_widget import IconWidget
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.selfdrive.ui.mici.layouts.home import NetworkIcon, NETWORK_TYPES
from openpilot.selfdrive.ui.mici.layouts.home import AlertsPill, NetworkIcon, NETWORK_TYPES
from openpilot.selfdrive.ui.bp.mici.widgets.bg_radial import BPRadialBackground
from openpilot.selfdrive.ui.bp.mici.widgets.aurora_wordmark import AuroraWordmark
from openpilot.selfdrive.ui.bp.mici.widgets.state_pill import StatePill
Expand All @@ -37,6 +37,8 @@ class MiciHomeLayoutBP(Widget):
def __init__(self):
super().__init__()
self._on_settings_click: Callable | None = None
self._on_alerts_click: Callable | None = None
self._alert_count_callback: Callable[[], int] | None = None

# Long-press state
self._mouse_down_t: float | None = None
Expand All @@ -52,6 +54,7 @@ def __init__(self):
self._bg = self._child(BPRadialBackground())
self._aurora = self._child(AuroraWordmark(get_mode=self._mode))
self._pill = self._child(StatePill(get_state=self._pill_state))
self._alerts_pill = self._child(AlertsPill())
self._long_press_bar = self._child(LongPressBar())

# Gear (top-right)
Expand All @@ -70,8 +73,13 @@ def __init__(self):
)

# ---- public API mirroring MiciHomeLayout ----
def set_callbacks(self, on_settings: Callable | None = None):
def set_callbacks(self, on_settings: Callable | None = None, on_alerts: Callable | None = None,
alert_count_callback: Callable[[], int] | None = None,
max_severity_callback: Callable[[], int | None] | None = None):
self._on_settings_click = on_settings
self._on_alerts_click = on_alerts
self._alert_count_callback = alert_count_callback
self._alerts_pill.set_alert_count_callback(alert_count_callback, max_severity_callback)

# ---- mode helpers ----
def _mode(self) -> str:
Expand Down Expand Up @@ -128,6 +136,11 @@ def _handle_mouse_release(self, mouse_pos: MousePos):
if self._did_long_press:
self._did_long_press = False
return
# Tap on alerts pill → alerts pane.
if self._point_in_alerts(mouse_pos):
if self._on_alerts_click:
self._on_alerts_click()
return
# Tap on gear → settings.
if self._point_in_gear(mouse_pos):
if self._on_settings_click:
Expand All @@ -147,6 +160,10 @@ def _gear_rect(self) -> rl.Rectangle:
def _point_in_gear(self, p: MousePos) -> bool:
return rl.check_collision_point_rec(p, self._gear_rect())

def _point_in_alerts(self, p: MousePos) -> bool:
has_alerts = self._alert_count_callback and self._alert_count_callback() > 0
return bool(has_alerts and rl.check_collision_point_rec(p, self._alerts_pill.rect))

def _gear_pressed(self) -> bool:
"""Is the user currently pressing inside the gear hit rect?"""
rect = self._gear_rect()
Expand Down Expand Up @@ -188,6 +205,11 @@ def _render(self, _):
visible_y = g.y + (g.height - GEAR_VISIBLE) / 2
self._gear_icon.render(rl.Rectangle(visible_x, visible_y, GEAR_VISIBLE, GEAR_VISIBLE))

# Alerts pill below the gear so offroad alerts remain reachable.
alert_rect = self._alerts_pill.rect
self._alerts_pill.set_position(r.x + r.width - alert_rect.width - 10, g.y + g.height + 8)
self._alerts_pill.render()

# Meta strip (bottom-left): network icon + label
meta_y = r.y + r.height - META_FONT_SIZE - 12
icon_rect = rl.Rectangle(r.x + HOME_PADDING_X, meta_y - 4, 54, 30)
Expand Down
2 changes: 1 addition & 1 deletion selfdrive/ui/bp/mici/widgets/aurora_wordmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def _render(self, _):
# Three radial passes give a soft falloff with fixed-cost circle draws.
for r_mul, alpha_mul in ((1.00, 1.00), (0.62, 0.85), (0.35, 0.70)):
col = rl.Color(inner.r, inner.g, inner.b, int(inner.a * alpha_mul))
rl.draw_circle_gradient(int(cx), int(cy), radius * r_mul, col, P.AURORA_OUTER)
rl.draw_circle_gradient(rl.Vector2(int(cx), int(cy)), radius * r_mul, col, P.AURORA_OUTER)

# ---- Wordmark ----
# UnifiedLabel doesn't expose center alignment cleanly with wrap_text=False,
Expand Down
6 changes: 3 additions & 3 deletions selfdrive/ui/bp/mici/widgets/bg_radial.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,16 @@ def _render(self, _):

# Outer fade (62% stop) — barely-perceptible navy lift at the edges of
# the gradient extent.
rl.draw_circle_gradient(cx, cy, diag * 1.2,
rl.draw_circle_gradient(rl.Vector2(cx, cy), diag * 1.2,
rl.Color(0x14, 0x28, 0x5A, int(0.50 * 255)),
rl.Color(0, 0, 0, 0))

# Mid stop (28%) — navy tint covering most of the upper-left half.
rl.draw_circle_gradient(cx, cy, diag * 0.85,
rl.draw_circle_gradient(rl.Vector2(cx, cy), diag * 0.85,
rl.Color(0x14, 0x28, 0x5A, int(0.65 * 255)),
rl.Color(0, 0, 0, 0))

# Inner glow (0%) — bright blue near the corner.
rl.draw_circle_gradient(cx, cy, diag * 0.55,
rl.draw_circle_gradient(rl.Vector2(cx, cy), diag * 0.55,
rl.Color(0x4A, 0x8C, 0xFF, int(0.42 * 255)),
rl.Color(0, 0, 0, 0))
49 changes: 31 additions & 18 deletions selfdrive/ui/bp/mici/widgets/big_input_dialog_bp.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ def __init__(self,
# 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)
self._enter_pressed = False
self._backspace_pressed = False

self._ret = None # Only set to CONFIRM when user taps enter; prevents AttributeError in _render before first confirm
def confirm_callback_wrapper():
Expand All @@ -53,7 +55,7 @@ def _update_state(self):
super()._update_state()

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 last_mouse_event.left_down and self._backspace_pressed and rl.check_collision_point_rec(last_mouse_event.pos, self._top_right_button_rect):
if self._backspace_held_time is None:
self._backspace_held_time = rl.get_time()

Expand Down Expand Up @@ -116,26 +118,33 @@ def _render(self, _):

# draw backspace icon with nice fade
self._backspace_img_alpha.update(255 * bool(text))
backspace_x = int(self._rect.width - self._backspace_img.width - 27)
backspace_y = int(text_field_rect.y)
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._backspace_img.width - 27), int(text_field_rect.y), color)
rl.draw_texture(self._backspace_img, backspace_x, backspace_y, 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)

# 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 (enabled + disabled states, same as stock BigInputDialog)
self._enter_img_alpha.update(255 if (len(text) >= self._minimum_length) else 0)
enter_x = int(self._rect.x + 15)
enter_y = int(text_field_rect.y)
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)
rl.draw_texture(self._enter_img, enter_x, enter_y, color)
color = rl.Color(255, 255, 255, 255 - int(self._enter_img_alpha.x))
rl.draw_texture(self._enter_disabled_img, int(self._rect.x + 15), int(text_field_rect.y), color)
rl.draw_texture(self._enter_disabled_img, enter_x, enter_y, color)

# Use the visible icon bounds with a small padding instead of full header halves.
hit_pad = 14
self._top_left_button_rect = rl.Rectangle(
enter_x - hit_pad, enter_y - hit_pad,
self._enter_img.width + hit_pad * 2, self._enter_img.height + hit_pad * 2)
self._top_right_button_rect = rl.Rectangle(
backspace_x - hit_pad, backspace_y - hit_pad,
self._backspace_img.width + hit_pad * 2, self._backspace_img.height + hit_pad * 2)

# keyboard goes over everything
self._keyboard.render(self._rect)
Expand All @@ -151,11 +160,15 @@ def _render(self, _):

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:
# 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()
self._backspace_pressed = rl.check_collision_point_rec(mouse_pos, self._top_right_button_rect)
self._enter_pressed = rl.check_collision_point_rec(mouse_pos, self._top_left_button_rect)

def _handle_mouse_release(self, mouse_pos: MousePos):
if self._backspace_pressed and rl.check_collision_point_rec(mouse_pos, self._top_right_button_rect) and self._keyboard.text():
if self._backspace_held_time is None:
self._keyboard.backspace()
self._backspace_held_time = None
elif self._enter_pressed and rl.check_collision_point_rec(mouse_pos, self._top_left_button_rect) and len(self._keyboard.text()) >= self._minimum_length:
self._confirm_callback()
self._enter_pressed = False
self._backspace_pressed = False
21 changes: 16 additions & 5 deletions selfdrive/ui/bp/mici/widgets/bp_dialogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,40 +166,51 @@ def __init__(self,

self._top_left_button_rect = rl.Rectangle(0, 0, 0, 0)
self._top_right_button_rect = rl.Rectangle(0, 0, 0, 0)
self._enter_pressed = False
self._backspace_pressed = False

def _do_confirm():
text = self._keyboard.text()
self.dismiss((lambda: confirm_callback(text)) if confirm_callback else None)
self._do_confirm = _do_confirm

# ---- input ----
def _handle_mouse_press(self, mouse_pos: MousePos):
super()._handle_mouse_press(mouse_pos)
self._backspace_pressed = rl.check_collision_point_rec(mouse_pos, self._top_right_button_rect)
self._enter_pressed = rl.check_collision_point_rec(mouse_pos, self._top_left_button_rect)

def _handle_mouse_release(self, mouse_pos: MousePos):
if rl.check_collision_point_rec(mouse_pos, self._top_right_button_rect):
if self._backspace_pressed and rl.check_collision_point_rec(mouse_pos, self._top_right_button_rect):
# Backspace
if self._backspace_held_time is None:
self._keyboard.backspace()
self._backspace_held_time = None
return
if rl.check_collision_point_rec(mouse_pos, self._top_left_button_rect):
elif self._enter_pressed and rl.check_collision_point_rec(mouse_pos, self._top_left_button_rect):
# Enter
if len(self._keyboard.text()) >= self._minimum_length:
self._do_confirm()
return
self._enter_pressed = False
self._backspace_pressed = False

def _update_state(self):
super()._update_state()
if self.is_dismissing:
self._enter_pressed = False
self._backspace_pressed = False
self._backspace_held_time = None
return

# Held backspace repeat (mirrors stock BigInputDialog behavior)
last = gui_app.last_mouse_event
if last.left_down and rl.check_collision_point_rec(last.pos, self._top_right_button_rect):
if last.left_down and self._backspace_pressed and rl.check_collision_point_rec(last.pos, self._top_right_button_rect):
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, _):
r = self._rect
Expand Down
2 changes: 1 addition & 1 deletion selfdrive/ui/bp/mici/widgets/icons.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def _halo(cx: float, cy: float, size: float, color: rl.Color) -> None:
"""Soft blue halo behind an icon. Three concentric circles fading outward."""
for r_mul, alpha in ((1.6, 0.06), (1.1, 0.10), (0.7, 0.12)):
c = rl.Color(color.r, color.g, color.b, int(255 * alpha))
rl.draw_circle_gradient(int(cx), int(cy), size * r_mul, c, rl.Color(0, 0, 0, 0))
rl.draw_circle_gradient(rl.Vector2(int(cx), int(cy)), size * r_mul, c, rl.Color(0, 0, 0, 0))


def _draw_with_halo(draw_fn, cx: float, cy: float, size: float,
Expand Down
Loading