11 Widgets
dxtr edited this page 2026-10-20 10:09:08 +02:00

Widgets

pyssel/ui's widgets are Godot's, with the looks of Godot's default theme: ranges, labels, buttons and button groups, check boxes, colour rects, separators, sliders, progress bars, text fields, spin boxes, lists and trees. A settings form at the end puts several together.

  • Ranges hold a value between range-minimum and range-maximum, on steps of range-step, and at most the maximum less range-page, the part that shows at once; setting range-value keeps it so and emits value-changed, and range-ratio is where it is between them. range-allow-greater and range-allow-lesser let the value past the maximum or below the minimum, as Godot's allow_greater and allow_lesser. The numbers are double-floats, as Godot's are, taken from any real, so that a spin box edits a 2D node's position a million units out to the thousandth; their readers and writers are inline, and setting the value allocates nothing. value-changed gives the new value, a double-float, which Lisp makes anew each time, so a range emits it only while something is connected to it. The steps are the decimals they stand for, as in Godot: 250.5 in steps of 0.001 stays 250.5, although the double-float 0.001 is a little more than a thousandth, and a step given as a single-float, as Lisp reads 0.1, stands for the decimal it was written as. Scroll bars, hscroll-bar and vscroll-bar, are ranges, drawn as the theme's :scroll track with a :grabber on it; moving a scroll container's bar scrolls it.

  • Widgets, Godot's, with the looks of Godot's default theme:

    • label shows label-text, aligned across by label-horizontal-alignment (:start, :center, :end or :fill) and down by label-vertical-alignment; with label-autowrap its lines wrap at its width, and it is as tall as that makes it; with label-clip-text they are cut short there with an ellipsis, so it needs no width.
    • texture-rect shows an image, texture-rect-image, as Godot's TextureRect: stretched to fill it, or kept at its size or in shape, as texture-rect-stretch-mode says, and tinted with texture-rect-color. It needs its image's size in units, its pixels over its scale, unless texture-rect-expand-mode is :ignore-size, so an image made at the display's scale with that scale is as large as at a scale of 1, and sharp. A scene saves how it draws, but not its image.
    • viewport-container shows the root viewport of another tree, as Godot's SubViewportContainer shows its SubViewport, and as an editor shows the scene it edits: viewport-container-tree, a tree on the same engine made with :window nil (see Scene tree), whose root is a viewport, or another set with setf. The engine draws that root viewport into a render target of the container's size, at its window's scale, in the frames viewport-container-update-mode allows, as Godot's render_target_update_mode: :when-visible, each frame in which the container is shown, by default, so a hidden one costs nothing; :always; :once, after which it is :disabled; or :disabled, so the container shows the picture drawn last. In those frames it draws the tree only when what the tree draws changed, as pyssel/scene-2d:prepare-viewport finds, so a still scene keeps its picture however often the container's window is drawn. It draws it over viewport-container-clear-color, transparent by default, and the container draws the target, a pixel of it to each of the window's. The root keeps its own size, which its controls are laid out in; its canvas transform zooms and pans its own canvas in the container, and its global canvas transform all it draws, and target-to-canvas takes the container's units to the root's canvas. A tree is shown by one container at a time. The container hands the tree no input: its pointer's events are its own, as any control's.
    • button shows button-text, and button-icon, an image, before it, its text cut short at its width with button-clip-text, as Godot's clip_text, so that it needs none of the text's width, and emits pressed as it is clicked, or pressed with Return or Space while it has the focus; it acts as it is released over it, unless button-action-mode is :press. An icon alone makes an icon button, whose control-tooltip-text says what it does; the theme tints it as the button is hovered, pressed or disabled, and a scene does not save it, as images are made as the program runs. With :toggle-mode t it stays pressed until pressed again, button-pressed, and emits toggled. Its button-shortcut, an action or a binding such as (:symbol :s :control), acts on it from the keyboard while it shows, and its tooltip shows the shortcut's text; with a button-shortcut-context, a node, as Godot's shortcut_context, only while the keyboard focus is in that node, as a toolbar's shortcuts act only while its part of the window has the focus. button-disabled turns it off; button-draw-mode says how it looks now, for a button class of your own on base-button.
    • A button-group, a resource that toggle buttons hold as their button-group, as Godot's ButtonGroup, presses them one at a time: pressing one turns the others off, and pressing it again leaves it on, unless button-group-allow-unpress is true. It emits pressed with the button pressed on, a signal of the group; button-group-buttons lists the buttons in a tree that belong to it, and button-group-pressed-button gives the one pressed. A scene saves the group once, which its buttons share.
    • check-box is a toggle button with a box before its text, checked while it is on; in a button group it is a radio button, a circle with a dot.
    • color-rect fills its rectangle with color-rect-color, as Godot's ColorRect: a background, a veil over a paused game, or a bar a game stretches. It stops the pointer, as a control does, unless its pointer filter is :ignore.
    • hseparator and vseparator draw a line across or down between controls, as Godot's separators: their theme's :separator style box, a line-style-box, in their middle, and :separation units across their line.
    • hslider and vslider are ranges set by dragging a grabber, or by the wheel and the arrow keys.
    • progress-bar is a range shown filled as far as its value, from the side progress-bar-fill-mode says, with its percentage, in whole percent rounded down. The percentage is counted in its steps, a hundredth by default, so that 0.29 of 1 shows 29%, where Godot 4.5 shows 28%, as the double-float nearest 0.29 is a little less.
    • line-edit is a line of text to type into, line-edit-text, with a caret and a selection. The arrow keys, Home and End move the caret, by words with Control, and Shift selects; a click puts the caret, a drag selects, and a double or triple click selects a word or the line. Control and X, C and V cut, copy and paste, on the clipboard clipboard-text reads: the desktop's, or, for a user interface that draws into no window, one its session shares. Control and Z undoes, and Control and Y redoes. What an input method composes, as for Chinese or Japanese, shows at the caret, underlined. It emits text-changed as the user changes the text, text-submitted at Return, and text-change-rejected when line-edit-max-length leaves text out. line-edit-placeholder shows while it is empty, and line-edit-secret shows a password as bullets. With line-edit-clear-button-enabled, a button at its right, while it has text, clears it, as Godot's clear button, an edit Control and Z undoes; line-edit-right-icon draws an image there, as a search field's magnifier.
    • text-edit is text of several lines to type into, text-edit-text, as Godot's TextEdit: a place in it is a line and a column, text-edit-caret-line and text-edit-caret-column. Its lines wrap at its width with text-edit-wrap-mode :boundary, or scroll across with :none, the default, and scroll bars show where its text does not fit. Its keys are Godot's: Up and Down move by rows, Home and End to a row's ends and then the line's, Page Up and Page Down a page, Return breaks the line, and Tab types a tab while text-edit-tab-input-mode, which Control and M switch. Copying or cutting with nothing selected copies or cuts the caret's line, which pasting then puts in above the caret's line. As the user changes its text it emits lines-edited, with the first line the change touched, the last line it replaced and the last line it put there, as Godot's lines_edited_from tells, then text-changed, with nothing, as Godot's text_changed: text-edit-text gives the text. text-edit-version numbers the text's versions, as Godot's get_version: each change makes a new one, undoing it takes it back, and text-edit-tag-saved-version marks one saved, so a text whose version is text-edit-saved-version is as it was saved, as an editor shows a file unchanged once undone back to it. text-edit-set-lines sets a text from a sequence of lines, without making them one string, as a file read into lines is shown. A text of any length types as quickly as a short one: its lines are its text, and the string text-edit-text gives is made from them when asked for; a line is laid out when it first shows, or the caret needs its rows, so a text of 100,000 lines lays out a screenful as it is set; and a long text keeps the layouts of the lines that show and of the 2,048 it laid out last, and the heights of all. text-edit-search finds text from a place, line by line, and text-edit-search-text, with text-edit-search-match-case and text-edit-search-whole-words, marks the occurrences in the lines that show, as a find bar does; text-edit-center-caret scrolls the caret's row to the middle, as going to a line does. text-edit-highlight-current-line shows the caret's line behind its text, and without text-edit-deselect-on-focus-loss-enabled the selection stays as the keyboard goes elsewhere. It needs only its style box's margins, so give it a custom minimum size or room in a container: text-edit-line-height times the rows to show. A text edit for code, with colours, gutters, folding, completion and a find bar, is a code edit.
    • spin-box is a range shown as a field with two buttons: the buttons step its value, again and again while held, and dragging from them moves it; the number typed, or a sum such as 2*(3+4), sets it at Return or as the field loses the focus. The number shows with as many decimals as the step has, less the zeros at the end but one, as Godot's: 2.5 and 3.0 in steps of 0.25. spin-box-prefix and spin-box-suffix show around the number.
    • item-list shows items of text to choose from, in a column or, with item-list-max-columns, a grid: a click selects one, or several as item-list-select-mode allows, emitting item-selected or multi-selected, and a double click or Return emits item-activated. The arrow keys move through them, typing goes to the item that starts so, and the wheel scrolls them. Its items are list-item objects, which make-list-item makes, of a text, an icon, an image drawn before the text, tinted with its list-item-icon-modulate, a tooltip, whether it is disabled or selectable, a colour of its own for its text, list-item-custom-fg-color, a small list-item-tag-icon drawn at the icon's corner, and metadata, any value; item-list-item gives the one at an index, list-item-at the index under a point, and list-item-rectangle where an item is. With item-list-icon-mode :top, icons go above their text, as thumbnails do, each fitted to item-list-fixed-icon-width by item-list-fixed-icon-height, the text wrapped on item-list-max-text-lines lines at most, the rest left out with an ellipsis; with :left, the default, text too wide for its item is cut short so.
    • tree shows tree-item objects in a hierarchy, each a row of cells in tree-columns columns, with titles if tree-column-titles-visible: create-tree-item makes them, the first the root, and setf of tree-item-text, tree-item-icon, an image drawn before the text, tree-item-cell-mode, :string or :check, tree-item-editable, tree-item-custom-color, a colour of the cell's own for its text, tree-item-custom-bg-color, one for its background, the first column's taking the row's indentation too, and the rest sets a cell. add-tree-item-button puts a button at the end of a cell, as Godot's TreeItem.add_button, which emits button-clicked with the item, the column and its id as it is clicked, and shows its tooltip; tree-item-buttons lists them. An item's children show under it, behind an arrow that folds them away. A click selects a cell, or as tree-select-mode says a row or several cells, emitting item-selected, cell-selected or multi-selected; a click on an editable cell checks its box, or edits its text in place, emitting item-edited, as edit-tree-selected does for the cell chosen, with :selection-start and :selection-end the part of the text selected, as a file's name without its extension, and with :force a cell that is not editable, as Godot's file system dock renames its files; and a double click or Return emits item-activated. The arrow keys move through the rows and fold them, typing goes to the row that starts so, and the wheel scrolls them. Text too wide for its cell is cut short with an ellipsis, and shows whole as the cell's tooltip.

    Their text is drawn in the theme's :font at :font-size, laid out once, and again only when it changes.

