Table and DataTable
ManyUI.Table — Type
mutable struct Table{R, F, A, C} <: RowsWidgetColumns with headers over a Vector of rows.
Table OWNS its rows. A view that owns nothing cannot answer _tc_sync!, cannot reindex_* its selection, and forces a SECOND data model across Table/DataTable – which is a second paint loop or a messier seam. One data model, one paint loop, and view_source is the only delta between the two widgets. A caller with a lazy source materialises a window into a Vector themselves.
THE CELL CALLBACK IS ON THE WIDGET, NOT ON THE COLUMN, and this is the decisive type decision of the tier. A Vector{Column{T,G}} parametric on the projection is IMPOSSIBLE – each column's closure has its own type, so one G cannot serve them all – which forces Column to hold an ABSTRACT projection field and pay ONE DYNAMIC DISPATCH PER VISIBLE CELL, FOREVER. cell::F on the widget is one CONCRETE type parameter, a STATIC dispatch, the same expressive power (cell(row, j) can branch on j however it likes), and it is the Button{F} pattern this package already uses three times.
rows is a PLAIN field, ALIASED, not a Reactive – List's reason, List's price, List's render!-time self-heal via _tc_sync!.
version is Dirty.PAINT-reactive – List's reason, verbatim.
cell(row, j) MUST return an AbstractString, MUST be PURE and CHEAP, and MUST NOT contain a newline. It is called ONCE PER VISIBLE CELL PER FRAME – never for a culled column, never for a row outside the window, and never during _tc_auto! for a non-AUTO column.
Fields
node::WidgetNode: Per-widget state.grid::TableGridrows::Vectorcell::Anyversion::Reactive{Int64}: Bumped by every data OR selection change. Dirty.PAINT.sel::Selectionfocused::Reactive{Bool}: True while focused. PAINT-reactive.disabled::Reactive{Bool}: True if the table is disabled.on_submit::Any: Called ason_submit(table)on ENTER.on_change::Any: Called ason_change(w)when the cursor moves.
ManyUI.Table — Method
A table of rows under cols, calling on_submit(table) on ENTER.
rows and cols are both ALIASED, never copied. Focusable by construction. Seeds every AUTO column from the headers and SOURCE rows 1 : min(sample, length(rows)), once – see _tc_auto!.
ManyUI._tb_reseed! — Method
_tb_reseed!(w::Table)
Reset every AUTO mark to its header seed and re-measure SOURCE rows 1 : min(sample, n) – the seeding pass, exactly as at construction.
THE ONE PLACE the sample is re-scanned, and it is never a data change: push_row!/insert_row! raise the marks from the NEW ROW ALONE, which is what keeps a push O(1). Internal.
ManyUI._tc_cell_default — Method
_tc_cell_default(row, j::Int64) -> AbstractString
The default cell function: _tc_show(row[j]). Works for a Vector of Tuples, of NamedTuples and of Vectors – the three shapes a table's rows actually arrive in – and for nothing else, which is exactly what the cell keyword is for. Internal.
ManyUI._tc_header_rows — Method
_tc_header_rows(w::Table) -> Int64
(show_header ? 1 : 0) + (show_header && rule ? 1 : 0). The rows of the content box that are pinned CHROME. 0, 1 or 2. Internal.
ManyUI.content_extent — Method
content_extent(w::Table) -> Size
Size(cache_total, row_count + header_rows). OVERRIDES the container default: a Table's content is DATA, not children. THIS OVERRIDE IS THE WHOLE INTEGRATION WITH Scrollbar – Scrollbar{Table{R,F,A}} works with ZERO new code in scroll.jl. See _tc_extent for why the header rows are counted and why dropping them would make the last hh rows of every table unreachable. O(1) on the FRAME PATH.
ManyUI.delete_row! — Method
delete_row!(w::Table, i::Int64) -> Bool
Delete row i. False when out of range. REINDEXES via reindex_delete!. An AUTO column stays TOO WIDE until refresh_extent! – TextArea.widest makes exactly this trade (textarea.jl:176).
ManyUI.insert_row! — Method
insert_row!(w::Table{R}, i::Int64, r)
Insert r at i, clamped. REINDEXES the selection via reindex_insert! and raises the AUTO marks from the new row alone.
ManyUI.measure — Method
measure(w::Table, avail::Size) -> Size
avail. A Table takes the space it is OFFERED and scrolls its content: an auto-HEIGHT table would be as tall as its data and would never scroll at all. This is also what licenses version's PAINT reactivity. Pure w.r.t. the tree.
ManyUI.on_blur! — Method
on_blur!(w::Table)
Hide the cursor.
ManyUI.on_event! — Method
on_event!(w::Table, d::Dispatch{KeyEvent})
Keys: see _tc_key!. Consumes only when something actually moved.
ManyUI.on_event! — Method
on_event!(w::Table, d::Dispatch{MouseEvent})
Mouse: see _tc_mouse!. A press on the header does nothing: a Table never sorts.
ManyUI.on_focus! — Method
on_focus!(w::Table)
Show the cursor, and scroll every ancestor pane until this table is visible. reveal! is called EXPLICITLY because overriding on_focus! REPLACES the default that would have called it (widget.jl:666).
ManyUI.push_row! — Method
push_row!(w::Table{R}, r)
Append r. O(1) plus an O(cap) mark raise for that row alone – nothing rescans.
ManyUI.refresh_columns! — Method
refresh_columns!(w::Table)
Re-seed the AUTO marks and invalidate the memo after grid_of(w).cols was written in place.
RE-MEASURES THE SAMPLE, exactly as construction does, and not the headers alone. _tc_auto_reset! lowers every AUTO mark to its header seed, so a reset that stopped there would collapse every AUTO column to its own caption: changing one column's ALIGNMENT would silently resize its NEIGHBOURS. A column model rewritten is a column model measured, and the cost is the construction cost – O(min(sample, n) x AUTO columns), bounded, on an explicit call and never on a frame.
ManyUI.refresh_extent! — Method
refresh_extent!(w::Table) -> Size
Reset and re-measure every AUTO mark over EVERY row, re-resolve, and return the new content_extent. O(rows x AUTO columns), each cell capped by _tc_measure.
THE OPT-IN EXACT RESCAN, and the same name and meaning as refresh_extent!(::TextArea) and refresh_extent!(::List). This is the ONLY thing that can make an AUTO column NARROWER, and it is the documented escape from the sampling rule.
ManyUI.refresh_rows! — Method
refresh_rows!(w::Table)
THE single exit of every data change; also the public escape hatch for a direct mutation of rows. Bumps version (which invalidates the resolve memo), re-syncs the selection, re-clamps the scroll, follows the cursor.
ManyUI.set_columns! — Method
set_columns!(w::Table, cols::Vector{Column})
Replace the columns. Resizes widths/xs/autos, re-seeds the AUTO marks and invalidates the memo.
ManyUI.set_rows! — Method
set_rows!(w::Table{R}, rows::AbstractVector)
Replace the contents. CLEARS the selection and cursor, rewinds the scroll and RE-SEEDS the AUTO marks: every index the selection held names a row that may no longer exist.
ManyUI.DataTable — Type
mutable struct DataTable{R, F, K, A, C} <: RowsWidgetA Table plus a SORT: the same data, the same columns, the same paint loop, viewed through an index PERMUTATION.
WHAT IT ADDS OVER Table, EXHAUSTIVELY:
key::K `key(row, j)` -> something `isless` accepts
order::Vector{Int} view -> source. THE sort.
rank::Vector{Int} source -> view. The inverse.
sort_col::Int `0` == source order
sort_dir::SortDir.T
a sort indicator in the header, and a header click that sorts.Everything else – the columns, the sizing, the header, the truncation, the selection, the navigation, the scrolling, the paint – is tablecore's, reached through tcrender_table! and the seam.
WHY TWO TYPES AND NOT ONE WITH A NULLABLE order: because view_source is the ONLY behavioural difference, and it is k for one and order[k] for the other. One type would branch on a nullable field ONCE PER VISIBLE ROW PER FRAME, forever, on every table in every app, to serve the ones that never sort. Two types make the compiler take that branch ONCE, at specialisation time, and a Table that never sorts pays neither the branch nor the 2n Ints – 1.6 MB on 100 000 rows – that order and rank cost. That is not a new argument in this codebase, it is the house one (label.jl:98, Scrollbar{V}).
TWO CALLBACKS, cell AND key, and the separation is the whole point:
cell(row, j)::AbstractString -- what the user SEES
key(row, j) -- what the sort COMPARESSorting a numeric column by its RENDERED string puts "10" before "9". That is a bug in every table library that conflates the two.
key IS A REQUIRED KEYWORD. It has NO default, and in particular it does NOT default to cell. A default that silently does the wrong thing is an honest confession of a dishonest default; a sort_by! that silently no-ops is a quiet nothing on the one call whose entire purpose is to change something. A required keyword makes the decision UNAVOIDABLE AT CONSTRUCTION and unmissable in review. A DataTable exists to sort; one without a sort key is a Table.
key(row, j) must return values mutually isless-comparable WITHIN a column. Across columns it may return anything: the sort closes over a FIXED j, so every comparison in a given sort_by! is homogeneous. A column of MIXED types THROWS a MethodError from isless – it is not "merely slow".
Fields
node::WidgetNode: Per-widget state.grid::TableGridrows::Vectorcell::Anykey::Anyorder::Vector{Int64}rank::Vector{Int64}sort_col::Int64: The sorted column, or0for source order.sort_dir::ManyUI.SortDir.T: The sort direction.NONEiffsort_col == 0.version::Reactive{Int64}: Bumped by every data OR selection change. Dirty.PAINT.sel::Selectionfocused::Reactive{Bool}: True while focused. PAINT-reactive.disabled::Reactive{Bool}: True if the table is disabled.on_submit::Any: Called ason_submit(table)on ENTER.on_change::Any: Called ason_change(w)when the cursor moves.
ManyUI.DataTable — Method
A sortable table of rows under cols, calling on_submit(table) on ENTER. Starts in SOURCE order.
key is REQUIRED and has no default: see the type's docstring. rows and cols are both ALIASED, never copied.
ManyUI._dt_fit! — Method
_dt_fit!(w::DataTable) -> Bool
Resize order/rank to length(rows). True iff either was resized – in which case the grown slots are UNDEFINED and every caller must write order whole before _dt_rank! reads it. Internal.
ManyUI._dt_identity! — Method
_dt_identity!(w::DataTable) -> Bool
Restore SOURCE order: order[k] = k. True iff any entry moved. Internal.
ManyUI._dt_on_header — Method
_dt_on_header(w::DataTable, e::MouseEvent) -> Bool
True when e points at the pinned header rows of w – the caption row and the rule under it, which is exactly the block _tc_header_rows counts and exactly the block _tc_row_at refuses to call a body row.
_tc_local, NEVER local_offset: inside a Scrollpane scrolled to y = 3 the unshifted border box would call a BODY row a header and sort on a click that meant to select. Internal.
ManyUI._dt_order! — Method
_dt_order!(w::DataTable, p::AbstractVector{Int64}) -> Bool
Write p into order. True iff any entry moved. p is a permutation of 1:length(rows) – sortperm's answer, or the identity. Internal.
ManyUI._dt_pad — Method
_dt_pad(n::Int64) -> String
n spaces; the empty string at or below zero. Pure. Internal.
ManyUI._dt_perm — Method
_dt_perm(
w::DataTable,
j::Int64,
dir::ManyUI.SortDir.T
) -> Vector{Int64}
The permutation column j in direction dir puts rows in, WITHOUT touching rows. See sort_by! for why the keys are materialised first and why MergeSort is spelled. O(n) keys + O(n log n). Internal.
ManyUI._dt_rank! — Method
_dt_rank!(w::DataTable)
Rebuild rank from order in ONE pass: rank[order[k]] = k. O(n), off the frame path. Internal.
ManyUI._dt_reapply! — Method
_dt_reapply!(w::DataTable)
Rebuild order/rank from the CURRENT sort state: the identity when sort_col == 0, the permutation otherwise. THE reapply, and the whole of what a data change owes the sort. Internal.
ManyUI._dt_reseed! — Method
_dt_reseed!(w::DataTable)
Reset every AUTO mark to its header seed and re-measure SOURCE rows 1 : min(sample, n) – the seeding pass, exactly as at construction.
THE ONE PLACE the sample is re-scanned, and it is never a data change. Mirrors _tb_reseed!(::Table). Internal.
ManyUI._tc_header_reserve — Method
_tc_header_reserve(w::DataTable, j::Int64) -> Int64
1 for a sortable column: the indicator needs a cell to live in, and an AUTO column sized to its header text alone would have nowhere to draw it. Internal.
ManyUI._tc_header_rows — Method
_tc_header_rows(w::DataTable) -> Int64
(show_header ? 1 : 0) + (show_header && rule ? 1 : 0). The rows of the content box that are pinned CHROME. 0, 1 or 2. Internal.
ManyUI._tc_header_text — Method
_tc_header_text(w::DataTable, j::Int64) -> String
The caption, and the sort indicator when this is the sorted column.
THE INDICATOR IS PAINTED IN THE COLUMN'S LAST CELL BY _tc_put!, not APPENDED to the caption. Appending is the obvious spelling and is wrong twice: a narrow column would TRUNCATE THE INDICATOR AWAY – so the one column whose state you must see is the one that hides it – and the column's AUTO width would depend on its sort state, so CLICKING A HEADER WOULD RESIZE IT.
THE GUTTER IS RESERVED IN EVERY SORTABLE COLUMN, ALWAYS, and left blank when unsorted (_tc_header_reserve). Reserving it only when sorted would change the caption's width the moment you sort, so the caption would re-truncate and the header would twitch. ScrollMode.AUTO's doctrine, quoted: THE GUTTER IS STABLE, THE INK IS NOT. Internal.
ManyUI.content_extent — Method
content_extent(w::DataTable) -> Size
Size(cache_total, view_count + header_rows). OVERRIDES the container default; THIS OVERRIDE IS THE WHOLE INTEGRATION WITH Scrollbar – Scrollbar{DataTable{R,F,K,A}} works with ZERO new code in scroll.jl. O(1) on the FRAME PATH.
ManyUI.delete_row! — Method
delete_row!(w::DataTable, i::Int64) -> Bool
Delete SOURCE row i. False when out of range. REINDEXES via reindex_delete! and rebuilds order/rank.
ManyUI.insert_row! — Method
insert_row!(w::DataTable{R}, i::Int64, r)
Insert r at SOURCE index i, clamped. REINDEXES the selection via reindex_insert!, then REAPPLIES the current sort.
ManyUI.measure — Method
measure(w::DataTable, avail::Size) -> Size
avail. A DataTable takes the space it is OFFERED and scrolls its content. Pure w.r.t. the tree.
ManyUI.on_blur! — Method
on_blur!(w::DataTable)
Hide the cursor.
ManyUI.on_event! — Method
on_event!(w::DataTable, d::Dispatch{KeyEvent})
Keys: see _tc_key!. Consumes only when something actually moved.
ManyUI.on_event! — Method
on_event!(w::DataTable, d::Dispatch{MouseEvent})
A LEFT PRESS on the HEADER rows sorts the column under the pointer; everything else is _tc_mouse!. That one branch is DataTable's entire mouse delta. A press on a non-sortable column consumes nothing and does nothing.
Uses grid_of(w).xs/widths – LAST frame's – which is exactly the header the user saw and clicked. Before the first paint they are zeros and a click does nothing, which is correct: there was no header to click.
ManyUI.on_focus! — Method
on_focus!(w::DataTable)
Show the cursor, and scroll every ancestor pane until this table is visible. reveal! is called EXPLICITLY because overriding on_focus! REPLACES the default that would have called it (widget.jl:666).
ManyUI.push_row! — Method
push_row!(w::DataTable{R}, r)
Append r. Raises the AUTO marks from the new row alone, then REAPPLIES the current sort.
ManyUI.refresh_columns! — Method
refresh_columns!(w::DataTable)
Re-seed the AUTO marks and invalidate the memo after grid_of(w).cols was written in place.
RE-MEASURES THE SAMPLE, exactly as construction does, and not the headers alone. _tc_auto_reset! LOWERS every AUTO mark to its header seed, so a reset that stopped there would collapse every AUTO column to its own caption: re-speccing ONE column's alignment would silently resize its NEIGHBOURS. A column model rewritten is a column model measured, and the cost is the construction cost – O(min(sample, n) x AUTO columns), bounded, on an explicit call and never on a frame.
The Table twin's refresh_columns! states the same rule, and this is what DataTable itself does at construction and at set_rows!. No data-change path (push_row!/insert_row!/delete_row!) rescans.
ManyUI.refresh_extent! — Method
refresh_extent!(w::DataTable) -> Size
Reset and re-measure every AUTO mark over EVERY row, re-resolve, and return the new content_extent. THE OPT-IN EXACT RESCAN, and the ONLY thing that can make an AUTO column NARROWER.
ManyUI.refresh_rows! — Method
refresh_rows!(w::DataTable)
THE single exit. Rebuilds order/rank to 1:n, REAPPLIES the current sort when sort_col != 0, bumps version, re-syncs the selection, re-clamps the scroll, follows the cursor. O(n log n) when sorted – a data change, never a frame.
ManyUI.set_columns! — Method
set_columns!(w::DataTable, cols::Vector{Column})
Replace the columns. Resizes widths/xs/autos, re-seeds the AUTO marks and invalidates the memo. Resets the sort when sort_col is no longer a valid column.
ManyUI.set_rows! — Method
set_rows!(w::DataTable{R}, rows::AbstractVector)
Replace the contents. CLEARS the selection and cursor, rewinds the scroll, re-seeds the AUTO marks and rebuilds order/rank.
ManyUI.sort_by! — Method
sort_by!(w::DataTable, j::Int64; dir) -> Bool
Sort the VIEW by column j in direction dir. j == 0 or dir === SortDir.NONE restores SOURCE order. Returns true iff the order changed.
THE ORDER, PRECISELY: the view order OR the sorted column OR the direction – "something actually changed", which is the convention every mutator in this family follows (set_scroll!, widget.jl:204). The three are ONE fact and the wider reading is the load-bearing one: a column whose keys are ALL EQUAL flips ASCENDING to DESCENDING without moving a single row, and a false there would skip the PAINT bump and leave the header showing an arrow that contradicts sort_direction(w). The indicator is state, not order.
THROWS ArgumentError when 1 <= j <= ncols and !cols[j].sortable, and BoundsError on a j outside 0:ncols. It does NOT silently no-op: a quiet nothing on the one call whose whole purpose is to change something is the worst available failure.
DOES NOT MUTATE THE CALLER'S DATA, and the mechanism IS the whole answer: sortperm READS rows and returns a PERMUTATION. rows is never touched, never permuted, never copied. Sorting a COPY costs O(n) memory and O(n) moves of R and severs identity; sorting IN PLACE mutates a Vector the caller handed us and may still be using. The permutation costs 8n bytes, moves Ints, and view_source's one array load is the entire read side.
THE ALGORITHM IS SPELLED, NOT DEFAULTED:
ks = [w.key(w.rows[i], j) for i in 1:n]
p = sortperm(ks; alg = MergeSort, rev = (dir === DESCENDING))- MATERIALIZING
ksFIRST IS NOT AN OPTIMISATION, IT IS THE ALGORITHM.sort!(order; by = i -> key(rows[i], j))runsbyTWICE PER COMPARISON, becauseBase.Order.Byhas no Schwartzian transform. MEASURED: 17824bycalls at n=2000 versus 2000 for this spelling – ~9x. The comprehension also INFERS its eltype, so a numeric column sorts a concreteVector{Float64}. alg = MergeSortIS SPELLED even thoughsortperm's default is already stable. Stability is a CONTRACT here, and a contract you get from a default is a contract you will lose.rev = trueREVERSES THE ORDERING, NOT THE OUTPUT, so ties keep their SOURCE order in BOTH directions:sortperm([2,1,2,1,2]; rev=true, alg=MergeSort) == [1,3,5,2,4], NOT[5,3,1,4,2]. That is what "stable" has to mean for a user who sorts by Department then by Name and expects the Names still in order within a Department.orderis RESET to1:nbefore every sort, sosort_by!(w, 2, ASCENDING)gives the same answer whatever the history. THE PRICE: no multi-key sort by chaining. That feature is not lost, it is RELOCATED to where it costs nothing –key = (r, j) -> (r.dept, r.name)IS the multi-key sort, it is one line of user code, and unlike chaining it says what it means.
THE SELECTION DOES NOT MOVE, AND THERE IS NO CODE HERE THAT MOVES IT. Selection stores SOURCE indices, so a sort – a claim about ORDER – cannot touch a selection – a claim about ROWS. That is the entire reason the index space is source-based, and it is why this function has no remap! to get wrong. The cursor does not change ROW; it changes SCREEN POSITION, because view_rank changed. _tc_follow_cursor! then runs, so THE USER'S ROW STAYS UNDER THEIR EYES across the sort.
THE COLUMN WIDTHS DO NOT CHANGE: _tc_auto! samples SOURCE rows 1:sample, never view rows, so a width that moved under a sort is impossible by construction.
O(n) key materialisation + O(n log n) sortperm + O(n) _dt_rank!. ALL OF IT ON A USER ACTION, once per header click. The frame path is untouched.
ManyUI.sort_column — Method
sort_column(w::DataTable) -> Int64
The sorted column, or 0 for source order. Pure.
ManyUI.sort_direction — Method
sort_direction(w::DataTable) -> ManyUI.SortDir.T
The sort direction. NONE iff sort_column(w) == 0. Pure.
ManyUI.sort_indicator — Method
sort_indicator(w::DataTable, j::Int64) -> String
TC_SORT_ASC, TC_SORT_DESC, or "" when column j is not the sorted one.
ManyUI.source_index — Method
source_index(w::DataTable, k::Int64) -> Int64
The caller's row index behind VIEW row k. The map a selection needs – and selected_rows(w) is ALREADY in source indices, so this is for reading the view, not for repairing it. Pure.
ManyUI.toggle_sort! — Method
toggle_sort!(w::DataTable, j::Int64) -> Bool
Cycle column j: ASCENDING -> DESCENDING -> ASCENDING. What a header CLICK does.
There is NO click path to SortDir.NONE: a third state that looks identical to "sorted by whatever it was before" is a state a user cannot see and therefore cannot want. sort_by!(w, 0) restores source order for a caller who means it. Clicking a NEW column sorts ASCENDING.