Drop-down, forms and popups
Drop-down
ManyUI.DD_ARROW_W — Constant
Cells the arrow and its separating space claim on the closed row.
ManyUI.DD_CLOSED — Constant
Arrow, closed. Width 1 BY CONSTRUCTION, asserted.
ManyUI.DD_FOCUS — Constant
The focused head. TC_CURSOR's meaning.
ManyUI.DD_MAX_ROWS — Constant
Rows a DropDown shows before its list scrolls, by default.
ManyUI.DD_OPEN — Constant
Arrow, open. Width 1 BY CONSTRUCTION, asserted.
ManyUI.DropDown — Type
mutable struct DropDown{T, F, C} <: WidgetA drop-down selection control: a one-row head, and an open list the App paints OVER the tree through the popup layer.
panel is THE popup's content; NOT a child, and absent from children(w) – so it is not laid out with the tree, not painted with the tree, and NOT IN THE TAB ORDER. The popup is a paint-and-hit layer, not a focus layer: the DropDown KEEPS FOCUS while it is open and forwards, exactly as an HTML <select> does.
selected is the COMMITTED option and row_cursor(w.panel.list) is the BROWSING position while open – TWO fields on purpose. ESCAPE and a click-away abandon the highlight and KEEP the selection; ENTER and a row click promote the highlight TO the selection. ONE field could not express "arrowed to Large, pressed ESCAPE, still Medium".
open and selected are Dirty.PAINT-reactive: the arrow is one cell, and measure is _lst_scan!'s widest-over-ALL-options plus a constant, a function of the DATA and not of the selection, so the box does not jump when you pick a shorter option.
Fields
node::WidgetNode: Per-widget state.panel::DropDownList{List{T, F, typeof(ManyUI._tc_noop), typeof(ManyUI._tc_noop)}} where {T, F}open::Reactive{Bool}: True while the popup is open. PAINT-reactive: the arrow flips.selected::Reactive{Int64}: The COMMITTED option, 1-based;0when nothing is chosen.focused::Reactive{Bool}: True while focused. PAINT-reactive.disabled::Reactive{Bool}: True if the dropdown is disabled.max_rows::Int64placeholder::Stringon_change::Any: Called ason_change(dd)when the selection COMMITS.
ManyUI.DropDown — Method
A drop-down over any AbstractVector, collected ONCE into a Vector.
ManyUI.DropDown — Method
A drop-down over items, calling on_change(dropdown) when the selection COMMITS.
items is ALIASED by the underlying List, never copied. format, max_rows and placeholder shape the head and the open list; the head sizes to the WIDEST option (never avail), so a Label beside it keeps its rows.
Focusable by construction; the open list is NOT – it is the popup and the popup is not a focus layer.
ManyUI.DropDownList — Type
mutable struct DropDownList{L<:Widget} <: WidgetA DropDown's open list: the popup's CONTENT ROOT, its BORDER, and its click interceptor.
INTERNAL machinery – the Scrollpane row/canvas/holder pattern (scroll.jl:216) – with one difference that matters: IT IS NOT IN app.root's TREE AT ALL. It is what Popup.content points at, it is UNPARENTED, and open_popup! throws if it ever stops being.
IT EXISTS FOR EXACTLY ONE REASON, and the reason is two source lines. A row click must COMMIT, and a List click cannot: _tc_mouse! calls set_cursor!, which under SINGLE selects AND CONSUMES (tablecore.jl:1145), and _walk! RETURNS THE INSTANT is_consumed(d) (dispatch.jl:88) – so nothing above the list ever hears about it. This node sits between the popup's root and the List and takes the LEFT PRESS IN CAPTURE, BEFORE the list can eat it. "Capture belongs to ancestors that want to intercept" (button.jl:78) – a DropDown is precisely an interceptor of its own list, and this is the one widget in the codebase for which that sentence is a specification rather than an aside.
It earns its keep TWICE: it is also the frame (the border) the list needs, and the node popup_opaque! makes opaque.
owner is Union{Nothing,Widget} and bound AFTER construction: DropDown holds this and this holds the DropDown, and a self-referential parametric type is not expressible in Julia. ONE dynamic dispatch per CLICK; NEVER on the frame path.
list is CONCRETE (L<:Widget), so _tc_row_at(w.list, e) dispatches STATICALLY – the Scrollbar{V}/Button{F}/List{T,F,A} pattern.
Fields
node::WidgetNode: Per-widget state.list::Widgetowner::Union{Nothing, Widget}: TheDropDowna row click commits to. Bound by the constructor.
ManyUI._dd_acts — Method
_dd_acts(d::Dispatch, e::KeyEvent) -> Bool
True while d is live and at or past its target, with no modifiers.
A DropDown activates on the way up, never in capture – the same rule Button uses – and leaves MODIFIED keys, TAB and BACK_TAB unconsumed so the tab order stays alive and ctrl+* bindings are not shadowed. Internal.
ManyUI._dd_caption — Method
_dd_caption(w::DropDown) -> String
The head's caption: the placeholder when nothing is selected, else the committed option through the List's own formatter. Internal.
ManyUI._dd_close! — Method
_dd_close!(w::DropDown) -> Bool
Close OUR popup. True iff it was open.
close_popup!(a, w) – the GUARDED form – because the unguarded one would slam a popup a DIFFERENT widget opened in the same event burst, which is exactly what TABbing from one dropdown onto another does. Also clears open[] when there is no App, so a standalone widget cannot be stranded believing it is open. Internal.
ManyUI._dd_commit! — Method
_dd_commit!(w::DropDown, k::Int64) -> Bool
Promote VIEW row k to the selection, close, and fire on_change IFF it really changed. True iff k was in range.
THE single exit of every commit – keyboard ENTER, keyboard SPACE and a row click all land here. Internal.
ManyUI._dd_key_closed! — Method
_dd_key_closed!(w::DropDown, d::Dispatch, e::KeyEvent)
Keys while CLOSED: UP/DOWN cycle the committed selection and fire on_change – what an HTML <select> does closed – and ENTER or SPACE open the list. SPACE is Key.SPACE. Consumes ONLY what it handles. Internal.
ManyUI._dd_key_open! — Method
_dd_key_open!(w::DropDown, d::Dispatch, e::KeyEvent)
Keys while OPEN: UP/DOWN move the list's browsing cursor, ENTER and SPACE COMMIT the highlight, ESCAPE closes without committing. SPACE is Key.SPACE (and the legacy Key.CHAR(' ') form), never one or the other. Consumes ONLY what it handles. Internal.
ManyUI._dd_open! — Method
_dd_open!(w::DropDown) -> Bool
Open the popup. False when it is already open, when there are no options, or when there is no App.
focus!(a, w) FIRST, AND IT IS NOT A COURTESY: there is no click-to-focus in ManyUI, so without it a MOUSE-OPENED dropdown leaves focus on whatever had it and UP/DOWN/ENTER/ESCAPE go somewhere else – an open list you cannot drive. focus! is a no-op when w is already focused, so the keyboard path pays nothing. BEFORE open_popup!, so that on_blur! on the previously focused widget – which may be another DropDown closing its own popup through the GUARDED close_popup!(app, owner) – cannot slam the popup we are about to open.
A DROPDOWN WITH NO APP CANNOT OPEN, stated rather than worked around: what survives standalone is the whole of the CLOSED widget – it constructs, measures, paints its selection, and UP/DOWN cycle that selection and fire on_change.
Puts the list's cursor on the CURRENT selection, so DOWN-DOWN-ENTER from a selected option moves TWO, not to the top. Internal.
ManyUI._dd_popup_size — Method
_dd_popup_size(w::DropDown) -> Size
Size the OPEN popup occupies, INCLUDING the frame's border.
clamp(n, 1, max_rows) + 2 rows and AT LEAST as wide as the head, which is what makes a dropdown look attached to its anchor. THIS IS THE OWNER'S DECLARATION that Popup.size is a field for: asking the content would ask a List, whose measure returns avail (list.jl:246) – the entire viewport. popup_region then clamps this to the screen, so a 400-item list on a 24-row terminal is not a special case here.
_lst_scan! is MEMOIZED (list.jl:200), so this is O(1) per open after the first. Internal.
ManyUI._dd_select! — Method
_dd_select!(w::DropDown, k::Int64) -> Bool
The CLOSED-state selection cycle: clamp to 1:n, early-out on no change, write selected[], fire on_change. Does NOT touch the popup. Internal.
ManyUI.is_open — Method
is_open(w::DropDown) -> Bool
True while the popup is open. Pure.
ManyUI.measure — Method
measure(w::DropDown, avail::Size) -> Size
The head's extent: the widest option (or the placeholder) plus the arrow. NOT avail – a greedy measure would shrink a Label beside this to zero rows.
O(1) after the first call, because _lst_scan! is MEMOIZED on List.scanned (list.jl:200), and THIS IS THE FRAME PATH. Writing maximum(text_width, items) here is forbidden.
ManyUI.on_blur! — Method
on_blur!(w::DropDown)
Lose focus: the popup's lifetime is bounded by its owner's focus, which is what makes TAB-while-open correct with ZERO code in app.jl – TAB is not consumed -> :focus_next -> focus! -> on_blur! -> here.
ManyUI.on_event! — Method
on_event!(w::DropDown, d::Dispatch{KeyEvent})
Forward the keyboard: OPEN and CLOSED behave differently and each consumes only what it handles, so TAB, BACK_TAB and modified keys stay unconsumed and the tab order – and close-on-TAB – keep working.
ManyUI.on_event! — Method
on_event!(w::DropDown, d::Dispatch{MouseEvent})
A LEFT press on the head TOGGLES the popup: it opens a closed list and closes an open one. A press outside is _popup_dismiss!'s job; a press ON the head is delivered here (that clause of _popup_dismiss! returns false), which is why clicking the head of an open dropdown closes it here instead of dismiss-then-reopen. Wheel and drag fall through.
ManyUI.on_event! — Method
on_event!(w::DropDownList, d::Dispatch{MouseEvent})
Intercept a LEFT PRESS on a row IN CAPTURE and COMMIT it, before the List can select-and-consume it.
is_scroll FIRST, so a wheel notch over the open list falls through to the List at target and _tc_wheel! scrolls it – a wheel over a dropdown's list is a scroll, not a choice. A DRAG likewise falls through. _tc_row_at and NOT row_cursor: at capture the List has not moved its cursor yet, so row_cursor is still the OLD row; _tc_row_at reads the POINTER, over painted_region, and is correct inside a scrolled pane.
ManyUI.on_focus! — Method
on_focus!(w::DropDown)
Gain focus: show as focused and reveal into any scrolling ancestor. reveal! is called EXPLICITLY because overriding on_focus! REPLACES the default that would have called it.
ManyUI.on_popup_close! — Method
on_popup_close!(w::DropDown)
The App closed our popup – by _dd_close!, a press outside, a resize, a blur, an unmount, or another widget opening one. NOTIFICATION ONLY, and provably non-looping because app.popup is cleared first.
REVERTS the list's cursor to the COMMITTED option, which is what makes ESCAPE and click-away both ABANDON the highlight. After _dd_commit! the two are already equal and this is a no-op – so COMMIT NEEDS NO SPECIAL CASE.
ManyUI.on_unmount! — Method
on_unmount!(w::DropDown)
Close on unmount: a popup outliving its owner is a list anchored to a widget that is no longer in any tree. Relies on unmount! calling on_unmount! BEFORE it clears node(w).app (widget.jl:367, 374), so app(w) still finds the App and the popup actually goes.
ManyUI.options — Method
options(w::DropDown) -> Vector
The options, ALIASED. Pure.
ManyUI.selected — Method
selected(w::DropDown) -> Int64
The committed option index, 0 when nothing is chosen. Pure.
ManyUI.selected_item — Method
selected_item(w::DropDown{T}) -> Any
The committed option, or nothing when nothing is chosen. Pure.
ManyUI.set_items! — Method
set_items!(w::DropDown, xs::AbstractVector)
Replace the options. CLOSES the popup first, clears the selection, and marks the head for relayout EXPLICITLY.
The list is UNBOUND while closed (close_popup! clears node(w).app), so its Reactive marks nothing and posts nothing, and the DropDown's own measure just changed – hence the explicit mark! and post!. Clearing selected is set_items!(::List)'s argument verbatim (list.jl:394): an index into data that no longer exists names the WRONG option.
ManyUI.set_open! — Method
set_open!(w::DropDown, v::Bool) -> Bool
Open or close the popup. true iff the state changed. Public.
Form
ManyUI.Form — Type
mutable struct Form{S, V} <: WidgetA Container that can be submitted and read. Parametric on the two handlers, so on_submit and on_validate are CONCRETE fields and never boxed closures – the Button{F} pattern.
Fields
node::WidgetNode: Per-widget state.fields::Vector{Pair{Symbol, Widget}}on_submit::Any: Called ason_submit(form)whensubmit!passes validation.on_validate::Any: Called ason_validate(form)::Bool. False VETOES the submit.disabled::Reactive{Bool}: True if the form is disabled.
ManyUI.Form — Method
A Container that can be submitted and read.
WHAT THIS ADDS OVER A Container, EXHAUSTIVELY, BECAUSE THE HONEST ANSWER IS "NOT MUCH":
submit!(f)–on_validate(f)and, iff it returns true,on_submit(f).form_value(w)– a PROTOCOL, and the only reusable thing here.form_values(f)– every named field's value, once, at the boundary.
WHAT IT DOES NOT ADD, and the claims are worth refuting because every framework makes them:
- "It groups fields." – so does a
Container. It IS one, and it defines norender!and nomeasureforContainer's reason. - "It traverses focus." –
focusable_widgetsis a pre-orderwalk_visible. TAB ALREADY WORKS through a bareContainer. This is not a feature, it is the core. - "ENTER submits." – IT DOES NOT, AND IT CANNOT.
TextInputconsumes ENTER unconditionally and_walk!STOPS DEAD on a consume, so a bubble handler here is NEVER REACHED from a text field. Capture-phase would fire and silently killTextInput.on_submit, which is shipped and tested. So this widget DEFINES NOon_event!AT ALL, and you wireTextInput("", _ -> submit!(f))– one closure, explicit, and exactly as many characters as the magic would have been. A testitem PINS this limitation, so it is a fact and not a comment somebody deletes.
Validation is ONE PREDICATE AND A VETO. No rule objects, no error vector, no :invalid class, no DSL. Validation is a function you already know how to write; a Form that ships a validation framework ships a DSL, and this codebase has no DSL.
IF THAT LIST DOES NOT EARN A FILE FOR YOU, USE A Container. A Form IS one, and nothing built on a Form is out of a Container's reach. It earns its forty lines because form_value is worth a protocol and because "wire ENTER through the field that consumes it" is the thing every app gets wrong once.
The Form exists BEFORE its fields, so the closure has something to capture:
f = Form(save_it)
add_field!(f, :name, TextInput("", _ -> submit!(f)))
add_field!(f, :remember, Checkbox("Remember me"))
mount!(f, Button("Save", _ -> submit!(f)))No chicken-and-egg, no registry, no Ref.
ManyUI._fm_ok — Method
_fm_ok(_::Widget) -> Bool
The default validator: everything passes. Internal.
ManyUI.add_field! — Method
add_field!(f::Form, name::Symbol, w::Widget) -> Form
Mount w as a field NAMED name and return f.
children(f) is the TAB ORDER – pre-order, unchanged – and f.fields is the NAME MAP; they are the same widgets in the same order. Plain mount!(f, w) still works and adds an UNNAMED field: a field with no name is one form_values does not report, which is right for a Button.
ManyUI.field — Method
field(f::Form, name::Symbol) -> Union{Nothing, Widget}
The field named name, or nothing. O(fields); a form has ten. Pure.
ManyUI.form_value — Function
THE value protocol: what w MEANS to a form. Add ONE method to make a widget a form field.
THROWS MethodError BY DEFAULT ON PURPOSE, and does NOT return nothing: a Label has no value, and a form that silently reports nothing for a field the author expected to read is a form that SAVES A BLANK. A MethodError names the type.
form_value and not value: value is far too generic a name for a UI package to export, and form_values below would then read as its plural. grid_of (tablecore.jl) is the precedent – a seam is a FUNCTION, not a supertype, which is what lets six unrelated widget types share it.
THE METHODS LIVE HERE, NOT IN EACH WIDGET'S FILE, and the layering and the manifest agree for once: "what this widget means to a form" is the FORM'S concept, not the text field's.
ManyUI.form_values — Method
form_values(f::Form) -> Dict{Symbol, Any}
Every NAMED field's form_value, keyed by its name.
ALLOCATES and is TYPE-UNSTABLE BY CONSTRUCTION – a form is heterogeneous by definition, its Strings and CheckState.Ts and Ints sharing one Dict{Symbol,Any}. A BOUNDARY call, made once on submit and NEVER on a frame path, which is what licenses both.
ManyUI.submit! — Method
submit!(w::Form) -> Bool
Validate, then submit. on_validate(f) || return false; on_submit(f); return true. THE whole validation story: one predicate and a veto. True iff the submit ran.
Popups
A popup is a second root painted over the tree and hit-tested before it – the layer a DropDown's list rides on. The App owns the one open popup; an owner opens it with open_popup! and is notified through on_popup_close!. Anchored controls use BELOW, ABOVE, or AUTO placement; modal dialogs use PopupPlacement.CENTER to ignore the owner geometry and center within the current viewport.
ManyUI.MODAL_DIM — Constant
What a modal folds over everything it covers.
DIM and not a black fill: dimming KEEPS the tree readable underneath, which is what tells the user the application is still there and merely waiting. A fill would say it had gone.
ManyUI.Popup — Type
mutable struct PopupOne open popup: a content root painted over the tree, the owner that opened it, the size the owner declares for it, and a placement.
content is UNPARENTED – not in app.root's tree – which is what lets it be laid out and painted as a second root without disturbing the first. owner is the widget notified when the popup closes (on_popup_close!). size is the owner's declaration, not the content's measure: a List's measure returns the whole viewport, so the owner is the only thing that knows how big its own popup should be.
Fields
content::Widgetowner::Widgetsize::Sizeplacement::ManyUI.PopupPlacement.Tmodal::Bool
ManyUI.Popup — Method
Popup(
content::Widget,
owner::Widget,
size::Size;
placement,
modal
) -> Popup
Popup(content, owner, size; placement, modal)
A popup over content, owned by owner, size cells, placed by placement (default AUTO).
modal = true dims the viewport behind it, traps focus and the keyboard inside it, and stops an outside press from dismissing it. PopupPlacement.CENTER is the placement such a dialog usually wants.
ManyUI.on_popup_close! — Method
on_popup_close!(_::Widget)
on_popup_close!(_)
Notify w that its popup has closed. The default is a no-op; a widget that opens popups (a DropDown) overrides it to reset its own state. Called AFTER app.popup is cleared, so an override may reopen without looping.
ManyUI.popup_region — Method
popup_region(
head::Region,
size::Size,
placement::ManyUI.PopupPlacement.T,
viewport::Size
) -> Region
popup_region(head, size, placement, viewport)
The on-screen Region a popup of size occupies when opened off head (the owner's border box) inside viewport. Pure, so placement is testable with no App.
CENTER is centred on both axes and does not depend on head. ABOVE always sits above. BELOW and AUTO both ANCHOR below and flip above only when the popup would run off the bottom and there is room above – an anchored dropdown wants to open downward but must stay on screen, so "below" is a preference, not a demand. Either way the result is clamped to the screen: a list taller than the terminal is not a special case, it is just clipped.
Code editor
ManyUI.CODE_FACES — Constant
Map from a JuliaSyntaxHighlighting face to a style.
Themed, so code tracks the palette. Faces not named here fall back to the widget's own style, which is why an unknown face is invisible rather than wrong.
ManyUI.CodeEditor — Function
CodeEditor(; ...) -> TextArea{typeof(ManyUI._ta_noop)}
CodeEditor(
text::AbstractString;
...
) -> TextArea{typeof(ManyUI._ta_noop)}
CodeEditor(
text::AbstractString,
on_change;
language,
highlight,
disabled,
id,
classes
) -> TextArea
A TextArea that paints its lines through highlight.
language selects a built-in highlighter; :julia is the only one, and :none gives a plain TextArea. Pass highlight directly for your own – it takes the whole source and returns one RichText per line, WHOLE-DOCUMENT because a line is not a lexical unit.
ManyUI.code_face_style — Method
code_face_style(face::Symbol) -> Style
The style a highlighter face maps to, or STYLE_NONE.
ManyUI.code_lines — Method
code_lines(w::TextArea) -> Vector{RichText}
w's lines as RichText, highlighted and cached.
Keyed on version, which every edit already bumps, so a keystroke costs one relex of the document and a frame costs none. Returns an empty vector when there is no highlighter – the caller then paints the plain lines, which is the same code path a TextArea always took.
ManyUI.highlight_julia — Method
highlight_julia(source::AbstractString) -> Vector{RichText}
Highlight Julia source into one RichText per line.
Uses the JuliaSyntaxHighlighting stdlib, so the lexer is the one Julia itself ships and this file owns no tokeniser. Its annotations are BYTE regions over the whole source; they are turned into per-line runs here, splitting a region that spans a newline rather than dropping it.
Total: a source that does not lex is returned unhighlighted rather than throwing. An editor that refuses to draw the moment the text is mid-edit would be unusable, and text under the cursor is invalid most of the time it is being typed.