The widgets make a settings form:

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

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

(defun save-settings (form &optional text)
  "Print what FORM says: as its Save button is pressed, and with the TEXT
of its name field as Return is pressed there."
  (declare (ignore text))
  (format t "~&~S: volume ~D, music ~:[off~;on~].~%"
          (ui:line-edit-text (sc:find-node form "box/name"))
          (round (ui:range-value (sc:find-node form "box/volume-row/volume")))
          (ui:button-pressed (sc:find-node form "box/music"))))

(defun make-form ()
  (let ((form (make-instance 'ui:panel-container
                             :name "form" :preset :center
                             :style-box-overrides
                             (list :panel (make-instance 'ui:flat-style-box
                                                         :background-color '(0.15 0.15 0.2)
                                                         :corner-radius 6 :content-margin 16))))
        (name (make-instance 'ui:line-edit :name "name" :placeholder "Your name"))
        (save (make-instance 'ui:button :name "save" :text "Save")))
    (sc:connect save 'ui:pressed form 'save-settings)
    (sc:connect name 'ui:text-submitted form 'save-settings)
    (add form
         (add (make-instance 'ui:vbox-container :name "box" :custom-minimum-width 260)
              (make-instance 'ui:label :text "Settings" :horizontal-alignment :center)
              name
              (add (make-instance 'ui:hbox-container :name "volume-row")
                   (make-instance 'ui:label :text "Volume")
                   (make-instance 'ui:hslider :name "volume" :value 70 :expand-horizontal t
                                              :size-vertical :shrink-center))
              (make-instance 'ui:check-box :name "music" :text "Music" :pressed t)
              (make-instance 'ui:progress-bar :name "download")
              save))))

(defvar *tree* (s2d:run (make-form) :title "Settings" :width 480 :height 320))

Type your name, drag the slider, click the check box, or Tab to them and use the arrow keys and Space; Save, or Return in the name, prints the settings in your REPL. The form's state is in its controls: the connections give save-settings the form, a node, which finds its parts by their paths, and *tree* is the REPL's way in. Fill the progress bar from there:

(defun form (tree)
  (sc:find-node (sc:tree-root tree) "form"))

(sc:call-in-tree *tree*
                 (lambda () (setf (ui:range-value (sc:find-node (form *tree*) "box/download")) 60)))

Popups, Menus and Dialogs adds to the form.