hnoss

Widgets

Every widget is a procedure taking the frame's ^Ui. Stateful ones take a pointer to state you own, which is also their identity for focus. Each has a runnable example: ./build.sh run w_<name>.

box

box :: proc(ui: ^Ui, area: Rect, title := "", opts := Box_Options{}) -> (inner: Rect)

A border with an optional markup title. Returns the area inside it, ready for the next widget.

box
Box_Options
border.Plain .Rounded .Double .Thick .None (default: theme.border_kind)
focuseddraw with theme.border_focused
aligntitle alignment
style · fillextra border style · blank the inside with this style first

label

label :: proc(ui: ^Ui, area: Rect, src: string, align := .Left, wrap := .Word) -> (lines: int)

One-off markup text in the theme's text style. For finer control use a pen.

paragraph

paragraph :: proc(ui: ^Ui, area: Rect, src: string, state: ^Scroll_State = nil, opts := Paragraph_Options{}) -> (total_lines: int)

Wrapped, scrolling markup text with an optional scrollbar. Clicking it gives it focus.

paragraph

Keys (focused): ↑ ↓ PgUp PgDn Home End · mouse wheel when hovered. Options: wrap, align, scrollbar.

scroll_view

scroll_view :: proc(ui: ^Ui, area: Rect, state: ^View_State, width, height: int) -> (content: Rect) scroll_view_end :: proc(ui: ^Ui, state: ^View_State)

A viewport onto content bigger than area: width × height cells, where 0 means as wide or as tall as the viewport. It returns where the content sits on screen, already shifted by the scroll offset; draw any widgets into it, then call scroll_view_end. Drawing is clipped to the viewport, and mouse events outside it don't reach the widgets inside, so a button scrolled out of sight can't be clicked.

Scrollbars appear on the right and bottom edges when the content overflows. state.x and state.y are the scroll offset; set them yourself to jump. Scroll views nest.

scroll_view

Keys (focused): ↑ ↓ ← → PgUp PgDn Home End · the wheel anywhere over it (Shift+wheel sideways) · a click on it focuses it unless a widget inside takes the click. Widgets inside that scroll themselves (a list) scroll with their keys, since the view takes the wheel.

content := hnoss.scroll_view(ui, inner, &view, 0, len(regions))
for name, i in regions {
    hnoss.checkbox(ui, {content.x, content.y + i, content.width, 1}, name, &picked[i])
}
hnoss.scroll_view_end(ui, &view)

list

list :: proc(ui: ^Ui, area: Rect, items: []string, state: ^List_State, opts := List_Options{}) -> (changed: bool)

A selectable, scrolling list. Returns true when the selection changed; state.activated is set for the frame Enter is pressed.

list

Keys (focused): ↑↓ or kj, PgUp PgDn Home End, Enter · click selects, wheel scrolls the view.

Field
List_Stateselected, offset (first visible), activated
List_Optionsmarker (default "› "), markup (items are markup), scrollbar

table

table :: proc(ui: ^Ui, area: Rect, columns: []Column, rows: [][]string, state: ^Table_State, opts := Table_Options{}) -> (changed: bool)

Rows of cells under a header. Columns are sized like a split; long cells end in …. Navigation and mouse work like list.

table
cols := []hnoss.Column{
    {title = "PID",  width = 7, align = .Right},
    {title = "Name", width = hnoss.fill()},
    {title = "CPU",  width = hnoss.percent(10), align = .Right},
}
hnoss.table(ui, inner, cols, rows, &state, {scrollbar = true})

tabs

tabs :: proc(ui: ^Ui, area: Rect, titles: []string, selected: ^int) -> (changed: bool)

A row of markup titles. The ^int is the state.

tabs

Keys (focused): ←→ or hl, Home End · click a title.

scrollbar

scrollbar :: proc(ui: ^Ui, area: Rect, total, visible: int, offset: ^int) -> (changed: bool)

A vertical bar for total lines of which visible fit. Click the track to jump, drag the thumb (dragging keeps working outside the bar), or use the wheel. paragraph, list and table can draw one for you. hscrollbar, with the same parameters, is the horizontal one along the bottom row of area.

scrollbar

gauge

gauge :: proc(ui: ^Ui, area: Rect, ratio: f64, label: Maybe(string) = nil)

A progress bar of any height, filled in eighths of a cell. The label (default: the percentage) is centered and drawn inverted where it covers the bar.

gauge

sparkline

sparkline :: proc(ui: ^Ui, area: Rect, data: []f64, max_value := 0.0, st: Maybe(Style) = nil)

The most recent values as bars, newest on the right, eight levels per row. max_value = 0 scales to the largest value shown.

sparkline

graph

graph :: proc(ui: ^Ui, area: Rect, data: []f64, opts := Graph_Options{})

An area chart in braille dots: each cell is 2 samples wide and 4 dots tall. Colors run along an optional gradient from bottom to top; downward grows from the top edge, for mirrored charts like the monitor's network panel.

