raylib: add branch switcher (#36359)

* it's adversarial

* try 2

* just do this

* kinda works but doesn' tmatch

* fine

* qt is banned word

* test

* fix test

* add elide support to Label

* fixup

* Revert "add elide support to Label"

This reverts commit 28c3e0e745.

* Reapply "add elide support to Label"

This reverts commit 92c2d66941.

* todo

* elide button value properly + debug/stash

* clean up

* clean up

* yep looks good

* clean up

* eval visible once

* no s

* don't need

* can do this

* but this also works

* clip to parent rect

* fixes and multilang

* clean up

* set target branch

* whops
pull/36434/head
Shane Smiskol 3 weeks ago committed by GitHub
parent 2c41dbc472
commit 8f720a54f6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 46
      selfdrive/ui/layouts/settings/software.py
  2. 19
      selfdrive/ui/tests/test_ui/raylib_screenshots.py
  3. 3
      system/ui/widgets/button.py
  4. 34
      system/ui/widgets/label.py
  5. 19
      system/ui/widgets/list_view.py
  6. 6
      system/ui/widgets/option_dialog.py

@ -8,6 +8,7 @@ from openpilot.system.ui.lib.multilang import tr, trn
from openpilot.system.ui.widgets import Widget, DialogResult
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
from openpilot.system.ui.widgets.list_view import button_item, text_item, ListItem
from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
from openpilot.system.ui.widgets.scroller import Scroller
# TODO: remove this. updater fails to respond on startup if time is not correct
@ -56,20 +57,20 @@ class SoftwareLayout(Widget):
self._waiting_for_updater = False
self._waiting_start_ts: float = 0.0
items = self._init_items()
self._scroller = Scroller(items, line_separator=True, spacing=0)
# Branch switcher
self._branch_btn = button_item(lambda: tr("Target Branch"), lambda: tr("SELECT"), callback=self._on_select_branch)
self._branch_btn.set_visible(not ui_state.params.get_bool("IsTestedBranch"))
self._branch_btn.action_item.set_value(ui_state.params.get("UpdaterTargetBranch") or "")
self._branch_dialog: MultiOptionDialog | None = None
def _init_items(self):
items = [
self._scroller = Scroller([
self._onroad_label,
self._version_item,
self._download_btn,
self._install_btn,
# TODO: implement branch switching
# button_item("Target Branch", "SELECT", callback=self._on_select_branch),
self._branch_btn,
button_item(lambda: tr("Uninstall"), lambda: tr("UNINSTALL"), callback=self._on_uninstall),
]
return items
], line_separator=True, spacing=0)
def show_event(self):
self._scroller.show_event()
@ -123,6 +124,10 @@ class SoftwareLayout(Widget):
# Only enable if we're not waiting for updater to flip out of idle
self._download_btn.action_item.set_enabled(not self._waiting_for_updater)
# Update target branch button value
current_branch = ui_state.params.get("UpdaterTargetBranch") or ""
self._branch_btn.action_item.set_value(current_branch)
# Update install button
self._install_btn.set_visible(ui_state.is_offroad() and update_available)
if update_available:
@ -163,4 +168,27 @@ class SoftwareLayout(Widget):
self._install_btn.action_item.set_enabled(False)
ui_state.params.put_bool("DoReboot", True)
def _on_select_branch(self): pass
def _on_select_branch(self):
# Get available branches and order
current_git_branch = ui_state.params.get("GitBranch") or ""
branches_str = ui_state.params.get("UpdaterAvailableBranches") or ""
branches = [b for b in branches_str.split(",") if b]
for b in [current_git_branch, "devel-staging", "devel", "nightly", "nightly-dev", "master"]:
if b in branches:
branches.remove(b)
branches.insert(0, b)
current_target = ui_state.params.get("UpdaterTargetBranch") or ""
self._branch_dialog = MultiOptionDialog("Select a branch", branches, current_target)
def handle_selection(result):
# Confirmed selection
if result == DialogResult.CONFIRM and self._branch_dialog is not None and self._branch_dialog.selection:
selection = self._branch_dialog.selection
ui_state.params.put("UpdaterTargetBranch", selection)
self._branch_btn.action_item.set_value(selection)
os.system("pkill -SIGUSR1 -f system.updated.updated")
self._branch_dialog = None
gui_app.set_modal_overlay(self._branch_dialog, callback=handle_selection)

@ -27,6 +27,9 @@ TEST_OUTPUT_DIR = TEST_DIR / "raylib_report"
SCREENSHOTS_DIR = TEST_OUTPUT_DIR / "screenshots"
UI_DELAY = 0.2
BRANCH_NAME = "this-is-a-really-super-mega-ultra-max-extreme-ultimate-long-branch-name"
VERSION = f"0.10.1 / {BRANCH_NAME} / 7864838 / Oct 03"
# Offroad alerts to test
OFFROAD_ALERTS = ['Offroad_IsTakingSnapshot']
@ -34,6 +37,7 @@ OFFROAD_ALERTS = ['Offroad_IsTakingSnapshot']
def put_update_params(params: Params):
params.put("UpdaterCurrentReleaseNotes", parse_release_notes(BASEDIR))
params.put("UpdaterNewReleaseNotes", parse_release_notes(BASEDIR))
params.put("UpdaterTargetBranch", BRANCH_NAME)
def setup_homescreen(click, pm: PubMaster):
@ -89,6 +93,15 @@ def setup_settings_software_release_notes(click, pm: PubMaster):
click(588, 110) # expand description for current version
def setup_settings_software_branch_switcher(click, pm: PubMaster):
setup_settings_software(click, pm)
params = Params()
params.put("UpdaterAvailableBranches", f"master,nightly,release,{BRANCH_NAME}")
params.put("GitBranch", BRANCH_NAME) # should be on top
params.put("UpdaterTargetBranch", "nightly") # should be selected
click(1984, 449)
def setup_settings_firehose(click, pm: PubMaster):
setup_settings(click, pm)
click(278, 845)
@ -240,6 +253,7 @@ CASES = {
"settings_software": setup_settings_software,
"settings_software_download": setup_settings_software_download,
"settings_software_release_notes": setup_settings_software_release_notes,
"settings_software_branch_switcher": setup_settings_software_branch_switcher,
"settings_firehose": setup_settings_firehose,
"settings_developer": setup_settings_developer,
"keyboard": setup_keyboard,
@ -309,9 +323,8 @@ def create_screenshots():
params.put("DongleId", "123456789012345")
# Set branch name
description = "0.10.1 / this-is-a-really-super-mega-long-branch-name / 7864838 / Oct 03"
params.put("UpdaterCurrentDescription", description)
params.put("UpdaterNewDescription", description)
params.put("UpdaterCurrentDescription", VERSION)
params.put("UpdaterNewDescription", VERSION)
if name == "homescreen_paired":
params.put("PrimeType", 0) # NONE

@ -88,6 +88,7 @@ class Button(Widget):
text_alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
text_padding: int = 20,
icon=None,
elide_right: bool = False,
multi_touch: bool = False,
):
@ -97,7 +98,7 @@ class Button(Widget):
self._background_color = BUTTON_BACKGROUND_COLORS[self._button_style]
self._label = Label(text, font_size, font_weight, text_alignment, text_padding=text_padding,
text_color=BUTTON_TEXT_COLOR[self._button_style], icon=icon)
text_color=BUTTON_TEXT_COLOR[self._button_style], icon=icon, elide_right=elide_right)
self._click_callback = click_callback
self._multi_touch = multi_touch

