import os
import pyray as rl
from collections . abc import Callable
from abc import ABC
from openpilot . system . ui . lib . application import gui_app , FontWeight
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 , 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_WIDTH = 600
ITEM_BASE_HEIGHT = 170
ITEM_PADDING = 20
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 = " " ) :
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 = BUTTON_HEIGHT , enabled : bool | Callable [ [ ] , bool ] = True ) :
super ( ) . __init__ ( )
self . set_rect ( rl . Rectangle ( 0 , 0 , width , 0 ) )
self . _enabled_source = enabled
@property
def enabled ( self ) :
return _resolve_value ( self . _enabled_source , False )
class ToggleAction ( ItemAction ) :
def __init__ ( self , initial_state : bool = False , width : int = TOGGLE_WIDTH , enabled : bool | Callable [ [ ] , bool ] = True ) :
super ( ) . __init__ ( width , enabled )
self . toggle = Toggle ( initial_state = initial_state )
self . state = initial_state
def _render ( self , rect : rl . Rectangle ) - > bool :
self . toggle . set_enabled ( self . enabled )
self . toggle . render ( rl . Rectangle ( rect . x , rect . y + ( rect . height - TOGGLE_HEIGHT ) / 2 , self . _rect . width , TOGGLE_HEIGHT ) )
return False
def set_state ( self , state : bool ) :
self . state = state
self . toggle . set_state ( state )
def get_state ( self ) - > bool :
return self . 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
@property
def text ( self ) :
return _resolve_value ( self . _text_source , " 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
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
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 )
@property
def text ( self ) :
return _resolve_value ( self . _text_source , " Error " )
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 )
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
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
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 )
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
if left_clicked and self . left_callback :
self . left_callback ( )
return True
if right_clicked and self . right_callback :
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 )
self . buttons = buttons
self . button_width = button_width
self . selected_button = selected_index
self . callback = callback
self . _font = gui_app . font ( FontWeight . MEDIUM )
def _render ( self , rect : rl . Rectangle ) - > bool :
spacing = 20
button_y = rect . y + ( rect . height - BUTTON_HEIGHT ) / 2
clicked = - 1
for i , text in enumerate ( self . buttons ) :
button_x = rect . x + i * ( self . button_width + spacing )
button_rect = rl . Rectangle ( button_x , button_y , self . button_width , BUTTON_HEIGHT )
# Check button state
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
# 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
# Draw button
rl . draw_rectangle_rounded ( button_rect , 1.0 , 20 , bg_color )
# Draw text
text_size = measure_text_cached ( self . _font , text , 40 )
text_x = button_x + ( self . button_width - text_size . x ) / 2
text_y = button_y + ( BUTTON_HEIGHT - 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 ) )
# Handle click
if is_hovered and rl . is_mouse_button_released ( rl . MouseButton . MOUSE_BUTTON_LEFT ) :
clicked = i
if clicked > = 0 :
self . selected_button = clicked
if self . callback :
self . callback ( clicked )
return True
return False
class ListItem ( Widget ) :
def __init__ ( self , title : str = " " , icon : str | None = None , description : str | Callable [ [ ] , str ] | None = None ,
description_visible : bool = False , callback : Callable | None = None ,
action_item : ItemAction | None = None ) :
super ( ) . __init__ ( )
self . title = title
self . icon = icon
self . description = description
self . description_visible = description_visible
self . callback = callback
self . action_item = action_item
self . set_rect ( rl . Rectangle ( 0 , 0 , ITEM_BASE_WIDTH , ITEM_BASE_HEIGHT ) )
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
def set_parent_rect ( self , parent_rect : rl . Rectangle ) :
super ( ) . set_parent_rect ( parent_rect )
self . _rect . width = parent_rect . width
def _handle_mouse_release ( self , mouse_pos : rl . Vector2 ) :
if not self . is_visible :
return
# Check not in action rect
if self . action_item :
action_rect = self . get_right_item_rect ( self . _rect )
if rl . check_collision_point_rec ( mouse_pos , action_rect ) :
# Click was on right item, don't toggle description
return
if self . description :
self . description_visible = not self . description_visible
content_width = self . get_content_width ( int ( self . _rect . width - ITEM_PADDING * 2 ) )
self . _rect . height = self . get_item_height ( self . _font , content_width )
def _render ( self , _ ) :
if not self . is_visible :
return
# Don't draw items that are not in parent's viewport
if ( ( self . _rect . y + self . rect . height ) < = self . _parent_rect . y or
self . _rect . y > = ( self . _parent_rect . y + self . _parent_rect . height ) ) :
return
content_x = self . _rect . x + ITEM_PADDING
text_x = content_x
# Only draw title and icon for items that have them
if self . title :
# Draw icon if present
if self . icon :
icon_texture = gui_app . texture ( os . path . join ( " icons " , self . icon ) , ICON_SIZE , ICON_SIZE )
rl . draw_texture ( icon_texture , int ( content_x ) , int ( self . _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 , self . title , ITEM_TEXT_FONT_SIZE )
item_y = self . _rect . y + ( ITEM_BASE_HEIGHT - text_size . y ) / / 2
rl . draw_text_ex ( self . _font , self . title , rl . Vector2 ( text_x , item_y ) , ITEM_TEXT_FONT_SIZE , 0 , ITEM_TEXT_COLOR )
# Draw description if visible
current_description = self . get_description ( )
if self . description_visible and current_description and self . _wrapped_description :
rl . draw_text_ex (
self . _font ,
self . _wrapped_description ,
rl . Vector2 ( text_x , self . _rect . y + ITEM_DESC_V_OFFSET ) ,
ITEM_DESC_FONT_SIZE ,
0 ,
ITEM_DESC_TEXT_COLOR ,
)
# Draw right item if present
if self . action_item :
right_rect = self . get_right_item_rect ( self . _rect )
right_rect . y = self . _rect . y
if self . action_item . render ( right_rect ) and self . action_item . enabled :
# Right item was clicked/activated
if self . callback :
self . callback ( )
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 . rect . width > 0 :
return total_width - int ( self . action_item . rect . 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 . rect . 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 )
# 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 | 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 button_item ( title : str , button_text : str | Callable [ [ ] , str ] , description : str | Callable [ [ ] , str ] | None = None ,
callback : Callable | None = None , enabled : bool | Callable [ [ ] , bool ] = True ) - > ListItem :
action = ButtonAction ( text = button_text , enabled = enabled )
return ListItem ( title = title , description = description , action_item = action , callback = callback )
def text_item ( title : str , value : str | Callable [ [ ] , str ] , description : str | Callable [ [ ] , str ] | None = None ,
callback : Callable | None = None , enabled : 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 )
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 ) - > ListItem :
action = DualButtonAction ( left_text , right_text , left_callback , right_callback , enabled )
return ListItem ( title = " " , description = description , action_item = action )
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 )