Skip to content

Commit 8f720a5

Browse files
authored
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 28c3e0e. * Reapply "add elide support to Label" This reverts commit 92c2d66. * 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
1 parent 2c41dbc commit 8f720a5

File tree

6 files changed

+101
-26
lines changed

6 files changed

+101
-26
lines changed

selfdrive/ui/layouts/settings/software.py

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from openpilot.system.ui.widgets import Widget, DialogResult
99
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
1010
from openpilot.system.ui.widgets.list_view import button_item, text_item, ListItem
11+
from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
1112
from openpilot.system.ui.widgets.scroller import Scroller
1213

1314
# TODO: remove this. updater fails to respond on startup if time is not correct
@@ -56,20 +57,20 @@ def __init__(self):
5657
self._waiting_for_updater = False
5758
self._waiting_start_ts: float = 0.0
5859

59-
items = self._init_items()
60-
self._scroller = Scroller(items, line_separator=True, spacing=0)
60+
# Branch switcher
61+
self._branch_btn = button_item(lambda: tr("Target Branch"), lambda: tr("SELECT"), callback=self._on_select_branch)
62+
self._branch_btn.set_visible(not ui_state.params.get_bool("IsTestedBranch"))
63+
self._branch_btn.action_item.set_value(ui_state.params.get("UpdaterTargetBranch") or "")
64+
self._branch_dialog: MultiOptionDialog | None = None
6165

62-
def _init_items(self):
63-
items = [
66+
self._scroller = Scroller([
6467
self._onroad_label,
6568
self._version_item,
6669
self._download_btn,
6770
self._install_btn,
68-
# TODO: implement branch switching
69-
# button_item("Target Branch", "SELECT", callback=self._on_select_branch),
71+
self._branch_btn,
7072
button_item(lambda: tr("Uninstall"), lambda: tr("UNINSTALL"), callback=self._on_uninstall),
71-
]
72-
return items
73+
], line_separator=True, spacing=0)
7374

7475
def show_event(self):
7576
self._scroller.show_event()
@@ -123,6 +124,10 @@ def _update_state(self):
123124
# Only enable if we're not waiting for updater to flip out of idle
124125
self._download_btn.action_item.set_enabled(not self._waiting_for_updater)
125126

127+
# Update target branch button value
128+
current_branch = ui_state.params.get("UpdaterTargetBranch") or ""
129+
self._branch_btn.action_item.set_value(current_branch)
130+
126131
# Update install button
127132
self._install_btn.set_visible(ui_state.is_offroad() and update_available)
128133
if update_available:
@@ -163,4 +168,27 @@ def _on_install_update(self):
163168
self._install_btn.action_item.set_enabled(False)
164169
ui_state.params.put_bool("DoReboot", True)
165170

166-
def _on_select_branch(self): pass
171+
def _on_select_branch(self):
172+
# Get available branches and order
173+
current_git_branch = ui_state.params.get("GitBranch") or ""
174+
branches_str = ui_state.params.get("UpdaterAvailableBranches") or ""
175+
branches = [b for b in branches_str.split(",") if b]
176+
177+
for b in [current_git_branch, "devel-staging", "devel", "nightly", "nightly-dev", "master"]:
178+
if b in branches:
179+
branches.remove(b)
180+
branches.insert(0, b)
181+
182+
current_target = ui_state.params.get("UpdaterTargetBranch") or ""
183+
self._branch_dialog = MultiOptionDialog("Select a branch", branches, current_target)
184+
185+
def handle_selection(result):
186+
# Confirmed selection
187+
if result == DialogResult.CONFIRM and self._branch_dialog is not None and self._branch_dialog.selection:
188+
selection = self._branch_dialog.selection
189+
ui_state.params.put("UpdaterTargetBranch", selection)
190+
self._branch_btn.action_item.set_value(selection)
191+
os.system("pkill -SIGUSR1 -f system.updated.updated")
192+
self._branch_dialog = None
193+
194+
gui_app.set_modal_overlay(self._branch_dialog, callback=handle_selection)

selfdrive/ui/tests/test_ui/raylib_screenshots.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,17 @@
2727
SCREENSHOTS_DIR = TEST_OUTPUT_DIR / "screenshots"
2828
UI_DELAY = 0.2
2929

30+
BRANCH_NAME = "this-is-a-really-super-mega-ultra-max-extreme-ultimate-long-branch-name"
31+
VERSION = f"0.10.1 / {BRANCH_NAME} / 7864838 / Oct 03"
32+
3033
# Offroad alerts to test
3134
OFFROAD_ALERTS = ['Offroad_IsTakingSnapshot']
3235

3336

