ui(console): center menu in a bordered box with better spacing

Both renderers now draw the menu inside a centered box with:
- Rounded corners (modern) / box-drawing (VT220)
- Title centered inside the box top
- Section separators as horizontal lines within the box
- Selected item highlighted across the full box width
- Vertical and horizontal centering on screen

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-15 02:31:54 +00:00
parent a3aa2a7608
commit 7866194e65
2 changed files with 103 additions and 29 deletions

View File

@@ -258,46 +258,94 @@ class TextualRenderer(BaseRenderer):
def draw_menu(self, items, selected_index=0, title=''):
h, w = self.get_size()
visible_lines = h - 6
# Calculate menu box dimensions
max_label = 0
for num, label in items:
if num != "---" and num != "\u2500":
max_label = max(max_label, len(f" {num}. {label} "))
box_inner = max(max_label + 4, 40)
box_inner = min(box_inner, w - 8)
# Vertical centering: blank lines before menu
item_count = len(items)
menu_height = item_count + (3 if title else 0)
top_pad = max((h - menu_height - 6) // 2 - 1, 0)
for _ in range(top_pad):
self._add_line(Text(""))
# Horizontal padding
pad_left = max((w - box_inner - 4) // 2, 2)
pad_str = " " * pad_left
# Box top border
self._add_line(Text(
f"{pad_str}\u256d{'' * (box_inner + 2)}\u256e",
style="blue",
))
if title:
self._add_line(Text(f" {title}", style="bold white"))
self._add_line(Text(""))
visible_lines -= 2
# Title centered inside box
title_pad = max((box_inner - len(title)) // 2, 0)
line = Text()
line.append(f"{pad_str}\u2502 ", style="blue")
line.append(" " * title_pad, style="")
line.append(title, style="bold white")
line.append(
" " * (box_inner - title_pad - len(title)),
style="",
)
line.append(" \u2502", style="blue")
self._add_line(line)
# Separator inside box
self._add_line(Text(
f"{pad_str}\u251c{'' * (box_inner + 2)}\u2524",
style="blue",
))
if visible_lines < 1:
return
visible = h - self._line_count - 4
if visible < 1:
visible = item_count
offset = 0
if selected_index >= visible_lines:
offset = selected_index - visible_lines + 1
if selected_index >= visible:
offset = selected_index - visible + 1
drawn = 0
for idx, (num, label) in enumerate(items):
if drawn >= visible_lines:
if drawn >= visible:
break
if idx < offset:
continue
if num == "\u2500" or num == "---":
self._add_line(
Text(" " + "" * (w - 4), style="dim blue")
)
self._add_line(Text(
f"{pad_str}\u251c{'' * (box_inner + 2)}\u2524",
style="dim blue",
))
drawn += 1
continue
marker = "\u25b8 " if idx == selected_index else " "
entry = f"{marker}{num}. {label}"
entry = pad_right(entry, box_inner)
line = Text()
line.append(f"{pad_str}\u2502 ", style="blue")
if idx == selected_index:
entry = pad_right(f"{marker}{num}. {label}", w - 4)
self._add_line(
Text(f" {entry}", style="bold white on rgb(30,60,120)")
)
line.append(f" {entry}", style="bold white on rgb(30,60,120)")
else:
self._add_line(
Text(f" {marker}{num}. {label}", style="white")
)
line.append(f" {entry}", style="white")
line.append(" \u2502", style="blue")
self._add_line(line)
drawn += 1
# Box bottom border
self._add_line(Text(
f"{pad_str}\u2570{'' * (box_inner + 2)}\u256f",
style="blue",
))
def draw_table(self, headers, rows, widths, page_info=None,
selected_row=-1):
h, w = self.get_size()