AREA512
OPERATING SYSTEM
FOR CARDPUTER
RUBY / PYTHON

WIDGET API

← Manual

Themed UI components for Area512/Cardputer apps. One require gives every app the same theme palette, screen-size-aware layout, and font metrics — no more copying COL_* constants and pixel offsets between apps.

require 'area512-sprite'
require 'area512-widget'

All drawing goes onto the app's own full-screen Sprite (font size 12), passed as the first argument. Widgets never call push; the app keeps its usual draw everything, then push(0, 0) frame loop. Only the modal helpers (input, menu, ...) run their own key loop and push by themselves.

def draw
  @sp.fill(Widget.bg)
  Widget.header(@sp, "SCHEDULER", "JUL 2026")
  @list.draw(@sp)
  Widget.footer(@sp, @msg)
  @sp.push(0, 0)
end

def handle(key)
  return if @list.handle(key)
  case key
  when "a"
    title = Widget.input(@sp, "TITLE:")
    add_event(title) if title
  when "q"
    @quit = true
  end
end

# main loop:  draw; handle(Widget.read_key) while !@quit

Theme and metrics

Method Value Meaning
Widget.theme_background 0x000000 screen background
Widget.theme_text 0xCFA45F body text
Widget.theme_emphasis 0xF5972D headings, key chips, tags, chart values
Widget.theme_border 0xF5972D frames, rules, ticks
Widget.theme_selected 0xFFD966 selected or focused rows and controls
Widget.theme_box 0x241604 fill inside panels, popups, table headers
Widget.char_width 6 ASCII glyph width (font 12)
Widget.row_height 16 standard list/table row
Widget.header_height 17 header band height
Widget.body_top / body_bottom / body_height 20 / 119 / 100 content area between header and footer (values for 240x135)

The theme_* values are the defaults; /etc/theme on the card overrides them at boot, one key=0xRRGGBB per line:

background=0x000000
text=0xCFA45F
emphasis=0xF5972D
border=0xF5972D
selected=0xFFD966
box=0x241604

Widget.bg / amber / dim / gold / dark return the old palette as fixed values, so apps written before the theme existed keep their looks.

Text measurement uses the real renderer, so Japanese text measures correctly:

Widget.text_width(sp, text)      # => px
Widget.clip(sp, text, width_px)  # => String, ">"-terminated when cut

Screen chrome

Widget.header(sp, title, right = "")  # top band; highlight title, right text
Widget.footer(sp, message)            # bottom rule + message (clipped)
Widget.hints(sp, [["ENT", "open"], ["q", "quit"]])  # footer-position key chips
Widget.separator(sp, y)               # full-width horizontal rule
Widget.vseparator(sp, x, y, h)        # vertical rule
Widget.tabs(sp, y, ["CAL", "DAY"], active_index)
Widget.battery(sp, x, y)              # battery icon + charge level
Widget.splash(sp, title, subtitle = "")  # full-screen title in the 24px font

Containers and text

Widget.panel(sp, x, y, w, h)                # theme fill + accent frame
Widget.titled_panel(sp, x, y, w, h, title)
Widget.toast(sp, message)                   # bottom-center chip; app decides how long to show it
Widget.text_center(sp, y, text, color = Widget.theme_text)
Widget.text_right(sp, right_x, y, text, color = Widget.theme_text)
Widget.center_lines(sp, [["Cleared!", Widget.theme_emphasis], [""], ["Press any key."]])
Widget.wrap_text(sp, x, y, w, h, text, color = Widget.theme_text)  # => lines drawn
Widget.marquee(sp, x, y, w, text, offset)   # => text px width; advance offset per frame
Widget.big_text(sp, x, y, text, color = Widget.theme_emphasis)   # 24px font

Tables and records

Widget.cell(sp, x, y, w, h, text, selected)
Widget.table_header(sp, x, y, [60, 90, 60], ["ID", "NAME", "QTY"])
Widget.table_row(sp, x, y, [60, 90, 60], ["3", "Bolt M3", "12"], selected)
Widget.field(sp, x, y, w, label, value, focused)  # settings row: label, value

Indicators and charts

Widget.gauge(sp, x, y, w, value, max)             # read-only progress bar
Widget.slider(sp, x, y, w, value, max, focused)   # track + knob
Widget.scrollbar(sp, x, y, h, top, visible, total)    # hidden when total <= visible
Widget.hscrollbar(sp, x, y, w, left, visible, total)
Widget.badge(sp, x, y, text)                      # inverted chip
Widget.busy(sp, x, y, frame)                      # 4-phase busy marker; pass a frame counter
Widget.page_dots(sp, y, count, active)            # centered page indicator
Widget.bar_chart(sp, x, y, w, h, values, max)     # max <= 0 auto-scales to the data
Widget.line_chart(sp, x, y, w, h, values, max)

Form controls

Widget.button(sp, x, y, w, label, focused)
Widget.checkbox(sp, x, y, label, checked, focused)
Widget.radio(sp, x, y, label, selected, focused)
Widget.toggle(sp, x, y, on, focused)
Widget.spinner(sp, x, y, w, text, focused)   # "< text >"; the app handles left/right keys

Widgets do not manage focus. The app keeps a focus index and passes focused to each control; storage/home/tool/gallery shows the pattern.

Modals

Each runs its own key loop over the current sprite content and returns the result. The app simply redraws its next frame afterwards.

Widget.input(sp, label, initial = "")   # => String on Enter (may be ""), nil on ESC
Widget.input_number(sp, label, initial, min, max)  # => Integer clamped, nil on ESC
Widget.confirm(sp, question)            # => true (y) / false (n, ESC)
Widget.dialog(sp, message, ["OK", "Cancel"])  # => button index, nil on ESC
Widget.menu(sp, title, ["Open", "Save"], initial = 0)  # => item index, nil on ESC
Widget.alert(sp, message)               # any key; => nil

Key input

Widget.read_key  # => "UP" "DOWN" "LEFT" "RIGHT" "ENTER" "ESC" "BS" or a 1-char String

Escape sequences are decoded; keypad arrows that arrive as bare ; . / , are returned as-is so they stay typable — list widgets accept them in handle.

WidgetList

Scrollable, selectable rows with an optional tag column and multi-select marks.

list = WidgetList.new
list.area(x, y, w, h)          # default: whole body area
list.clear
list.add("Meeting", "07:30")   # tag is optional, drawn in the accent color
list.empty_text = "No schedule"
list.show_marks = true
list.toggle_mark               # cursor row
list.mark(i, true) / list.marked?(i)
list.count / list.index / list.index = n / list.top
list.handle(key)               # consumes UP/DOWN, k/j, ;/. — returns true when consumed
list.draw(sp)

Capacity: 128 rows, 39-byte text, 11-byte tag (longer input is truncated).

WidgetTextView

Word-wrapped scrollable text (help screens, long descriptions).

view = WidgetTextView.new
view.area(x, y, w, h)     # default: whole body area
view.text = long_string   # copied, up to 2048 bytes
view.scroll / view.scroll = n
view.handle(key)          # UP/DOWN, k/j, ;/.
view.draw(sp)

Demo

storage/home/tool/gallery shows every component across six tabs (PARTS / TABLE / CHART / LIST / TEXT / MODAL) and doubles as the on-device test. Type signatures for the ti checker live in components/area512/mrbgems/picoruby-ti/sig/widget.rbs, widget_list.rbs, and widget_text_view.rbs.