Skip to content
Merged
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
13 changes: 7 additions & 6 deletions selfdrive/ui/layouts/home.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ def show_event(self):
def _setup_callbacks(self):
self.update_alert.set_dismiss_callback(lambda: self._set_state(HomeLayoutState.HOME))
self.offroad_alert.set_dismiss_callback(lambda: self._set_state(HomeLayoutState.HOME))
self._exp_mode_button.set_click_callback(lambda: self.settings_callback() if self.settings_callback else None)

def set_settings_callback(self, callback: Callable):
self.settings_callback = callback
Expand Down Expand Up @@ -147,9 +148,9 @@ def _render_header(self):
rl.draw_rectangle_rounded(self.update_notif_rect, 0.3, 10, highlight_color)

text = "UPDATE"
text_width = measure_text_cached(font, text, HEAD_BUTTON_FONT_SIZE).x
text_x = self.update_notif_rect.x + (self.update_notif_rect.width - text_width) // 2
text_y = self.update_notif_rect.y + (self.update_notif_rect.height - HEAD_BUTTON_FONT_SIZE) // 2
text_size = measure_text_cached(font, text, HEAD_BUTTON_FONT_SIZE)
text_x = self.update_notif_rect.x + (self.update_notif_rect.width - text_size.x) // 2
text_y = self.update_notif_rect.y + (self.update_notif_rect.height - text_size.y) // 2
rl.draw_text_ex(font, text, rl.Vector2(int(text_x), int(text_y)), HEAD_BUTTON_FONT_SIZE, 0, rl.WHITE)

# Alert notification button
Expand All @@ -161,9 +162,9 @@ def _render_header(self):
rl.draw_rectangle_rounded(self.alert_notif_rect, 0.3, 10, highlight_color)

alert_text = f"{self.alert_count} ALERT{'S' if self.alert_count > 1 else ''}"
text_width = measure_text_cached(font, alert_text, HEAD_BUTTON_FONT_SIZE).x
text_x = self.alert_notif_rect.x + (self.alert_notif_rect.width - text_width) // 2
text_y = self.alert_notif_rect.y + (self.alert_notif_rect.height - HEAD_BUTTON_FONT_SIZE) // 2
text_size = measure_text_cached(font, alert_text, HEAD_BUTTON_FONT_SIZE)
text_x = self.alert_notif_rect.x + (self.alert_notif_rect.width - text_size.x) // 2
text_y = self.alert_notif_rect.y + (self.alert_notif_rect.height - text_size.y) // 2
rl.draw_text_ex(font, alert_text, rl.Vector2(int(text_x), int(text_y)), HEAD_BUTTON_FONT_SIZE, 0, rl.WHITE)

# Version text (right aligned)
Expand Down
1 change: 1 addition & 0 deletions selfdrive/ui/layouts/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def _setup_callbacks(self):
on_flag=self._on_bookmark_clicked,
open_settings=lambda: self.open_settings(PanelType.TOGGLES))
self._layouts[MainState.HOME]._setup_widget.set_open_settings_callback(lambda: self.open_settings(PanelType.FIREHOSE))
self._layouts[MainState.HOME].set_settings_callback(lambda: self.open_settings(PanelType.TOGGLES))
self._layouts[MainState.SETTINGS].set_callbacks(on_close=self._set_mode_for_state)
self._layouts[MainState.ONROAD].set_click_callback(self._on_onroad_clicked)
device.add_interactive_timeout_callback(self._set_mode_for_state)
Expand Down
77 changes: 26 additions & 51 deletions selfdrive/ui/layouts/settings/firehose.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
from openpilot.common.swaglog import cloudlog
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
from openpilot.system.ui.lib.wrap_text import wrap_text
from openpilot.system.ui.widgets import Widget
Expand All @@ -21,7 +22,7 @@
)
INSTRUCTIONS = (
"For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.\n\n"
+ "Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.\n\n"
+ "Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.\n\n\n"
+ "Frequently Asked Questions\n\n"
+ "Does it matter how or where I drive? Nope, just drive as you normally would.\n\n"
+ "Do all of my segments get pulled in Firehose Mode? No, we selectively pull a subset of your segments.\n\n"
Expand All @@ -43,6 +44,7 @@ def __init__(self):
self.params = Params()
self.segment_count = self._get_segment_count()
self.scroll_panel = GuiScrollPanel()
self._content_height = 0

self.running = True
self.update_thread = threading.Thread(target=self._update_loop, daemon=True)
Expand All @@ -69,88 +71,61 @@ def __del__(self):

def _render(self, rect: rl.Rectangle):
# Calculate content dimensions
content_width = rect.width - 80
content_height = self._calculate_content_height(int(content_width))
content_rect = rl.Rectangle(rect.x, rect.y, rect.width, content_height)
content_rect = rl.Rectangle(rect.x, rect.y, rect.width, self._content_height)

# Handle scrolling and render with clipping
scroll_offset = self.scroll_panel.update(rect, content_rect)
rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height))
self._render_content(rect, scroll_offset)
self._content_height = self._render_content(rect, scroll_offset)
rl.end_scissor_mode()

