import os
import pyray as rl
from dataclasses import dataclass
from collections . abc import Callable
from abc import ABC , abstractmethod
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 . text_measure import measure_text_cached
from openpilot . system . ui . lib . wrap_text import wrap_text
from openpilot . system . ui . lib . button import gui_button
from openpilot . system . ui . lib . toggle import Toggle , WIDTH as TOGGLE_WIDTH , HEIGHT as TOGGLE_HEIGHT
LINE_PADDING = 40
LINE_COLOR = rl . GRAY
ITEM_PADDING = 20
ITEM_SPACING = 80
ITEM_BASE_HEIGHT = 170
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 = 130
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
# Abstract base class for right-side items
class RightItem ( Widget , ABC ) :
def __init__ ( self , width : int = 100 ) :
super ( ) . __init__ ( )
self . width = width
self . enabled = True
@abstractmethod
def get_width ( self ) - > int :
pass
class ToggleRightItem ( RightItem ) :
def __init__ ( self , initial_state : bool = False , width : int = TOGGLE_WIDTH ) :
super ( ) . __init__ ( width )
self . toggle = Toggle ( initial_state = initial_state )
self . state = initial_state
self . enabled = True
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 . state = not self . state
return True
return False
def get_width ( self ) - > int :
return self . width
def set_state ( self , state : bool ) :
self . state = state
self . toggle . set_state ( state )
def get_state ( self ) - > bool :
return self . state
def set_enabled ( self , enabled : bool ) :
self . enabled = enabled
class ButtonRightItem ( RightItem ) :
def __init__ ( self , text : str , width : int = BUTTON_WIDTH ) :
super ( ) . __init__ ( width )
self . text = text
self . enabled = True
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 ,
is_enabled = self . enabled ,
)
== 1
)
def get_width ( self ) - > int :
return self . width
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 . font_size = font_size
font = gui_app . font ( FontWeight . NORMAL )
text_width = measure_text_cached ( font , text , font_size ) . x
super ( ) . __init__ ( int ( text_width + 20 ) )
def _render ( self , rect : rl . Rectangle ) - > bool :
font = gui_app . font ( FontWeight . NORMAL )
text_size = measure_text_cached ( font , self . text , self . font_size )
# Center the text in the allocated rectangle
text_x = rect . x + ( rect . width - text_size . x ) / 2
text_y = rect . y + ( rect . height - text_size . y ) / 2
rl . draw_text_ex ( font , self . text , rl . Vector2 ( text_x , text_y ) , self . font_size , 0 , self . color )
return False
def get_width ( self ) - > int :
return self . width
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
class ListItem :
title : str
icon : str | None = None
description : str | None = None
description_visible : bool = False
rect : " rl.Rectangle | None " = None
callback : Callable | None = None
right_item : RightItem | None = None
# Cached properties for performance
_wrapped_description : str | None = None
_description_height : float = 0
def get_right_item ( self ) - > RightItem | None :
return self . right_item
def get_item_height ( self , font : rl . Font , max_width : int ) - > float :
if self . description_visible and self . description :
if not self . _wrapped_description :
wrapped_lines = wrap_text ( font , self . description , ITEM_DESC_FONT_SIZE , max_width )
self . _wrapped_description = " \n " . join ( wrapped_lines )
self . _description_height = len ( wrapped_lines ) * 20 + 10 # Line height + padding
return ITEM_BASE_HEIGHT + self . _description_height - ( ITEM_BASE_HEIGHT - ITEM_DESC_V_OFFSET ) + ITEM_SPACING
return ITEM_BASE_HEIGHT
def get_content_width ( self , total_width : int ) - > int :
if self . right_item :
return total_width - self . right_item . get_width ( ) - RIGHT_ITEM_PADDING
return total_width
def get_right_item_rect ( self , item_rect : rl . Rectangle ) - > rl . Rectangle :
if not self . right_item :
return rl . Rectangle ( 0 , 0 , 0 , 0 )
right_width = self . right_item . get_width ( )
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 : list [ ListItem ] = items
self . _last_dim : tuple [ float , float ] = ( 0 , 0 )
self . scroll_panel = GuiScrollPanel ( )
self . _font_normal = gui_app . font ( FontWeight . NORMAL )
# Interaction state
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 ) :
if self . _last_dim != ( rect . width , rect . height ) :
self . _update_item_rects ( rect )
self . _last_dim = ( rect . width , rect . height )
# Update layout and handle scrolling
content_rect = rl . Rectangle ( rect . x , rect . y , rect . width , self . _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 ) )
# Calculate visible range for performance
self . _calculate_visible_range ( rect , - scroll_offset . y )
# Render only visible items
for i in range ( self . _visible_range [ 0 ] , min ( self . _visible_range [ 1 ] , len ( self . _items ) ) ) :
item = self . _items [ i ]
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 ( )
def _render_item ( self , item : ListItem , rect : rl . Rectangle , index : int ) :
content_x = rect . x + ITEM_PADDING
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
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 ( rect . 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_normal , item . title , ITEM_TEXT_FONT_SIZE )
item_y = rect . 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 )
# 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 )
rl . draw_text_ex (
self . _font_normal ,
item . _wrapped_description ,
rl . Vector2 ( text_x , desc_y ) ,
ITEM_DESC_FONT_SIZE ,
0 ,
ITEM_DESC_TEXT_COLOR ,
)
# Draw right item if present
if item . right_item :
right_rect = item . get_right_item_rect ( rect )
# Adjust for scroll offset
right_rect . y = right_rect . y
if item . right_item . render ( right_rect ) :
# Right item was clicked/activated
if item . callback :
item . callback ( )
def _update_item_rects ( self , container_rect : rl . Rectangle ) - > None :
current_y : float = 0.0
self . _total_height = 0
for item in self . _items :
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 . rect = rl . Rectangle ( container_rect . x , container_rect . y + current_y , container_rect . width , item_height )
current_y + = item_height
self . _total_height + = item_height
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 ) :
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 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 . right_item and item . rect :
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 handled by right item, don't process main item click
return
# Toggle description visibility if item has description
if item . description :
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
def simple_item ( title : str , callback : Callable | None = None ) - > ListItem :
return ListItem ( title = title , callback = callback )
def toggle_item (
title : str , description : str = None , initial_state : bool = False , callback : Callable | None = None , icon : str = " "
) - > ListItem :
toggle = ToggleRightItem ( initial_state = initial_state )
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 :
text_item = TextRightItem ( text = value , color = rl . Color ( 170 , 170 , 170 , 255 ) )
return ListItem ( title = title , description = description , right_item = text_item , callback = callback )