Skip to content

Latest commit

 

History

History
608 lines (483 loc) · 17 KB

File metadata and controls

608 lines (483 loc) · 17 KB

Layout

Layout primitives help you arrange UI elements in structured ways: horizontally, vertically, in boxes, or in tables.

1 Horizontal Layout - vui-hstack

Arranges children horizontally with optional spacing:

(vui-hstack
 (vui-text "One")
 (vui-text "Two")
 (vui-text "Three"))

Output:

One Two Three

1.1 Spacing

Default spacing is 1 character. Customize with :spacing:

;; No spacing
(vui-hstack :spacing 0
            (vui-text "A")
            (vui-text "B")
            (vui-text "C"))
;; Output: ABC

;; Extra spacing
(vui-hstack :spacing 3
            (vui-text "One")
            (vui-text "Two"))
;; Output: One   Two

1.2 Practical Example: Button Bar

(vui-hstack :spacing 2
            (vui-button "Save" :on-click #'save-data)
            (vui-button "Cancel" :on-click #'cancel)
            (vui-button "Delete" :on-click #'delete-item
                        :face '(:foreground "red")))

Output:

[Save]  [Cancel]  [Delete]

2 Vertical Layout - vui-vstack

Arranges children vertically with optional spacing and indentation:

(vui-vstack
 (vui-text "Line 1")
 (vui-text "Line 2")
 (vui-text "Line 3"))

Output:

Line 1
Line 2
Line 3

2.1 Spacing (Blank Lines)

Add blank lines between items with :spacing:

(vui-vstack :spacing 1
            (vui-text "Paragraph 1")
            (vui-text "Paragraph 2")
            (vui-text "Paragraph 3"))

Output:

Paragraph 1

Paragraph 2

Paragraph 3

2.2 Indentation

Indent all children with :indent:

(vui-vstack :indent 4
            (vui-text "- Item 1")
            (vui-text "- Item 2")
            (vui-text "- Item 3"))

Output:

- Item 1
- Item 2
- Item 3

2.3 Combining Spacing and Indent

(vui-fragment
 (vui-text "Tasks:")
 (vui-newline)
 (vui-vstack :spacing 1 :indent 2
             (vui-text "• Write documentation")
             (vui-text "• Add tests")
             (vui-text "• Deploy")))

Output:

Tasks:
  • Write documentation

  • Add tests

  • Deploy

3 Fixed-Width Box - vui-box

Creates a fixed-width container with text alignment:

(vui-box (vui-text "Centered")
         :width 20
         :align :center)

Output (20 chars wide):

Centered

3.1 Alignment Options

ValueDescription
:leftAlign to left (default)
:centerCenter in box
:rightAlign to right
(vui-vstack
 (vui-box (vui-text "Left") :width 20 :align :left)
 (vui-box (vui-text "Center") :width 20 :align :center)
 (vui-box (vui-text "Right") :width 20 :align :right))

Output:

Left
        Center
               Right

3.2 Padding

Add padding inside the box:

(vui-box (vui-text "Padded")
         :width 20
         :align :center
         :padding-left 2
         :padding-right 2)

The padding reduces the available content width.

3.3 Practical Example: Aligned Form

(vui-vstack
 (vui-hstack
  (vui-box (vui-text "Name:") :width 10 :align :right)
  (vui-field :value name :size 20))
 (vui-hstack
  (vui-box (vui-text "Email:") :width 10 :align :right)
  (vui-field :value email :size 20))
 (vui-hstack
  (vui-box (vui-text "Phone:") :width 10 :align :right)
  (vui-field :value phone :size 20)))

Output:

 Name: [                    ]
Email: [                    ]
Phone: [                    ]

4 Tables - vui-table

Creates formatted tables with columns, rows, and optional borders.

4.1 Basic Table

(vui-table
 :columns '((:min-width 8) (:min-width 10) (:min-width 8))
 :rows '(("Alice" "Developer" "NYC")
         ("Bob" "Designer" "LA")
         ("Carol" "Manager" "Chicago")))

Output:

Alice    Developer   NYC
Bob      Designer    LA
Carol    Manager     Chicago

4.2 With Headers and Borders

(vui-table
 :columns '((:header "Name" :width 8)
            (:header "Role" :width 10)
            (:header "Location" :width 10))
 :rows '(("Alice" "Developer" "NYC")
         ("Bob" "Designer" "LA"))
 :border :ascii)

Output:

+----------+------------+------------+
| Name     | Role       | Location   |
+----------+------------+------------+
| Alice    | Developer  | NYC        |
| Bob      | Designer   | LA         |
+----------+------------+------------+

Bordered tables automatically add padding around cell content (| value | instead of |value|) for readability.

4.3 Unicode Borders

(vui-table
 :columns '((:header "Name" :width 8)
            (:header "Role" :width 10))
 :rows '(("Alice" "Developer")
         ("Bob" "Designer"))
 :border :unicode)