def _calculate_content_height(self, content_width: int) -> int:
height = 80 # Top margin

# Title
height += 100 + 40

# Description
desc_font = gui_app.font(FontWeight.NORMAL)
desc_lines = wrap_text(desc_font, DESCRIPTION, 45, content_width)
height += len(desc_lines) * 45 + 40

# Status section
height += 32 # Separator
status_text, _ = self._get_status()
status_lines = wrap_text(gui_app.font(FontWeight.BOLD), status_text, 60, content_width)
height += len(status_lines) * 60 + 20

# Contribution count (if available)
if self.segment_count > 0:
contrib_text = f"{self.segment_count} segment(s) of your driving is in the training dataset so far."
contrib_lines = wrap_text(gui_app.font(FontWeight.BOLD), contrib_text, 52, content_width)
height += len(contrib_lines) * 52 + 20

# Instructions section
height += 32 # Separator
inst_lines = wrap_text(gui_app.font(FontWeight.NORMAL), INSTRUCTIONS, 40, content_width)
height += len(inst_lines) * 40 + 40 # Bottom margin

return height

def _render_content(self, rect: rl.Rectangle, scroll_offset: float):
def _render_content(self, rect: rl.Rectangle, scroll_offset: float) -> int:
x = int(rect.x + 40)
y = int(rect.y + 40 + scroll_offset)
w = int(rect.width - 80)

# Title
# Title (centered)
title_font = gui_app.font(FontWeight.MEDIUM)
rl.draw_text_ex(title_font, TITLE, rl.Vector2(x, y), 100, 0, rl.WHITE)
y += 140
text_width = measure_text_cached(title_font, TITLE, 100).x
title_x = rect.x + (rect.width - text_width) / 2
rl.draw_text_ex(title_font, TITLE, rl.Vector2(title_x, y), 100, 0, rl.WHITE)
y += 200

# Description
y = self._draw_wrapped_text(x, y, w, DESCRIPTION, gui_app.font(FontWeight.NORMAL), 45, rl.WHITE)
y += 40
y += 40 + 20

# Separator
rl.draw_rectangle(x, y, w, 2, self.GRAY)
y += 30
y += 30 + 20

# Status
status_text, status_color = self._get_status()
y = self._draw_wrapped_text(x, y, w, status_text, gui_app.font(FontWeight.BOLD), 60, status_color)
y += 20
y += 20 + 20

# Contribution count (if available)
if self.segment_count > 0:
contrib_text = f"{self.segment_count} segment(s) of your driving is in the training dataset so far."
y = self._draw_wrapped_text(x, y, w, contrib_text, gui_app.font(FontWeight.BOLD), 52, rl.WHITE)
y += 20
y += 20 + 20

# Separator
rl.draw_rectangle(x, y, w, 2, self.GRAY)
y += 30
y += 30 + 20

