Backport Mici UI polish from SP-Dev-Dom (exclude wifi_manager)
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
@@ -91,7 +91,7 @@ class DeveloperLayoutMici(NavWidget):
|
||||
self._long_maneuver_toggle,
|
||||
self._alpha_long_toggle,
|
||||
self._debug_mode_toggle,
|
||||
], snap_items=False)
|
||||
], snap_items=False, scroll_indicator=True, edge_shadows=True)
|
||||
|
||||
# Toggle lists
|
||||
self._refresh_toggles = (
|
||||
|
||||
@@ -76,6 +76,8 @@ def _engaged_confirmation_callback(callback: Callable, action_text: str):
|
||||
icon = "icons_mici/settings/device/reboot.png"
|
||||
elif action_text == "reset":
|
||||
icon = "icons_mici/settings/device/lkas.png"
|
||||
elif action_text == "reset driver monitoring":
|
||||
icon = "icons_mici/settings/device/cameras.png"
|
||||
elif action_text == "uninstall":
|
||||
icon = "icons_mici/settings/device/uninstall.png"
|
||||
else:
|
||||
@@ -83,7 +85,7 @@ def _engaged_confirmation_callback(callback: Callable, action_text: str):
|
||||
icon = "icons_mici/settings/comma_icon.png"
|
||||
|
||||
dlg: BigConfirmationDialogV2 | BigDialog = BigConfirmationDialogV2(f"slide to\n{action_text.lower()}", icon, red=red,
|
||||
exit_on_confirm=action_text == "reset",
|
||||
exit_on_confirm=action_text in {"reset", "reset driver monitoring"},
|
||||
confirm_callback=confirm_callback)
|
||||
gui_app.set_modal_overlay(dlg)
|
||||
else:
|
||||
@@ -459,9 +461,17 @@ class DeviceLayoutMici(NavWidget):
|
||||
params.remove("LiveDelay")
|
||||
params.put_bool("OnroadCycleRequested", True)
|
||||
|
||||
def reset_driver_monitoring_callback():
|
||||
params = ui_state.params
|
||||
params.remove("IsRhdDetected")
|
||||
params.put_bool("OnroadCycleRequested", True)
|
||||
|
||||
def uninstall_openpilot_callback():
|
||||
ui_state.params.put_bool("DoUninstall", True)
|
||||
|
||||
reset_driver_monitoring_btn = BigButton("reset driver monitoring calibration", "", "icons_mici/settings/device/cameras.png")
|
||||
reset_driver_monitoring_btn.set_click_callback(lambda: _engaged_confirmation_callback(reset_driver_monitoring_callback, "reset driver monitoring"))
|
||||
|
||||
reset_calibration_btn = BigButton("reset calibration", "", "icons_mici/settings/device/lkas.png")
|
||||
reset_calibration_btn.set_click_callback(lambda: _engaged_confirmation_callback(reset_calibration_callback, "reset"))
|
||||
|
||||
@@ -508,13 +518,14 @@ class DeviceLayoutMici(NavWidget):
|
||||
PairBigButton(),
|
||||
review_training_guide_btn,
|
||||
driver_cam_btn,
|
||||
reset_driver_monitoring_btn,
|
||||
# lang_button,
|
||||
reset_calibration_btn,
|
||||
uninstall_openpilot_btn,
|
||||
regulatory_btn,
|
||||
reboot_btn,
|
||||
self._power_off_btn,
|
||||
], snap_items=False)
|
||||
], snap_items=False, scroll_indicator=True, edge_shadows=True)
|
||||
|
||||
# Set up back navigation
|
||||
self.set_back_callback(back_callback)
|
||||
|
||||
@@ -3,7 +3,7 @@ from enum import IntEnum
|
||||
from collections.abc import Callable
|
||||
|
||||
from openpilot.system.ui.widgets.scroller import Scroller
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiUIMici
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiUIMici, WifiIcon, normalize_ssid
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigMultiToggle, BigToggle, BigParamControl
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigInputDialog
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
@@ -75,8 +75,14 @@ class NetworkLayoutMici(NavWidget):
|
||||
self._network_metered_btn = BigMultiToggle("network usage", ["default", "metered", "unmetered"], select_callback=network_metered_callback)
|
||||
self._network_metered_btn.set_enabled(False)
|
||||
|
||||
wifi_button = BigButton("wi-fi")
|
||||
self._wifi_slash_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_slash.png", 64, 56)
|
||||
self._wifi_low_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_low.png", 64, 47)
|
||||
self._wifi_medium_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_medium.png", 64, 47)
|
||||
self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 64, 47)
|
||||
|
||||
wifi_button = BigButton("wi-fi", "not connected", self._wifi_slash_txt)
|
||||
wifi_button.set_click_callback(lambda: self._switch_to_panel(NetworkPanelType.WIFI))
|
||||
self._wifi_button = wifi_button
|
||||
|
||||
# ******** Advanced settings ********
|
||||
# ******** Roaming toggle ********
|
||||
@@ -101,7 +107,7 @@ class NetworkLayoutMici(NavWidget):
|
||||
self._cellular_metered_btn,
|
||||
# */
|
||||
self._ip_address_btn,
|
||||
], snap_items=False)
|
||||
], snap_items=False, scroll_indicator=True, edge_shadows=True)
|
||||
|
||||
# Set initial config
|
||||
roaming_enabled = ui_state.params.get_bool("GsmRoaming")
|
||||
|
||||
@@ -90,7 +90,7 @@ class SettingsLayout(NavWidget):
|
||||
#BigDialogButton("manual", "", "icons_mici/settings/manual_icon.png", "Check out the mici user\nmanual at comma.ai/setup"),
|
||||
firehose_btn,
|
||||
developer_btn,
|
||||
], snap_items=False)
|
||||
], snap_items=False, scroll_indicator=True, edge_shadows=True)
|
||||
|
||||
# Set up back navigation
|
||||
self.set_back_callback(self.close_settings)
|
||||
|
||||
@@ -35,7 +35,7 @@ class TogglesLayoutMici(NavWidget):
|
||||
record_front,
|
||||
record_mic,
|
||||
enable_openpilot,
|
||||
], snap_items=False)
|
||||
], snap_items=False, scroll_indicator=True, edge_shadows=True)
|
||||
|
||||
# Toggle lists
|
||||
self._refresh_toggles = (
|
||||
|
||||
@@ -110,6 +110,7 @@ class BigButton(Widget):
|
||||
self.text = text
|
||||
self.value = value
|
||||
self.set_icon(icon)
|
||||
self._label_font_size_override: int | None = None
|
||||
|
||||
self._scale_filter = BounceFilter(1.0, 0.1, 1 / gui_app.target_fps)
|
||||
|
||||
@@ -136,6 +137,18 @@ class BigButton(Widget):
|
||||
def set_icon(self, icon: Union[str, rl.Texture]):
|
||||
self._txt_icon = gui_app.texture(icon, 64, 64) if isinstance(icon, str) and len(icon) else icon
|
||||
|
||||
def _refresh_label_metrics(self):
|
||||
font_size = self._label_font_size_override if self._label_font_size_override is not None else self._get_label_font_size()
|
||||
self._label.set_font_size(font_size)
|
||||
self._needs_scroll = measure_text_cached(self._label_font, self.text, font_size).x + 25 > self._rect.width
|
||||
self._scroll_offset = 0
|
||||
self._scroll_timer = 0
|
||||
self._scroll_state = ScrollState.PRE_SCROLL
|
||||
|
||||
def _set_label_font_size_override(self, font_size: int | None):
|
||||
self._label_font_size_override = font_size
|
||||
self._refresh_label_metrics()
|
||||
|
||||
def set_rotate_icon(self, rotate: bool):
|
||||
if rotate and self._rotate_icon_t is not None:
|
||||
return
|
||||
@@ -165,10 +178,12 @@ class BigButton(Widget):
|
||||
def set_text(self, text: str):
|
||||
self.text = text
|
||||
self._label.set_text(text)
|
||||
self._refresh_label_metrics()
|
||||
|
||||
def set_value(self, value: str):
|
||||
self.value = value
|
||||
self._sub_label.set_text(value)
|
||||
self._refresh_label_metrics()
|
||||
|
||||
def get_value(self) -> str:
|
||||
return self.value
|
||||
@@ -256,7 +271,7 @@ class BigToggle(BigButton):
|
||||
self._checked = initial_state
|
||||
self._toggle_callback = toggle_callback
|
||||
|
||||
self._label.set_font_size(48)
|
||||
self._set_label_font_size_override(48)
|
||||
|
||||
def _load_images(self):
|
||||
super()._load_images()
|
||||
@@ -296,8 +311,8 @@ class BigMultiToggle(BigToggle):
|
||||
self._select_callback = select_callback
|
||||
|
||||
self._label.set_width(int(self._rect.width - LABEL_HORIZONTAL_PADDING * 2 - self._txt_enabled_toggle.width))
|
||||
# TODO: why isn't this automatic?
|
||||
self._label.set_font_size(self._get_label_font_size())
|
||||
# Keep the title size stable when the selected option changes.
|
||||
self._set_label_font_size_override(self._get_label_font_size())
|
||||
|
||||
self.set_value(self._options[0])
|
||||
|
||||
|
||||
@@ -113,6 +113,14 @@ class BigConfirmationDialogV2(BigDialogBase):
|
||||
self._slider = BigSlider(title, icon_txt, confirm_callback=self._on_confirm)
|
||||
self._slider.set_enabled(lambda: not self._swiping_away)
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._slider.show_event()
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
self._slider.hide_event()
|
||||
|
||||
def _on_confirm(self):
|
||||
if self._confirm_callback:
|
||||
self._confirm_callback()
|
||||
@@ -122,7 +130,7 @@ class BigConfirmationDialogV2(BigDialogBase):
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
if self._swiping_away and not self._slider.confirmed:
|
||||
self._slider.reset()
|
||||
self._slider.reset(reset_shimmer=False)
|
||||
|
||||
def _render(self, _) -> DialogResult:
|
||||
self._slider.render(self._rect)
|
||||
|
||||
@@ -363,6 +363,26 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) {
|
||||
connect(dcamBtn, &ButtonControl::clicked, [=]() { emit showDriverView(); });
|
||||
addItem(dcamBtn);
|
||||
|
||||
resetDmCalibBtn = new ButtonControl(
|
||||
tr("Reset Driver Monitoring"),
|
||||
tr("RESET"),
|
||||
tr("Clears the saved driver monitoring wheel-side calibration if the device thinks you're seated on the wrong side. "
|
||||
"Resetting will restart openpilot if the car is powered on.")
|
||||
);
|
||||
connect(resetDmCalibBtn, &ButtonControl::clicked, [&]() {
|
||||
if (!uiState()->engaged()) {
|
||||
if (ConfirmationDialog::confirm(tr("Are you sure you want to reset driver monitoring calibration?"), tr("Reset"), this)) {
|
||||
if (!uiState()->engaged()) {
|
||||
params.remove("IsRhdDetected");
|
||||
params.putBool("OnroadCycleRequested", true);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ConfirmationDialog::alert(tr("Disengage to Reset Driver Monitoring"), this);
|
||||
}
|
||||
});
|
||||
addItem(resetDmCalibBtn);
|
||||
|
||||
resetCalibBtn = new ButtonControl(tr("Reset Calibration"), tr("RESET"), "");
|
||||
connect(resetCalibBtn, &ButtonControl::showDescriptionEvent, this, &DevicePanel::updateCalibDescription);
|
||||
connect(resetCalibBtn, &ButtonControl::clicked, [&]() {
|
||||
@@ -420,7 +440,7 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) {
|
||||
});
|
||||
QObject::connect(uiState(), &UIState::offroadTransition, [=](bool offroad) {
|
||||
for (auto btn : findChildren<ButtonControl *>()) {
|
||||
if (btn != pair_device && btn != resetCalibBtn) {
|
||||
if (btn != pair_device && btn != resetCalibBtn && btn != resetDmCalibBtn) {
|
||||
btn->setEnabled(offroad);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +94,7 @@ private:
|
||||
ButtonControl *pair_galaxy;
|
||||
QPushButton *galaxy_qr_btn;
|
||||
ButtonControl *resetCalibBtn;
|
||||
ButtonControl *resetDmCalibBtn;
|
||||
};
|
||||
|
||||
class TogglesPanel : public ListWidget {
|
||||
|
||||
@@ -133,11 +133,17 @@ class SoftwareSelectionPage(Widget):
|
||||
super().__init__()
|
||||
|
||||
self._openpilot_slider = LargerSlider("slide to use\nstarpilot", use_openpilot_callback)
|
||||
self._custom_software_slider = LargerSlider("slide to use\ncustom software", use_custom_software_callback, green=False)
|
||||
self._custom_software_slider = LargerSlider("slide to use\ncustom software", use_custom_software_callback,
|
||||
green=False, shimmer_offset=0.4)
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._openpilot_slider.show_event()
|
||||
self._custom_software_slider.show_event()
|
||||
|
||||
def reset(self):
|
||||
self._openpilot_slider.reset()
|
||||
self._custom_software_slider.reset()
|
||||
self._openpilot_slider.reset(reset_shimmer=False)
|
||||
self._custom_software_slider.reset(reset_shimmer=False)
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
self._openpilot_slider.set_opacity(1.0 - self._custom_software_slider.slider_percentage)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import math
|
||||
from enum import IntEnum
|
||||
from collections.abc import Callable
|
||||
from itertools import zip_longest
|
||||
@@ -401,6 +402,12 @@ class UnifiedLabel(Widget):
|
||||
- Proper multiline vertical alignment
|
||||
- Height calculation for layout purposes
|
||||
"""
|
||||
SHIMMER_BAND_WIDTH = 0.3
|
||||
SHIMMER_BLUR_RADIUS = 0.12
|
||||
SHIMMER_CYCLE_PERIOD = 2.5
|
||||
SHIMMER_SWEEP_FRACTION = 0.9
|
||||
SHIMMER_LOW_OPACITY = 0.65
|
||||
|
||||
def __init__(self,
|
||||
text: str | Callable[[], str],
|
||||
font_size: int = DEFAULT_TEXT_SIZE,
|
||||
@@ -414,7 +421,8 @@ class UnifiedLabel(Widget):
|
||||
wrap_text: bool = True,
|
||||
scroll: bool = False,
|
||||
line_height: float = 1.0,
|
||||
letter_spacing: float = 0.0):
|
||||
letter_spacing: float = 0.0,
|
||||
shimmer: bool = False):
|
||||
super().__init__()
|
||||
self._text = text
|
||||
self._font_size = font_size
|
||||
@@ -431,6 +439,8 @@ class UnifiedLabel(Widget):
|
||||
self._line_height = line_height * 0.9
|
||||
self._letter_spacing = letter_spacing # 0.1 = 10%
|
||||
self._spacing_pixels = font_size * letter_spacing
|
||||
self._shimmer = shimmer
|
||||
self._shimmer_start_time = 0.0
|
||||
|
||||
# Scroll state
|
||||
self._scroll = scroll
|
||||
@@ -489,6 +499,13 @@ class UnifiedLabel(Widget):
|
||||
self._spacing_pixels = self._font_size * letter_spacing
|
||||
self._cached_text = None # Invalidate cache
|
||||
|
||||
def set_line_height(self, line_height: float):
|
||||
"""Update line height (multiplier, e.g., 1.0 = default)."""
|
||||
new_line_height = line_height * 0.9
|
||||
if self._line_height != new_line_height:
|
||||
self._line_height = new_line_height
|
||||
self._cached_text = None
|
||||
|
||||
def set_font_weight(self, font_weight: FontWeight):
|
||||
"""Update the font weight."""
|
||||
if self._font_weight != font_weight:
|
||||
@@ -510,6 +527,9 @@ class UnifiedLabel(Widget):
|
||||
self._scroll_pause_t = None
|
||||
self._scroll_state = ScrollState.STARTING
|
||||
|
||||
def reset_shimmer(self, offset: float = 0.0):
|
||||
self._shimmer_start_time = rl.get_time() + offset
|
||||
|
||||
def set_max_width(self, max_width: int | None):
|
||||
"""Set the maximum width constraint for wrapping/eliding."""
|
||||
if self._max_width != max_width:
|
||||
@@ -627,6 +647,25 @@ class UnifiedLabel(Widget):
|
||||
return self._cached_total_height
|
||||
return 0.0
|
||||
|
||||
def _compute_shimmer_alpha(self, char_center_x: float, text_left: float, text_width: float) -> float:
|
||||
if text_width <= 0:
|
||||
return self.SHIMMER_LOW_OPACITY
|
||||
|
||||
elapsed = rl.get_time() - self._shimmer_start_time
|
||||
sigma = text_width * self.SHIMMER_BLUR_RADIUS
|
||||
|
||||
t_raw = (elapsed % self.SHIMMER_CYCLE_PERIOD) / self.SHIMMER_CYCLE_PERIOD
|
||||
t_clamped = max(0.0, min(t_raw / self.SHIMMER_SWEEP_FRACTION, 1.0))
|
||||
t = t_clamped * t_clamped * (3.0 - 2.0 * t_clamped)
|
||||
|
||||
margin = text_width * self.SHIMMER_BAND_WIDTH
|
||||
text_right = text_left + text_width
|
||||
center = text_right + margin - t * (text_width + 2.0 * margin)
|
||||
|
||||
d = char_center_x - center
|
||||
shimmer = math.exp(-0.5 * d * d / (sigma * sigma)) if sigma > 0 else 0.0
|
||||
return self.SHIMMER_LOW_OPACITY + (1.0 - self.SHIMMER_LOW_OPACITY) * shimmer
|
||||
|
||||
def _render(self, _):
|
||||
"""Render the label."""
|
||||
if self._rect.width <= 0 or self._rect.height <= 0:
|
||||
@@ -770,6 +809,20 @@ class UnifiedLabel(Widget):
|
||||
line_x = self._rect.x + self._text_padding
|
||||
line_x += self._scroll_offset + x_offset
|
||||
|
||||
if self._shimmer and not emojis and line:
|
||||
base_alpha = self._text_color.a / 255.0
|
||||
text_width = max(size.x, 1.0)
|
||||
cursor_x = line_x
|
||||
for char in line:
|
||||
char_width = measure_text_cached(self._font, char, self._font_size, self._spacing_pixels).x
|
||||
char_center_x = cursor_x + char_width / 2.0
|
||||
shimmer_alpha = self._compute_shimmer_alpha(char_center_x, line_x, text_width)
|
||||
char_alpha = int(255 * base_alpha * shimmer_alpha)
|
||||
char_color = rl.Color(self._text_color.r, self._text_color.g, self._text_color.b, char_alpha)
|
||||
rl.draw_text_ex(self._font, char, rl.Vector2(cursor_x, current_y), self._font_size, self._spacing_pixels, char_color)
|
||||
cursor_x += char_width
|
||||
return
|
||||
|
||||
# Render line with emojis
|
||||
line_pos = rl.Vector2(line_x, current_y)
|
||||
prev_index = 0
|
||||
|
||||
@@ -11,6 +11,7 @@ ITEM_SPACING = 20
|
||||
LINE_COLOR = rl.GRAY
|
||||
LINE_PADDING = 40
|
||||
ANIMATION_SCALE = 0.6
|
||||
EDGE_SHADOW_WIDTH = 20
|
||||
|
||||
MIN_ZOOM_ANIMATION_TIME = 0.075 # seconds
|
||||
DO_ZOOM = False
|
||||
@@ -33,9 +34,50 @@ class LineSeparator(Widget):
|
||||
LINE_COLOR)
|
||||
|
||||
|
||||
class ScrollIndicator(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._txt_scroll_indicator = gui_app.texture("icons_mici/settings/horizontal_scroll_indicator.png", 96, 48)
|
||||
self._scroll_offset: float = 0.0
|
||||
self._content_size: float = 0.0
|
||||
self._viewport: rl.Rectangle = rl.Rectangle(0, 0, 0, 0)
|
||||
|
||||
def update(self, scroll_offset: float, content_size: float, viewport: rl.Rectangle) -> None:
|
||||
self._scroll_offset = scroll_offset
|
||||
self._content_size = content_size
|
||||
self._viewport = viewport
|
||||
|
||||
def _render(self, _):
|
||||
if self._viewport.width <= 0 or self._viewport.height <= 0:
|
||||
return
|
||||
|
||||
indicator_w = min(float(np.interp(self._content_size, [1000, 3000], [300, 100])), self._viewport.width)
|
||||
max_scroll = self._content_size - self._viewport.width
|
||||
if max_scroll > 0:
|
||||
scroll_ratio = -self._scroll_offset / max_scroll
|
||||
slide_range = max(self._viewport.width - indicator_w, 0.0)
|
||||
x = self._viewport.x + scroll_ratio * slide_range
|
||||
else:
|
||||
x = self._viewport.x + (self._viewport.width - indicator_w) / 2
|
||||
y = max(self._viewport.y, 0) + self._viewport.height - self._txt_scroll_indicator.height / 2
|
||||
|
||||
dest_left = max(x, self._viewport.x)
|
||||
dest_right = min(x + indicator_w, self._viewport.x + self._viewport.width)
|
||||
dest_w = max(indicator_w / 2, dest_right - dest_left)
|
||||
|
||||
dest_left = min(dest_left, self._viewport.x + self._viewport.width - dest_w)
|
||||
dest_left = max(dest_left, self._viewport.x)
|
||||
|
||||
src_rec = rl.Rectangle(0, 0, self._txt_scroll_indicator.width, self._txt_scroll_indicator.height)
|
||||
dest_rec = rl.Rectangle(dest_left, y, dest_w, self._txt_scroll_indicator.height)
|
||||
rl.draw_texture_pro(self._txt_scroll_indicator, src_rec, dest_rec, rl.Vector2(0, 0), 0.0,
|
||||
rl.Color(255, 255, 255, int(255 * 0.45)))
|
||||
|
||||
|
||||
class Scroller(Widget):
|
||||
def __init__(self, items: list[Widget], horizontal: bool = True, snap_items: bool = True, spacing: int = ITEM_SPACING,
|
||||
line_separator: bool = False, pad_start: int = ITEM_SPACING, pad_end: int = ITEM_SPACING):
|
||||
line_separator: bool = False, pad_start: int = ITEM_SPACING, pad_end: int = ITEM_SPACING,
|
||||
scroll_indicator: bool = False, edge_shadows: bool = False):
|
||||
super().__init__()
|
||||
self._items: list[Widget] = []
|
||||
self._horizontal = horizontal
|
||||
@@ -65,11 +107,18 @@ class Scroller(Widget):
|
||||
self.scroll_panel = GuiScrollPanel2(self._horizontal, handle_out_of_bounds=not self._snap_items)
|
||||
self._scroll_enabled: bool | Callable[[], bool] = True
|
||||
|
||||
self._txt_scroll_indicator = gui_app.texture("icons_mici/settings/vertical_scroll_indicator.png", 40, 80)
|
||||
self._txt_vertical_scroll_indicator = gui_app.texture("icons_mici/settings/vertical_scroll_indicator.png", 40, 80)
|
||||
self._show_scroll_indicator = scroll_indicator and self._horizontal
|
||||
self._scroll_indicator = ScrollIndicator()
|
||||
self._edge_shadows = edge_shadows and self._horizontal
|
||||
|
||||
for item in items:
|
||||
self.add_widget(item)
|
||||
|
||||
@property
|
||||
def items(self) -> list[Widget]:
|
||||
return self._items
|
||||
|
||||
def set_reset_scroll_at_show(self, scroll: bool):
|
||||
self._reset_scroll_at_show = scroll
|
||||
|
||||
@@ -93,6 +142,14 @@ class Scroller(Widget):
|
||||
self._items.append(item)
|
||||
item.set_touch_valid_callback(lambda: self.scroll_panel.is_touch_valid() and self.enabled)
|
||||
|
||||
def move_item(self, from_index: int, to_index: int) -> None:
|
||||
if from_index == to_index:
|
||||
return
|
||||
if not (0 <= from_index < len(self._items) and 0 <= to_index < len(self._items)):
|
||||
return
|
||||
item = self._items.pop(from_index)
|
||||
self._items.insert(to_index, item)
|
||||
|
||||
def set_scrolling_enabled(self, enabled: bool | Callable[[], bool]) -> None:
|
||||
"""Set whether scrolling is enabled (does not affect widget enabled state)."""
|
||||
self._scroll_enabled = enabled
|
||||
@@ -243,13 +300,27 @@ class Scroller(Widget):
|
||||
|
||||
# Draw scroll indicator
|
||||
if SCROLL_BAR and not self._horizontal and len(self._visible_items) > 0:
|
||||
_real_content_size = self._content_size - self._rect.height + self._txt_scroll_indicator.height
|
||||
_real_content_size = self._content_size - self._rect.height + self._txt_vertical_scroll_indicator.height
|
||||
scroll_bar_y = -self._scroll_offset / _real_content_size * self._rect.height
|
||||
scroll_bar_y = min(max(scroll_bar_y, self._rect.y), self._rect.y + self._rect.height - self._txt_scroll_indicator.height)
|
||||
rl.draw_texture_ex(self._txt_scroll_indicator, rl.Vector2(self._rect.x, scroll_bar_y), 0, 1.0, rl.WHITE)
|
||||
scroll_bar_y = min(max(scroll_bar_y, self._rect.y), self._rect.y + self._rect.height - self._txt_vertical_scroll_indicator.height)
|
||||
rl.draw_texture_ex(self._txt_vertical_scroll_indicator, rl.Vector2(self._rect.x, scroll_bar_y), 0, 1.0, rl.WHITE)
|
||||
|
||||
rl.end_scissor_mode()
|
||||
|
||||
if self._edge_shadows:
|
||||
rl.draw_rectangle_gradient_h(int(self._rect.x), int(self._rect.y),
|
||||
EDGE_SHADOW_WIDTH, int(self._rect.height),
|
||||
rl.Color(0, 0, 0, 166), rl.BLANK)
|
||||
|
||||
right_x = int(self._rect.x + self._rect.width - EDGE_SHADOW_WIDTH)
|
||||
rl.draw_rectangle_gradient_h(right_x, int(self._rect.y),
|
||||
EDGE_SHADOW_WIDTH, int(self._rect.height),
|
||||
rl.BLANK, rl.Color(0, 0, 0, 166))
|
||||
|
||||
if self._show_scroll_indicator and len(self._visible_items) > 0:
|
||||
self._scroll_indicator.update(self._scroll_offset, self._content_size, self._rect)
|
||||
self._scroll_indicator.render()
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
if self._reset_scroll_at_show:
|
||||
|
||||
+39
-15
@@ -5,17 +5,19 @@ import pyray as rl
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.common.filter_simple import BounceFilter, FirstOrderFilter
|
||||
|
||||
|
||||
class SmallSlider(Widget):
|
||||
HORIZONTAL_PADDING = 8
|
||||
CONFIRM_DELAY = 0.2
|
||||
PRESSED_SCALE = 1.07
|
||||
|
||||
def __init__(self, title: str, confirm_callback: Callable | None = None):
|
||||
def __init__(self, title: str, confirm_callback: Callable | None = None, shimmer_offset: float = 0.0):
|
||||
# TODO: unify this with BigConfirmationDialogV2
|
||||
super().__init__()
|
||||
self._confirm_callback = confirm_callback
|
||||
self._shimmer_offset = shimmer_offset
|
||||
|
||||
self._font = gui_app.font(FontWeight.DISPLAY)
|
||||
|
||||
@@ -30,29 +32,40 @@ class SmallSlider(Widget):
|
||||
self._start_x_circle = 0.0
|
||||
self._scroll_x_circle = 0.0
|
||||
self._scroll_x_circle_filter = FirstOrderFilter(0, 0.05, 1 / gui_app.target_fps)
|
||||
self._circle_scale_filter = BounceFilter(1.0, 0.1, 1 / gui_app.target_fps)
|
||||
self._circle_press_time: float | None = None
|
||||
|
||||
self._is_dragging_circle = False
|
||||
|
||||
self._label = UnifiedLabel(title, font_size=36, font_weight=FontWeight.MEDIUM, text_color=rl.Color(255, 255, 255, int(255 * 0.65)),
|
||||
self._label = UnifiedLabel(title, font_size=36, font_weight=FontWeight.SEMI_BOLD, text_color=rl.WHITE,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE, line_height=0.9)
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE, line_height=0.9, shimmer=True)
|
||||
|
||||
def _load_assets(self):
|
||||
self.set_rect(rl.Rectangle(0, 0, 316 + self.HORIZONTAL_PADDING * 2, 100))
|
||||
|
||||
self._bg_txt = gui_app.texture("icons_mici/setup/small_slider/slider_bg.png", 316, 100)
|
||||
self._circle_bg_txt = gui_app.texture("icons_mici/setup/small_slider/slider_red_circle.png", 100, 100)
|
||||
self._circle_bg_pressed_txt = self._circle_bg_txt
|
||||
self._circle_arrow_txt = gui_app.texture("icons_mici/setup/small_slider/slider_arrow.png", 37, 32)
|
||||
|
||||
@property
|
||||
def confirmed(self) -> bool:
|
||||
return self._confirmed_time > 0.0
|
||||
|
||||
def reset(self):
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self.reset()
|
||||
|
||||
def reset(self, reset_shimmer: bool = True):
|
||||
# reset all slider state
|
||||
self._is_dragging_circle = False
|
||||
self._confirmed_time = 0.0
|
||||
self._confirm_callback_called = False
|
||||
self._circle_press_time = None
|
||||
self._circle_scale_filter.x = 1.0
|
||||
if reset_shimmer:
|
||||
self._label.reset_shimmer(self._shimmer_offset)
|
||||
|
||||
def set_opacity(self, opacity: float, smooth: bool = False):
|
||||
if smooth:
|
||||
@@ -83,6 +96,7 @@ class SmallSlider(Widget):
|
||||
if rl.check_collision_point_rec(mouse_event.pos, circle_button_rect):
|
||||
self._start_x_circle = mouse_event.pos.x
|
||||
self._is_dragging_circle = True
|
||||
self._circle_press_time = rl.get_time()
|
||||
|
||||
elif mouse_event.left_released:
|
||||
# swiped to left
|
||||
@@ -129,8 +143,9 @@ class SmallSlider(Widget):
|
||||
btn_x = bg_txt_x + self._bg_txt.width - self._circle_bg_txt.width + self._scroll_x_circle_filter.x
|
||||
btn_y = self._rect.y + (self._rect.height - self._circle_bg_txt.height) / 2
|
||||
|
||||
if self._confirmed_time == 0.0 or self._scroll_x_circle > 0:
|
||||
self._label.set_text_color(rl.Color(255, 255, 255, int(255 * 0.65 * (1.0 - self.slider_percentage) * self._opacity_filter.x)))
|
||||
label_alpha = int(255 * (1.0 - self.slider_percentage) * self._opacity_filter.x)
|
||||
if label_alpha > 0:
|
||||
self._label.set_text_color(rl.Color(255, 255, 255, label_alpha))
|
||||
label_rect = rl.Rectangle(
|
||||
self._rect.x + 20,
|
||||
self._rect.y,
|
||||
@@ -139,18 +154,24 @@ class SmallSlider(Widget):
|
||||
)
|
||||
self._label.render(label_rect)
|
||||
|
||||
# circle and arrow
|
||||
rl.draw_texture_ex(self._circle_bg_txt, rl.Vector2(btn_x, btn_y), 0.0, 1.0, white)
|
||||
circle_pressed = self._is_dragging_circle or self.confirmed or (
|
||||
self._circle_press_time is not None and rl.get_time() - self._circle_press_time < 0.075
|
||||
)
|
||||
circle_bg_txt = self._circle_bg_pressed_txt if circle_pressed else self._circle_bg_txt
|
||||
scale = self._circle_scale_filter.update(self.PRESSED_SCALE if circle_pressed else 1.0)
|
||||
scaled_btn_x = btn_x + (self._circle_bg_txt.width * (1 - scale)) / 2
|
||||
scaled_btn_y = btn_y + (self._circle_bg_txt.height * (1 - scale)) / 2
|
||||
rl.draw_texture_ex(circle_bg_txt, rl.Vector2(scaled_btn_x, scaled_btn_y), 0.0, scale, white)
|
||||
|
||||
arrow_x = btn_x + (self._circle_bg_txt.width - self._circle_arrow_txt.width) / 2
|
||||
arrow_y = btn_y + (self._circle_bg_txt.height - self._circle_arrow_txt.height) / 2
|
||||
arrow_x = scaled_btn_x + (self._circle_bg_txt.width * scale - self._circle_arrow_txt.width) / 2
|
||||
arrow_y = scaled_btn_y + (self._circle_bg_txt.height * scale - self._circle_arrow_txt.height) / 2
|
||||
rl.draw_texture_ex(self._circle_arrow_txt, rl.Vector2(arrow_x, arrow_y), 0.0, 1.0, white)
|
||||
|
||||
|
||||
class LargerSlider(SmallSlider):
|
||||
def __init__(self, title: str, confirm_callback: Callable | None = None, green: bool = True):
|
||||
def __init__(self, title: str, confirm_callback: Callable | None = None, green: bool = True, shimmer_offset: float = 0.0):
|
||||
self._green = green
|
||||
super().__init__(title, confirm_callback=confirm_callback)
|
||||
super().__init__(title, confirm_callback=confirm_callback, shimmer_offset=shimmer_offset)
|
||||
|
||||
def _load_assets(self):
|
||||
self.set_rect(rl.Rectangle(0, 0, 520 + self.HORIZONTAL_PADDING * 2, 115))
|
||||
@@ -158,6 +179,7 @@ class LargerSlider(SmallSlider):
|
||||
self._bg_txt = gui_app.texture("icons_mici/setup/small_slider/slider_bg_larger.png", 520, 115)
|
||||
circle_fn = "slider_green_rounded_rectangle" if self._green else "slider_black_rounded_rectangle"
|
||||
self._circle_bg_txt = gui_app.texture(f"icons_mici/setup/small_slider/{circle_fn}.png", 180, 115)
|
||||
self._circle_bg_pressed_txt = self._circle_bg_txt
|
||||
self._circle_arrow_txt = gui_app.texture("icons_mici/setup/small_slider/slider_arrow.png", 64, 55)
|
||||
|
||||
|
||||
@@ -165,15 +187,16 @@ class BigSlider(SmallSlider):
|
||||
def __init__(self, title: str, icon: rl.Texture, confirm_callback: Callable | None = None):
|
||||
self._icon = icon
|
||||
super().__init__(title, confirm_callback=confirm_callback)
|
||||
self._label = UnifiedLabel(title, font_size=48, font_weight=FontWeight.DISPLAY, text_color=rl.Color(255, 255, 255, int(255 * 0.65)),
|
||||
self._label = UnifiedLabel(title, font_size=48, font_weight=FontWeight.DISPLAY, text_color=rl.WHITE,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE,
|
||||
line_height=0.875)
|
||||
line_height=0.875, shimmer=True)
|
||||
|
||||
def _load_assets(self):
|
||||
self.set_rect(rl.Rectangle(0, 0, 520 + self.HORIZONTAL_PADDING * 2, 180))
|
||||
|
||||
self._bg_txt = gui_app.texture("icons_mici/buttons/slider_bg.png", 520, 180)
|
||||
self._circle_bg_txt = gui_app.texture("icons_mici/buttons/button_circle.png", 180, 180)
|
||||
self._circle_bg_pressed_txt = gui_app.texture("icons_mici/buttons/button_circle_hover.png", 180, 180)
|
||||
self._circle_arrow_txt = self._icon
|
||||
|
||||
|
||||
@@ -183,4 +206,5 @@ class RedBigSlider(BigSlider):
|
||||
|
||||
self._bg_txt = gui_app.texture("icons_mici/buttons/slider_bg.png", 520, 180)
|
||||
self._circle_bg_txt = gui_app.texture("icons_mici/buttons/button_circle_red.png", 180, 180)
|
||||
self._circle_bg_pressed_txt = gui_app.texture("icons_mici/buttons/button_circle_red_hover.png", 180, 180)
|
||||
self._circle_arrow_txt = self._icon
|
||||
|
||||
Reference in New Issue
Block a user