3437
def put_update_params(params: Params):
3538
params.put("UpdaterCurrentReleaseNotes", parse_release_notes(BASEDIR))
3639
params.put("UpdaterNewReleaseNotes", parse_release_notes(BASEDIR))
40+
params.put("UpdaterTargetBranch", BRANCH_NAME)
3741

3842

3943
def setup_homescreen(click, pm: PubMaster):
@@ -89,6 +93,15 @@ def setup_settings_software_release_notes(click, pm: PubMaster):
8993
click(588, 110) # expand description for current version
9094

9195

96+
def setup_settings_software_branch_switcher(click, pm: PubMaster):
97+
setup_settings_software(click, pm)
98+
params = Params()
99+
params.put("UpdaterAvailableBranches", f"master,nightly,release,{BRANCH_NAME}")
100+
params.put("GitBranch", BRANCH_NAME) # should be on top
101+
params.put("UpdaterTargetBranch", "nightly") # should be selected
102+
click(1984, 449)
103+
104+
92105
def setup_settings_firehose(click, pm: PubMaster):
93106
setup_settings(click, pm)
94107
click(278, 845)
@@ -240,6 +253,7 @@ def setup_onroad_full_alert_long_text(click, pm: PubMaster):
240253
"settings_software": setup_settings_software,
241254
"settings_software_download": setup_settings_software_download,
242255
"settings_software_release_notes": setup_settings_software_release_notes,
256+
"settings_software_branch_switcher": setup_settings_software_branch_switcher,
243257
"settings_firehose": setup_settings_firehose,
244258
"settings_developer": setup_settings_developer,
245259
"keyboard": setup_keyboard,
@@ -309,9 +323,8 @@ def create_screenshots():
309323
params.put("DongleId", "123456789012345")
310324

311325
# Set branch name
312-
description = "0.10.1 / this-is-a-really-super-mega-long-branch-name / 7864838 / Oct 03"
313-
params.put("UpdaterCurrentDescription", description)
314-
params.put("UpdaterNewDescription", description)
326+
params.put("UpdaterCurrentDescription", VERSION)
327+
params.put("UpdaterNewDescription", VERSION)
315328

316329
if name == "homescreen_paired":
317330
params.put("PrimeType", 0) # NONE

system/ui/widgets/button.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ def __init__(self,
8888
text_alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
8989
text_padding: int = 20,
9090
icon=None,
91+
elide_right: bool = False,
9192
multi_touch: bool = False,
9293
):
9394

@@ -97,7 +98,7 @@ def __init__(self,
9798
self._background_color = BUTTON_BACKGROUND_COLORS[self._button_style]
9899

99100
self._label = Label(text, font_size, font_weight, text_alignment, text_padding=text_padding,
100-
text_color=BUTTON_TEXT_COLOR[self._button_style], icon=icon)
101+
text_color=BUTTON_TEXT_COLOR[self._button_style], icon=icon, elide_right=elide_right)
101102

102103
self._click_callback = click_callback
103104
self._multi_touch = multi_touch