Output:

┌──────────┬────────────┐
│ Name     │ Role       │
├──────────┼────────────┤
│ Alice    │ Developer  │
│ Bob      │ Designer   │
└──────────┴────────────┘

4.4 Column Alignment

(vui-table
 :columns '((:header "ID" :width 5 :align :right)
            (:header "Product" :width 12 :align :left)
            (:header "Price" :width 8 :align :right))
 :rows '(("1" "Widget" "$9.99")
         ("2" "Gadget" "$19.99")
         ("3" "Gizmo" "$29.99"))
 :border :ascii)

Output:

+-------+--------------+----------+
|    ID | Product      |    Price |
+-------+--------------+----------+
|     1 | Widget       |    $9.99 |
|     2 | Gadget       |   $19.99 |
|     3 | Gizmo        |   $29.99 |
+-------+--------------+----------+

4.5 Column Properties

PropertyDescription
:headerColumn header text
:widthTarget width for cell content
:min-widthMinimum width, expands as needed
:growIf t, pad short content and expand for long
:truncateIf t, truncate long content with “…”
:align:left, :center, or :right

4.5.1 Width Behavior

The :width, :grow, and :truncate options interact to control how columns handle content that doesn’t match the declared width:

:width:grow:truncateContent vs WidthResult
Wnilnilcontent < WColumn shrinks to content size
Wnilnilcontent > WOverflow with broken bar (¦)
Wtnilcontent < WColumn = W, content padded
Wtnilcontent > WColumn expands to fit content
Wniltcontent < WColumn shrinks to content size
Wniltcontent > WColumn = W, content truncated with “…”
Wttcontent < WColumn = W, content padded
Wttcontent > WColumn = W, content truncated with “…”
nil--anyColumn auto-sizes to content

Notes:

  • :grow t acts as a minimum width guarantee
  • :truncate t acts as a maximum width guarantee
  • Without :grow, short content shrinks the column
  • Without :truncate, long content either overflows (¦) or expands the column

4.6 Interactive Cells

Table cells can contain any vnode, including buttons, fields, and components:

(vui-table
 :columns '((:header "Item" :width 12)
            (:header "Qty" :width 6)
            (:header "Action" :width 10))
 :rows `(("Apples" "5" ,(vui-button "[Edit]" :on-click edit-apples))
         ("Oranges" "3" ,(vui-button "[Edit]" :on-click edit-oranges)))
 :border :ascii)

For interactive tables with complex state, embed components in cells:

(vui-table
 :columns '((:header "Name" :width 15)
            (:header "Score" :width 8))
 :rows (mapcar (lambda (item)
                 (list (plist-get item :name)
                       (vui-component 'editable-score
                                      :key (plist-get item :id)
                                      :value (plist-get item :score)
                                      :on-change handle-change)))
               items)
 :border :ascii)

4.7 Dynamic Column Widths

Calculate column widths dynamically based on content:

(let* ((max-name-len (apply #'max (mapcar #'length names)))
       (col-width (max 18 (min 48 (+ max-name-len 2)))))  ; min 18, max 48
  (vui-table
   :columns `((:header "Name" :width ,col-width)
              (:header "Value" :width 10))
   :rows ...))

See docs/examples/05-wine-tasting.el for a complete example with:

  • Interactive buttons in table cells
  • Dynamic column width calculation
  • Real-time statistics updates

5 Dynamic Lists - vui-list

Renders a list of items using a render function. Returns a vstack for vertical lists (default) or hstack for horizontal lists, ensuring proper indent propagation.

(vui-list '("Apple" "Banana" "Cherry")
          (lambda (item)
            (vui-text (format "%s" item))))

Output:

• Apple
• Banana
• Cherry

5.1 With Key Function

For efficient updates, provide a key function:

