Add 3D-style buttons with box-drawing characters

Create DOS/BIOS-style 3D button effects using Unicode characters:

Button3D (multi-line):
  ┌──────────┐
  │  Label   │▄
  └──────────┘█

Button3DCompact (single-line):
  ▐ Label ▌▄

Changes:
- Add Button3D class for large standalone buttons
- Add Button3DCompact class for inline/dialog buttons
- Update palette with shadow colors (shadow_edge, button_highlight, button_shadow)
- Replace all ClickableButton/urwid.Button with Button3DCompact throughout:
  - Main menu action buttons (Create, Edit, Delete, Test, Providers, EXIT)
  - Dialog OK/Cancel buttons
  - Load buttons in code/prompt dialogs
  - Provider dropdown buttons
  - Auto-adjust button

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
rob 2025-12-05 01:46:20 -04:00
parent b8c85df398
commit aef5c0f99f
1 changed files with 175 additions and 32 deletions

View File

@ -10,22 +10,33 @@ from .tool import (
from .providers import Provider, load_providers, add_provider, delete_provider, get_provider from .providers import Provider, load_providers, add_provider, delete_provider, get_provider
# Color palette - BIOS style # Color palette - BIOS style with 3D button effects
PALETTE = [ PALETTE = [
('body', 'white', 'dark blue'), ('body', 'white', 'dark blue'),
('header', 'white', 'dark red', 'bold'), ('header', 'white', 'dark red', 'bold'),
('footer', 'black', 'light gray'), ('footer', 'black', 'light gray'),
# Button colors - raised 3D effect
('button', 'black', 'light gray'), ('button', 'black', 'light gray'),
('button_focus', 'white', 'dark red', 'bold'), ('button_focus', 'white', 'dark red', 'bold'),
('button_highlight', 'white', 'light gray'), # Top/left edge (light)
('button_shadow', 'dark gray', 'light gray'), # Bottom/right edge (dark)
('button_pressed', 'black', 'dark gray'), # Pressed state
# Edit fields
('edit', 'black', 'light gray'), ('edit', 'black', 'light gray'),
('edit_focus', 'black', 'yellow'), ('edit_focus', 'black', 'yellow'),
# List items
('listbox', 'black', 'light gray'), ('listbox', 'black', 'light gray'),
('listbox_focus', 'white', 'dark red'), ('listbox_focus', 'white', 'dark red'),
# Dialog
('dialog', 'black', 'light gray'), ('dialog', 'black', 'light gray'),
('dialog_border', 'white', 'dark blue'), ('dialog_border', 'white', 'dark blue'),
# Text styles
('label', 'yellow', 'dark blue', 'bold'), ('label', 'yellow', 'dark blue', 'bold'),
('error', 'white', 'dark red', 'bold'), ('error', 'white', 'dark red', 'bold'),
('success', 'light green', 'dark blue', 'bold'), ('success', 'light green', 'dark blue', 'bold'),
# 3D shadow elements
('shadow', 'black', 'black'),
('shadow_edge', 'dark gray', 'dark blue'),
] ]
@ -55,8 +66,149 @@ class SelectableText(urwid.WidgetWrap):
return False return False
class Button3D(urwid.WidgetWrap):
"""A 3D-style button using box-drawing characters for depth.
Creates a raised button effect like DOS/BIOS interfaces:
Label
When focused, colors change to show selection.
"""
signals = ['click']
def __init__(self, label, on_press=None, user_data=None):
self.label = label
self.on_press = on_press
self.user_data = user_data
self._pressed = False
# Build the 3D button structure
self._build_widget()
super().__init__(self._widget)
def _build_widget(self):
"""Build the 3D button widget structure."""
label = self.label
width = len(label) + 4 # Padding inside button
# Button face with border
# Top edge: ┌────┐
top = '' + '' * (width - 2) + ''
# Middle: │ Label │ with shadow
middle_text = '' + label + ''
# Bottom edge: └────┘ with shadow
bottom = '' + '' * (width - 2) + ''
# Shadow characters (right and bottom)
shadow_right = ''
shadow_bottom = ''
# Create the rows
top_row = urwid.Text(top + ' ') # Space for shadow alignment
middle_row = urwid.Columns([
('pack', urwid.Text(middle_text)),
('pack', urwid.Text(('shadow_edge', shadow_right))),
])
bottom_row = urwid.Columns([
('pack', urwid.Text(bottom)),
('pack', urwid.Text(('shadow_edge', shadow_right))),
])
shadow_row = urwid.Text(('shadow_edge', ' ' + shadow_bottom * (width - 1)))
# Stack them
pile = urwid.Pile([
top_row,
middle_row,
bottom_row,
shadow_row,
])
self._widget = urwid.AttrMap(pile, 'button', 'button_focus')
def selectable(self):
return True
def keypress(self, size, key):
if key == 'enter':
self._activate()
return None
return key
def mouse_event(self, size, event, button, col, row, focus):
if button == 1:
if event == 'mouse press':
self._pressed = True
return True
elif event == 'mouse release' and self._pressed:
self._pressed = False
self._activate()
return True
return False
def _activate(self):
"""Trigger the button callback."""
if self.on_press:
self.on_press(self.user_data)
self._emit('click')
class Button3DCompact(urwid.WidgetWrap):
"""A compact 3D button that fits on a single line with shadow effect.
Creates a subtle 3D effect: [ Label ]
Better for inline use where vertical space is limited.
"""
signals = ['click']
def __init__(self, label, on_press=None, user_data=None):
self.label = label
self.on_press = on_press
self.user_data = user_data
# Build compact button: ▐ Label ▌ with shadow
# Using block characters for edges
button_text = urwid.Text([
('button_highlight', ''),
('button', f' {label} '),
('button_shadow', ''),
('shadow_edge', ''),
])
self._widget = urwid.AttrMap(button_text, None, {
'button': 'button_focus',
'button_highlight': 'button_focus',
'button_shadow': 'button_focus',
})
super().__init__(self._widget)
def selectable(self):
return True
def keypress(self, size, key):
if key == 'enter':
self._activate()
return None
return key
def mouse_event(self, size, event, button, col, row, focus):
if button == 1 and event == 'mouse release':
self._activate()
return True
return False
def _activate(self):
if self.on_press:
self.on_press(self.user_data)
self._emit('click')
class ClickableButton(urwid.WidgetWrap): class ClickableButton(urwid.WidgetWrap):
"""A button that responds to mouse clicks.""" """A button that responds to mouse clicks (legacy wrapper)."""
def __init__(self, label, on_press=None, user_data=None): def __init__(self, label, on_press=None, user_data=None):
self.on_press = on_press self.on_press = on_press
@ -466,16 +618,16 @@ class ToolBuilderLayout(urwid.WidgetWrap):
class Dialog(urwid.WidgetWrap): class Dialog(urwid.WidgetWrap):
"""A dialog box overlay.""" """A dialog box overlay with 3D-style buttons."""
def __init__(self, title, body, buttons, width=60, height=None): def __init__(self, title, body, buttons, width=60, height=None):
# Title # Title
title_widget = urwid.Text(('header', f' {title} '), align='center') title_widget = urwid.Text(('header', f' {title} '), align='center')
# Buttons row # Buttons row - use 3D compact buttons for dialog actions
button_widgets = [] button_widgets = []
for label, callback in buttons: for label, callback in buttons:
btn = ClickableButton(label, callback) btn = Button3DCompact(label, callback)
button_widgets.append(btn) button_widgets.append(btn)
buttons_row = urwid.Columns([('pack', b) for b in button_widgets], dividechars=2) buttons_row = urwid.Columns([('pack', b) for b in button_widgets], dividechars=2)
buttons_centered = urwid.Padding(buttons_row, align='center', width='pack') buttons_centered = urwid.Padding(buttons_row, align='center', width='pack')
@ -647,22 +799,22 @@ class SmartToolsUI:
tool_listbox = ToolListBox(self._tool_walker, on_focus_change=self._on_tool_focus) tool_listbox = ToolListBox(self._tool_walker, on_focus_change=self._on_tool_focus)
tool_box = urwid.LineBox(tool_listbox, title='Tools') tool_box = urwid.LineBox(tool_listbox, title='Tools')
# Action buttons - Tab navigates here from tool list # Action buttons - Tab navigates here from tool list (3D style)
create_btn = ClickableButton("Create", lambda _: self._create_tool_before_selected()) create_btn = Button3DCompact("Create", lambda _: self._create_tool_before_selected())
edit_btn = ClickableButton("Edit", lambda _: self._edit_selected_tool()) edit_btn = Button3DCompact("Edit", lambda _: self._edit_selected_tool())
delete_btn = ClickableButton("Delete", lambda _: self._delete_selected_tool()) delete_btn = Button3DCompact("Delete", lambda _: self._delete_selected_tool())
test_btn = ClickableButton("Test", lambda _: self._test_selected_tool()) test_btn = Button3DCompact("Test", lambda _: self._test_selected_tool())
providers_btn = ClickableButton("Providers", lambda _: self.manage_providers()) providers_btn = Button3DCompact("Providers", lambda _: self.manage_providers())
buttons_row = urwid.Columns([ buttons_row = urwid.Columns([
('pack', create_btn), ('pack', create_btn),
('pack', urwid.Text(" ")), ('pack', urwid.Text(" ")),
('pack', edit_btn), ('pack', edit_btn),
('pack', urwid.Text(" ")), ('pack', urwid.Text(" ")),
('pack', delete_btn), ('pack', delete_btn),
('pack', urwid.Text(" ")), ('pack', urwid.Text(" ")),
('pack', test_btn), ('pack', test_btn),
('pack', urwid.Text(" ")), ('pack', urwid.Text(" ")),
('pack', providers_btn), ('pack', providers_btn),
]) ])
buttons_padded = urwid.Padding(buttons_row, align='left', left=1) buttons_padded = urwid.Padding(buttons_row, align='left', left=1)
@ -687,9 +839,9 @@ class SmartToolsUI:
info_filler = urwid.Filler(info_content, valign='top') info_filler = urwid.Filler(info_content, valign='top')
info_box = urwid.LineBox(info_filler, title='Tool Info') info_box = urwid.LineBox(info_filler, title='Tool Info')
# Exit button at bottom # Exit button at bottom (3D style)
exit_btn = ClickableButton("EXIT", lambda _: self.exit_app()) exit_btn = Button3DCompact("EXIT", lambda _: self.exit_app())
exit_centered = urwid.Padding(exit_btn, align='center', width=10) exit_centered = urwid.Padding(exit_btn, align='center', width=12)
# Use a custom Pile that handles Tab to cycle between tool list and buttons # Use a custom Pile that handles Tab to cycle between tool list and buttons
self._main_pile = TabCyclePile([ self._main_pile = TabCyclePile([
@ -1267,10 +1419,7 @@ class SmartToolsUI:
popup = Dialog("Select Provider", body, []) popup = Dialog("Select Provider", body, [])
self.show_overlay(popup, width=50, height=min(len(provider_names) + 6, 16)) self.show_overlay(popup, width=50, height=min(len(provider_names) + 6, 16))
provider_select_btn = urwid.AttrMap( provider_select_btn = Button3DCompact("", on_press=show_provider_dropdown)
urwid.Button("Select", on_press=show_provider_dropdown),
'button', 'button_focus'
)
# File input for external prompt # File input for external prompt
default_file = existing.prompt_file if existing and existing.prompt_file else "prompt.txt" default_file = existing.prompt_file if existing and existing.prompt_file else "prompt.txt"
@ -1357,7 +1506,7 @@ class SmartToolsUI:
def on_cancel(_): def on_cancel(_):
self.close_overlay() self.close_overlay()
load_btn = urwid.AttrMap(urwid.Button("Load", on_load), 'button', 'button_focus') load_btn = Button3DCompact("Load", on_load)
# Prompt editor in a box - use ListBox for proper focus handling and scrolling # Prompt editor in a box - use ListBox for proper focus handling and scrolling
# Wrap in DOSScrollBar for DOS-style scrollbar with arrow buttons # Wrap in DOSScrollBar for DOS-style scrollbar with arrow buttons
@ -1481,10 +1630,7 @@ class SmartToolsUI:
popup = Dialog("Select Provider", popup_body, []) popup = Dialog("Select Provider", popup_body, [])
self.show_overlay(popup, width=50, height=min(len(provider_names) + 6, 16)) self.show_overlay(popup, width=50, height=min(len(provider_names) + 6, 16))
ai_provider_select_btn = urwid.AttrMap( ai_provider_select_btn = Button3DCompact("", on_press=show_ai_provider_dropdown)
urwid.Button("", on_press=show_ai_provider_dropdown),
'button', 'button_focus'
)
# Default prompt template for AI code generation/adjustment # Default prompt template for AI code generation/adjustment
default_ai_prompt = f"""Modify or generate Python code according to my instruction below. default_ai_prompt = f"""Modify or generate Python code according to my instruction below.
@ -1547,10 +1693,7 @@ Return ONLY the Python code, no explanations or markdown fencing."""
error_msg = result.error or "Unknown error" error_msg = result.error or "Unknown error"
ai_output_text.set_text(('error', f"✗ Error from {provider_name}:\n\n{error_msg}")) ai_output_text.set_text(('error', f"✗ Error from {provider_name}:\n\n{error_msg}"))
auto_adjust_btn = urwid.AttrMap( auto_adjust_btn = Button3DCompact("Auto-adjust", on_auto_adjust)
urwid.Button("Auto-adjust", on_auto_adjust),
'button', 'button_focus'
)
# Build the AI assist box with provider selector, prompt editor, output area, and button # Build the AI assist box with provider selector, prompt editor, output area, and button
ai_provider_row = urwid.Columns([ ai_provider_row = urwid.Columns([
@ -1644,7 +1787,7 @@ Return ONLY the Python code, no explanations or markdown fencing."""
def on_cancel(_): def on_cancel(_):
self.close_overlay() self.close_overlay()
load_btn = urwid.AttrMap(urwid.Button("Load", on_load), 'button', 'button_focus') load_btn = Button3DCompact("Load", on_load)
# Code editor in a box - use ListBox for proper focus handling and scrolling # Code editor in a box - use ListBox for proper focus handling and scrolling
# Wrap in DOSScrollBar for DOS-style scrollbar with arrow buttons # Wrap in DOSScrollBar for DOS-style scrollbar with arrow buttons