# Instructions
self._draw_wrapped_text(x, y, w, INSTRUCTIONS, gui_app.font(FontWeight.NORMAL), 40, self.LIGHT_GRAY)
y = self._draw_wrapped_text(x, y, w, INSTRUCTIONS, gui_app.font(FontWeight.NORMAL), 40, self.LIGHT_GRAY)

# bottom margin + remove effect of scroll offset
return int(round(y - self.scroll_panel.offset + 40))

def _draw_wrapped_text(self, x, y, width, text, font, size, color):
wrapped = wrap_text(font, text, size, width)
def _draw_wrapped_text(self, x, y, width, text, font, font_size, color):
wrapped = wrap_text(font, text, font_size, width)
for line in wrapped:
rl.draw_text_ex(font, line, rl.Vector2(x, y), size, 0, color)
y += size
return y
rl.draw_text_ex(font, line, rl.Vector2(x, y), font_size, 0, color)
y += font_size * FONT_SCALE
return round(y)

def _get_status(self) -> tuple[str, rl.Color]:
network_type = ui_state.sm["deviceState"].networkType
Expand Down
4 changes: 2 additions & 2 deletions selfdrive/ui/layouts/sidebar.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from collections.abc import Callable
from cereal import log
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, FONT_SCALE
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.widgets import Widget

Expand Down Expand Up @@ -218,7 +218,7 @@ def _draw_metric(self, rect: rl.Rectangle, metric: MetricData, y: float):

# Draw label and value
labels = [metric.label, metric.value]
text_y = metric_rect.y + (metric_rect.height / 2 - len(labels) * FONT_SIZE)
text_y = metric_rect.y + (metric_rect.height / 2 - len(labels) * FONT_SIZE * FONT_SCALE)
for text in labels:
text_size = measure_text_cached(self._font_bold, text, FONT_SIZE)
text_y += text_size.y
Expand Down
11 changes: 3 additions & 8 deletions selfdrive/ui/widgets/exp_mode_button.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import pyray as rl
from openpilot.common.params import Params
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE
from openpilot.system.ui.widgets import Widget


Expand All @@ -9,7 +9,7 @@ def __init__(self):
super().__init__()

self.img_width = 80
self.horizontal_padding = 50
self.horizontal_padding = 25
self.button_height = 125

self.params = Params()
Expand All @@ -31,11 +31,6 @@ def _draw_gradient_background(self, rect):
rl.draw_rectangle_gradient_h(int(rect.x), int(rect.y), int(rect.width), int(rect.height),
start_color, end_color)

def _handle_mouse_release(self, mouse_pos):
self.experimental_mode = not self.experimental_mode
# TODO: Opening settings for ExperimentalMode
self.params.put_bool("ExperimentalMode", self.experimental_mode)

def _render(self, rect):
rl.draw_rectangle_rounded(rect, 0.08, 20, rl.WHITE)

Expand All @@ -51,7 +46,7 @@ def _render(self, rect):
# Draw text label (left aligned)
text = "EXPERIMENTAL MODE ON" if self.experimental_mode else "CHILL MODE ON"
text_x = rect.x + self.horizontal_padding
text_y = rect.y + rect.height / 2 - 45 // 2 # Center vertically
text_y = rect.y + rect.height / 2 - 45 * FONT_SCALE // 2 # Center vertically

rl.draw_text_ex(gui_app.font(FontWeight.NORMAL), text, rl.Vector2(int(text_x), int(text_y)), 45, 0, rl.BLACK)