(vui-list todos
          (lambda (todo)
            (vui-component 'todo-item :todo todo))
          (lambda (todo)
            (plist-get todo :id)))  ; Key function

The key function returns a unique identifier for each item.

5.2 With Indentation

Lists properly inherit indent from parent containers, or you can set it directly:

;; Inherits indent from parent vstack
(vui-vstack :indent 2
            (vui-text "Items:")
            (vui-list items #'vui-text))

;; Or set indent directly
(vui-list items #'vui-text nil :indent 2)

5.3 Complex List Items

(vui-defcomponent product-list ()
  :state ((products '((:id 1 :name "Widget" :price 9.99)
                      (:id 2 :name "Gadget" :price 19.99))))
  :render
  (vui-vstack :spacing 1
              (vui-list products
                        (lambda (product)
                          (vui-hstack
                           (vui-text (plist-get product :name))
                           (vui-text (format " - $%.2f"
                                             (plist-get product :price)))))
                        (lambda (product)
                          (plist-get product :id)))))

6 Flexible Width - vui-flex

Sometimes a child should fill whatever space is left rather than have a fixed width - a field stretching between a label and a button, a footer with content pushed to both edges. vui-flex renders a row with a known total width and distributes the leftover:

(vui-flex :width 'fill-column
  (vui-text "Name:")
  (vui-flex-item :grow 1
    (lambda (width) (vui-field :size width :key 'name)))
  (vui-button "Save"))

Children render at their natural width. A child wrapped in vui-flex-item takes a share of the leftover, proportional to its :grow weight. When the wrapped child is a function, it receives its allotted width - that is what lets a field’s :size be computed instead of guessed. A plain vnode child is padded inside a box instead, which is enough for text and backgrounds:

;; Two growers splitting leftover 1:2
(vui-flex :width 40
  (vui-flex-item :grow 1 (vui-text "left"))
  (vui-flex-item :grow 2 (vui-text "right")))

:width accepts a number, fill-column (the default), window, or a function resolved at render time. Inside an indented vui-vstack, the inherited indent is subtracted automatically - no more (- fill-column 20) arithmetic.

When nothing grows, :justify places the leftover:

(vui-flex :width 'fill-column :justify :space-between
  (vui-text "v1.2.0")
  (vui-text "47 items"))
;; v1.2.0                                 47 items

Rows are single-line by design. If the children’s natural widths already exceed the total, everything renders at natural width and growers get zero - the row degrades instead of erroring.

For live resizing, use window or a function as :width and enable vui-rerender-on-resize in the buffer:

(vui-flex :width (lambda () (- (window-width) 2)) ...)

(with-current-buffer "*my-app*"
  (vui-rerender-on-resize))

7 Nesting Layouts

Layouts can be nested for complex structures:

(vui-vstack :spacing 1
            ;; Header row
            (vui-hstack :spacing 2
                        (vui-text "Dashboard" :face 'bold)
                        (vui-button "Refresh"))

            ;; Two-column content
            (vui-hstack :spacing 4
                        ;; Left column
                        (vui-vstack
                         (vui-text "Statistics")
                         (vui-text "Users: 100")
                         (vui-text "Sales: 50"))

                        ;; Right column
                        (vui-vstack
                         (vui-text "Actions")
                         (vui-button "Add User")
                         (vui-button "View Report"))))

8 Styling Containers - :face and :keymap

Stacks, boxes, and lists accept :face and :keymap options that apply to their whole rendered extent - including the whitespace the container itself inserts (separators, padding, indentation), which cannot be styled child-by-child.

As with vui-text, :face accepts either a face symbol or an anonymous face spec such as (:inherit highlight :weight bold) - see Primitives for the forms and gotchas.

The face is layered beneath the children’s own faces, so children keep their styling while everything else gets the container face. This makes selected-row highlighting one prop:

(vui-list items
          (lambda (item)
            (vui-hstack
             :face (when (equal item selected) 'highlight)
             (vui-text (item-name item))
             (vui-button "open" :on-click (lambda () (open item)))))
          #'item-id)

The keymap cascades innermost-first: a button’s own keymap wins for keys it defines (default buttons keep RET / TAB), unbound keys fall through to the container’s map, then to the buffer’s usual keymaps. Input fields keep their own keymap.

(vui-vstack :keymap my-section-map
            (vui-text "Section content")
            (vui-button "Action" :keymap my-button-map))
;; On the button: my-button-map > my-section-map > vui-mode-map
;; On the text:   my-section-map > vui-mode-map

For arbitrary content that is not a container, the same styling is available through the vui-region wrapper:

(vui-region :face 'highlight :keymap my-map
  (vui-fragment child1 child2))

9 Summary

PrimitivePurposeKey Props
vui-hstackHorizontal arrangement:spacing, :face, :keymap
vui-vstackVertical arrangement:spacing, :indent, :face, :keymap
vui-flexWidth distribution:width, :justify, vui-flex-item :grow
vui-boxFixed-width container:width, :align, :padding-left/right
vui-tableTabular data:columns, :rows, :border
vui-listDynamic list renderingitems, render-fn, key-fn, :indent, :spacing
vui-regionStyling wrapper:face, :keymap

10 Try It Yourself

Exercise: Create a dashboard component that displays:

  1. A title “My Dashboard” at the top
  2. A table of tasks with columns: Status, Task, Due Date
  3. A button bar at the bottom with “Add Task” and “Clear Done” buttons
(vui-defcomponent dashboard ()
  :state ((tasks '(("[ ]" "Write docs" "2024-01-15")
                   ("[X]" "Add tests" "2024-01-10")
                   ("[ ]" "Deploy" "2024-01-20"))))
  :render
  ;; Your implementation here
  )

11 What’s Next?