ui: enhanced ListView with improved actions, dynamic content, and better UX (#35485)

improve list view
pull/35502/head
Dean Lee 7 days ago committed by GitHub
parent af48d23a68
commit 191d0d429e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 19
      system/ui/lib/button.py
  2. 321
      system/ui/lib/list_view.py

@ -10,6 +10,7 @@ class ButtonStyle(IntEnum):
DANGER = 2 # For critical actions, like reboot or delete DANGER = 2 # For critical actions, like reboot or delete
TRANSPARENT = 3 # For buttons with transparent background and border TRANSPARENT = 3 # For buttons with transparent background and border
ACTION = 4 ACTION = 4
LIST_ACTION = 5 # For list items with action buttons
class TextAlignment(IntEnum): class TextAlignment(IntEnum):
@ -20,10 +21,18 @@ class TextAlignment(IntEnum):
ICON_PADDING = 15 ICON_PADDING = 15
DEFAULT_BUTTON_FONT_SIZE = 60 DEFAULT_BUTTON_FONT_SIZE = 60
BUTTON_ENABLED_TEXT_COLOR = rl.Color(228, 228, 228, 255)
BUTTON_DISABLED_TEXT_COLOR = rl.Color(228, 228, 228, 51) BUTTON_DISABLED_TEXT_COLOR = rl.Color(228, 228, 228, 51)
ACTION_BUTTON_FONT_SIZE = 48 ACTION_BUTTON_FONT_SIZE = 48
ACTION_BUTTON_TEXT_COLOR = rl.Color(0, 0, 0, 255)
BUTTON_TEXT_COLOR = {
ButtonStyle.NORMAL: rl.Color(228, 228, 228, 255),
ButtonStyle.PRIMARY: rl.Color(228, 228, 228, 255),
ButtonStyle.DANGER: rl.Color(228, 228, 228, 255),
ButtonStyle.TRANSPARENT: rl.BLACK,
ButtonStyle.ACTION: rl.Color(0, 0, 0, 255),
ButtonStyle.LIST_ACTION: rl.Color(228, 228, 228, 255),
}
BUTTON_BACKGROUND_COLORS = { BUTTON_BACKGROUND_COLORS = {
ButtonStyle.NORMAL: rl.Color(51, 51, 51, 255), ButtonStyle.NORMAL: rl.Color(51, 51, 51, 255),
@ -31,6 +40,7 @@ BUTTON_BACKGROUND_COLORS = {
ButtonStyle.DANGER: rl.Color(255, 36, 36, 255), ButtonStyle.DANGER: rl.Color(255, 36, 36, 255),
ButtonStyle.TRANSPARENT: rl.BLACK, ButtonStyle.TRANSPARENT: rl.BLACK,
ButtonStyle.ACTION: rl.Color(189, 189, 189, 255), ButtonStyle.ACTION: rl.Color(189, 189, 189, 255),
ButtonStyle.LIST_ACTION: rl.Color(57, 57, 57, 255),
} }
BUTTON_PRESSED_BACKGROUND_COLORS = { BUTTON_PRESSED_BACKGROUND_COLORS = {
@ -39,6 +49,7 @@ BUTTON_PRESSED_BACKGROUND_COLORS = {
ButtonStyle.DANGER: rl.Color(255, 36, 36, 255), ButtonStyle.DANGER: rl.Color(255, 36, 36, 255),
ButtonStyle.TRANSPARENT: rl.BLACK, ButtonStyle.TRANSPARENT: rl.BLACK,
ButtonStyle.ACTION: rl.Color(130, 130, 130, 255), ButtonStyle.ACTION: rl.Color(130, 130, 130, 255),
ButtonStyle.LIST_ACTION: rl.Color(74, 74, 74, 74),
} }
_pressed_buttons: set[str] = set() # Track mouse press state globally _pressed_buttons: set[str] = set() # Track mouse press state globally
@ -132,7 +143,7 @@ def gui_button(
# Draw the button text if any # Draw the button text if any
if text: if text:
text_color = ACTION_BUTTON_TEXT_COLOR if button_style == ButtonStyle.ACTION else BUTTON_ENABLED_TEXT_COLOR if is_enabled else BUTTON_DISABLED_TEXT_COLOR color = BUTTON_TEXT_COLOR[button_style] if is_enabled else BUTTON_DISABLED_TEXT_COLOR
rl.draw_text_ex(font, text, text_pos, font_size, 0, text_color) rl.draw_text_ex(font, text, text_pos, font_size, 0, color)
return result return result

@ -2,24 +2,25 @@ import os
import pyray as rl import pyray as rl
from dataclasses import dataclass from dataclasses import dataclass
from collections.abc import Callable from collections.abc import Callable
from abc import ABC, abstractmethod from abc import ABC
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
from openpilot.system.ui.lib.application import gui_app, FontWeight, Widget from openpilot.system.ui.lib.application import gui_app, FontWeight, Widget
from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.wrap_text import wrap_text from openpilot.system.ui.lib.wrap_text import wrap_text
from openpilot.system.ui.lib.button import gui_button 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.toggle import Toggle, WIDTH as TOGGLE_WIDTH, HEIGHT as TOGGLE_HEIGHT
ITEM_BASE_HEIGHT = 170
LINE_PADDING = 40 LINE_PADDING = 40
LINE_COLOR = rl.GRAY LINE_COLOR = rl.GRAY
ITEM_PADDING = 20 ITEM_PADDING = 20
ITEM_SPACING = 80 ITEM_SPACING = 80
ITEM_BASE_HEIGHT = 170
ITEM_TEXT_FONT_SIZE = 50 ITEM_TEXT_FONT_SIZE = 50
ITEM_TEXT_COLOR = rl.WHITE ITEM_TEXT_COLOR = rl.WHITE
ITEM_DESC_TEXT_COLOR = rl.Color(128, 128, 128, 255) ITEM_DESC_TEXT_COLOR = rl.Color(128, 128, 128, 255)
ITEM_DESC_FONT_SIZE = 40 ITEM_DESC_FONT_SIZE = 40
ITEM_DESC_V_OFFSET = 130 ITEM_DESC_V_OFFSET = 140
RIGHT_ITEM_PADDING = 20 RIGHT_ITEM_PADDING = 20
ICON_SIZE = 80 ICON_SIZE = 80
BUTTON_WIDTH = 250 BUTTON_WIDTH = 250
@ -28,35 +29,38 @@ BUTTON_BORDER_RADIUS = 50
BUTTON_FONT_SIZE = 35 BUTTON_FONT_SIZE = 35
BUTTON_FONT_WEIGHT = FontWeight.MEDIUM BUTTON_FONT_WEIGHT = FontWeight.MEDIUM
TEXT_PADDING = 20
def _resolve_value(value, default=""):
return value() if callable(value) else (value or default)
# Abstract base class for right-side items # Abstract base class for right-side items
class RightItem(Widget, ABC): class ItemAction(Widget, ABC):
def __init__(self, width: int = 100): def __init__(self, width: int = 100, enabled: bool | Callable[[], bool] = True):
super().__init__() super().__init__()
self.width = width self.width = width
self.enabled = True self._enabled_source = enabled
@property
def enabled(self):
return _resolve_value(self._enabled_source, False)
@abstractmethod
def get_width(self) -> int: def get_width(self) -> int:
pass return self.width
class ToggleRightItem(RightItem): class ToggleAction(ItemAction):
def __init__(self, initial_state: bool = False, width: int = TOGGLE_WIDTH): def __init__(self, initial_state: bool = False, width: int = TOGGLE_WIDTH, enabled: bool | Callable[[], bool] = True):
super().__init__(width) super().__init__(width, enabled)
self.toggle = Toggle(initial_state=initial_state) self.toggle = Toggle(initial_state=initial_state)
self.state = initial_state self.state = initial_state
self.enabled = True
def _render(self, rect: rl.Rectangle) -> bool: def _render(self, rect: rl.Rectangle) -> bool:
if self.toggle.render(rl.Rectangle(rect.x, rect.y + (rect.height - TOGGLE_HEIGHT) / 2, self.width, TOGGLE_HEIGHT)): self.toggle.set_enabled(self.enabled)
self.state = not self.state self.toggle.render(rl.Rectangle(rect.x, rect.y + (rect.height - TOGGLE_HEIGHT) / 2, self.width, TOGGLE_HEIGHT))
return True
return False return False
def get_width(self) -> int:
return self.width
def set_state(self, state: bool): def set_state(self, state: bool):
self.state = state self.state = state
self.toggle.set_state(state) self.toggle.set_state(state)
@ -64,103 +68,101 @@ class ToggleRightItem(RightItem):
def get_state(self) -> bool: def get_state(self) -> bool:
return self.state return self.state
def set_enabled(self, enabled: bool):
self.enabled = enabled
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 ButtonRightItem(RightItem): @property
def __init__(self, text: str, width: int = BUTTON_WIDTH): def text(self):
super().__init__(width) return _resolve_value(self._text_source, "Error")
self.text = text
self.enabled = True
def _render(self, rect: rl.Rectangle) -> bool: def _render(self, rect: rl.Rectangle) -> bool:
return ( return gui_button(
gui_button( rl.Rectangle(rect.x, rect.y + (rect.height - BUTTON_HEIGHT) / 2, BUTTON_WIDTH, BUTTON_HEIGHT),
rl.Rectangle(rect.x, rect.y + (rect.height - BUTTON_HEIGHT) / 2, BUTTON_WIDTH, BUTTON_HEIGHT), self.text,
self.text, border_radius=BUTTON_BORDER_RADIUS,
border_radius=BUTTON_BORDER_RADIUS, font_weight=BUTTON_FONT_WEIGHT,
font_weight=BUTTON_FONT_WEIGHT, font_size=BUTTON_FONT_SIZE,
font_size=BUTTON_FONT_SIZE, button_style=ButtonStyle.LIST_ACTION,
is_enabled=self.enabled, is_enabled=self.enabled,
) ) == 1
== 1
)
class TextAction(ItemAction):
def get_width(self) -> int: def __init__(self, text: str | Callable[[], str], color: rl.Color = ITEM_TEXT_COLOR, enabled: bool | Callable[[], bool] = True):
return self.width self._text_source = text
def set_enabled(self, enabled: bool):
self.enabled = enabled
class TextRightItem(RightItem):
def __init__(self, text: str, color: rl.Color = ITEM_TEXT_COLOR, font_size: int = ITEM_TEXT_FONT_SIZE):
self.text = text
self.color = color self.color = color
self.font_size = font_size
font = gui_app.font(FontWeight.NORMAL) self._font = gui_app.font(FontWeight.NORMAL)
text_width = measure_text_cached(font, text, font_size).x initial_text = _resolve_value(text, "")
super().__init__(int(text_width + 20)) text_width = measure_text_cached(self._font, initial_text, ITEM_TEXT_FONT_SIZE).x
super().__init__(int(text_width + TEXT_PADDING), enabled)
@property
def text(self):
return _resolve_value(self._text_source, "Error")
def _render(self, rect: rl.Rectangle) -> bool: def _render(self, rect: rl.Rectangle) -> bool:
font = gui_app.font(FontWeight.NORMAL) current_text = self.text
text_size = measure_text_cached(font, self.text, self.font_size) text_size = measure_text_cached(self._font, current_text, ITEM_TEXT_FONT_SIZE)
# Center the text in the allocated rectangle
text_x = rect.x + (rect.width - text_size.x) / 2 text_x = rect.x + (rect.width - text_size.x) / 2
text_y = rect.y + (rect.height - text_size.y) / 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)
rl.draw_text_ex(font, self.text, rl.Vector2(text_x, text_y), self.font_size, 0, self.color)
return False return False
def get_width(self) -> int: def get_width(self) -> int:
return self.width text_width = measure_text_cached(self._font, self.text, ITEM_TEXT_FONT_SIZE).x
return int(text_width + TEXT_PADDING)
def set_text(self, text: str):
self.text = text
font = gui_app.font(FontWeight.NORMAL)
text_width = measure_text_cached(font, text, self.font_size).x
self.width = int(text_width + 20)
@dataclass @dataclass
class ListItem: class ListItem:
title: str title: str
icon: str | None = None icon: str | None = None
description: str | None = None description: str | Callable[[], str] | None = None
description_visible: bool = False description_visible: bool = False
rect: "rl.Rectangle | None" = None rect: "rl.Rectangle" = rl.Rectangle(0, 0, 0, 0)
callback: Callable | None = None callback: Callable | None = None
right_item: RightItem | None = None action_item: ItemAction | None = None
# Cached properties for performance # Cached properties for performance
_prev_max_width: int = 0
_wrapped_description: str | None = None _wrapped_description: str | None = None
_prev_description: str | None = None
_description_height: float = 0 _description_height: float = 0
def get_right_item(self) -> RightItem | None: def actiion(self) -> ItemAction | None:
return self.right_item return self.action_item
def get_description(self):
return _resolve_value(self.description, None)
def get_item_height(self, font: rl.Font, max_width: int) -> float: def get_item_height(self, font: rl.Font, max_width: int) -> float:
if self.description_visible and self.description: current_description = self.get_description()
if not self._wrapped_description: if self.description_visible and current_description:
wrapped_lines = wrap_text(font, self.description, ITEM_DESC_FONT_SIZE, max_width) 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._wrapped_description = "\n".join(wrapped_lines)
self._description_height = len(wrapped_lines) * 20 + 10 # Line height + padding self._description_height = len(wrapped_lines) * ITEM_DESC_FONT_SIZE + 10 # Line height + padding
return ITEM_BASE_HEIGHT + self._description_height - (ITEM_BASE_HEIGHT - ITEM_DESC_V_OFFSET) + ITEM_SPACING return ITEM_BASE_HEIGHT + self._description_height - (ITEM_BASE_HEIGHT - ITEM_DESC_V_OFFSET) + ITEM_PADDING
return ITEM_BASE_HEIGHT return ITEM_BASE_HEIGHT
def get_content_width(self, total_width: int) -> int: def get_content_width(self, total_width: int) -> int:
if self.right_item: if self.action_item:
return total_width - self.right_item.get_width() - RIGHT_ITEM_PADDING return total_width - self.action_item.get_width() - RIGHT_ITEM_PADDING
return total_width return total_width
def get_right_item_rect(self, item_rect: rl.Rectangle) -> rl.Rectangle: def get_right_item_rect(self, item_rect: rl.Rectangle) -> rl.Rectangle:
if not self.right_item: if not self.action_item:
return rl.Rectangle(0, 0, 0, 0) return rl.Rectangle(0, 0, 0, 0)
right_width = self.right_item.get_width() right_width = self.action_item.get_width()
right_x = item_rect.x + item_rect.width - right_width right_x = item_rect.x + item_rect.width - right_width
right_y = item_rect.y right_y = item_rect.y
return rl.Rectangle(right_x, right_y, right_width, ITEM_BASE_HEIGHT) return rl.Rectangle(right_x, right_y, right_width, ITEM_BASE_HEIGHT)
@ -170,28 +172,15 @@ class ListView(Widget):
def __init__(self, items: list[ListItem]): def __init__(self, items: list[ListItem]):
super().__init__() super().__init__()
self._items: list[ListItem] = items self._items: list[ListItem] = items
self._last_dim: tuple[float, float] = (0, 0)
self.scroll_panel = GuiScrollPanel() self.scroll_panel = GuiScrollPanel()
self._font = gui_app.font(FontWeight.NORMAL)
self._font_normal = gui_app.font(FontWeight.NORMAL)
# Interaction state
self._hovered_item: int = -1 self._hovered_item: int = -1
self._last_mouse_pos = rl.Vector2(0, 0)
self._total_height: float = 0
self._visible_range = (0, 0)
def invalid_height_cache(self):
self._last_dim = (0, 0)
def _render(self, rect: rl.Rectangle): def _render(self, rect: rl.Rectangle):
if self._last_dim != (rect.width, rect.height): total_height = self._update_item_rects(rect)
self._update_item_rects(rect)
self._last_dim = (rect.width, rect.height)
# Update layout and handle scrolling # Update layout and handle scrolling
content_rect = rl.Rectangle(rect.x, rect.y, rect.width, self._total_height) content_rect = rl.Rectangle(rect.x, rect.y, rect.width, total_height)
scroll_offset = self.scroll_panel.handle_scroll(rect, content_rect) scroll_offset = self.scroll_panel.handle_scroll(rect, content_rect)
# Handle mouse interaction # Handle mouse interaction
@ -201,112 +190,62 @@ class ListView(Widget):
# Set scissor mode for clipping # Set scissor mode for clipping
rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height)) rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height))
# Calculate visible range for performance for i, item in enumerate(self._items):
self._calculate_visible_range(rect, -scroll_offset.y) y = int(item.rect.y + scroll_offset.y)
if y + item.rect.height <= rect.y or y >= rect.y + rect.height:
continue
self._render_item(item, y)
# Render only visible items if i < len(self._items) - 1:
for i in range(self._visible_range[0], min(self._visible_range[1], len(self._items))): line_y = int(y + item.rect.height - 1)
item = self._items[i] 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 item.rect:
adjusted_rect = rl.Rectangle(item.rect.x, item.rect.y + scroll_offset.y, item.rect.width, item.rect.height)
self._render_item(item, adjusted_rect, i)
if i != len(self._items) - 1:
rl.draw_line_ex(
rl.Vector2(adjusted_rect.x + LINE_PADDING, adjusted_rect.y + adjusted_rect.height - 1),
rl.Vector2(
adjusted_rect.x + adjusted_rect.width - LINE_PADDING * 2, adjusted_rect.y + adjusted_rect.height - 1
),
1.0,
LINE_COLOR,
)
rl.end_scissor_mode() rl.end_scissor_mode()
def _render_item(self, item: ListItem, rect: rl.Rectangle, index: int): def _render_item(self, item: ListItem, y: int):
content_x = rect.x + ITEM_PADDING content_x = item.rect.x + ITEM_PADDING
text_x = content_x text_x = content_x
# Calculate available width for main content
content_width = item.get_content_width(int(rect.width - ITEM_PADDING * 2))
# Draw icon if present # Draw icon if present
if item.icon: if item.icon:
icon_texture = gui_app.texture(os.path.join("icons", item.icon), ICON_SIZE, ICON_SIZE) icon_texture = gui_app.texture(os.path.join("icons", item.icon), ICON_SIZE, ICON_SIZE)
rl.draw_texture( rl.draw_texture(icon_texture, int(content_x), int(y + (ITEM_BASE_HEIGHT - icon_texture.width) // 2), rl.WHITE)
icon_texture, int(content_x), int(rect.y + (ITEM_BASE_HEIGHT - icon_texture.width) // 2), rl.WHITE
)
text_x += ICON_SIZE + ITEM_PADDING text_x += ICON_SIZE + ITEM_PADDING
# Draw main text # Draw main text
text_size = measure_text_cached(self._font_normal, item.title, ITEM_TEXT_FONT_SIZE) text_size = measure_text_cached(self._font, item.title, ITEM_TEXT_FONT_SIZE)
item_y = rect.y + (ITEM_BASE_HEIGHT - text_size.y) // 2 item_y = y + (ITEM_BASE_HEIGHT - text_size.y) // 2
rl.draw_text_ex(self._font_normal, item.title, rl.Vector2(text_x, item_y), ITEM_TEXT_FONT_SIZE, 0, ITEM_TEXT_COLOR) 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 (adjust width for right item)
if item.description_visible and item._wrapped_description:
desc_y = rect.y + ITEM_DESC_V_OFFSET
desc_max_width = int(content_width - (text_x - content_x))
# Re-wrap description if needed due to right item
if (item.right_item and item.description) and not item._wrapped_description:
wrapped_lines = wrap_text(self._font_normal, item.description, ITEM_DESC_FONT_SIZE, desc_max_width)
item._wrapped_description = "\n".join(wrapped_lines)
# Draw description if visible
current_description = item.get_description()
if item.description_visible and current_description and item._wrapped_description:
rl.draw_text_ex( rl.draw_text_ex(
self._font_normal, self._font,
item._wrapped_description, item._wrapped_description,
rl.Vector2(text_x, desc_y), (text_x, y + ITEM_DESC_V_OFFSET),
ITEM_DESC_FONT_SIZE, ITEM_DESC_FONT_SIZE,
0, 0,
ITEM_DESC_TEXT_COLOR, ITEM_DESC_TEXT_COLOR,
) )
# Draw right item if present # Draw right item if present
if item.right_item: if item.action_item:
right_rect = item.get_right_item_rect(rect) right_rect = item.get_right_item_rect(item.rect)
# Adjust for scroll offset right_rect.y = y
right_rect.y = right_rect.y if item.action_item.render(right_rect) and item.action_item.enabled:
if item.right_item.render(right_rect):
# Right item was clicked/activated # Right item was clicked/activated
if item.callback: if item.callback:
item.callback() item.callback()
def _update_item_rects(self, container_rect: rl.Rectangle) -> None: def _update_item_rects(self, container_rect: rl.Rectangle) -> float:
current_y: float = 0.0 current_y = 0.0
self._total_height = 0
for item in self._items: for item in self._items:
content_width = item.get_content_width(int(container_rect.width - ITEM_PADDING * 2)) content_width = item.get_content_width(int(container_rect.width - ITEM_PADDING * 2))
item_height = item.get_item_height(self._font_normal, content_width) 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) item.rect = rl.Rectangle(container_rect.x, container_rect.y + current_y, container_rect.width, item_height)
current_y += item_height current_y += item_height
self._total_height += item_height return current_y # total height of all items
def _calculate_visible_range(self, rect: rl.Rectangle, scroll_offset: float):
if not self._items:
self._visible_range = (0, 0)
return
visible_top = scroll_offset
visible_bottom = scroll_offset + rect.height
start_idx = 0
end_idx = len(self._items)
# Find first visible item
for i, item in enumerate(self._items):
if item.rect and item.rect.y + item.rect.height >= visible_top:
start_idx = max(0, i - 1)
break
# Find last visible item
for i in range(start_idx, len(self._items)):
item = self._items[i]
if item.rect and item.rect.y > visible_bottom:
end_idx = min(len(self._items), i + 2)
break
self._visible_range = (start_idx, end_idx)
def _handle_mouse_interaction(self, rect: rl.Rectangle, scroll_offset: rl.Vector2): def _handle_mouse_interaction(self, rect: rl.Rectangle, scroll_offset: rl.Vector2):
mouse_pos = rl.get_mouse_position() mouse_pos = rl.get_mouse_position()
@ -336,41 +275,35 @@ class ListView(Widget):
item = self._items[self._hovered_item] item = self._items[self._hovered_item]
# Check if click was on right item area # Check if click was on right item area
if item.right_item and item.rect: 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) 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) right_rect = item.get_right_item_rect(adjusted_rect)
if rl.check_collision_point_rec(mouse_pos, right_rect): if rl.check_collision_point_rec(mouse_pos, right_rect):
# Click was handled by right item, don't process main item click # Click was on right item, don't toggle description
return return
# Toggle description visibility if item has description # Toggle description visibility if item has description
if item.description: if item.description:
item.description_visible = not item.description_visible item.description_visible = not item.description_visible
# Force layout update when description visibility changes
self._last_dim = (0, 0)
# Call item callback
if item.callback:
item.callback()
# Factory functions # Factory functions
def simple_item(title: str, callback: Callable | None = None) -> ListItem: def simple_item(title: str, callback: Callable | None = None) -> ListItem:
return ListItem(title=title, callback=callback) return ListItem(title=title, callback=callback)
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) -> ListItem:
action = ToggleAction(initial_state=initial_state, enabled=enabled)
return ListItem(title=title, description=description, action_item=action, icon=icon, callback=callback)
def toggle_item( def button_item(title: str, button_text: str | Callable[[], str], description: str | Callable[[], str] | None = None,
title: str, description: str = None, initial_state: bool = False, callback: Callable | None = None, icon: str = "" callback: Callable | None = None, enabled: bool | Callable[[], bool] = True) -> ListItem:
) -> ListItem: action = ButtonAction(text=button_text, enabled=enabled)
toggle = ToggleRightItem(initial_state=initial_state) return ListItem(title=title, description=description, action_item=action, callback=callback)
return ListItem(title=title, description=description, right_item=toggle, icon=icon, callback=callback)
def button_item(title: str, button_text: str, description: str = None, callback: Callable | None = None) -> ListItem:
button = ButtonRightItem(text=button_text)
return ListItem(title=title, description=description, right_item=button, callback=callback)
def text_item(title: str, value: str, description: str = None, callback: Callable | None = None) -> ListItem: def text_item(title: str, value: str | Callable[[], str], description: str | Callable[[], str] | None = None,
text_item = TextRightItem(text=value, color=rl.Color(170, 170, 170, 255)) callback: Callable | None = None, enabled: bool | Callable[[], bool] = True) -> ListItem:
return ListItem(title=title, description=description, right_item=text_item, callback=callback) action = TextAction(text=value, color=rl.Color(170, 170, 170, 255), enabled=enabled)
return ListItem(title=title, description=description, action_item=action, callback=callback)

Loading…
Cancel
Save