diff --git a/system/ui/lib/button.py b/system/ui/lib/button.py index 034189275f..1857fd21cc 100644 --- a/system/ui/lib/button.py +++ b/system/ui/lib/button.py @@ -10,6 +10,12 @@ class ButtonStyle(IntEnum): TRANSPARENT = 3 # For buttons with transparent background and border +class TextAlignment(IntEnum): + LEFT = 0 + CENTER = 1 + RIGHT = 2 + + 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) @@ -38,6 +44,8 @@ def gui_button( button_style: ButtonStyle = ButtonStyle.NORMAL, is_enabled: bool = True, border_radius: int = 10, # Corner rounding in pixels + text_alignment: TextAlignment = TextAlignment.CENTER, + text_padding: int = 20, # Padding for left/right alignment ) -> int: result = 0 @@ -58,11 +66,16 @@ def gui_button( rl.draw_rectangle_rounded_lines_ex(rect, roundness, 20, 2, rl.WHITE) font = gui_app.font(font_weight) - # Center text in the button text_size = rl.measure_text_ex(font, text, font_size, 0) - text_pos = rl.Vector2( - rect.x + (rect.width - text_size.x) // 2, rect.y + (rect.height - text_size.y) // 2 - ) + text_pos = rl.Vector2(0, rect.y + (rect.height - text_size.y) // 2) # Vertical centering + + # Horizontal alignment + if text_alignment == TextAlignment.LEFT: + text_pos.x = rect.x + text_padding + elif text_alignment == TextAlignment.CENTER: + text_pos.x = rect.x + (rect.width - text_size.x) // 2 + elif text_alignment == TextAlignment.RIGHT: + text_pos.x = rect.x + rect.width - text_size.x - text_padding # Draw the button text text_color = BUTTON_ENABLED_TEXT_COLOR if is_enabled else BUTTON_DISABLED_TEXT_COLOR diff --git a/system/ui/widgets/option_dialog.py b/system/ui/widgets/option_dialog.py new file mode 100644 index 0000000000..c25c5c6e6c --- /dev/null +++ b/system/ui/widgets/option_dialog.py @@ -0,0 +1,81 @@ +import pyray as rl + +from openpilot.system.ui.lib.button import gui_button, ButtonStyle, TextAlignment +from openpilot.system.ui.lib.label import gui_label +from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel + + +class MultiOptionDialog: + def __init__(self, title, options, current=""): + self._title = title + self._options = options + self._current = current if current in options else "" + self._selection = self._current + self._option_height = 80 + self._padding = 20 + self.scroll_panel = GuiScrollPanel() + + @property + def selection(self): + return self._selection + + def render(self, rect): + title_rect = rl.Rectangle(rect.x + self._padding, rect.y + self._padding, rect.width - 2 * self._padding, 70) + gui_label(title_rect, self._title, 70) + + options_y_start = rect.y + 120 + options_height = len(self._options) * (self._option_height + 10) + options_rect = rl.Rectangle(rect.x + self._padding, options_y_start, rect.width - 2 * self._padding, options_height) + + view_rect = rl.Rectangle( + rect.x + self._padding, options_y_start, rect.width - 2 * self._padding, rect.height - 200 - 2 * self._padding + ) + + offset = self.scroll_panel.handle_scroll(view_rect, options_rect) + is_click_valid = self.scroll_panel.is_click_valid() + + rl.begin_scissor_mode(int(view_rect.x), int(view_rect.y), int(view_rect.width), int(view_rect.height)) + + for i, option in enumerate(self._options): + y_pos = view_rect.y + i * (self._option_height + 10) + offset.y + item_rect = rl.Rectangle(view_rect.x, y_pos, view_rect.width, self._option_height) + + if not rl.check_collision_recs(item_rect, view_rect): + continue + + is_selected = option == self._selection + button_style = ButtonStyle.PRIMARY if is_selected else ButtonStyle.NORMAL + + if gui_button(item_rect, option, button_style=button_style, text_alignment=TextAlignment.LEFT) and is_click_valid: + self._selection = option + + rl.end_scissor_mode() + + button_y = rect.y + rect.height - 80 - self._padding + button_width = (rect.width - 3 * self._padding) / 2 + + cancel_rect = rl.Rectangle(rect.x + self._padding, button_y, button_width, 80) + if gui_button(cancel_rect, "Cancel"): + return 0 # Canceled + + select_rect = rl.Rectangle(rect.x + 2 * self._padding + button_width, button_y, button_width, 80) + has_new_selection = self._selection != "" and self._selection != self._current + + if gui_button(select_rect, "Select", is_enabled=has_new_selection, button_style=ButtonStyle.PRIMARY): + return 1 # Selected + + return -1 # Still active + + +if __name__ == "__main__": + from openpilot.system.ui.lib.application import gui_app + + gui_app.init_window("Multi Option Dialog Example") + options = [f"Option {i}" for i in range(1, 11)] + dialog = MultiOptionDialog("Choose an option", options, options[0]) + + for _ in gui_app.render(): + result = dialog.render(rl.Rectangle(100, 100, 1024, 800)) + if result >= 0: + print(f"Selected: {dialog.selection}" if result > 0 else "Canceled") + break