mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-06-30 11:02:19 +08:00
BigUI WIP: In-line sliders
This commit is contained in:
@@ -216,6 +216,17 @@ class AetherListMetrics:
|
||||
toggle_width: int = 78
|
||||
toggle_height: int = 42
|
||||
toggle_right_inset: int = 34
|
||||
adjustor_row_height: int = 94
|
||||
adjustor_row_active_height: int = 154
|
||||
adjustor_preset_height: int = 30
|
||||
adjustor_preset_gap: int = 10
|
||||
adjustor_scrubber_height: int = 52
|
||||
adjustor_value_pill_height: int = 36
|
||||
adjustor_value_pill_width: int = 144
|
||||
range_row_height: int = 140
|
||||
range_control_height: int = 56
|
||||
range_control_bottom: int = 14
|
||||
range_control_inset_x: int = 18
|
||||
utility_value_right: int = 270
|
||||
utility_value_width: int = 220
|
||||
utility_chevron_right: int = 62
|
||||
@@ -754,6 +765,645 @@ def draw_settings_list_row(
|
||||
)
|
||||
|
||||
|
||||
def format_adjustor_value(value: float, *, step: float = 1.0, unit: str = "", labels: dict[float, str] | None = None) -> str:
|
||||
label_map = labels or {}
|
||||
tolerance = max(abs(step) * 0.5, 1e-4) if step != 0 else 1e-4
|
||||
for label_value, label in label_map.items():
|
||||
if abs(float(label_value) - float(value)) <= tolerance:
|
||||
return label
|
||||
|
||||
if step > 0 and step < 1:
|
||||
decimals = max(1, min(3, len(f"{step:.6f}".rstrip("0").split(".")[-1])))
|
||||
return f"{value:.{decimals}f}{unit}"
|
||||
if abs(value - round(value)) <= tolerance:
|
||||
return f"{int(round(value))}{unit}"
|
||||
return f"{value:.2f}".rstrip("0").rstrip(".") + unit
|
||||
|
||||
|
||||
def draw_range_setting_row(
|
||||
rect: rl.Rectangle,
|
||||
*,
|
||||
title: str,
|
||||
subtitle: str = "",
|
||||
value: str = "",
|
||||
enabled: bool = True,
|
||||
hovered: bool = False,
|
||||
pressed: bool = False,
|
||||
is_last: bool = False,
|
||||
title_size: int = 24,
|
||||
subtitle_size: int = 18,
|
||||
value_size: int = 24,
|
||||
separator_inset: int = 22,
|
||||
control_height: int = AETHER_LIST_METRICS.range_control_height,
|
||||
control_bottom: int = AETHER_LIST_METRICS.range_control_bottom,
|
||||
control_inset_x: int = AETHER_LIST_METRICS.range_control_inset_x,
|
||||
title_color: rl.Color | None = None,
|
||||
subtitle_color: rl.Color | None = None,
|
||||
value_color: rl.Color | None = None,
|
||||
style: PanelStyle = DEFAULT_PANEL_STYLE,
|
||||
) -> rl.Rectangle:
|
||||
draw_rect = _snap_rect(rect)
|
||||
resolved_title_color = title_color or (style.title_color if enabled else style.muted_color)
|
||||
resolved_subtitle_color = subtitle_color or (style.subtitle_color if enabled else style.muted_color)
|
||||
resolved_value_color = value_color or (style.title_color if enabled else style.muted_color)
|
||||
value_reserved = min(240.0, draw_rect.width * 0.24) if value else 0.0
|
||||
content_left = draw_rect.x + 24
|
||||
content_right = draw_rect.x + draw_rect.width - 24
|
||||
if value_reserved:
|
||||
content_right -= value_reserved + 20
|
||||
content_width = max(120.0, content_right - content_left)
|
||||
|
||||
draw_list_row_shell(
|
||||
draw_rect,
|
||||
hovered=hovered and enabled,
|
||||
pressed=pressed and enabled,
|
||||
is_last=is_last,
|
||||
row_bg=rl.Color(255, 255, 255, 0),
|
||||
row_border=rl.Color(255, 255, 255, 0),
|
||||
row_separator=style.divider_color,
|
||||
current_bg=rl.Color(255, 255, 255, 0),
|
||||
current_border=rl.Color(255, 255, 255, 0),
|
||||
separator_inset=separator_inset,
|
||||
)
|
||||
|
||||
gui_label(
|
||||
rl.Rectangle(content_left, draw_rect.y + 14, content_width, title_size + 6),
|
||||
title,
|
||||
title_size,
|
||||
resolved_title_color,
|
||||
FontWeight.MEDIUM,
|
||||
)
|
||||
if subtitle:
|
||||
gui_label(
|
||||
rl.Rectangle(content_left, draw_rect.y + 46, content_width, subtitle_size + 8),
|
||||
subtitle,
|
||||
subtitle_size,
|
||||
resolved_subtitle_color,
|
||||
FontWeight.NORMAL,
|
||||
)
|
||||
if value:
|
||||
gui_label(
|
||||
rl.Rectangle(draw_rect.x + draw_rect.width - value_reserved - 24, draw_rect.y + 18, value_reserved, value_size + 6),
|
||||
value,
|
||||
value_size,
|
||||
resolved_value_color,
|
||||
FontWeight.MEDIUM,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT,
|
||||
)
|
||||
|
||||
return _snap_rect(
|
||||
rl.Rectangle(
|
||||
draw_rect.x + control_inset_x,
|
||||
draw_rect.y + draw_rect.height - control_height - control_bottom,
|
||||
draw_rect.width - control_inset_x * 2,
|
||||
control_height,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class AetherInlineRangeControl(Widget):
|
||||
def __init__(
|
||||
self,
|
||||
min_val: float,
|
||||
max_val: float,
|
||||
step: float,
|
||||
current_val: float,
|
||||
on_change: Callable[[float], None],
|
||||
*,
|
||||
on_commit: Callable[[float], None] | None = None,
|
||||
unit: str = "",
|
||||
labels: dict[float, str] | None = None,
|
||||
color: rl.Color = AetherListColors.PRIMARY,
|
||||
major_tick_count: int = 5,
|
||||
):
|
||||
super().__init__()
|
||||
self.min_val = min_val
|
||||
self.max_val = max_val
|
||||
self.step = step
|
||||
self.current_val = current_val
|
||||
self._on_change = on_change
|
||||
self._on_commit = on_commit
|
||||
self._unit = unit
|
||||
self._labels = labels or {}
|
||||
self._color = color
|
||||
self._major_tick_count = max(0, major_tick_count)
|
||||
self._font = gui_app.font(FontWeight.SEMI_BOLD)
|
||||
|
||||
self._smooth_value = current_val
|
||||
self._thumb_focus = 0.0
|
||||
self._pending_drag = False
|
||||
self._is_dragging = False
|
||||
self._started_on_thumb = False
|
||||
self._press_start = rl.Vector2(0, 0)
|
||||
self._value_at_press = current_val
|
||||
|
||||
self._pressed_button: int = 0
|
||||
self._button_press_started = 0.0
|
||||
self._next_repeat_at = 0.0
|
||||
self._repeat_count = 0
|
||||
self._button_press_changed = False
|
||||
|
||||
self._minus_rect = rl.Rectangle(0, 0, 0, 0)
|
||||
self._plus_rect = rl.Rectangle(0, 0, 0, 0)
|
||||
self._track_rect = rl.Rectangle(0, 0, 0, 0)
|
||||
self._thumb_rect = rl.Rectangle(0, 0, 0, 0)
|
||||
|
||||
@property
|
||||
def is_interacting(self) -> bool:
|
||||
return self._pending_drag or self._is_dragging or self._pressed_button != 0
|
||||
|
||||
def set_value(self, value: float) -> None:
|
||||
self.current_val = self._clamp_and_snap(value)
|
||||
|
||||
def reset_interaction(self) -> None:
|
||||
self._pending_drag = False
|
||||
self._is_dragging = False
|
||||
self._started_on_thumb = False
|
||||
self._pressed_button = 0
|
||||
self._repeat_count = 0
|
||||
self._button_press_changed = False
|
||||
|
||||
def set_touch_valid_callback(self, touch_callback: Callable[[], bool]) -> None:
|
||||
super().set_touch_valid_callback(touch_callback)
|
||||
|
||||
def _cancel_interaction(self, *, revert: bool = False) -> None:
|
||||
if revert and self.current_val != self._value_at_press:
|
||||
self.current_val = self._value_at_press
|
||||
self._on_change(self.current_val)
|
||||
self.reset_interaction()
|
||||
|
||||
def _clamp_and_snap(self, value: float) -> float:
|
||||
if self.step <= 0:
|
||||
return max(self.min_val, min(self.max_val, value))
|
||||
snapped = round((value - self.min_val) / self.step) * self.step + self.min_val
|
||||
return max(self.min_val, min(self.max_val, snapped))
|
||||
|
||||
def _step_value(self, direction: int) -> bool:
|
||||
new_val = self._clamp_and_snap(self.current_val + direction * self.step)
|
||||
if new_val == self.current_val:
|
||||
return False
|
||||
self.current_val = new_val
|
||||
self._on_change(self.current_val)
|
||||
self._button_press_changed = True
|
||||
return True
|
||||
|
||||
def _value_fraction(self, value: float) -> float:
|
||||
value_range = self.max_val - self.min_val
|
||||
if value_range == 0:
|
||||
return 0.0
|
||||
return max(0.0, min(1.0, (value - self.min_val) / value_range))
|
||||
|
||||
def _update_val_from_mouse(self, mouse_pos: MousePos) -> None:
|
||||
if self._track_rect.width <= 0:
|
||||
return
|
||||
rel_x = max(0.0, min(1.0, (mouse_pos.x - self._track_rect.x) / self._track_rect.width))
|
||||
value = self.min_val + rel_x * (self.max_val - self.min_val)
|
||||
snapped = self._clamp_and_snap(value)
|
||||
if snapped != self.current_val:
|
||||
self.current_val = snapped
|
||||
self._on_change(self.current_val)
|
||||
|
||||
def _commit_if_needed(self) -> None:
|
||||
if self.current_val != self._value_at_press and self._on_commit is not None:
|
||||
self._on_commit(self.current_val)
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
if self._pressed_button == 0:
|
||||
return
|
||||
now = time.monotonic()
|
||||
if now < self._next_repeat_at:
|
||||
return
|
||||
|
||||
if self._step_value(self._pressed_button):
|
||||
self._repeat_count += 1
|
||||
repeat_interval = 0.12 if self._repeat_count < 3 else 0.075
|
||||
self._next_repeat_at = now + repeat_interval
|
||||
|
||||
def _handle_mouse_press(self, mouse_pos: MousePos):
|
||||
if not self._touch_valid() or not rl.check_collision_point_rec(mouse_pos, self._rect):
|
||||
return
|
||||
|
||||
self._value_at_press = self.current_val
|
||||
self._button_press_changed = False
|
||||
now = time.monotonic()
|
||||
if rl.check_collision_point_rec(mouse_pos, self._minus_rect):
|
||||
self._pressed_button = -1
|
||||
self._button_press_started = now
|
||||
self._next_repeat_at = now + 0.34
|
||||
self._repeat_count = 0
|
||||
return
|
||||
if rl.check_collision_point_rec(mouse_pos, self._plus_rect):
|
||||
self._pressed_button = 1
|
||||
self._button_press_started = now
|
||||
self._next_repeat_at = now + 0.34
|
||||
self._repeat_count = 0
|
||||
return
|
||||
|
||||
self._pending_drag = True
|
||||
self._started_on_thumb = rl.check_collision_point_rec(mouse_pos, _inflate_rect(self._thumb_rect, 8, 8))
|
||||
self._press_start = rl.Vector2(mouse_pos.x, mouse_pos.y)
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
if self._pressed_button != 0:
|
||||
direction = self._pressed_button
|
||||
button_rect = self._minus_rect if direction < 0 else self._plus_rect
|
||||
if rl.check_collision_point_rec(mouse_pos, button_rect) and self._repeat_count == 0:
|
||||
self._step_value(direction)
|
||||
self._pressed_button = 0
|
||||
self._repeat_count = 0
|
||||
self._commit_if_needed()
|
||||
return
|
||||
|
||||
if self._is_dragging:
|
||||
self._is_dragging = False
|
||||
self._commit_if_needed()
|
||||
return
|
||||
|
||||
if self._pending_drag:
|
||||
if not self._started_on_thumb and rl.check_collision_point_rec(mouse_pos, self._rect):
|
||||
self._update_val_from_mouse(mouse_pos)
|
||||
self._commit_if_needed()
|
||||
self._pending_drag = False
|
||||
self._started_on_thumb = False
|
||||
|
||||
def _handle_mouse_event(self, mouse_event: MouseEvent):
|
||||
mouse_in_rect = rl.check_collision_point_rec(mouse_event.pos, self._rect)
|
||||
if mouse_event.left_released and self.is_interacting and not mouse_in_rect:
|
||||
if self._pressed_button != 0:
|
||||
self._pressed_button = 0
|
||||
self._repeat_count = 0
|
||||
self._commit_if_needed()
|
||||
elif self._is_dragging or self._pending_drag:
|
||||
self._cancel_interaction(revert=True)
|
||||
return
|
||||
|
||||
if not self._touch_valid():
|
||||
self._cancel_interaction(revert=True)
|
||||
return
|
||||
|
||||
if self._pressed_button != 0:
|
||||
button_rect = self._minus_rect if self._pressed_button < 0 else self._plus_rect
|
||||
if not rl.check_collision_point_rec(mouse_event.pos, _inflate_rect(button_rect, 4, 4)):
|
||||
self._pressed_button = 0
|
||||
self._repeat_count = 0
|
||||
if self._button_press_changed:
|
||||
self._commit_if_needed()
|
||||
return
|
||||
|
||||
if self._pending_drag and not self._is_dragging:
|
||||
dx = mouse_event.pos.x - self._press_start.x
|
||||
dy = mouse_event.pos.y - self._press_start.y
|
||||
if abs(dy) > 12 and abs(dy) > abs(dx):
|
||||
self._pending_drag = False
|
||||
self._started_on_thumb = False
|
||||
return
|
||||
if abs(dx) > 12 and abs(dx) >= abs(dy):
|
||||
self._pending_drag = False
|
||||
self._is_dragging = True
|
||||
|
||||
if self._is_dragging:
|
||||
dx = mouse_event.pos.x - self._press_start.x
|
||||
dy = mouse_event.pos.y - self._press_start.y
|
||||
if abs(dy) > 18 and abs(dy) > abs(dx) * 1.15:
|
||||
self._cancel_interaction(revert=True)
|
||||
return
|
||||
self._update_val_from_mouse(mouse_event.pos)
|
||||
|
||||
def _draw_button(self, rect: rl.Rectangle, label: str, *, pressed: bool = False):
|
||||
fill = rl.Color(255, 255, 255, 8 if not pressed else 14)
|
||||
border = rl.Color(255, 255, 255, 18 if not pressed else 28)
|
||||
_draw_rounded_fill(rect, fill, radius_px=14)
|
||||
_draw_rounded_stroke(rect, border, radius_px=14)
|
||||
_draw_text_fit_common(
|
||||
self._font,
|
||||
label,
|
||||
rl.Vector2(rect.x + 10, rect.y + (rect.height - 22) / 2),
|
||||
max(1.0, rect.width - 20),
|
||||
22,
|
||||
align_center=True,
|
||||
color=AetherListColors.HEADER,
|
||||
)
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
rect = _snap_rect(rect)
|
||||
self.set_rect(rect)
|
||||
dt = rl.get_frame_time()
|
||||
self._smooth_value += (self.current_val - self._smooth_value) * (1 - math.exp(-dt / 0.075))
|
||||
thumb_target = 1.0 if self._is_dragging or self._pending_drag else 0.0
|
||||
self._thumb_focus += (thumb_target - self._thumb_focus) * (1 - math.exp(-dt / 0.070))
|
||||
|
||||
button_size = min(rect.height, 44)
|
||||
button_y = rect.y + (rect.height - button_size) / 2
|
||||
self._minus_rect = _snap_rect(rl.Rectangle(rect.x, button_y, button_size, button_size))
|
||||
self._plus_rect = _snap_rect(rl.Rectangle(rect.x + rect.width - button_size, button_y, button_size, button_size))
|
||||
self._draw_button(self._minus_rect, "-", pressed=self._pressed_button < 0)
|
||||
self._draw_button(self._plus_rect, "+", pressed=self._pressed_button > 0)
|
||||
|
||||
track_x = self._minus_rect.x + self._minus_rect.width + 14
|
||||
track_w = max(1.0, self._plus_rect.x - 14 - track_x)
|
||||
lane_h = rect.height
|
||||
track_h = 4.0
|
||||
track_y = rect.y + (lane_h - track_h) / 2
|
||||
self._track_rect = _snap_rect(rl.Rectangle(track_x, track_y, track_w, track_h))
|
||||
|
||||
rl.draw_rectangle_rounded(self._track_rect, 1.0, 12, rl.Color(255, 255, 255, 18))
|
||||
if self._major_tick_count > 1:
|
||||
for index in range(self._major_tick_count):
|
||||
frac = index / max(1, self._major_tick_count - 1)
|
||||
tick_x = self._track_rect.x + frac * self._track_rect.width
|
||||
rl.draw_rectangle_rec(rl.Rectangle(tick_x - 1, rect.y + rect.height / 2 - 8, 2, 16), rl.Color(255, 255, 255, 24))
|
||||
|
||||
fill_frac = self._value_fraction(self._smooth_value)
|
||||
fill_w = fill_frac * self._track_rect.width
|
||||
if fill_w > 1:
|
||||
fill_rect = _snap_rect(rl.Rectangle(self._track_rect.x, self._track_rect.y, fill_w, self._track_rect.height))
|
||||
rl.draw_rectangle_rounded(fill_rect, 1.0, 12, _with_alpha(self._color, 220))
|
||||
|
||||
thumb_w = 18 + self._thumb_focus * 4
|
||||
thumb_h = 28 + self._thumb_focus * 4
|
||||
thumb_center_x = self._track_rect.x + fill_frac * self._track_rect.width
|
||||
thumb_center_y = rect.y + rect.height / 2
|
||||
self._thumb_rect = _snap_rect(rl.Rectangle(thumb_center_x - thumb_w / 2, thumb_center_y - thumb_h / 2, thumb_w, thumb_h))
|
||||
thumb_fill = _mix_colors(rl.Color(224, 230, 238, 255), self._color, 0.12)
|
||||
_draw_rounded_fill(self._thumb_rect, thumb_fill, radius_px=12)
|
||||
_draw_rounded_stroke(self._thumb_rect, rl.Color(14, 17, 23, 52), radius_px=12)
|
||||
if self._thumb_focus > 0.02:
|
||||
_draw_rounded_stroke(_inflate_rect(self._thumb_rect, 1, 1), _with_alpha(self._color, int(70 * self._thumb_focus)), radius_px=13)
|
||||
|
||||
if self._is_dragging or self._pressed_button != 0:
|
||||
bubble_text = format_adjustor_value(self.current_val, step=self.step, unit=self._unit, labels=self._labels)
|
||||
bubble_w = max(80.0, min(132.0, 44.0 + len(bubble_text) * 10.0))
|
||||
bubble_rect = _snap_rect(rl.Rectangle(thumb_center_x - bubble_w / 2, rect.y - 40, bubble_w, 32))
|
||||
bubble_fill = _mix_colors(rl.Color(18, 22, 28, 255), self._color, 0.18)
|
||||
bubble_border = _with_alpha(self._color, 70)
|
||||
_draw_rounded_fill(bubble_rect, bubble_fill, radius_px=14)
|
||||
_draw_rounded_stroke(bubble_rect, bubble_border, radius_px=14)
|
||||
_draw_text_fit_common(
|
||||
gui_app.font(FontWeight.MEDIUM),
|
||||
bubble_text,
|
||||
rl.Vector2(bubble_rect.x + 10, bubble_rect.y + 7),
|
||||
max(1.0, bubble_rect.width - 20),
|
||||
16,
|
||||
align_center=True,
|
||||
color=AetherListColors.HEADER,
|
||||
)
|
||||
|
||||
|
||||
class AetherAdjustorRow(Widget):
|
||||
def __init__(
|
||||
self,
|
||||
title: str,
|
||||
subtitle: str,
|
||||
min_val: float,
|
||||
max_val: float,
|
||||
step: float,
|
||||
get_value: Callable[[], float],
|
||||
on_change: Callable[[float], None],
|
||||
*,
|
||||
on_commit: Callable[[float], None] | None = None,
|
||||
unit: str = "",
|
||||
labels: dict[float, str] | None = None,
|
||||
presets: list[float] | None = None,
|
||||
is_active: bool | Callable[[], bool] = False,
|
||||
set_active: Callable[[bool], None] | None = None,
|
||||
style: PanelStyle = DEFAULT_PANEL_STYLE,
|
||||
color: rl.Color | None = None,
|
||||
):
|
||||
super().__init__()
|
||||
self._title = title
|
||||
self._subtitle = subtitle
|
||||
self._get_value = get_value
|
||||
self._is_active = is_active
|
||||
self._set_active = set_active
|
||||
self._style = style
|
||||
self._color = color or style.accent
|
||||
self._presets = presets or []
|
||||
self._font_title = gui_app.font(FontWeight.MEDIUM)
|
||||
self._font_subtitle = gui_app.font(FontWeight.NORMAL)
|
||||
self._font_value = gui_app.font(FontWeight.SEMI_BOLD)
|
||||
self._focus_progress = 0.0
|
||||
self._pressed_zone: str | None = None
|
||||
self._is_last = False
|
||||
self._header_rect = rl.Rectangle(0, 0, 0, 0)
|
||||
self._value_rect = rl.Rectangle(0, 0, 0, 0)
|
||||
self._hint_rect = rl.Rectangle(0, 0, 0, 0)
|
||||
self._preset_rects: list[tuple[float, rl.Rectangle]] = []
|
||||
self._scrubber_rect = rl.Rectangle(0, 0, 0, 0)
|
||||
self._scrubber = self._child(
|
||||
AetherInlineRangeControl(
|
||||
min_val,
|
||||
max_val,
|
||||
step,
|
||||
get_value(),
|
||||
on_change,
|
||||
on_commit=on_commit,
|
||||
unit=unit,
|
||||
labels=labels,
|
||||
color=self._color,
|
||||
)
|
||||
)
|
||||
self._unit = unit
|
||||
self._labels = labels or {}
|
||||
self._step = step
|
||||
|
||||
@property
|
||||
def is_interacting(self) -> bool:
|
||||
return self._scrubber.is_interacting
|
||||
|
||||
def _active(self) -> bool:
|
||||
return bool(self._is_active() if callable(self._is_active) else self._is_active)
|
||||
|
||||
def reset_interaction(self) -> None:
|
||||
self._pressed_zone = None
|
||||
self._scrubber.reset_interaction()
|
||||
|
||||
def set_touch_valid_callback(self, touch_callback: Callable[[], bool]) -> None:
|
||||
super().set_touch_valid_callback(touch_callback)
|
||||
self._scrubber.set_touch_valid_callback(touch_callback)
|
||||
|
||||
def set_is_last(self, is_last: bool) -> None:
|
||||
self._is_last = is_last
|
||||
|
||||
def _current_value(self) -> float:
|
||||
return self._scrubber.current_val if (self._active() or self._scrubber.is_interacting) else self._get_value()
|
||||
|
||||
def formatted_value(self) -> str:
|
||||
return format_adjustor_value(self._current_value(), step=self._step, unit=self._unit, labels=self._labels)
|
||||
|
||||
def measure_height(self, width: float) -> float:
|
||||
del width
|
||||
if not self._active():
|
||||
return float(AETHER_LIST_METRICS.adjustor_row_height)
|
||||
preset_height = AETHER_LIST_METRICS.adjustor_preset_height + AETHER_LIST_METRICS.adjustor_preset_gap if self._presets else 0
|
||||
return float(AETHER_LIST_METRICS.adjustor_row_active_height + preset_height)
|
||||
|
||||
def _set_active_state(self, active: bool) -> None:
|
||||
if self._set_active is not None:
|
||||
self._set_active(active)
|
||||
|
||||
def _handle_mouse_press(self, mouse_pos: MousePos):
|
||||
if not self._touch_valid() or not rl.check_collision_point_rec(mouse_pos, self._rect):
|
||||
return
|
||||
self._pressed_zone = None
|
||||
|
||||
if self._active():
|
||||
for preset_value, preset_rect in self._preset_rects:
|
||||
if rl.check_collision_point_rec(mouse_pos, _inflate_rect(preset_rect, 4, 4)):
|
||||
self._pressed_zone = f"preset:{preset_value}"
|
||||
return
|
||||
if rl.check_collision_point_rec(mouse_pos, self._scrubber_rect):
|
||||
self._pressed_zone = "scrubber"
|
||||
return
|
||||
|
||||
if rl.check_collision_point_rec(mouse_pos, self._header_rect) or rl.check_collision_point_rec(mouse_pos, self._value_rect) or rl.check_collision_point_rec(mouse_pos, self._hint_rect):
|
||||
self._pressed_zone = "header"
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
pressed_zone = self._pressed_zone
|
||||
self._pressed_zone = None
|
||||
|
||||
if pressed_zone == "scrubber":
|
||||
return
|
||||
|
||||
if pressed_zone and pressed_zone.startswith("preset:"):
|
||||
self._scrubber._value_at_press = self._scrubber.current_val
|
||||
try:
|
||||
preset_value = float(pressed_zone.split(":", 1)[1])
|
||||
except ValueError:
|
||||
return
|
||||
for value, preset_rect in self._preset_rects:
|
||||
if value == preset_value and rl.check_collision_point_rec(mouse_pos, _inflate_rect(preset_rect, 4, 4)):
|
||||
self._scrubber.set_value(preset_value)
|
||||
self._scrubber._on_change(self._scrubber.current_val)
|
||||
self._scrubber._commit_if_needed()
|
||||
return
|
||||
return
|
||||
|
||||
if pressed_zone == "header":
|
||||
active = self._active()
|
||||
if rl.check_collision_point_rec(mouse_pos, _inflate_rect(self._header_rect, 6, 4)) or rl.check_collision_point_rec(mouse_pos, _inflate_rect(self._value_rect, 6, 4)) or rl.check_collision_point_rec(mouse_pos, _inflate_rect(self._hint_rect, 6, 4)):
|
||||
self._set_active_state(not active)
|
||||
|
||||
def _handle_mouse_event(self, mouse_event: MouseEvent):
|
||||
del mouse_event
|
||||
|
||||
def _render_preset_chip(self, rect: rl.Rectangle, text: str, *, current: bool, pressed: bool):
|
||||
fill = rl.Color(255, 255, 255, 5)
|
||||
border = rl.Color(255, 255, 255, 14)
|
||||
text_color = self._style.subtitle_color
|
||||
if current:
|
||||
fill = _mix_colors(rl.Color(18, 22, 28, 255), self._color, 0.22, alpha=255)
|
||||
border = _with_alpha(self._color, 72)
|
||||
text_color = self._style.title_color
|
||||
elif pressed:
|
||||
fill = rl.Color(255, 255, 255, 10)
|
||||
border = rl.Color(255, 255, 255, 22)
|
||||
|
||||
_draw_rounded_fill(rect, fill, radius_px=13)
|
||||
_draw_rounded_stroke(rect, border, radius_px=13)
|
||||
_draw_text_fit_common(
|
||||
gui_app.font(FontWeight.MEDIUM),
|
||||
text,
|
||||
rl.Vector2(rect.x + 10, rect.y + 7),
|
||||
max(1.0, rect.width - 20),
|
||||
15,
|
||||
align_center=True,
|
||||
color=text_color,
|
||||
)
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
rect = _snap_rect(rect)
|
||||
self.set_rect(rect)
|
||||
active = self._active()
|
||||
if not self._scrubber.is_interacting:
|
||||
self._scrubber.set_value(self._get_value())
|
||||
|
||||
dt = rl.get_frame_time()
|
||||
focus_target = 1.0 if active else 0.0
|
||||
self._focus_progress += (focus_target - self._focus_progress) * (1 - math.exp(-dt / 0.09))
|
||||
|
||||
hovered = _point_hits(gui_app.last_mouse_event.pos, rect, self._parent_rect, pad_x=6, pad_y=0)
|
||||
current_bg = _with_alpha(_mix_colors(rl.Color(18, 22, 28, 255), self._color, 0.16), int(18 + self._focus_progress * 14))
|
||||
current_border = _with_alpha(self._color, int(34 + self._focus_progress * 34))
|
||||
draw_list_row_shell(
|
||||
rect,
|
||||
current=active or self._focus_progress > 0.02,
|
||||
hovered=hovered and not self.is_interacting,
|
||||
pressed=self._pressed_zone == "header",
|
||||
is_last=self._is_last,
|
||||
row_bg=rl.Color(255, 255, 255, 0),
|
||||
row_border=rl.Color(255, 255, 255, 0),
|
||||
row_separator=self._style.divider_color,
|
||||
current_bg=current_bg,
|
||||
current_border=current_border,
|
||||
)
|
||||
|
||||
value_pill_w = min(float(AETHER_LIST_METRICS.adjustor_value_pill_width), max(118.0, rect.width * 0.22))
|
||||
self._header_rect = rl.Rectangle(rect.x, rect.y, rect.width, min(rect.height, 78))
|
||||
self._value_rect = _snap_rect(rl.Rectangle(rect.x + rect.width - value_pill_w - 18, rect.y + 14, value_pill_w, AETHER_LIST_METRICS.adjustor_value_pill_height))
|
||||
content_right = self._value_rect.x - 18
|
||||
content_left = rect.x + 24
|
||||
content_width = max(120.0, content_right - content_left)
|
||||
|
||||
gui_label(rl.Rectangle(content_left, rect.y + 14, content_width, 28), self._title, 24, self._style.title_color, FontWeight.MEDIUM)
|
||||
gui_label(rl.Rectangle(content_left, rect.y + 44, content_width, 22), self._subtitle, 18, self._style.subtitle_color, FontWeight.NORMAL)
|
||||
|
||||
pill_fill = rl.Color(255, 255, 255, 5)
|
||||
pill_border = rl.Color(255, 255, 255, 14)
|
||||
if active:
|
||||
pill_fill = _mix_colors(rl.Color(18, 22, 28, 255), self._color, 0.20, alpha=255)
|
||||
pill_border = _with_alpha(self._color, 64)
|
||||
_draw_rounded_fill(self._value_rect, pill_fill, radius_px=16)
|
||||
_draw_rounded_stroke(self._value_rect, pill_border, radius_px=16)
|
||||
_draw_text_fit_common(
|
||||
self._font_value,
|
||||
self.formatted_value(),
|
||||
rl.Vector2(self._value_rect.x + 14, self._value_rect.y + 8),
|
||||
max(1.0, self._value_rect.width - 28),
|
||||
18,
|
||||
align_center=True,
|
||||
color=self._style.title_color,
|
||||
)
|
||||
|
||||
hint_y = rect.y + 76
|
||||
self._hint_rect = _snap_rect(rl.Rectangle(content_left, hint_y, rect.width - 48, 8))
|
||||
hint_track = _snap_rect(rl.Rectangle(self._hint_rect.x, self._hint_rect.y + 2, self._hint_rect.width, 4))
|
||||
rl.draw_rectangle_rounded(hint_track, 1.0, 10, rl.Color(255, 255, 255, 10))
|
||||
fill_w = hint_track.width * self._scrubber._value_fraction(self._current_value())
|
||||
if fill_w > 0:
|
||||
rl.draw_rectangle_rounded(_snap_rect(rl.Rectangle(hint_track.x, hint_track.y, fill_w, hint_track.height)), 1.0, 10, _with_alpha(self._color, 180 if active else 120))
|
||||
|
||||
if not active:
|
||||
return
|
||||
|
||||
tray_alpha = max(0, min(255, int(255 * self._focus_progress)))
|
||||
tray_top = rect.y + 92 - (1.0 - self._focus_progress) * 6
|
||||
current_y = tray_top
|
||||
self._preset_rects.clear()
|
||||
|
||||
if self._presets:
|
||||
chip_gap = 8.0
|
||||
chip_h = float(AETHER_LIST_METRICS.adjustor_preset_height)
|
||||
chip_w = max(68.0, (rect.width - 48 - chip_gap * (len(self._presets) - 1)) / max(1, len(self._presets)))
|
||||
chip_x = content_left
|
||||
for preset_value in self._presets:
|
||||
chip_rect = _snap_rect(rl.Rectangle(chip_x, current_y, chip_w, chip_h))
|
||||
self._preset_rects.append((preset_value, chip_rect))
|
||||
self._render_preset_chip(
|
||||
chip_rect,
|
||||
format_adjustor_value(preset_value, step=self._step, unit=self._unit, labels=self._labels),
|
||||
current=abs(self._current_value() - preset_value) <= max(abs(self._step) * 0.5, 1e-4),
|
||||
pressed=self._pressed_zone == f"preset:{preset_value}",
|
||||
)
|
||||
chip_x += chip_w + chip_gap
|
||||
current_y += chip_h + AETHER_LIST_METRICS.adjustor_preset_gap
|
||||
|
||||
self._scrubber_rect = _snap_rect(rl.Rectangle(content_left, current_y, rect.width - 48, AETHER_LIST_METRICS.adjustor_scrubber_height))
|
||||
self._scrubber.set_parent_rect(self._parent_rect)
|
||||
self._scrubber.render(self._scrubber_rect)
|
||||
|
||||
|
||||
def draw_selection_list_row(
|
||||
rect: rl.Rectangle,
|
||||
*,
|
||||
@@ -1705,10 +2355,14 @@ class AetherSlider(Widget):
|
||||
unit: str = "",
|
||||
labels: dict[float, str] | None = None,
|
||||
color: rl.Color = rl.Color(54, 77, 239, 255),
|
||||
on_commit: Callable[[float], None] | None = None,
|
||||
show_value_label: bool = True,
|
||||
):
|
||||
super().__init__()
|
||||
self.min_val, self.max_val, self.step, self.current_val = min_val, max_val, step, current_val
|
||||
self.on_change, self.unit, self.labels, self.color = on_change, unit, labels or {}, color
|
||||
self.on_change, self.on_commit = on_change, on_commit
|
||||
self.unit, self.labels, self.color = unit, labels or {}, color
|
||||
self.show_value_label = show_value_label
|
||||
self._is_dragging = False
|
||||
self._font = gui_app.font(FontWeight.BOLD)
|
||||
self._thumb_offset: float = 0.0
|
||||
@@ -1716,6 +2370,70 @@ class AetherSlider(Widget):
|
||||
self._plus_offset: float = 0.0
|
||||
self._minus_pressed = False
|
||||
self._plus_pressed = False
|
||||
self._pending_drag = False
|
||||
self._press_start = rl.Vector2(0, 0)
|
||||
self._started_on_thumb = False
|
||||
self._value_at_press = current_val
|
||||
|
||||
@property
|
||||
def is_interacting(self) -> bool:
|
||||
return self._is_dragging or self._pending_drag or self._minus_pressed or self._plus_pressed
|
||||
|
||||
def set_value(self, value: float) -> None:
|
||||
self.current_val = self._clamp_and_snap(value)
|
||||
|
||||
def reset_interaction(self) -> None:
|
||||
self._is_dragging = False
|
||||
self._pending_drag = False
|
||||
self._started_on_thumb = False
|
||||
self._thumb_offset = 0.0
|
||||
self._minus_pressed = False
|
||||
self._plus_pressed = False
|
||||
|
||||
def _cancel_interaction(self, *, revert: bool = False) -> None:
|
||||
if revert and self.current_val != self._value_at_press:
|
||||
self.current_val = self._value_at_press
|
||||
self.on_change(self.current_val)
|
||||
self.reset_interaction()
|
||||
|
||||
def _finalize_interaction(self, mouse_pos: MousePos, *, inside_release: bool) -> None:
|
||||
button_w = self._button_width(self._rect)
|
||||
changed = False
|
||||
|
||||
if self._minus_pressed:
|
||||
self._minus_pressed = False
|
||||
if inside_release and rl.check_collision_point_rec(mouse_pos, rl.Rectangle(self._rect.x, self._rect.y, button_w, self._rect.height)):
|
||||
new_val = self._clamp_and_snap(self.current_val - self.step)
|
||||
if new_val != self.current_val:
|
||||
self.current_val = new_val
|
||||
self.on_change(self.current_val)
|
||||
changed = True
|
||||
|
||||
if self._plus_pressed:
|
||||
self._plus_pressed = False
|
||||
if inside_release and rl.check_collision_point_rec(mouse_pos, rl.Rectangle(self._rect.x + self._rect.width - button_w, self._rect.y, button_w, self._rect.height)):
|
||||
new_val = self._clamp_and_snap(self.current_val + self.step)
|
||||
if new_val != self.current_val:
|
||||
self.current_val = new_val
|
||||
self.on_change(self.current_val)
|
||||
changed = True
|
||||
|
||||
if self._is_dragging:
|
||||
changed = changed or self.current_val != self._value_at_press
|
||||
self._is_dragging = False
|
||||
self._thumb_offset = 0.0
|
||||
elif self._pending_drag:
|
||||
if inside_release and not self._started_on_thumb and rl.check_collision_point_rec(mouse_pos, self._rect):
|
||||
before_tap = self.current_val
|
||||
self._update_val_from_mouse(mouse_pos)
|
||||
changed = changed or before_tap != self.current_val
|
||||
self._pending_drag = False
|
||||
self._thumb_offset = 0.0
|
||||
changed = changed or self.current_val != self._value_at_press
|
||||
|
||||
self._started_on_thumb = False
|
||||
if changed and self.on_commit is not None:
|
||||
self.on_commit(self.current_val)
|
||||
|
||||
def _clamp_and_snap(self, val: float) -> float:
|
||||
if self.step <= 0:
|
||||
@@ -1786,8 +2504,9 @@ class AetherSlider(Widget):
|
||||
if self.step > 0:
|
||||
n_steps = int(round(value_range / self.step))
|
||||
if n_steps > 0:
|
||||
for i in range(n_steps + 1):
|
||||
tick_x = track_x + (i / n_steps) * track_w
|
||||
tick_count = min(n_steps, 24)
|
||||
for i in range(tick_count + 1):
|
||||
tick_x = track_x + (i / max(1, tick_count)) * track_w
|
||||
tick_h = int(track_h * 0.6)
|
||||
tick_y = track_rect.y + (track_h - tick_h) / 2
|
||||
rl.draw_rectangle_rec(rl.Rectangle(tick_x - 1, tick_y, 2, tick_h), rl.Color(255, 255, 255, 60))
|
||||
@@ -1799,16 +2518,18 @@ class AetherSlider(Widget):
|
||||
_draw_rounded_fill(t_face_rect, rl.Color(230, 235, 242, 255), radius_px=12)
|
||||
_draw_rounded_stroke(t_face_rect, rl.Color(20, 22, 28, 46), radius_px=12)
|
||||
rl.draw_rectangle_rec(rl.Rectangle(t_face_rect.x, t_face_rect.y, t_face_rect.width, 1), rl.Color(255, 255, 255, 40))
|
||||
val_str = self.labels.get(self.current_val, f"{self.current_val:.2f}".rstrip('0').rstrip('.') + self.unit)
|
||||
label_size = max(18, int(round(rect.height * 0.38)))
|
||||
ts = measure_text_cached(self._font, val_str, label_size)
|
||||
val_x = max(rect.x, min(thumb_x + (thumb_w - ts.x) / 2, rect.x + rect.width - ts.x))
|
||||
val_pos = rl.Vector2(val_x, thumb_y - label_size - 10)
|
||||
rl.draw_text_ex(self._font, val_str, rl.Vector2(round(val_pos.x), round(val_pos.y)), label_size, 0, rl.WHITE)
|
||||
if self.show_value_label:
|
||||
val_str = self.labels.get(self.current_val, f"{self.current_val:.2f}".rstrip('0').rstrip('.') + self.unit)
|
||||
label_size = max(18, int(round(rect.height * 0.38)))
|
||||
ts = measure_text_cached(self._font, val_str, label_size)
|
||||
val_x = max(rect.x, min(thumb_x + (thumb_w - ts.x) / 2, rect.x + rect.width - ts.x))
|
||||
val_pos = rl.Vector2(val_x, thumb_y - label_size - 10)
|
||||
rl.draw_text_ex(self._font, val_str, rl.Vector2(round(val_pos.x), round(val_pos.y)), label_size, 0, rl.WHITE)
|
||||
|
||||
def _handle_mouse_press(self, mouse_pos: MousePos):
|
||||
if not rl.check_collision_point_rec(mouse_pos, self._rect):
|
||||
if not self._touch_valid() or not rl.check_collision_point_rec(mouse_pos, self._rect):
|
||||
return
|
||||
self._value_at_press = self.current_val
|
||||
button_w = self._button_width(self._rect)
|
||||
minus_rect = rl.Rectangle(self._rect.x, self._rect.y, button_w, self._rect.height)
|
||||
plus_rect = rl.Rectangle(self._rect.x + self._rect.width - button_w, self._rect.y, button_w, self._rect.height)
|
||||
@@ -1823,38 +2544,48 @@ class AetherSlider(Widget):
|
||||
thumb_y = self._rect.y + (self._rect.height - thumb_h) / 2
|
||||
thumb_rect = rl.Rectangle(thumb_x - 8, thumb_y - 8, thumb_w + 16, thumb_h + 16)
|
||||
if rl.check_collision_point_rec(mouse_pos, thumb_rect):
|
||||
self._is_dragging = True
|
||||
self._pending_drag = True
|
||||
self._started_on_thumb = True
|
||||
self._press_start = rl.Vector2(mouse_pos.x, mouse_pos.y)
|
||||
self._thumb_offset = 1.0
|
||||
else:
|
||||
track_x = self._rect.x + button_w
|
||||
track_w = self._rect.width - 2 * button_w
|
||||
if track_w > 0:
|
||||
rel_x = max(0.0, min(1.0, (mouse_pos.x - track_x) / track_w))
|
||||
val = self.min_val + rel_x * (self.max_val - self.min_val)
|
||||
snapped = self._clamp_and_snap(val)
|
||||
if snapped != self.current_val:
|
||||
self.current_val = snapped
|
||||
self.on_change(self.current_val)
|
||||
self._pending_drag = True
|
||||
self._started_on_thumb = False
|
||||
self._press_start = rl.Vector2(mouse_pos.x, mouse_pos.y)
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
button_w = self._button_width(self._rect)
|
||||
if self._minus_pressed:
|
||||
self._minus_pressed = False
|
||||
if rl.check_collision_point_rec(mouse_pos, rl.Rectangle(self._rect.x, self._rect.y, button_w, self._rect.height)):
|
||||
new_val = self._clamp_and_snap(self.current_val - self.step)
|
||||
if new_val != self.current_val:
|
||||
self.current_val = new_val
|
||||
self.on_change(self.current_val)
|
||||
if self._plus_pressed:
|
||||
self._plus_pressed = False
|
||||
if rl.check_collision_point_rec(mouse_pos, rl.Rectangle(self._rect.x + self._rect.width - button_w, self._rect.y, button_w, self._rect.height)):
|
||||
new_val = self._clamp_and_snap(self.current_val + self.step)
|
||||
if new_val != self.current_val:
|
||||
self.current_val = new_val
|
||||
self.on_change(self.current_val)
|
||||
self._finalize_interaction(mouse_pos, inside_release=True)
|
||||
|
||||
def _handle_mouse_event(self, mouse_event: MouseEvent):
|
||||
mouse_in_rect = rl.check_collision_point_rec(mouse_event.pos, self._rect)
|
||||
if mouse_event.left_released and self.is_interacting and not mouse_in_rect:
|
||||
self._finalize_interaction(mouse_event.pos, inside_release=False)
|
||||
return
|
||||
|
||||
if not self._touch_valid():
|
||||
self._cancel_interaction(revert=True)
|
||||
return
|
||||
|
||||
if self._pending_drag and not self._is_dragging:
|
||||
dx = mouse_event.pos.x - self._press_start.x
|
||||
dy = mouse_event.pos.y - self._press_start.y
|
||||
if abs(dy) > 12 and abs(dy) > abs(dx):
|
||||
self._pending_drag = False
|
||||
self._started_on_thumb = False
|
||||
self._thumb_offset = 0.0
|
||||
return
|
||||
if abs(dx) > 12 and abs(dx) >= abs(dy):
|
||||
self._pending_drag = False
|
||||
self._is_dragging = True
|
||||
self._thumb_offset = 1.0
|
||||
|
||||
if self._is_dragging:
|
||||
self._is_dragging = False
|
||||
self._thumb_offset = 0.0
|
||||
dx = mouse_event.pos.x - self._press_start.x
|
||||
dy = mouse_event.pos.y - self._press_start.y
|
||||
if abs(dy) > 18 and abs(dy) > abs(dx) * 1.15:
|
||||
self._cancel_interaction(revert=True)
|
||||
return
|
||||
self._update_val_from_mouse(mouse_event.pos)
|
||||
|
||||
def _update_val_from_mouse(self, mouse_pos: MousePos):
|
||||
button_w = self._button_width(self._rect)
|
||||
@@ -2211,8 +2942,9 @@ class TileGrid(Widget):
|
||||
|
||||
def add_tile(self, tile: Widget):
|
||||
self.tiles.append(tile)
|
||||
if self._touch_valid_callback is not None and hasattr(tile, "set_touch_valid_callback"):
|
||||
tile.set_touch_valid_callback(self._touch_valid_callback)
|
||||
touch_valid_callback = getattr(self, "_touch_valid_callback", None)
|
||||
if touch_valid_callback is not None and hasattr(tile, "set_touch_valid_callback"):
|
||||
tile.set_touch_valid_callback(touch_valid_callback)
|
||||
|
||||
def set_touch_valid_callback(self, touch_callback: Callable[[], bool]) -> None:
|
||||
super().set_touch_valid_callback(touch_callback)
|
||||
|
||||
@@ -25,9 +25,9 @@ from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPan
|
||||
from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import (
|
||||
AETHER_COMPACT_ROW_HEIGHT,
|
||||
AETHER_LIST_METRICS,
|
||||
AetherAdjustorRow,
|
||||
AetherScrollbar,
|
||||
AetherSegmentedControl,
|
||||
AetherSliderDialog,
|
||||
AetherListColors,
|
||||
DEFAULT_PANEL_STYLE,
|
||||
_point_hits,
|
||||
@@ -89,6 +89,7 @@ class SystemSettingsManagerView(Widget):
|
||||
SECTION_GAP = AETHER_LIST_METRICS.section_gap
|
||||
SECTION_HEADER_HEIGHT = AETHER_LIST_METRICS.section_header_height
|
||||
SECTION_HEADER_GAP = AETHER_LIST_METRICS.section_header_gap
|
||||
SLIDER_ROW_HEIGHT = AETHER_LIST_METRICS.range_row_height
|
||||
ROW_HEIGHT = AETHER_COMPACT_ROW_HEIGHT
|
||||
CONTENT_GUTTER = AETHER_LIST_METRICS.content_right_gutter
|
||||
FADE_HEIGHT = AETHER_LIST_METRICS.fade_height
|
||||
@@ -106,10 +107,13 @@ class SystemSettingsManagerView(Widget):
|
||||
self._scrollbar = AetherScrollbar()
|
||||
self._content_height = 0.0
|
||||
self._scroll_offset = 0.0
|
||||
self._ensure_visible_key: str | None = None
|
||||
self._interactive_rects: dict[str, rl.Rectangle] = {}
|
||||
self._pressed_target: str | None = None
|
||||
self._can_click = True
|
||||
self._active_tab_key = "basics"
|
||||
self._active_adjustor_key: str | None = None
|
||||
self._adjustor_rows: dict[str, AetherAdjustorRow] = {}
|
||||
self._display_slider_keys = ["ScreenBrightness", "ScreenBrightnessOnroad", "ScreenTimeout", "ScreenTimeoutOnroad"]
|
||||
self._power_slider_keys = ["DeviceShutdown", "LowVoltageShutdown"]
|
||||
|
||||
@@ -127,6 +131,8 @@ class SystemSettingsManagerView(Widget):
|
||||
"min": 0,
|
||||
"max": 101,
|
||||
"step": 1,
|
||||
"live": True,
|
||||
"presets": [0, 25, 50, 75, 101],
|
||||
"get": lambda: float(self._controller._params.get_int("ScreenBrightness")),
|
||||
"set": lambda v: self._controller._set_brightness("ScreenBrightness", v),
|
||||
},
|
||||
@@ -138,6 +144,8 @@ class SystemSettingsManagerView(Widget):
|
||||
"min": 1,
|
||||
"max": 101,
|
||||
"step": 1,
|
||||
"live": True,
|
||||
"presets": [1, 35, 60, 80, 101],
|
||||
"get": lambda: float(max(1, self._controller._params.get_int("ScreenBrightnessOnroad"))),
|
||||
"set": lambda v: self._controller._set_brightness("ScreenBrightnessOnroad", max(1, int(v))),
|
||||
},
|
||||
@@ -149,6 +157,8 @@ class SystemSettingsManagerView(Widget):
|
||||
"min": 5,
|
||||
"max": 60,
|
||||
"step": 5,
|
||||
"live": False,
|
||||
"presets": [5, 15, 30, 60],
|
||||
"get": lambda: float(self._controller._params.get_int("ScreenTimeout")),
|
||||
"set": lambda v: self._controller._params.put_int("ScreenTimeout", int(v)),
|
||||
},
|
||||
@@ -160,6 +170,8 @@ class SystemSettingsManagerView(Widget):
|
||||
"min": 5,
|
||||
"max": 60,
|
||||
"step": 5,
|
||||
"live": False,
|
||||
"presets": [5, 15, 30, 60],
|
||||
"get": lambda: float(self._controller._params.get_int("ScreenTimeoutOnroad")),
|
||||
"set": lambda v: self._controller._params.put_int("ScreenTimeoutOnroad", int(v)),
|
||||
},
|
||||
@@ -171,6 +183,8 @@ class SystemSettingsManagerView(Widget):
|
||||
"min": 0,
|
||||
"max": 33,
|
||||
"step": 1,
|
||||
"live": False,
|
||||
"presets": [0, 1, 4, 8],
|
||||
"get": lambda: float(self._controller._params.get_int("DeviceShutdown")),
|
||||
"set": lambda v: self._controller._params.put_int("DeviceShutdown", int(v)),
|
||||
},
|
||||
@@ -182,11 +196,38 @@ class SystemSettingsManagerView(Widget):
|
||||
"min": 11.8,
|
||||
"max": 12.5,
|
||||
"step": 0.1,
|
||||
"live": False,
|
||||
"presets": [11.8, 12.0, 12.2, 12.5],
|
||||
"get": lambda: float(self._controller._params.get_float("LowVoltageShutdown")),
|
||||
"set": lambda v: self._controller._params.put_float("LowVoltageShutdown", float(v)),
|
||||
},
|
||||
}
|
||||
|
||||
for key, spec in self._slider_specs.items():
|
||||
on_change = (lambda value, setter=spec["set"]: setter(value)) if spec.get("live") else (lambda _value: None)
|
||||
on_commit = None if spec.get("live") else (lambda value, setter=spec["set"]: setter(value))
|
||||
adjustor = self._child(
|
||||
AetherAdjustorRow(
|
||||
spec["title"],
|
||||
spec["subtitle"],
|
||||
spec["min"],
|
||||
spec["max"],
|
||||
spec["step"],
|
||||
spec["get"],
|
||||
on_change,
|
||||
on_commit=on_commit,
|
||||
unit=spec["unit"],
|
||||
labels=spec["labels"],
|
||||
presets=spec.get("presets", []),
|
||||
is_active=lambda key=key: self._active_adjustor_key == key,
|
||||
set_active=lambda active, key=key: self._set_active_adjustor(key, active),
|
||||
style=self.PANEL_STYLE,
|
||||
color=self.PANEL_STYLE.accent,
|
||||
)
|
||||
)
|
||||
adjustor.set_touch_valid_callback(lambda adjustor=adjustor: self._scroll_panel.is_touch_valid() or adjustor.is_interacting)
|
||||
self._adjustor_rows[key] = adjustor
|
||||
|
||||
self._toggle_defs = [
|
||||
{
|
||||
"id": "StandbyMode",
|
||||
@@ -352,36 +393,24 @@ class SystemSettingsManagerView(Widget):
|
||||
return self._controller.backup_status_text()
|
||||
|
||||
def _format_slider_value(self, key: str) -> str:
|
||||
adjustor = self._adjustor_rows.get(key)
|
||||
if adjustor is not None:
|
||||
return adjustor.formatted_value()
|
||||
spec = self._slider_specs[key]
|
||||
current_val = spec["get"]()
|
||||
if current_val in spec["labels"]:
|
||||
return spec["labels"][current_val]
|
||||
if spec["step"] < 1:
|
||||
suffix = spec["unit"]
|
||||
return f"{current_val:.1f}{suffix}"
|
||||
suffix = spec["unit"]
|
||||
return f"{int(current_val)}{suffix}"
|
||||
return f"{current_val:.1f}{spec['unit']}"
|
||||
return f"{int(current_val)}{spec['unit']}"
|
||||
|
||||
def _open_slider_dialog(self, key: str):
|
||||
spec = self._slider_specs[key]
|
||||
|
||||
def on_close(result, value):
|
||||
if result == DialogResult.CONFIRM:
|
||||
spec["set"](value)
|
||||
|
||||
gui_app.push_widget(
|
||||
AetherSliderDialog(
|
||||
spec["title"],
|
||||
spec["min"],
|
||||
spec["max"],
|
||||
spec["step"],
|
||||
spec["get"](),
|
||||
on_close,
|
||||
unit=spec["unit"],
|
||||
labels=spec["labels"],
|
||||
color=AetherListColors.PRIMARY,
|
||||
)
|
||||
)
|
||||
def _set_active_adjustor(self, key: str, active: bool):
|
||||
if active:
|
||||
self._active_adjustor_key = key
|
||||
self._ensure_visible_key = key
|
||||
elif self._active_adjustor_key == key:
|
||||
self._active_adjustor_key = None
|
||||
self._ensure_visible_key = None
|
||||
|
||||
def _get_drive_mode_index(self):
|
||||
state = self._controller._get_force_drive_state()
|
||||
@@ -401,6 +430,10 @@ class SystemSettingsManagerView(Widget):
|
||||
def _clear_ephemeral_state(self):
|
||||
self._pressed_target = None
|
||||
self._can_click = True
|
||||
self._active_adjustor_key = None
|
||||
self._ensure_visible_key = None
|
||||
for adjustor in self._adjustor_rows.values():
|
||||
adjustor.reset_interaction()
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
@@ -439,11 +472,11 @@ class SystemSettingsManagerView(Widget):
|
||||
if not target_id:
|
||||
return
|
||||
prefix, _, value = target_id.partition(":")
|
||||
if prefix == "slider":
|
||||
self._open_slider_dialog(value)
|
||||
return
|
||||
if prefix == "tab":
|
||||
self._active_tab_key = value
|
||||
self._active_adjustor_key = None
|
||||
for adjustor in self._adjustor_rows.values():
|
||||
adjustor.reset_interaction()
|
||||
return
|
||||
if prefix == "toggle":
|
||||
toggle_def = self._lookup_toggle(value)
|
||||
@@ -525,8 +558,8 @@ class SystemSettingsManagerView(Widget):
|
||||
return self.TAB_HEIGHT + self.TAB_BOTTOM_GAP + content_height
|
||||
|
||||
def _measure_active_tab_height(self, width: float) -> float:
|
||||
display_h = self._section_block_height(self._section_height(len(self._display_slider_keys), self.ROW_HEIGHT))
|
||||
power_h = self._section_block_height(self._section_height(len(self._power_slider_keys), self.ROW_HEIGHT))
|
||||
display_h = self._section_block_height(self._slider_section_height(self._display_slider_keys, width))
|
||||
power_h = self._section_block_height(self._slider_section_height(self._power_slider_keys, width))
|
||||
backups_h = self._section_block_height(self._section_height(2, self.ROW_HEIGHT))
|
||||
maintenance_h = self._section_block_height(self._maintenance_section_content_height())
|
||||
if self._active_tab_key == "basics":
|
||||
@@ -547,6 +580,13 @@ class SystemSettingsManagerView(Widget):
|
||||
def _section_block_height(self, content_height: float) -> float:
|
||||
return self.SECTION_HEADER_HEIGHT + self.SECTION_HEADER_GAP + content_height + self.SECTION_GAP
|
||||
|
||||
def _slider_section_height(self, keys: list[str], width: float) -> float:
|
||||
total = 0.0
|
||||
for key in keys:
|
||||
adjustor = self._adjustor_rows[key]
|
||||
total += adjustor.measure_height(width)
|
||||
return total
|
||||
|
||||
def _maintenance_section_content_height(self) -> float:
|
||||
support_h = self._section_height(len(self._support_rows), self.ROW_HEIGHT)
|
||||
danger_h = self._section_height(len(self._danger_rows), self.ROW_HEIGHT)
|
||||
@@ -618,30 +658,31 @@ class SystemSettingsManagerView(Widget):
|
||||
def _draw_slider_section(self, y: float, x: float, width: float, title: str, keys: list[str]) -> float:
|
||||
draw_section_header(rl.Rectangle(x, y, width, self.SECTION_HEADER_HEIGHT), title, style=self.PANEL_STYLE)
|
||||
y += self.SECTION_HEADER_HEIGHT + self.SECTION_HEADER_GAP
|
||||
group_rect = rl.Rectangle(x, y, width, self._section_height(len(keys), self.ROW_HEIGHT))
|
||||
group_rect = rl.Rectangle(x, y, width, self._slider_section_height(keys, width))
|
||||
draw_list_group_shell(group_rect)
|
||||
current_y = group_rect.y
|
||||
for index, key in enumerate(keys):
|
||||
self._draw_slider_row(rl.Rectangle(group_rect.x, y + index * self.ROW_HEIGHT, group_rect.width, self.ROW_HEIGHT), key, is_last=index == len(keys) - 1)
|
||||
current_y = self._draw_slider_row(rl.Rectangle(group_rect.x, current_y, group_rect.width, 0), key, is_last=index == len(keys) - 1)
|
||||
return y + group_rect.height + self.SECTION_GAP
|
||||
|
||||
def _draw_slider_row(self, rect: rl.Rectangle, key: str, is_last: bool):
|
||||
spec = self._slider_specs[key]
|
||||
target_id = f"slider:{key}"
|
||||
hovered, pressed = self._interactive_state(target_id, rect)
|
||||
draw_settings_list_row(
|
||||
rect,
|
||||
title=spec["title"],
|
||||
subtitle=spec["subtitle"],
|
||||
value=self._format_slider_value(key),
|
||||
hovered=hovered,
|
||||
pressed=pressed,
|
||||
is_last=is_last,
|
||||
show_chevron=True,
|
||||
title_size=24,
|
||||
subtitle_size=18,
|
||||
value_size=22,
|
||||
style=self.PANEL_STYLE,
|
||||
)
|
||||
def _draw_slider_row(self, rect: rl.Rectangle, key: str, is_last: bool) -> float:
|
||||
adjustor = self._adjustor_rows[key]
|
||||
adjustor.set_is_last(is_last)
|
||||
row_h = adjustor.measure_height(rect.width)
|
||||
row_rect = rl.Rectangle(rect.x, rect.y, rect.width, row_h)
|
||||
if self._ensure_visible_key == key:
|
||||
padding = 12.0
|
||||
min_offset = min(0.0, self._scroll_rect.y + padding - row_rect.y)
|
||||
max_offset = min(0.0, self._scroll_rect.y + self._scroll_rect.height - padding - (row_rect.y + row_rect.height))
|
||||
current_offset = self._scroll_panel.get_offset()
|
||||
if current_offset < max_offset:
|
||||
self._scroll_panel.set_offset(max_offset)
|
||||
elif current_offset > min_offset:
|
||||
self._scroll_panel.set_offset(min_offset)
|
||||
self._ensure_visible_key = None
|
||||
adjustor.set_parent_rect(self._scroll_rect)
|
||||
adjustor.render(row_rect)
|
||||
return rect.y + row_h
|
||||
|
||||
def _draw_toggle_group_section(self, y: float, x: float, width: float, group: dict) -> float:
|
||||
toggles = self._toggle_defs_for_group(group)
|
||||
|
||||
Reference in New Issue
Block a user