hnoss.graph(ui, area, history[:], {
    max_value = 100,
    gradient  = []hnoss.Rgb{{80, 250, 123}, {241, 250, 140}, {255, 85, 85}},
})

input

input :: proc(ui: ^Ui, area: Rect, state: ^Input_State, opts := Input_Options{}) -> (changed: bool)

A single-line text field. Edits by grapheme, scrolls to keep the caret visible and shows the terminal cursor at it. Keys it uses are consumed, so typing q into it never quits your app.

input
Keys
← → Home End Ctrl+A/Emove
Ctrl+←/→ · Alt+B/Fmove by word
Backspace Delete Ctrl+Ddelete a character
Ctrl+W · Alt+Backspacedelete the previous word
Ctrl+U / Ctrl+Kdelete to start / to end
Entersets state.submitted for this frame
pasteinserted with newlines turned into spaces
search: hnoss.Input_State
defer hnoss.input_destroy(&search)
if hnoss.input(ui, field, &search, {placeholder = "filter…"}) {
    filter(hnoss.input_text(&search))
}

Options: placeholder, mask (e.g. "•" for passwords). Helpers: input_text, input_set, input_destroy.

text_area

text_area :: proc(ui: ^Ui, area: Rect, state: ^Input_State, opts := Text_Area_Options{}) -> (changed: bool)

A multi-line text field, word-wrapped to the width of area. It edits the same Input_State as input, so input_text, input_set and input_destroy work on it and every key of the single-line field does too; state.scroll is the first visible row. It scrolls to keep the caret visible and shows the terminal cursor there.

text_area
Keys (besides input's)
Enternew line
↑ ↓ PgUp PgDnmove by wrapped row, keeping the column
Home End Ctrl+A/Estart / end of the line
Ctrl+Home/Endstart / end of the text
Ctrl+U / Ctrl+Kdelete to the line's start / end; at the end, Ctrl+K joins the next line
pastekeeps its newlines; tabs become four spaces
note: hnoss.Input_State
defer hnoss.input_destroy(&note)
hnoss.text_area(ui, inner, &note, {placeholder = "Write something…", scrollbar = true})

Options: placeholder, scrollbar. Click places the cursor, the wheel scrolls.

checkbox · toggle · radio

checkbox :: proc(ui: ^Ui, area: Rect, label: string, checked: ^bool) -> (changed: bool) toggle :: proc(ui: ^Ui, area: Rect, label: string, on: ^bool) -> (changed: bool) radio :: proc(ui: ^Ui, area: Rect, options: []string, selected: ^int) -> (changed: bool)

The ^bool or ^int you pass is the state and the widget's identity for focus. Labels are markup. A focused choice is drawn like a selected list row; the mark uses theme.check.

checkbox, toggle and radio

Keys (focused): checkbox and toggle Space Enter · radio ↑↓ or kj, Home End · click to toggle or choose.

wifi := true
city := 0
hnoss.focus_order(ui, {&wifi, &city})
hnoss.checkbox(ui, hnoss.cut_top(&area, 1), "Wi-Fi", &wifi)
if hnoss.radio(ui, area, {"Bakı", "Gəncə", "Şəki"}, &city) {
    reload(city)
}

button

button :: proc(ui: ^Ui, area: Rect, label: string, state: ^Button_State) -> (pressed: bool)

Fills area and centers the markup label in it. Returns true on the frame it is pressed (also in state.pressed). A mouse press fires on release, only if the pointer is still over the button, so dragging away cancels; while held it uses theme.button_pressed.

button

Keys (focused): Enter Space · click.

save, cancel: hnoss.Button_State
hnoss.focus_order(ui, {&save, &cancel})
if hnoss.button(ui, cols[0], "Save", &save) { write_file() }
if hnoss.button(ui, cols[1], "Cancel", &cancel) { close() }

Theme: button, button_focused, button_pressed.

modal :: proc(ui: ^Ui, area: Rect, open: ^bool, title := "") -> (inner: Rect)

A dialog over everything drawn before it: the frame is dimmed, area is cleared and boxed, and the area inside is returned for the dialog's own widgets. Draw it last.

open is your flag for showing it. While it is true, the widgets under the dialog get no keys or mouse events, so a list behind it doesn't move when you press ↓. Esc or a click outside sets it to false; so can your own code, e.g. a Cancel button.

modal
if confirm {
    d := hnoss.modal(ui, hnoss.center(f.area, 36, 7), &confirm, "[b]Delete city[/]")
    hnoss.focus_order(ui, {&cancel, &ok})
    cols := hnoss.split(hnoss.cut_bottom(&d, 1), .Horizontal, {hnoss.fill(), hnoss.fill()}, gap = 2)
    if hnoss.button(ui, cols[0], "Cancel", &cancel) { confirm = false }
    if hnoss.button(ui, cols[1], "Delete", &ok)     { remove(); confirm = false }
}

Theme: modal for the inside; the border uses border_focused.