mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-06-28 18:12:05 +08:00
ui: refactor ListView for generic widget support and simplified item architecture (#35536)
refactor list view apply reviews
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
from openpilot.system.ui.lib.list_view import ListView, toggle_item
|
||||
from openpilot.system.ui.lib.widget import Widget
|
||||
from openpilot.system.ui.lib.list_view import ListView, ToggleItem
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.ui.widgets.ssh_key import ssh_key_item
|
||||
from openpilot.selfdrive.ui.widgets.ssh_key import SshKeyItem
|
||||
from openpilot.system.ui.lib.widget import Widget
|
||||
|
||||
# Description constants
|
||||
DESCRIPTIONS = {
|
||||
@@ -16,34 +16,33 @@ DESCRIPTIONS = {
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class DeveloperLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._params = Params()
|
||||
items = [
|
||||
toggle_item(
|
||||
ToggleItem(
|
||||
"Enable ADB",
|
||||
description=DESCRIPTIONS["enable_adb"],
|
||||
DESCRIPTIONS["enable_adb"],
|
||||
initial_state=self._params.get_bool("AdbEnabled"),
|
||||
callback=self._on_enable_adb,
|
||||
),
|
||||
ssh_key_item("SSH Key", description=DESCRIPTIONS["ssh_key"]),
|
||||
toggle_item(
|
||||
SshKeyItem("SSH Key", description=DESCRIPTIONS["ssh_key"]),
|
||||
ToggleItem(
|
||||
"Joystick Debug Mode",
|
||||
description=DESCRIPTIONS["joystick_debug_mode"],
|
||||
DESCRIPTIONS["joystick_debug_mode"],
|
||||
initial_state=self._params.get_bool("JoystickDebugMode"),
|
||||
callback=self._on_joystick_debug_mode,
|
||||
),
|
||||
toggle_item(
|
||||
ToggleItem(
|
||||
"Longitudinal Maneuver Mode",
|
||||
description="",
|
||||
"",
|
||||
initial_state=self._params.get_bool("LongitudinalManeuverMode"),
|
||||
callback=self._on_long_maneuver_mode,
|
||||
),
|
||||
toggle_item(
|
||||
ToggleItem(
|
||||
"openpilot Longitudinal Control (Alpha)",
|
||||
description="",
|
||||
"",
|
||||
initial_state=self._params.get_bool("AlphaLongitudinalEnabled"),
|
||||
callback=self._on_alpha_long_enabled,
|
||||
),
|
||||
@@ -54,7 +53,7 @@ class DeveloperLayout(Widget):
|
||||
def _render(self, rect):
|
||||
self._list_widget.render(rect)
|
||||
|
||||
def _on_enable_adb(self): pass
|
||||
def _on_joystick_debug_mode(self): pass
|
||||
def _on_long_maneuver_mode(self): pass
|
||||
def _on_alpha_long_enabled(self): pass
|
||||
def _on_enable_adb(self, state): pass
|
||||
def _on_joystick_debug_mode(self, state): pass
|
||||
def _on_long_maneuver_mode(self, state): pass
|
||||
def _on_alpha_long_enabled(self, state): pass
|
||||
|
||||
@@ -7,7 +7,7 @@ from openpilot.selfdrive.ui.onroad.driver_camera_dialog import DriverCameraDialo
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.hardware import TICI
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.list_view import ListView, text_item, button_item, dual_button_item
|
||||
from openpilot.system.ui.lib.list_view import ListView, TextItem, ButtonItem, DualButtonItem
|
||||
from openpilot.system.ui.lib.widget import Widget, DialogResult
|
||||
from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog
|
||||
from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
|
||||
@@ -44,15 +44,15 @@ class DeviceLayout(Widget):
|
||||
serial = self._params.get("HardwareSerial") or "N/A"
|
||||
|
||||
items = [
|
||||
text_item("Dongle ID", dongle_id),
|
||||
text_item("Serial", serial),
|
||||
button_item("Pair Device", "PAIR", DESCRIPTIONS['pair_device'], callback=self._pair_device),
|
||||
button_item("Driver Camera", "PREVIEW", DESCRIPTIONS['driver_camera'], callback=self._show_driver_camera, enabled=ui_state.is_offroad),
|
||||
button_item("Reset Calibration", "RESET", DESCRIPTIONS['reset_calibration'], callback=self._reset_calibration_prompt),
|
||||
button_item("Regulatory", "VIEW", callback=self._on_regulatory, visible=TICI),
|
||||
button_item("Review Training Guide", "REVIEW", DESCRIPTIONS['review_guide'], self._on_review_training_guide),
|
||||
button_item("Change Language", "CHANGE", callback=self._show_language_selection, enabled=ui_state.is_offroad),
|
||||
dual_button_item("Reboot", "Power Off", left_callback=self._reboot_prompt, right_callback=self._power_off_prompt),
|
||||
TextItem("Dongle ID", dongle_id),
|
||||
TextItem("Serial", serial),
|
||||
ButtonItem("Pair Device", "PAIR", DESCRIPTIONS['pair_device'], callback=self._pair_device),
|
||||
ButtonItem("Driver Camera", "PREVIEW", DESCRIPTIONS['driver_camera'], callback=self._show_driver_camera, enabled=ui_state.is_offroad),
|
||||
ButtonItem("Reset Calibration", "RESET", DESCRIPTIONS['reset_calibration'], callback=self._reset_calibration_prompt),
|
||||
ButtonItem("Regulatory", "VIEW", callback=self._on_regulatory, visible=TICI),
|
||||
ButtonItem("Review Training Guide", "REVIEW", DESCRIPTIONS['review_guide'], self._on_review_training_guide),
|
||||
ButtonItem("Change Language", "CHANGE", callback=self._show_language_selection, enabled=ui_state.is_offroad),
|
||||
DualButtonItem("Reboot", "Power Off", left_callback=self._reboot_prompt, right_callback=self._power_off_prompt),
|
||||
]
|
||||
return items
|
||||
|
||||
|
||||
@@ -115,13 +115,12 @@ class SettingsLayout(Widget):
|
||||
# Draw button text (right-aligned)
|
||||
text_size = measure_text_cached(self._font_medium, panel_info.name, 65)
|
||||
text_pos = rl.Vector2(
|
||||
button_rect.x + button_rect.width - text_size.x, button_rect.y + (button_rect.height - text_size.y) / 2
|
||||
button_rect.x + button_rect.width - text_size.x, y + (button_rect.height - text_size.y) / 2
|
||||
)
|
||||
rl.draw_text_ex(self._font_medium, panel_info.name, text_pos, 65, 0, text_color)
|
||||
|
||||
# Store button rect for click detection
|
||||
panel_info.button_rect = button_rect
|
||||
|
||||
y += NAV_BTN_HEIGHT + button_spacing
|
||||
|
||||
def _draw_current_panel(self, rect: rl.Rectangle):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.list_view import ListView, button_item, text_item
|
||||
from openpilot.system.ui.lib.list_view import ListView, ButtonItem, TextItem
|
||||
from openpilot.system.ui.lib.widget import Widget, DialogResult
|
||||
from openpilot.system.ui.widgets.confirm_dialog import confirm_dialog
|
||||
|
||||
@@ -15,11 +15,11 @@ class SoftwareLayout(Widget):
|
||||
|
||||
def _init_items(self):
|
||||
items = [
|
||||
text_item("Current Version", ""),
|
||||
button_item("Download", "CHECK", callback=self._on_download_update),
|
||||
button_item("Install Update", "INSTALL", callback=self._on_install_update),
|
||||
button_item("Target Branch", "SELECT", callback=self._on_select_branch),
|
||||
button_item("Uninstall", "UNINSTALL", callback=self._on_uninstall),
|
||||
TextItem("Current Version", ""),
|
||||
ButtonItem("Download", "CHECK", callback=self._on_download_update),
|
||||
ButtonItem("Install Update", "INSTALL", callback=self._on_install_update),
|
||||
ButtonItem("Target Branch", "SELECT", callback=self._on_select_branch),
|
||||
ButtonItem("Uninstall", "UNINSTALL", callback=self._on_uninstall),
|
||||
]
|
||||
return items
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from openpilot.system.ui.lib.list_view import ListView, multiple_button_item, toggle_item
|
||||
from openpilot.system.ui.lib.list_view import ListView, MultipleButtonItem, ToggleItem
|
||||
from openpilot.system.ui.lib.widget import Widget
|
||||
from openpilot.common.params import Params
|
||||
|
||||
@@ -29,24 +29,24 @@ class TogglesLayout(Widget):
|
||||
super().__init__()
|
||||
self._params = Params()
|
||||
items = [
|
||||
toggle_item(
|
||||
ToggleItem(
|
||||
"Enable openpilot",
|
||||
DESCRIPTIONS["OpenpilotEnabledToggle"],
|
||||
self._params.get_bool("OpenpilotEnabledToggle"),
|
||||
icon="chffr_wheel.png",
|
||||
),
|
||||
toggle_item(
|
||||
ToggleItem(
|
||||
"Experimental Mode",
|
||||
initial_state=self._params.get_bool("ExperimentalMode"),
|
||||
icon="experimental_white.png",
|
||||
),
|
||||
toggle_item(
|
||||
ToggleItem(
|
||||
"Disengage on Accelerator Pedal",
|
||||
DESCRIPTIONS["DisengageOnAccelerator"],
|
||||
self._params.get_bool("DisengageOnAccelerator"),
|
||||
icon="disengage_on_accelerator.png",
|
||||
),
|
||||
multiple_button_item(
|
||||
MultipleButtonItem(
|
||||
"Driving Personality",
|
||||
DESCRIPTIONS["LongitudinalPersonality"],
|
||||
buttons=["Aggressive", "Standard", "Relaxed"],
|
||||
@@ -55,25 +55,25 @@ class TogglesLayout(Widget):
|
||||
selected_index=int(self._params.get("LongitudinalPersonality") or 0),
|
||||
icon="speed_limit.png"
|
||||
),
|
||||
toggle_item(
|
||||
ToggleItem(
|
||||
"Enable Lane Departure Warnings",
|
||||
DESCRIPTIONS["IsLdwEnabled"],
|
||||
self._params.get_bool("IsLdwEnabled"),
|
||||
icon="warning.png",
|
||||
),
|
||||
toggle_item(
|
||||
ToggleItem(
|
||||
"Always-On Driver Monitoring",
|
||||
DESCRIPTIONS["AlwaysOnDM"],
|
||||
self._params.get_bool("AlwaysOnDM"),
|
||||
icon="monitoring.png",
|
||||
),
|
||||
toggle_item(
|
||||
ToggleItem(
|
||||
"Record and Upload Driver Camera",
|
||||
DESCRIPTIONS["RecordFront"],
|
||||
self._params.get_bool("RecordFront"),
|
||||
icon="monitoring.png",
|
||||
),
|
||||
toggle_item(
|
||||
ToggleItem(
|
||||
"Use Metric System", DESCRIPTIONS["IsMetric"], self._params.get_bool("IsMetric"), icon="monitoring.png"
|
||||
),
|
||||
]
|
||||
|
||||
@@ -8,10 +8,8 @@ from openpilot.common.params import Params
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.button import gui_button, ButtonStyle
|
||||
from openpilot.system.ui.lib.list_view import (
|
||||
ItemAction,
|
||||
ListItem,
|
||||
BUTTON_HEIGHT,
|
||||
BUTTON_BORDER_RADIUS,
|
||||
BUTTON_FONT_SIZE,
|
||||
BUTTON_WIDTH,
|
||||
)
|
||||
@@ -21,18 +19,18 @@ from openpilot.system.ui.widgets.confirm_dialog import alert_dialog
|
||||
from openpilot.system.ui.widgets.keyboard import Keyboard
|
||||
|
||||
|
||||
class SshKeyActionState(Enum):
|
||||
class SshKeyState(Enum):
|
||||
LOADING = "LOADING"
|
||||
ADD = "ADD"
|
||||
REMOVE = "REMOVE"
|
||||
|
||||
|
||||
class SshKeyAction(ItemAction):
|
||||
class SshKeyItem(ListItem):
|
||||
HTTP_TIMEOUT = 15 # seconds
|
||||
MAX_WIDTH = 500
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(self.MAX_WIDTH, True)
|
||||
def __init__(self, title: str, description: str):
|
||||
super().__init__(title, description=description)
|
||||
|
||||
self._keyboard = Keyboard()
|
||||
self._params = Params()
|
||||
@@ -41,11 +39,14 @@ class SshKeyAction(ItemAction):
|
||||
|
||||
self._refresh_state()
|
||||
|
||||
def get_action_width(self) -> int:
|
||||
return self.MAX_WIDTH
|
||||
|
||||
def _refresh_state(self):
|
||||
self._username = self._params.get("GithubUsername", "")
|
||||
self._state = SshKeyActionState.REMOVE if self._params.get("GithubSshKeys") else SshKeyActionState.ADD
|
||||
self._state = SshKeyState.REMOVE if self._params.get("GithubSshKeys") else SshKeyState.ADD
|
||||
|
||||
def _render(self, rect: rl.Rectangle) -> bool:
|
||||
def render_action(self, rect: rl.Rectangle) -> bool:
|
||||
# Show error dialog if there's an error
|
||||
if self._error_message:
|
||||
message = copy.copy(self._error_message)
|
||||
@@ -71,8 +72,8 @@ class SshKeyAction(ItemAction):
|
||||
rect.x + rect.width - BUTTON_WIDTH, rect.y + (rect.height - BUTTON_HEIGHT) / 2, BUTTON_WIDTH, BUTTON_HEIGHT
|
||||
),
|
||||
self._state.value,
|
||||
is_enabled=self._state != SshKeyActionState.LOADING,
|
||||
border_radius=BUTTON_BORDER_RADIUS,
|
||||
is_enabled=self._state != SshKeyState.LOADING,
|
||||
border_radius=BUTTON_HEIGHT // 2,
|
||||
font_size=BUTTON_FONT_SIZE,
|
||||
button_style=ButtonStyle.LIST_ACTION,
|
||||
):
|
||||
@@ -81,11 +82,11 @@ class SshKeyAction(ItemAction):
|
||||
return False
|
||||
|
||||
def _handle_button_click(self):
|
||||
if self._state == SshKeyActionState.ADD:
|
||||
if self._state == SshKeyState.ADD:
|
||||
self._keyboard.clear()
|
||||
self._keyboard.set_title("Enter your GitHub username")
|
||||
gui_app.set_modal_overlay(self._keyboard, callback=self._on_username_submit)
|
||||
elif self._state == SshKeyActionState.REMOVE:
|
||||
elif self._state == SshKeyState.REMOVE:
|
||||
self._params.remove("GithubUsername")
|
||||
self._params.remove("GithubSshKeys")
|
||||
self._refresh_state()
|
||||
@@ -98,7 +99,7 @@ class SshKeyAction(ItemAction):
|
||||
if not username:
|
||||
return
|
||||
|
||||
self._state = SshKeyActionState.LOADING
|
||||
self._state = SshKeyState.LOADING
|
||||
threading.Thread(target=lambda: self._fetch_ssh_key(username), daemon=True).start()
|
||||
|
||||
def _fetch_ssh_key(self, username: str):
|
||||
@@ -113,16 +114,12 @@ class SshKeyAction(ItemAction):
|
||||
# Success - save keys
|
||||
self._params.put("GithubUsername", username)
|
||||
self._params.put("GithubSshKeys", keys)
|
||||
self._state = SshKeyActionState.REMOVE
|
||||
self._state = SshKeyState.REMOVE
|
||||
self._username = username
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
self._error_message = "Request timed out"
|
||||
self._state = SshKeyActionState.ADD
|
||||
self._state = SshKeyState.ADD
|
||||
except Exception:
|
||||
self._error_message = f"No SSH keys found for user '{username}'"
|
||||
self._state = SshKeyActionState.ADD
|
||||
|
||||
|
||||
def ssh_key_item(title: str, description: str):
|
||||
return ListItem(title=title, description=description, action_item=SshKeyAction())
|
||||
self._state = SshKeyState.ADD
|
||||
|
||||
+234
-332
@@ -1,8 +1,6 @@
|
||||
import os
|
||||
import pyray as rl
|
||||
from dataclasses import dataclass
|
||||
from collections.abc import Callable
|
||||
from abc import ABC
|
||||
from abc import ABC, abstractmethod
|
||||
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
@@ -11,154 +9,269 @@ from openpilot.system.ui.lib.button import gui_button, ButtonStyle
|
||||
from openpilot.system.ui.lib.toggle import Toggle, WIDTH as TOGGLE_WIDTH, HEIGHT as TOGGLE_HEIGHT
|
||||
from openpilot.system.ui.lib.widget import Widget
|
||||
|
||||
ITEM_BASE_HEIGHT = 170
|
||||
LINE_PADDING = 40
|
||||
LINE_COLOR = rl.GRAY
|
||||
ITEM_BASE_HEIGHT = 170
|
||||
ITEM_PADDING = 20
|
||||
ITEM_SPACING = 80
|
||||
ITEM_TEXT_FONT_SIZE = 50
|
||||
ITEM_TEXT_COLOR = rl.WHITE
|
||||
ITEM_DESC_TEXT_COLOR = rl.Color(128, 128, 128, 255)
|
||||
ITEM_DESC_FONT_SIZE = 40
|
||||
ITEM_DESC_V_OFFSET = 140
|
||||
RIGHT_ITEM_PADDING = 20
|
||||
ICON_SIZE = 80
|
||||
BUTTON_WIDTH = 250
|
||||
BUTTON_HEIGHT = 100
|
||||
BUTTON_BORDER_RADIUS = 50
|
||||
BUTTON_FONT_SIZE = 35
|
||||
BUTTON_FONT_WEIGHT = FontWeight.MEDIUM
|
||||
|
||||
TEXT_PADDING = 20
|
||||
|
||||
|
||||
def _resolve_value(value, default=""):
|
||||
# Type Aliases for Clarity
|
||||
StrSrc = str | Callable[[], str] | None
|
||||
BoolSrc = bool | Callable[[], bool]
|
||||
|
||||
|
||||
def _get_value(value, default=""):
|
||||
if callable(value):
|
||||
return value()
|
||||
return value if value is not None else default
|
||||
|
||||
|
||||
# Abstract base class for right-side items
|
||||
class ItemAction(Widget, ABC):
|
||||
def __init__(self, width: int = 100, enabled: bool | Callable[[], bool] = True):
|
||||
class ListItem(Widget, ABC):
|
||||
def __init__(self, title, description: StrSrc=None, enabled: BoolSrc=True, visible: BoolSrc=True, icon=None):
|
||||
super().__init__()
|
||||
self.width = width
|
||||
self.title = title
|
||||
self._icon = icon
|
||||
self.description = description
|
||||
self.show_desc = False
|
||||
|
||||
self._enabled_source = enabled
|
||||
self._visible_source = visible
|
||||
self._font = gui_app.font(FontWeight.NORMAL)
|
||||
|
||||
# Cached properties for performance
|
||||
self._prev_max_width: int = 0
|
||||
self._wrapped_description: str | None = None
|
||||
self._prev_description: str | None = None
|
||||
self._description_height: float = 0
|
||||
|
||||
@property
|
||||
def enabled(self):
|
||||
return _resolve_value(self._enabled_source, False)
|
||||
return _get_value(self._enabled_source, True)
|
||||
|
||||
def get_width(self) -> int:
|
||||
return self.width
|
||||
@property
|
||||
def is_visible(self):
|
||||
return _get_value(self._visible_source, True)
|
||||
|
||||
def set_visible(self, visible: bool):
|
||||
self._visible_source = visible
|
||||
|
||||
def set_enabled(self, enabled: bool):
|
||||
self._enabled_source = enabled
|
||||
|
||||
def get_desc(self):
|
||||
return _get_value(self.description, "")
|
||||
|
||||
def set_icon(self, icon: str):
|
||||
self._icon = icon
|
||||
|
||||
def set_desc(self, description: StrSrc):
|
||||
self.description = description
|
||||
current_description = self.get_desc()
|
||||
if current_description != self._prev_description:
|
||||
self._update_description_cache(self._prev_max_width, current_description)
|
||||
|
||||
def _update_description_cache(self, max_width: int, current_description: str):
|
||||
"""Update the cached description wrapping"""
|
||||
self._prev_max_width = max_width
|
||||
self._prev_description = current_description
|
||||
content_width = max_width - ITEM_PADDING * 2
|
||||
|
||||
# Account for icon width
|
||||
if self._icon:
|
||||
content_width -= ICON_SIZE + ITEM_PADDING
|
||||
|
||||
wrapped_lines = wrap_text(self._font, current_description, ITEM_DESC_FONT_SIZE, content_width)
|
||||
self._wrapped_description = "\n".join(wrapped_lines)
|
||||
self._description_height = len(wrapped_lines) * ITEM_DESC_FONT_SIZE + 10
|
||||
|
||||
def _get_height(self, max_width: int) -> float:
|
||||
if not self.is_visible:
|
||||
return 0
|
||||
|
||||
if not self.show_desc:
|
||||
return ITEM_BASE_HEIGHT
|
||||
|
||||
current_description = self.get_desc()
|
||||
if not current_description:
|
||||
return ITEM_BASE_HEIGHT
|
||||
|
||||
if current_description != self._prev_description or max_width != self._prev_max_width:
|
||||
self._update_description_cache(max_width, current_description)
|
||||
|
||||
return ITEM_BASE_HEIGHT + self._description_height - (ITEM_BASE_HEIGHT - ITEM_DESC_V_OFFSET) + ITEM_PADDING
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
# Handle click on title/description area for toggling description
|
||||
if self.description and rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT):
|
||||
mouse_pos = rl.get_mouse_position()
|
||||
|
||||
text_area_width = rect.width - self.get_action_width() - ITEM_PADDING
|
||||
text_area = rl.Rectangle(rect.x, rect.y, text_area_width, rect.height)
|
||||
if rl.check_collision_point_rec(mouse_pos, text_area):
|
||||
self.show_desc = not self.show_desc
|
||||
|
||||
# Render title and description
|
||||
x = rect.x + ITEM_PADDING
|
||||
|
||||
# Draw icon if present
|
||||
if self._icon:
|
||||
icon_texture = gui_app.texture(f"icons/{self._icon}", ICON_SIZE, ICON_SIZE)
|
||||
rl.draw_texture(icon_texture, int(x), int(rect.y + (ITEM_BASE_HEIGHT - ICON_SIZE) // 2), rl.WHITE)
|
||||
x += ICON_SIZE + ITEM_PADDING
|
||||
|
||||
text_size = measure_text_cached(self._font, self.title, ITEM_TEXT_FONT_SIZE)
|
||||
title_y = rect.y + (ITEM_BASE_HEIGHT - text_size.y) // 2
|
||||
rl.draw_text_ex(self._font, self.title, (x, title_y), ITEM_TEXT_FONT_SIZE, 0, ITEM_TEXT_COLOR)
|
||||
|
||||
# Draw description if visible
|
||||
if self.show_desc and self._wrapped_description:
|
||||
rl.draw_text_ex(self._font, self._wrapped_description, (x, rect.y + ITEM_DESC_V_OFFSET),
|
||||
ITEM_DESC_FONT_SIZE, 0, ITEM_DESC_TEXT_COLOR)
|
||||
|
||||
# Render action if needed
|
||||
action_width = self.get_action_width()
|
||||
action_rect = rl.Rectangle(rect.x + rect.width - action_width, rect.y, action_width, ITEM_BASE_HEIGHT)
|
||||
self.render_action(action_rect)
|
||||
|
||||
@abstractmethod
|
||||
def get_action_width(self) -> int:
|
||||
"""Return the width needed for the action part (right side)"""
|
||||
|
||||
@abstractmethod
|
||||
def render_action(self, rect: rl.Rectangle):
|
||||
"""Render the action part"""
|
||||
|
||||
|
||||
class ToggleAction(ItemAction):
|
||||
def __init__(self, initial_state: bool = False, width: int = TOGGLE_WIDTH, enabled: bool | Callable[[], bool] = True):
|
||||
super().__init__(width, enabled)
|
||||
class ToggleItem(ListItem):
|
||||
def __init__(self, title: str, description: StrSrc = None, initial_state: bool=False, callback=None, active_icon=None, **kwargs):
|
||||
super().__init__(title, description, **kwargs)
|
||||
self.toggle = Toggle(initial_state=initial_state)
|
||||
self.state = initial_state
|
||||
self.callback = callback
|
||||
self._inactive_icon = kwargs.get('icon', None)
|
||||
self._active_icon = active_icon
|
||||
if self._active_icon and initial_state:
|
||||
self.set_icon(self._active_icon)
|
||||
|
||||
def _render(self, rect: rl.Rectangle) -> bool:
|
||||
def get_action_width(self) -> int:
|
||||
return TOGGLE_WIDTH
|
||||
|
||||
def render_action(self, rect: rl.Rectangle):
|
||||
self.toggle.set_enabled(self.enabled)
|
||||
self.toggle.render(rl.Rectangle(rect.x, rect.y + (rect.height - TOGGLE_HEIGHT) / 2, self.width, TOGGLE_HEIGHT))
|
||||
return False
|
||||
toggle_rect = rl.Rectangle(rect.x, rect.y + (rect.height - TOGGLE_HEIGHT) // 2,
|
||||
TOGGLE_WIDTH, TOGGLE_HEIGHT)
|
||||
|
||||
if self.toggle.render(toggle_rect):
|
||||
if self._active_icon and self._inactive_icon:
|
||||
self.set_icon(self._active_icon if self.toggle.get_state() else self._inactive_icon)
|
||||
|
||||
if self.callback:
|
||||
self.callback(self)
|
||||
|
||||
def set_state(self, state: bool):
|
||||
self.state = state
|
||||
self.toggle.set_state(state)
|
||||
|
||||
def get_state(self) -> bool:
|
||||
return self.state
|
||||
def get_state(self):
|
||||
return self.toggle.get_state()
|
||||
|
||||
|
||||
class ButtonAction(ItemAction):
|
||||
def __init__(self, text: str | Callable[[], str], width: int = BUTTON_WIDTH, enabled: bool | Callable[[], bool] = True):
|
||||
super().__init__(width, enabled)
|
||||
self._text_source = text
|
||||
class ButtonItem(ListItem):
|
||||
def __init__(self, title: str, button_text, description=None, callback=None, **kwargs):
|
||||
super().__init__(title, description, **kwargs)
|
||||
self._button_text_src = button_text
|
||||
self._callback = callback
|
||||
|
||||
@property
|
||||
def text(self):
|
||||
return _resolve_value(self._text_source, "Error")
|
||||
def get_button_text(self):
|
||||
return _get_value(self._button_text_src, "Error")
|
||||
|
||||
def _render(self, rect: rl.Rectangle) -> bool:
|
||||
return gui_button(
|
||||
rl.Rectangle(rect.x, rect.y + (rect.height - BUTTON_HEIGHT) / 2, BUTTON_WIDTH, BUTTON_HEIGHT),
|
||||
self.text,
|
||||
border_radius=BUTTON_BORDER_RADIUS,
|
||||
font_weight=BUTTON_FONT_WEIGHT,
|
||||
font_size=BUTTON_FONT_SIZE,
|
||||
button_style=ButtonStyle.LIST_ACTION,
|
||||
is_enabled=self.enabled,
|
||||
) == 1
|
||||
def get_action_width(self) -> int:
|
||||
return BUTTON_WIDTH
|
||||
|
||||
def render_action(self, rect: rl.Rectangle):
|
||||
button_rect = rl.Rectangle(rect.x, rect.y + (rect.height - BUTTON_HEIGHT) // 2, BUTTON_WIDTH, BUTTON_HEIGHT)
|
||||
if gui_button(button_rect, self.get_button_text(), border_radius=BUTTON_HEIGHT // 2,
|
||||
font_size=BUTTON_FONT_SIZE, button_style=ButtonStyle.LIST_ACTION, is_enabled=self.enabled):
|
||||
if self._callback:
|
||||
self._callback()
|
||||
|
||||
|
||||
class TextAction(ItemAction):
|
||||
def __init__(self, text: str | Callable[[], str], color: rl.Color = ITEM_TEXT_COLOR, enabled: bool | Callable[[], bool] = True):
|
||||
self._text_source = text
|
||||
self.color = color
|
||||
class TextItem(ListItem):
|
||||
def __init__(self, title: str, value: str | Callable[[], str], **kwargs):
|
||||
super().__init__(title, **kwargs)
|
||||
self._value_src = value
|
||||
self.color = rl.Color(170, 170, 170, 255)
|
||||
|
||||
self._font = gui_app.font(FontWeight.NORMAL)
|
||||
initial_text = _resolve_value(text, "")
|
||||
text_width = measure_text_cached(self._font, initial_text, ITEM_TEXT_FONT_SIZE).x
|
||||
super().__init__(int(text_width + TEXT_PADDING), enabled)
|
||||
def get_value(self):
|
||||
return _get_value(self._value_src, "")
|
||||
|
||||
@property
|
||||
def text(self):
|
||||
return _resolve_value(self._text_source, "Error")
|
||||
def get_action_width(self) -> int:
|
||||
return int(measure_text_cached(self._font, self.get_value(), ITEM_TEXT_FONT_SIZE).x + ITEM_PADDING)
|
||||
|
||||
def _render(self, rect: rl.Rectangle) -> bool:
|
||||
current_text = self.text
|
||||
text_size = measure_text_cached(self._font, current_text, ITEM_TEXT_FONT_SIZE)
|
||||
|
||||
text_x = rect.x + (rect.width - text_size.x) / 2
|
||||
text_y = rect.y + (rect.height - text_size.y) / 2
|
||||
rl.draw_text_ex(self._font, current_text, rl.Vector2(text_x, text_y), ITEM_TEXT_FONT_SIZE, 0, self.color)
|
||||
return False
|
||||
|
||||
def get_width(self) -> int:
|
||||
text_width = measure_text_cached(self._font, self.text, ITEM_TEXT_FONT_SIZE).x
|
||||
return int(text_width + TEXT_PADDING)
|
||||
def render_action(self, rect: rl.Rectangle):
|
||||
value = self.get_value()
|
||||
text_size = measure_text_cached(self._font, value, ITEM_TEXT_FONT_SIZE)
|
||||
x = rect.x + (rect.width - text_size.x) // 2
|
||||
y = rect.y + (rect.height - text_size.y) // 2
|
||||
rl.draw_text_ex(self._font, value, rl.Vector2(x, y), ITEM_TEXT_FONT_SIZE, 0, self.color)
|
||||
|
||||
|
||||
class DualButtonAction(ItemAction):
|
||||
def __init__(self, left_text: str, right_text: str, left_callback: Callable = None,
|
||||
right_callback: Callable = None, enabled: bool | Callable[[], bool] = True):
|
||||
super().__init__(width=0, enabled=enabled) # Width 0 means use full width
|
||||
self.left_text, self.right_text = left_text, right_text
|
||||
self.left_callback, self.right_callback = left_callback, right_callback
|
||||
class DualButtonItem(Widget):
|
||||
def __init__(self, left_text: str, right_text: str, left_callback: Callable, right_callback: Callable):
|
||||
super().__init__()
|
||||
self.left_text = left_text
|
||||
self.right_text = right_text
|
||||
self.left_callback = left_callback
|
||||
self.right_callback = right_callback
|
||||
self._button_spacing = 30
|
||||
self._button_height = 120
|
||||
|
||||
def _render(self, rect: rl.Rectangle) -> bool:
|
||||
button_spacing = 30
|
||||
button_height = 120
|
||||
button_width = (rect.width - button_spacing) / 2
|
||||
button_y = rect.y + (rect.height - button_height) / 2
|
||||
def _get_height(self, max_width: int) -> float:
|
||||
return ITEM_BASE_HEIGHT
|
||||
|
||||
left_rect = rl.Rectangle(rect.x, button_y, button_width, button_height)
|
||||
right_rect = rl.Rectangle(rect.x + button_width + button_spacing, button_y, button_width, button_height)
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
button_width = (rect.width - self._button_spacing) / 2
|
||||
button_y = rect.y + (rect.height - self._button_height) / 2
|
||||
|
||||
left_clicked = gui_button(left_rect, self.left_text, button_style=ButtonStyle.LIST_ACTION) == 1
|
||||
right_clicked = gui_button(right_rect, self.right_text, button_style=ButtonStyle.DANGER) == 1
|
||||
left_rect = rl.Rectangle(rect.x, button_y, button_width, self._button_height)
|
||||
right_rect = rl.Rectangle(rect.x + button_width + self._button_spacing, button_y, button_width, self._button_height)
|
||||
|
||||
if left_clicked and self.left_callback:
|
||||
left_clicked = gui_button(left_rect, self.left_text, button_style=ButtonStyle.LIST_ACTION)
|
||||
right_clicked = gui_button(right_rect, self.right_text, button_style=ButtonStyle.DANGER)
|
||||
|
||||
if left_clicked and self.left_callback is not None:
|
||||
self.left_callback()
|
||||
return True
|
||||
if right_clicked and self.right_callback:
|
||||
if right_clicked and self.right_callback is not None:
|
||||
self.right_callback()
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class MultipleButtonAction(ItemAction):
|
||||
def __init__(self, buttons: list[str], button_width: int, selected_index: int = 0, callback: Callable = None):
|
||||
super().__init__(width=len(buttons) * (button_width + 20), enabled=True)
|
||||
class MultipleButtonItem(ListItem):
|
||||
def __init__(self, title: str, description: str, buttons: list[str], button_width: int, selected_index: int = 0, callback: Callable = None, **kwargs):
|
||||
super().__init__(title, description, **kwargs)
|
||||
self.buttons = buttons
|
||||
self.button_width = button_width
|
||||
self.selected_button = selected_index
|
||||
self.selected_index = selected_index
|
||||
self.callback = callback
|
||||
self._font = gui_app.font(FontWeight.MEDIUM)
|
||||
self._colors = {
|
||||
'normal': rl.Color(57, 57, 57, 255), # Gray
|
||||
'hovered': rl.Color(74, 74, 74, 255), # Dark gray
|
||||
'selected': rl.Color(51, 171, 76, 255), # Green
|
||||
'disabled': rl.Color(153, 51, 171, 76), # #9933Ab4C - Semi-transparent
|
||||
'text': rl.Color(228, 228, 228, 255), # Light gray
|
||||
'text_disabled': rl.Color(51, 228, 228, 228), # #33E4E4E4 - Semi-transparent
|
||||
}
|
||||
|
||||
def _render(self, rect: rl.Rectangle) -> bool:
|
||||
def get_action_width(self) -> int:
|
||||
return self.button_width * len(self.buttons) + (len(self.buttons) - 1) * 20
|
||||
|
||||
def render_action(self, rect: rl.Rectangle) -> bool:
|
||||
spacing = 20
|
||||
button_y = rect.y + (rect.height - 100) / 2
|
||||
clicked = -1
|
||||
@@ -171,15 +284,13 @@ class MultipleButtonAction(ItemAction):
|
||||
mouse_pos = rl.get_mouse_position()
|
||||
is_hovered = rl.check_collision_point_rec(mouse_pos, button_rect)
|
||||
is_pressed = is_hovered and rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT)
|
||||
is_selected = i == self.selected_button
|
||||
is_selected = i == self.selected_index
|
||||
|
||||
# Button colors
|
||||
if is_selected:
|
||||
bg_color = rl.Color(51, 171, 76, 255) # Green
|
||||
elif is_pressed:
|
||||
bg_color = rl.Color(74, 74, 74, 255) # Dark gray
|
||||
else:
|
||||
bg_color = rl.Color(57, 57, 57, 255) # Gray
|
||||
bg_color = (self._colors['disabled'] if not self.enabled and is_selected else
|
||||
self._colors['selected'] if is_selected else
|
||||
self._colors['hovered'] if is_pressed and self.enabled else
|
||||
self._colors['normal'])
|
||||
text_color = self._colors['text_disabled'] if not self.enabled else self._colors['text']
|
||||
|
||||
# Draw button
|
||||
rl.draw_rectangle_rounded(button_rect, 1.0, 20, bg_color)
|
||||
@@ -188,265 +299,56 @@ class MultipleButtonAction(ItemAction):
|
||||
text_size = measure_text_cached(self._font, text, 40)
|
||||
text_x = button_x + (self.button_width - text_size.x) / 2
|
||||
text_y = button_y + (100 - text_size.y) / 2
|
||||
rl.draw_text_ex(self._font, text, rl.Vector2(text_x, text_y), 40, 0, rl.Color(228, 228, 228, 255))
|
||||
rl.draw_text_ex(self._font, text, rl.Vector2(text_x, text_y), 40, 0, text_color)
|
||||
|
||||
# Handle click
|
||||
if is_hovered and rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT):
|
||||
# Handle click only if enabled
|
||||
if self.enabled and is_hovered and rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT):
|
||||
clicked = i
|
||||
|
||||
if clicked >= 0:
|
||||
self.selected_button = clicked
|
||||
self.selected_index = clicked
|
||||
if self.callback:
|
||||
self.callback(clicked)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@dataclass
|
||||
class ListItem:
|
||||
title: str
|
||||
icon: str | None = None
|
||||
description: str | Callable[[], str] | None = None
|
||||
description_visible: bool = False
|
||||
rect: "rl.Rectangle" = rl.Rectangle(0, 0, 0, 0)
|
||||
callback: Callable | None = None
|
||||
action_item: ItemAction | None = None
|
||||
visible: bool | Callable[[], bool] = True
|
||||
|
||||
# Cached properties for performance
|
||||
_prev_max_width: int = 0
|
||||
_wrapped_description: str | None = None
|
||||
_prev_description: str | None = None
|
||||
_description_height: float = 0
|
||||
|
||||
@property
|
||||
def is_visible(self) -> bool:
|
||||
return bool(_resolve_value(self.visible, True))
|
||||
|
||||
def get_description(self):
|
||||
return _resolve_value(self.description, None)
|
||||
|
||||
def get_item_height(self, font: rl.Font, max_width: int) -> float:
|
||||
if not self.is_visible:
|
||||
return 0
|
||||
|
||||
current_description = self.get_description()
|
||||
if self.description_visible and current_description:
|
||||
if (
|
||||
not self._wrapped_description
|
||||
or current_description != self._prev_description
|
||||
or max_width != self._prev_max_width
|
||||
):
|
||||
self._prev_max_width = max_width
|
||||
self._prev_description = current_description
|
||||
|
||||
wrapped_lines = wrap_text(font, current_description, ITEM_DESC_FONT_SIZE, max_width)
|
||||
self._wrapped_description = "\n".join(wrapped_lines)
|
||||
self._description_height = len(wrapped_lines) * ITEM_DESC_FONT_SIZE + 10
|
||||
return ITEM_BASE_HEIGHT + self._description_height - (ITEM_BASE_HEIGHT - ITEM_DESC_V_OFFSET) + ITEM_PADDING
|
||||
return ITEM_BASE_HEIGHT
|
||||
|
||||
def get_content_width(self, total_width: int) -> int:
|
||||
if self.action_item and self.action_item.get_width() > 0:
|
||||
return total_width - self.action_item.get_width() - RIGHT_ITEM_PADDING
|
||||
return total_width
|
||||
|
||||
def get_right_item_rect(self, item_rect: rl.Rectangle) -> rl.Rectangle:
|
||||
if not self.action_item:
|
||||
return rl.Rectangle(0, 0, 0, 0)
|
||||
|
||||
right_width = self.action_item.get_width()
|
||||
if right_width == 0: # Full width action (like DualButtonAction)
|
||||
return rl.Rectangle(item_rect.x + ITEM_PADDING, item_rect.y,
|
||||
item_rect.width - (ITEM_PADDING * 2), ITEM_BASE_HEIGHT)
|
||||
|
||||
right_x = item_rect.x + item_rect.width - right_width
|
||||
right_y = item_rect.y
|
||||
return rl.Rectangle(right_x, right_y, right_width, ITEM_BASE_HEIGHT)
|
||||
|
||||
|
||||
class ListView(Widget):
|
||||
def __init__(self, items: list[ListItem]):
|
||||
super().__init__()
|
||||
self._items = items
|
||||
self.items = items
|
||||
self.scroll_panel = GuiScrollPanel()
|
||||
self._font = gui_app.font(FontWeight.NORMAL)
|
||||
self._hovered_item = -1
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
total_height = self._update_item_rects(rect)
|
||||
total_height = sum(item._get_height(int(rect.width)) for item in self.items if item.is_visible)
|
||||
|
||||
# Update layout and handle scrolling
|
||||
# Handle scrolling
|
||||
content_rect = rl.Rectangle(rect.x, rect.y, rect.width, total_height)
|
||||
scroll_offset = self.scroll_panel.handle_scroll(rect, content_rect)
|
||||
|
||||
# Handle mouse interaction
|
||||
if self.scroll_panel.is_click_valid():
|
||||
self._handle_mouse_interaction(rect, scroll_offset)
|
||||
|
||||
# Set scissor mode for clipping
|
||||
rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height))
|
||||
|
||||
for i, item in enumerate(self._items):
|
||||
y = rect.y + scroll_offset.y
|
||||
for i, item in enumerate(self.items):
|
||||
if not item.is_visible:
|
||||
continue
|
||||
|
||||
y = int(item.rect.y + scroll_offset.y)
|
||||
if y + item.rect.height <= rect.y or y >= rect.y + rect.height:
|
||||
item_height = item._get_height(int(rect.width))
|
||||
|
||||
# Skip if outside viewport
|
||||
if y + item_height < rect.y or y > rect.y + rect.height:
|
||||
y += item_height
|
||||
continue
|
||||
|
||||
self._render_item(item, y)
|
||||
# Render item
|
||||
item.render(rl.Rectangle(rect.x, y, rect.width, item_height))
|
||||
|
||||
# Draw separator line
|
||||
next_visible_item = self._get_next_visible_item(i)
|
||||
if next_visible_item is not None:
|
||||
line_y = int(y + item.rect.height - 1)
|
||||
rl.draw_line(
|
||||
int(item.rect.x) + LINE_PADDING,
|
||||
line_y,
|
||||
int(item.rect.x + item.rect.width) - LINE_PADDING * 2,
|
||||
line_y,
|
||||
LINE_COLOR,
|
||||
)
|
||||
if i < len(self.items) - 1:
|
||||
line_y = int(y + item_height - 1)
|
||||
rl.draw_line(int(rect.x + ITEM_PADDING), line_y, int(rect.x + rect.width - ITEM_PADDING), line_y, rl.GRAY)
|
||||
|
||||
y += item_height
|
||||
|
||||
rl.end_scissor_mode()
|
||||
|
||||
def _get_next_visible_item(self, current_index: int) -> int | None:
|
||||
for i in range(current_index + 1, len(self._items)):
|
||||
if self._items[i].is_visible:
|
||||
return i
|
||||
return None
|
||||
|
||||
def _update_item_rects(self, container_rect: rl.Rectangle) -> float:
|
||||
current_y = 0.0
|
||||
for item in self._items:
|
||||
if not item.is_visible:
|
||||
item.rect = rl.Rectangle(container_rect.x, container_rect.y + current_y, container_rect.width, 0)
|
||||
continue
|
||||
|
||||
content_width = item.get_content_width(int(container_rect.width - ITEM_PADDING * 2))
|
||||
item_height = item.get_item_height(self._font, content_width)
|
||||
item.rect = rl.Rectangle(container_rect.x, container_rect.y + current_y, container_rect.width, item_height)
|
||||
current_y += item_height
|
||||
return current_y # total height of all items
|
||||
|
||||
def _render_item(self, item: ListItem, y: int):
|
||||
content_x = item.rect.x + ITEM_PADDING
|
||||
text_x = content_x
|
||||
|
||||
# Only draw title and icon for items that have them
|
||||
if item.title:
|
||||
# Draw icon if present
|
||||
if item.icon:
|
||||
icon_texture = gui_app.texture(os.path.join("icons", item.icon), ICON_SIZE, ICON_SIZE)
|
||||
rl.draw_texture(icon_texture, int(content_x), int(y + (ITEM_BASE_HEIGHT - icon_texture.width) // 2), rl.WHITE)
|
||||
text_x += ICON_SIZE + ITEM_PADDING
|
||||
|
||||
# Draw main text
|
||||
text_size = measure_text_cached(self._font, item.title, ITEM_TEXT_FONT_SIZE)
|
||||
item_y = y + (ITEM_BASE_HEIGHT - text_size.y) // 2
|
||||
rl.draw_text_ex(self._font, item.title, rl.Vector2(text_x, item_y), ITEM_TEXT_FONT_SIZE, 0, ITEM_TEXT_COLOR)
|
||||
|
||||
# Draw description if visible
|
||||
current_description = item.get_description()
|
||||
if item.description_visible and current_description and item._wrapped_description:
|
||||
rl.draw_text_ex(
|
||||
self._font,
|
||||
item._wrapped_description,
|
||||
rl.Vector2(text_x, y + ITEM_DESC_V_OFFSET),
|
||||
ITEM_DESC_FONT_SIZE,
|
||||
0,
|
||||
ITEM_DESC_TEXT_COLOR,
|
||||
)
|
||||
|
||||
# Draw right item if present
|
||||
if item.action_item:
|
||||
right_rect = item.get_right_item_rect(item.rect)
|
||||
right_rect.y = y
|
||||
if item.action_item.render(right_rect) and item.action_item.enabled:
|
||||
# Right item was clicked/activated
|
||||
if item.callback:
|
||||
item.callback()
|
||||
|
||||
def _handle_mouse_interaction(self, rect: rl.Rectangle, scroll_offset: rl.Vector2):
|
||||
mouse_pos = rl.get_mouse_position()
|
||||
|
||||
self._hovered_item = -1
|
||||
if not rl.check_collision_point_rec(mouse_pos, rect):
|
||||
return
|
||||
|
||||
content_mouse_y = mouse_pos.y - rect.y - scroll_offset.y
|
||||
|
||||
for i, item in enumerate(self._items):
|
||||
if not item.is_visible:
|
||||
continue
|
||||
|
||||
if item.rect:
|
||||
# Check if mouse is within this item's bounds in content space
|
||||
if (
|
||||
mouse_pos.x >= rect.x
|
||||
and mouse_pos.x <= rect.x + rect.width
|
||||
and content_mouse_y >= item.rect.y
|
||||
and content_mouse_y <= item.rect.y + item.rect.height
|
||||
):
|
||||
item_screen_y = item.rect.y + scroll_offset.y
|
||||
if item_screen_y < rect.height and item_screen_y + item.rect.height > 0:
|
||||
self._hovered_item = i
|
||||
break
|
||||
|
||||
# Handle click on main item (not right item)
|
||||
if rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT) and self._hovered_item >= 0:
|
||||
item = self._items[self._hovered_item]
|
||||
|
||||
# Check if click was on right item area
|
||||
if item.action_item and item.rect:
|
||||
# Use the same coordinate system as in _render_item
|
||||
adjusted_rect = rl.Rectangle(item.rect.x, item.rect.y + scroll_offset.y, item.rect.width, item.rect.height)
|
||||
right_rect = item.get_right_item_rect(adjusted_rect)
|
||||
|
||||
if rl.check_collision_point_rec(mouse_pos, right_rect):
|
||||
# Click was on right item, don't toggle description
|
||||
return
|
||||
|
||||
# Toggle description visibility if item has description
|
||||
if item.description:
|
||||
item.description_visible = not item.description_visible
|
||||
|
||||
|
||||
# Factory functions
|
||||
def simple_item(title: str, callback: Callable | None = None, visible: bool | Callable[[], bool] = True) -> ListItem:
|
||||
return ListItem(title=title, callback=callback, visible=visible)
|
||||
|
||||
|
||||
def toggle_item(title: str, description: str | Callable[[], str] | None = None, initial_state: bool = False,
|
||||
callback: Callable | None = None, icon: str = "", enabled: bool | Callable[[], bool] = True,
|
||||
visible: bool | Callable[[], bool] = True) -> ListItem:
|
||||
action = ToggleAction(initial_state=initial_state, enabled=enabled)
|
||||
return ListItem(title=title, description=description, action_item=action, icon=icon, callback=callback, visible=visible)
|
||||
|
||||
|
||||
def button_item(title: str, button_text: str | Callable[[], str], description: str | Callable[[], str] | None = None,
|
||||
callback: Callable | None = None, enabled: bool | Callable[[], bool] = True,
|
||||
visible: bool | Callable[[], bool] = True) -> ListItem:
|
||||
action = ButtonAction(text=button_text, enabled=enabled)
|
||||
return ListItem(title=title, description=description, action_item=action, callback=callback, visible=visible)
|
||||
|
||||
|
||||
def text_item(title: str, value: str | Callable[[], str], description: str | Callable[[], str] | None = None,
|
||||
callback: Callable | None = None, enabled: bool | Callable[[], bool] = True,
|
||||
visible: bool | Callable[[], bool] = True) -> ListItem:
|
||||
action = TextAction(text=value, color=rl.Color(170, 170, 170, 255), enabled=enabled)
|
||||
return ListItem(title=title, description=description, action_item=action, callback=callback, visible=visible)
|
||||
|
||||
|
||||
def dual_button_item(left_text: str, right_text: str, left_callback: Callable = None, right_callback: Callable = None,
|
||||
description: str | Callable[[], str] | None = None, enabled: bool | Callable[[], bool] = True,
|
||||
visible: bool | Callable[[], bool] = True) -> ListItem:
|
||||
action = DualButtonAction(left_text, right_text, left_callback, right_callback, enabled)
|
||||
return ListItem(title="", description=description, action_item=action, visible=visible)
|
||||
|
||||
|
||||
def multiple_button_item(title: str, description: str, buttons: list[str], selected_index: int,
|
||||
button_width: int = BUTTON_WIDTH, callback: Callable = None, icon: str = ""):
|
||||
action = MultipleButtonAction(buttons, button_width, selected_index, callback=callback)
|
||||
return ListItem(title=title, description=description, icon=icon, action_item=action)
|
||||
|
||||
@@ -36,3 +36,9 @@ class Widget(abc.ABC):
|
||||
def _handle_mouse_release(self, mouse_pos: rl.Vector2) -> bool:
|
||||
"""Handle mouse release events, if applicable."""
|
||||
return False
|
||||
|
||||
def is_visible(self):
|
||||
return True
|
||||
|
||||
def _get_height(self, max_width: int) -> float:
|
||||
raise NotImplementedError("Subclasses must implement the get_height method")
|
||||
|
||||
Reference in New Issue
Block a user