2 Containers
dxtr edited this page 2026-10-19 19:59:27 +02:00

Containers

Containers are the controls that place their children themselves, as Godot's do: rows and columns, grids, margins, centring, panels, scrolling, splits, flows and tabs. The controls they place are those of User Interface.

  • Containers place their children themselves, ignoring their anchors, and need what their children need. Each child fills the space a container gives it, or takes its minimum size at the start, centre or end of it, as its sizing says: control-size-horizontal and -vertical, :fill or :shrink-start, :shrink-center or :shrink-end. The containers, Godot's:

    • hbox-container and vbox-container lay their children out in a row or a column, the theme's :separation apart. Children that expand, control-expand-horizontal in a row and control-expand-vertical in a column, share the space the others leave by their control-stretch-ratio; when none does, box-container-alignment puts them at the :start, :center or :end.
    • grid-container lays its children out in rows of grid-container-columns, each column as wide as its widest child and each row as tall as its tallest, :h-separation and :v-separation apart. Columns and rows with an expanding child share the space left over.
    • margin-container keeps its children inside the margins its constants :margin-left, :margin-top, :margin-right and :margin-bottom say; center-container centres them at their minimum size; panel-container draws its style box :panel behind them and keeps them inside its content margins.
    • scroll-container shows part of larger content, below.
    • hsplit-container and vsplit-container place their two children side by side or one over the other, with a bar between that the pointer drags: split-container-offset moves it from where the children's sizing puts it, never past a child's minimum size, and it emits drag-started, dragged and drag-ended.
    • hflow-container and vflow-container lay their children out in lines that wrap where the next would not fit, as words in a paragraph, aligned by flow-container-alignment.
    • tab-container shows one of its children, its pages, under a tab-bar with a tab for each, titled by the page's name; tab-container-current-tab chooses the page, as does a click on a tab, and it emits tab-changed.

    Boxes and grids share space out in whole units, rounding minimum sizes up, so that their children's edges fall on whole units, sharp at a whole scale. A container class of your own defines place-children, which places each child with fit-child, which sets the child's rotation back to 0 and its scale to 1 by 1, as Godot's fit_child_in_rect does, and control-content-minimum-size, which adds up its children's, going through them with do-child-controls.

  • Scrolling: a scroll-container gives its content at least its minimum size and at least its view, and shows it scrolled by scroll-container-scroll-x and -scroll-y, which layout keeps within the content, cut off inside its style box's content margins. Its scroll bars show where the view is, each when scroll-container-horizontal-mode or -vertical-mode says: :auto, when the content overflows; :show-always; :show-never, though it scrolls; :reserve, as :auto but keeping the bar's room; or :disabled, not scrolling that way at all, which makes the content's size there its minimum. The wheel scrolls it, an eighth of the view a step, across with Shift; one at its end leaves the wheel to the scroll container around it. Its bars drag, and a press on a track pages. ensure-control-visible scrolls to a control inside, and with scroll-container-follow-focus it scrolls to one that gains the focus.

Containers lay controls out for you. Here a list scrolls down the left of the window and a grid of tiles fills the rest, inside a margin:

(defpackage #:layout
  (:use #:cl)
  (:local-nicknames (#:sc #:pyssel/scene) (#:s2d #:pyssel/scene-2d) (#:ui #:pyssel/ui)))
(in-package #:layout)

(defun rounded (color)
  (make-instance 'ui:flat-style-box :background-color color :corner-radius 4))

(defun tile (color &rest initargs)
  (let ((tile (apply #'make-instance 'ui:panel initargs)))
    (setf (ui:control-style-box tile :panel) (rounded color))
    tile))

(defun add (parent &rest children)
  (dolist (child children parent)
    (sc:add-child parent child)))

(defun make-layout ()
  (let ((list (make-instance 'ui:vbox-container :name "list"))
        (grid (make-instance 'ui:grid-container :name "grid" :columns 3 :expand-horizontal t)))
    (dotimes (index 30)
      (add list (tile '(0.3 0.4 0.6) :custom-minimum-height 28)))
    (dotimes (index 9)
      (add grid (tile '(0.6 0.45 0.3) :custom-minimum-height 60 :expand-horizontal t)))
    (add (make-instance 'ui:margin-container
                        :name "layout" :preset :full-rect
                        :constant-overrides '(:margin-left 12 :margin-top 12
                                              :margin-right 12 :margin-bottom 12))
         (add (make-instance 'ui:hbox-container :name "columns")
              (add (make-instance 'ui:scroll-container :name "scroll" :horizontal-mode :disabled
                                                       :custom-minimum-width 160)
                   list)
              grid))))

(defvar *tree* (s2d:run (make-layout) :title "Layout" :width 640 :height 400))

The list's rows are more than the window holds, so its scroll bar shows. Resize the window: the list keeps its width and the grid's columns share the rest. The parts are nodes with names, found by their paths, and *tree* the REPL's way in. Scroll the list from the REPL, by an offset or to a row:

(defun part (tree path)
  (sc:find-node (sc:tree-root tree) path))

(sc:call-in-tree *tree*
                 (lambda () (setf (ui:scroll-container-scroll-y (part *tree* "layout/columns/scroll"))
                                  300)))

(sc:call-in-tree *tree*
                 (lambda ()
                   (ui:ensure-control-visible (part *tree* "layout/columns/scroll")
                                              (sc:node-child (part *tree* "layout/columns/scroll/list")
                                                             25))))

An editor's docks are made of these. Here a tab container, its first page a tree of a scene's nodes, and a flow of tiles sit either side of a bar to drag, in a window of their own, which *docks* reaches:

(defun make-docks ()
  (let ((flow (make-instance 'ui:hflow-container :name "flow" :expand-horizontal t))
        (nodes (make-instance 'ui:tree :name "Scene" :hide-root t)))
    (dotimes (index 12)
      (add flow (tile '(0.45 0.6 0.3) :custom-minimum-width 80 :custom-minimum-height 60)))
    (let ((root (ui:create-tree-item nodes)))
      (dolist (name '("Player" "Camera" "Light"))
        (let ((item (ui:create-tree-item nodes root)))
          (setf (ui:tree-item-text item 0) name
                (ui:tree-item-text (ui:create-tree-item nodes item) 0) "Sprite"))))
    (sc:connect nodes 'ui:item-activated nil
                (lambda (item column) (format t "Open ~A~%" (ui:tree-item-text item column))))
    (add (make-instance 'ui:hsplit-container :name "docks" :preset :full-rect)
         (add (make-instance 'ui:tab-container :custom-minimum-width 180
                                               :drag-to-rearrange-enabled t)
              nodes
              (make-instance 'ui:label :name "Import" :text "How to import"))
         flow)))

(defvar *docks* (s2d:run (make-docks) :title "Docks" :width 640 :height 400))

Drag the bar between them: the tiles reflow into the room left. Click the tabs to switch pages, and drag one to move it. In the tree, the arrows fold the nodes' children away, a click selects a node, and a double click, or Return, opens it, as the function connected to item-activated says. A tab-bar on its own shows tabs without pages, from :tabs, a list of tab objects, which make-tab makes, of a title, a tooltip, and whether it is disabled or hidden, with close buttons if tab-bar-close-button-policy asks for them; tab-bar-tab gives the one at an index.