@ -37,17 +37,17 @@ def gui_label(
# Elide text to fit within the rectangle
if elide_right and text_size.x > rect.width:
ellipsis = "..."
_ellipsis = "..."
left, right = 0, len(text)
while left < right:
mid = (left + right) // 2
candidate = text[:mid] + ellipsis
candidate = text[:mid] + _ellipsis
candidate_size = measure_text_cached(font, candidate, font_size)
if candidate_size.x <= rect.width:
left = mid + 1
else:
right = mid
display_text = text[: left - 1] + ellipsis if left > 0 else ellipsis
display_text = text[: left - 1] + _ellipsis if left > 0 else _ellipsis
text_size = measure_text_cached(font, display_text, font_size)
# Calculate horizontal position based on alignment
@ -106,6 +106,7 @@ class Label(Widget):
text_padding: int = 0,
text_color: rl.Color = DEFAULT_TEXT_COLOR,
icon: Union[rl.Texture, None] = None, # noqa: UP007
elide_right: bool = False,
):
super().__init__()
@ -117,6 +118,7 @@ class Label(Widget):
self._text_padding = text_padding
self._text_color = text_color
self._icon = icon
self._elide_right = elide_right
self._text = text
self.set_text(text)
@ -138,7 +140,31 @@ class Label(Widget):
def _update_text(self, text):
self._emojis = []
self._text_size = []
self._text_wrapped = wrap_text(self._font, _resolve_value(text), self._font_size, round(self._rect.width - (self._text_padding * 2)))
text = _resolve_value(text)
if self._elide_right:
display_text = text
# Elide text to fit within the rectangle
text_size = measure_text_cached(self._font, text, self._font_size)
content_width = self._rect.width - self._text_padding * 2
if text_size.x > content_width:
_ellipsis = "..."
left, right = 0, len(text)
while left < right:
mid = (left + right) // 2
candidate = text[:mid] + _ellipsis
candidate_size = measure_text_cached(self._font, candidate, self._font_size)
if candidate_size.x <= content_width:
left = mid + 1
else:
right = mid
display_text = text[: left - 1] + _ellipsis if left > 0 else _ellipsis
self._text_wrapped = [display_text]
else:
self._text_wrapped = wrap_text(self._font, text, self._font_size, round(self._rect.width - (self._text_padding * 2)))
for t in self._text_wrapped:
self._emojis.append(find_emoji(t))
self._text_size.append(measure_text_cached(self._font, t, self._font_size))