system/ui/widgets/label.py

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,17 +37,17 @@ def gui_label(
3737

3838
# Elide text to fit within the rectangle
3939
if elide_right and text_size.x > rect.width:
40-
ellipsis = "..."
40+
_ellipsis = "..."
4141
left, right = 0, len(text)
4242
while left < right:
4343
mid = (left + right) // 2
44-
candidate = text[:mid] + ellipsis
44+
candidate = text[:mid] + _ellipsis
4545
candidate_size = measure_text_cached(font, candidate, font_size)
4646
if candidate_size.x <= rect.width:
4747
left = mid + 1
4848
else:
4949
right = mid
50-
display_text = text[: left - 1] + ellipsis if left > 0 else ellipsis
50+
display_text = text[: left - 1] + _ellipsis if left > 0 else _ellipsis
5151
text_size = measure_text_cached(font, display_text, font_size)
5252

5353
# Calculate horizontal position based on alignment
@@ -106,6 +106,7 @@ def __init__(self,
106106
text_padding: int = 0,
107107
text_color: rl.Color = DEFAULT_TEXT_COLOR,
108108
icon: Union[rl.Texture, None] = None, # noqa: UP007
109+
elide_right: bool = False,
109110
):
110111

111112
super().__init__()
@@ -117,6 +118,7 @@ def __init__(self,
117118
self._text_padding = text_padding
118119
self._text_color = text_color
119120
self._icon = icon
121+
self._elide_right = elide_right
120122

121123
self._text = text
122124
self.set_text(text)
@@ -138,7 +140,31 @@ def _update_layout_rects(self):
138140
def _update_text(self, text):
139141
self._emojis = []
140142
self._text_size = []
141-
self._text_wrapped = wrap_text(self._font, _resolve_value(text), self._font_size, round(self._rect.width - (self._text_padding * 2)))
143+
text = _resolve_value(text)
144+
145+
if self._elide_right:
146+
display_text = text
147+
148+
# Elide text to fit within the rectangle
149+
text_size = measure_text_cached(self._font, text, self._font_size)
150+
content_width = self._rect.width - self._text_padding * 2
151+
if text_size.x > content_width:
152+
_ellipsis = "..."
153+
left, right = 0, len(text)
154+
while left < right:
155+
mid = (left + right) // 2
156+
candidate = text[:mid] + _ellipsis
157+
candidate_size = measure_text_cached(self._font, candidate, self._font_size)
158+
if candidate_size.x <= content_width:
159+
left = mid + 1
160+
else:
161+
right = mid
162+
display_text = text[: left - 1] + _ellipsis if left > 0 else _ellipsis
163+
164+
self._text_wrapped = [display_text]
165+
else:
166+
self._text_wrapped = wrap_text(self._font, text, self._font_size, round(self._rect.width - (self._text_padding * 2)))
167+
142168
for t in self._text_wrapped:
143169
self._emojis.append(find_emoji(t))
144170
self._text_size.append(measure_text_cached(self._font, t, self._font_size))

system/ui/widgets/list_view.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,14 @@ def pressed():
100100
)
101101
self.set_enabled(enabled)
102102

103+
def get_width_hint(self) -> float:
104+
value_text = self.value
105+
if value_text:
106+
text_width = measure_text_cached(self._font, value_text, ITEM_TEXT_FONT_SIZE).x
107+
return text_width + BUTTON_WIDTH + TEXT_PADDING
108+
else:
109+
return BUTTON_WIDTH
110+
103111
def set_touch_valid_callback(self, touch_callback: Callable[[], bool]) -> None:
104112
super().set_touch_valid_callback(touch_callback)
105113
self._button.set_touch_valid_callback(touch_callback)
@@ -121,16 +129,15 @@ def value(self):
121129
def _render(self, rect: rl.Rectangle) -> bool:
122130
self._button.set_text(self.text)
123131
self._button.set_enabled(_resolve_value(self.enabled))
124-
button_rect = rl.Rectangle(rect.x, rect.y + (rect.height - BUTTON_HEIGHT) / 2, BUTTON_WIDTH, BUTTON_HEIGHT)
132+
button_rect = rl.Rectangle(rect.x + rect.width - BUTTON_WIDTH, rect.y + (rect.height - BUTTON_HEIGHT) / 2, BUTTON_WIDTH, BUTTON_HEIGHT)
125133
self._button.render(button_rect)
126134

127135
value_text = self.value
128136
if value_text:
129-
spacing = 20
130-
text_size = measure_text_cached(self._font, value_text, ITEM_TEXT_FONT_SIZE)
131-
text_x = button_rect.x - spacing - text_size.x
132-
text_y = rect.y + (rect.height - text_size.y) / 2
133-
rl.draw_text_ex(self._font, value_text, rl.Vector2(text_x, text_y), ITEM_TEXT_FONT_SIZE, 0, ITEM_TEXT_VALUE_COLOR)
137+
value_rect = rl.Rectangle(rect.x, rect.y, rect.width - BUTTON_WIDTH - TEXT_PADDING, rect.height)
138+
gui_label(value_rect, value_text, font_size=ITEM_TEXT_FONT_SIZE, color=ITEM_TEXT_VALUE_COLOR,
139+
font_weight=FontWeight.NORMAL, alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
140+
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
134141

135142
# TODO: just use the generic Widget click callbacks everywhere, no returning from render
136143
pressed = self._pressed

system/ui/widgets/option_dialog.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@ def __init__(self, title, options, current=""):
2828
# Create scroller with option buttons
2929
self.option_buttons = [Button(option, click_callback=lambda opt=option: self._on_option_clicked(opt),
3030
text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, button_style=ButtonStyle.NORMAL,
31-
text_padding=50) for option in options]
31+
text_padding=50, elide_right=True) for option in options]
3232
self.scroller = Scroller(self.option_buttons, spacing=LIST_ITEM_SPACING)
3333

34-
self.cancel_button = Button(tr("Cancel"), click_callback=lambda: self._set_result(DialogResult.CANCEL))
35-
self.select_button = Button(tr("Select"), click_callback=lambda: self._set_result(DialogResult.CONFIRM), button_style=ButtonStyle.PRIMARY)
34+
self.cancel_button = Button(lambda: tr("Cancel"), click_callback=lambda: self._set_result(DialogResult.CANCEL))
35+
self.select_button = Button(lambda: tr("Select"), click_callback=lambda: self._set_result(DialogResult.CONFIRM), button_style=ButtonStyle.PRIMARY)
3636

3737
def _set_result(self, result: DialogResult):
3838
self._result = result

0 commit comments

Comments
 (0)