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
# Color palette - BIOS style
# Color palette - BIOS style with 3D button effects
PALETTE = [
('body', 'white', 'dark blue'),
('header', 'white', 'dark red', 'bold'),
('footer', 'black', 'light gray'),
# Button colors - raised 3D effect
('button', 'black', 'light gray'),
('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_focus', 'black', 'yellow'),
# List items
('listbox', 'black', 'light gray'),
('listbox_focus', 'white', 'dark red'),
# Dialog
('dialog', 'black', 'light gray'),
('dialog_border', 'white', 'dark blue'),
# Text styles
('label', 'yellow', 'dark blue', 'bold'),
('error', 'white', 'dark red', '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
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):
"""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):
self.on_press = on_press
@ -466,16 +618,16 @@ class ToolBuilderLayout(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):
# Title
title_widget = urwid.Text(('header', f' {title} '), align='center')
# Buttons row
# Buttons row - use 3D compact buttons for dialog actions
button_widgets = []
for label, callback in buttons:
btn = ClickableButton(label, callback)
btn = Button3DCompact(label, callback)
button_widgets.append(btn)
buttons_row = urwid.Columns([('pack', b) for b in button_widgets], dividechars=2)
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_box = urwid.LineBox(tool_listbox, title='Tools')
# Action buttons - Tab navigates here from tool list
create_btn = ClickableButton("Create", lambda _: self._create_tool_before_selected())
edit_btn = ClickableButton("Edit", lambda _: self._edit_selected_tool())
delete_btn = ClickableButton("Delete", lambda _: self._delete_selected_tool())
test_btn = ClickableButton("Test", lambda _: self._test_selected_tool())
providers_btn = ClickableButton("Providers", lambda _: self.manage_providers())
# Action buttons - Tab navigates here from tool list (3D style)
create_btn = Button3DCompact("Create", lambda _: self._create_tool_before_selected())
edit_btn = Button3DCompact("Edit", lambda _: self._edit_selected_tool())
delete_btn = Button3DCompact("Delete", lambda _: self._delete_selected_tool())
test_btn = Button3DCompact("Test", lambda _: self._test_selected_tool())
providers_btn = Button3DCompact("Providers", lambda _: self.manage_providers())
buttons_row = urwid.Columns([
('pack', create_btn),
('pack', urwid.Text(" ")),
('pack', urwid.Text(" ")),
('pack', edit_btn),
('pack', urwid.Text(" ")),
('pack', urwid.Text(" ")),
('pack', delete_btn),
('pack', urwid.Text(" ")),
('pack', urwid.Text(" ")),
('pack', test_btn),
('pack', urwid.Text(" ")),
('pack', urwid.Text(" ")),
('pack', providers_btn),
])
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_box = urwid.LineBox(info_filler, title='Tool Info')
# Exit button at bottom
exit_btn = ClickableButton("EXIT", lambda _: self.exit_app())
exit_centered = urwid.Padding(exit_btn, align='center', width=10)
# Exit button at bottom (3D style)
exit_btn = Button3DCompact("EXIT", lambda _: self.exit_app())
exit_centered = urwid.Padding(exit_btn, align='center', width=12)
# Use a custom Pile that handles Tab to cycle between tool list and buttons
self._main_pile = TabCyclePile([
@ -1267,10 +1419,7 @@ class SmartToolsUI:
popup = Dialog("Select Provider", body, [])
self.show_overlay(popup, width=50, height=min(len(provider_names) + 6, 16))
provider_select_btn = urwid.AttrMap(
urwid.Button("Select", on_press=show_provider_dropdown),
'button', 'button_focus'
)
provider_select_btn = Button3DCompact("", on_press=show_provider_dropdown)
# File input for external prompt
default_file = existing.prompt_file if existing and existing.prompt_file else "prompt.txt"
@ -1357,7 +1506,7 @@ class SmartToolsUI:
def on_cancel(_):
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
# Wrap in DOSScrollBar for DOS-style scrollbar with arrow buttons
@ -1481,10 +1630,7 @@ class SmartToolsUI:
popup = Dialog("Select Provider", popup_body, [])
self.show_overlay(popup, width=50, height=min(len(provider_names) + 6, 16))
ai_provider_select_btn = urwid.AttrMap(
urwid.Button("", on_press=show_ai_provider_dropdown),
'button', 'button_focus'
)
ai_provider_select_btn = Button3DCompact("", on_press=show_ai_provider_dropdown)
# Default prompt template for AI code generation/adjustment
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"
ai_output_text.set_text(('error', f"✗ Error from {provider_name}:\n\n{error_msg}"))
auto_adjust_btn = urwid.AttrMap(
urwid.Button("Auto-adjust", on_auto_adjust),
'button', 'button_focus'
)
auto_adjust_btn = Button3DCompact("Auto-adjust", on_auto_adjust)
# Build the AI assist box with provider selector, prompt editor, output area, and button
ai_provider_row = urwid.Columns([
@ -1644,7 +1787,7 @@ Return ONLY the Python code, no explanations or markdown fencing."""
def on_cancel(_):
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
# Wrap in DOSScrollBar for DOS-style scrollbar with arrow buttons