@ -100,6 +100,14 @@ class ButtonAction(ItemAction):
)
self.set_enabled(enabled)
def get_width_hint(self) -> float:
value_text = self.value
if value_text:
text_width = measure_text_cached(self._font, value_text, ITEM_TEXT_FONT_SIZE).x
return text_width + BUTTON_WIDTH + TEXT_PADDING
else:
return BUTTON_WIDTH
def set_touch_valid_callback(self, touch_callback: Callable[[], bool]) -> None:
super().set_touch_valid_callback(touch_callback)
self._button.set_touch_valid_callback(touch_callback)
@ -121,16 +129,15 @@ class ButtonAction(ItemAction):
def _render(self, rect: rl.Rectangle) -> bool:
self._button.set_text(self.text)
self._button.set_enabled(_resolve_value(self.enabled))
button_rect = rl.Rectangle(rect.x, rect.y + (rect.height - BUTTON_HEIGHT) / 2, BUTTON_WIDTH, BUTTON_HEIGHT)
button_rect = rl.Rectangle(rect.x + rect.width - BUTTON_WIDTH, rect.y + (rect.height - BUTTON_HEIGHT) / 2, BUTTON_WIDTH, BUTTON_HEIGHT)
self._button.render(button_rect)
value_text = self.value
if value_text:
spacing = 20
text_size = measure_text_cached(self._font, value_text, ITEM_TEXT_FONT_SIZE)
text_x = button_rect.x - spacing - text_size.x
text_y = rect.y + (rect.height - text_size.y) / 2
rl.draw_text_ex(self._font, value_text, rl.Vector2(text_x, text_y), ITEM_TEXT_FONT_SIZE, 0, ITEM_TEXT_VALUE_COLOR)
value_rect = rl.Rectangle(rect.x, rect.y, rect.width - BUTTON_WIDTH - TEXT_PADDING, rect.height)
gui_label(value_rect, value_text, font_size=ITEM_TEXT_FONT_SIZE, color=ITEM_TEXT_VALUE_COLOR,
font_weight=FontWeight.NORMAL, alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
# TODO: just use the generic Widget click callbacks everywhere, no returning from render
pressed = self._pressed

@ -28,11 +28,11 @@ class MultiOptionDialog(Widget):
# Create scroller with option buttons
self.option_buttons = [Button(option, click_callback=lambda opt=option: self._on_option_clicked(opt),
text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, button_style=ButtonStyle.NORMAL,
text_padding=50) for option in options]
text_padding=50, elide_right=True) for option in options]
self.scroller = Scroller(self.option_buttons, spacing=LIST_ITEM_SPACING)
self.cancel_button = Button(tr("Cancel"), click_callback=lambda: self._set_result(DialogResult.CANCEL))
self.select_button = Button(tr("Select"), click_callback=lambda: self._set_result(DialogResult.CONFIRM), button_style=ButtonStyle.PRIMARY)
self.cancel_button = Button(lambda: tr("Cancel"), click_callback=lambda: self._set_result(DialogResult.CANCEL))
self.select_button = Button(lambda: tr("Select"), click_callback=lambda: self._set_result(DialogResult.CONFIRM), button_style=ButtonStyle.PRIMARY)
def _set_result(self, result: DialogResult):
self._result = result

Loading…
Cancel
Save