Expand Down
20 changes: 10 additions & 10 deletions selfdrive/ui/widgets/offroad_alerts.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from dataclasses import dataclass
from openpilot.common.params import Params
from openpilot.system.hardware import HARDWARE
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.wrap_text import wrap_text
Expand Down Expand Up @@ -65,8 +65,8 @@ def __init__(self, text: str, style: ButtonStyle = ButtonStyle.LIGHT,

def set_text(self, text: str):
self._text = text
self._text_width = measure_text_cached(gui_app.font(FontWeight.MEDIUM), self._text, AlertConstants.FONT_SIZE).x
self._rect.width = max(self._text_width + 60 * 2, self._min_width)
self._text_size = measure_text_cached(gui_app.font(FontWeight.MEDIUM), self._text, AlertConstants.FONT_SIZE)
self._rect.width = max(self._text_size.x + 60 * 2, self._min_width)
self._rect.height = AlertConstants.BUTTON_HEIGHT

def _render(self, _):
Expand All @@ -79,8 +79,8 @@ def _render(self, _):

# center text
color = rl.WHITE if self._style == ButtonStyle.DARK else rl.BLACK
text_x = int(self._rect.x + (self._rect.width - self._text_width) // 2)
text_y = int(self._rect.y + (self._rect.height - AlertConstants.FONT_SIZE) // 2)
text_x = int(self._rect.x + (self._rect.width - self._text_size.x) // 2)
text_y = int(self._rect.y + (self._rect.height - self._text_size.y) // 2)
rl.draw_text_ex(self._font, self._text, rl.Vector2(text_x, text_y), AlertConstants.FONT_SIZE, 0, color)


Expand Down Expand Up @@ -250,9 +250,9 @@ def get_content_height(self) -> float:
text_width = int(self.content_rect.width - (AlertConstants.ALERT_INSET * 2))
wrapped_lines = wrap_text(font, alert_data.text, AlertConstants.FONT_SIZE, text_width)
line_count = len(wrapped_lines)
text_height = line_count * (AlertConstants.FONT_SIZE + 5)
text_height = line_count * (AlertConstants.FONT_SIZE * FONT_SCALE)
alert_item_height = max(text_height + (AlertConstants.ALERT_INSET * 2), AlertConstants.ALERT_HEIGHT)
total_height += alert_item_height + AlertConstants.ALERT_SPACING
total_height += round(alert_item_height + AlertConstants.ALERT_SPACING)

if total_height > 20:
total_height = total_height - AlertConstants.ALERT_SPACING + 20
Expand All @@ -278,7 +278,7 @@ def _render_content(self, content_rect: rl.Rectangle):
text_width = int(content_rect.width - (AlertConstants.ALERT_INSET * 2))
wrapped_lines = wrap_text(font, alert_data.text, AlertConstants.FONT_SIZE, text_width)
line_count = len(wrapped_lines)
text_height = line_count * (AlertConstants.FONT_SIZE + 5)
text_height = line_count * (AlertConstants.FONT_SIZE * FONT_SCALE)
alert_item_height = max(text_height + (AlertConstants.ALERT_INSET * 2), AlertConstants.ALERT_HEIGHT)

alert_rect = rl.Rectangle(
Expand All @@ -298,13 +298,13 @@ def _render_content(self, content_rect: rl.Rectangle):
rl.draw_text_ex(
font,
line,
rl.Vector2(text_x, text_y + i * (AlertConstants.FONT_SIZE + 5)),
rl.Vector2(text_x, text_y + i * AlertConstants.FONT_SIZE * FONT_SCALE),
AlertConstants.FONT_SIZE,
0,
AlertColors.TEXT,
)

y_offset += alert_item_height + AlertConstants.ALERT_SPACING
y_offset += round(alert_item_height + AlertConstants.ALERT_SPACING)


class UpdateAlert(AbstractAlert):
Expand Down
4 changes: 2 additions & 2 deletions selfdrive/ui/widgets/pairing_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,8 @@ def _render_instructions(self, rect: rl.Rectangle) -> None:
# Circle and number
rl.draw_circle(int(circle_x), int(circle_y), circle_radius, rl.Color(70, 70, 70, 255))
number = str(i + 1)
number_width = measure_text_cached(font, number, 30).x
rl.draw_text_ex(font, number, (int(circle_x - number_width // 2), int(circle_y - 15)), 30, 0, rl.WHITE)
number_size = measure_text_cached(font, number, 30)
rl.draw_text_ex(font, number, (int(circle_x - number_size.x // 2), int(circle_y - number_size.y // 2)), 30, 0, rl.WHITE)

# Text
rl.draw_text_ex(font, "\n".join(wrapped), rl.Vector2(text_x, y), 47, 0.0, rl.BLACK)
Expand Down
2 changes: 1 addition & 1 deletion selfdrive/ui/widgets/prime.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def _render_for_non_prime_users(self, rect: rl.Rectangle):
def _render_for_prime_user(self, rect: rl.Rectangle):
"""Renders the prime user widget with subscription status."""

rl.draw_rectangle_rounded(rl.Rectangle(rect.x, rect.y, rect.width, 230), 0.02, 10, self.PRIME_BG_COLOR)
rl.draw_rectangle_rounded(rl.Rectangle(rect.x, rect.y, rect.width, 230), 0.05, 10, self.PRIME_BG_COLOR)

x = rect.x + 56
y = rect.y + 40
Expand Down
19 changes: 10 additions & 9 deletions selfdrive/ui/widgets/setup.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import pyray as rl
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE
from openpilot.system.ui.lib.wrap_text import wrap_text
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.button import Button, ButtonStyle
from openpilot.system.ui.widgets.label import Label


class SetupWidget(Widget):
Expand All @@ -15,6 +16,7 @@ def __init__(self):
self._pair_device_btn = Button("Pair device", self._show_pairing, button_style=ButtonStyle.PRIMARY)
self._open_settings_btn = Button("Open", lambda: self._open_settings_callback() if self._open_settings_callback else None,
button_style=ButtonStyle.PRIMARY)
self._firehose_label = Label("🔥Firehose Mode 🔥", font_weight=FontWeight.MEDIUM, font_size=64)

def set_open_settings_callback(self, callback):
self._open_settings_callback = callback
Expand All @@ -28,7 +30,7 @@ def _render(self, rect: rl.Rectangle):
def _render_registration(self, rect: rl.Rectangle):
"""Render registration prompt."""

rl.draw_rectangle_rounded(rl.Rectangle(rect.x, rect.y, rect.width, 630), 0.02, 20, rl.Color(51, 51, 51, 255))
rl.draw_rectangle_rounded(rl.Rectangle(rect.x, rect.y, rect.width, rect.height), 0.02, 20, rl.Color(51, 51, 51, 255))

x = rect.x + 64
y = rect.y + 48
Expand All @@ -45,15 +47,15 @@ def _render_registration(self, rect: rl.Rectangle):
wrapped = wrap_text(light_font, desc, 50, int(w))
for line in wrapped:
rl.draw_text_ex(light_font, line, rl.Vector2(x, y), 50, 0, rl.WHITE)
y += 50
y += 50 * FONT_SCALE

button_rect = rl.Rectangle(x, y + 50, w, 128)
button_rect = rl.Rectangle(x, y + 30, w, 200)
self._pair_device_btn.render(button_rect)

def _render_firehose_prompt(self, rect: rl.Rectangle):
"""Render firehose prompt widget."""

rl.draw_rectangle_rounded(rl.Rectangle(rect.x, rect.y, rect.width, 450), 0.02, 20, rl.Color(51, 51, 51, 255))
rl.draw_rectangle_rounded(rl.Rectangle(rect.x, rect.y, rect.width, 500), 0.02, 20, rl.Color(51, 51, 51, 255))

# Content margins (56, 40, 56, 40)
x = rect.x + 56
Expand All @@ -62,9 +64,8 @@ def _render_firehose_prompt(self, rect: rl.Rectangle):
spacing = 42

# Title with fire emojis
title_font = gui_app.font(FontWeight.MEDIUM)
title_text = "Firehose Mode"
rl.draw_text_ex(title_font, title_text, rl.Vector2(x, y), 64, 0, rl.WHITE)
# TODO: fix Label centering with emojis
self._firehose_label.render(rl.Rectangle(x - 40, y, w, 64))
y += 64 + spacing

# Description
Expand All @@ -74,7 +75,7 @@ def _render_firehose_prompt(self, rect: rl.Rectangle):

for line in wrapped_desc:
rl.draw_text_ex(desc_font, line, rl.Vector2(x, y), 40, 0, rl.WHITE)
y += 40
y += 40 * FONT_SCALE

y += spacing

Expand Down
Loading