Table and DataTable

ManyUI.TableType
mutable struct Table{R, F, A, C} <: RowsWidget

Columns 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 ReactiveList'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::TableGrid

  • rows::Vector

  • cell::Any

  • version::Reactive{Int64}: Bumped by every data OR selection change. Dirty.PAINT.

  • sel::Selection

  • focused::Reactive{Bool}: True while focused. PAINT-reactive.

  • disabled::Reactive{Bool}: True if the table is disabled.

  • on_submit::Any: Called as on_submit(table) on ENTER.

  • on_change::Any: Called as on_change(w) when the cursor moves.

source
ManyUI.TableMethod

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!.

source
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.

source
ManyUI._tc_cell_defaultMethod
_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.

source
ManyUI._tc_header_rowsMethod
_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.

source
ManyUI.content_extentMethod
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 ScrollbarScrollbar{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.

source
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).

source
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.

source
ManyUI.measureMethod
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.

source
ManyUI.on_event!Method
on_event!(w::Table, d::Dispatch{KeyEvent})

Keys: see _tc_key!. Consumes only when something actually moved.

source
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.

source
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).

source
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.

source
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.

source
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.

source
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.

source
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.

source
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.

source
ManyUI.DataTableType
mutable struct DataTable{R, F, K, A, C} <: RowsWidget

A 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 COMPARES

Sorting 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::TableGrid

  • rows::Vector

  • cell::Any

  • key::Any

  • order::Vector{Int64}

  • rank::Vector{Int64}

  • sort_col::Int64: The sorted column, or 0 for source order.

  • sort_dir::ManyUI.SortDir.T: The sort direction. NONE iff sort_col == 0.

  • version::Reactive{Int64}: Bumped by every data OR selection change. Dirty.PAINT.

  • sel::Selection

  • focused::Reactive{Bool}: True while focused. PAINT-reactive.

  • disabled::Reactive{Bool}: True if the table is disabled.

  • on_submit::Any: Called as on_submit(table) on ENTER.

  • on_change::Any: Called as on_change(w) when the cursor moves.

source
ManyUI.DataTableMethod

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.

source
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.

source
ManyUI._dt_identity!Method
_dt_identity!(w::DataTable) -> Bool

Restore SOURCE order: order[k] = k. True iff any entry moved. Internal.

source
ManyUI._dt_on_headerMethod
_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.

source
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.

source
ManyUI._dt_padMethod
_dt_pad(n::Int64) -> String

n spaces; the empty string at or below zero. Pure. Internal.

source
ManyUI._dt_permMethod
_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.

source
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.

source
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.

source
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.

source
ManyUI._tc_header_reserveMethod
_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.

source
ManyUI._tc_header_rowsMethod
_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.

source
ManyUI._tc_header_textMethod
_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.

source
ManyUI.content_extentMethod
content_extent(w::DataTable) -> Size

Size(cache_total, view_count + header_rows). OVERRIDES the container default; THIS OVERRIDE IS THE WHOLE INTEGRATION WITH ScrollbarScrollbar{DataTable{R,F,K,A}} works with ZERO new code in scroll.jl. O(1) on the FRAME PATH.

source
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.

source
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.

source
ManyUI.measureMethod
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.

source
ManyUI.on_event!Method
on_event!(w::DataTable, d::Dispatch{KeyEvent})

Keys: see _tc_key!. Consumes only when something actually moved.

source
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.

source
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).

source
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.

source
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.

source
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.

source
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.

source
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.

source
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.

source
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 ks FIRST IS NOT AN OPTIMISATION, IT IS THE ALGORITHM. sort!(order; by = i -> key(rows[i], j)) runs by TWICE PER COMPARISON, because Base.Order.By has no Schwartzian transform. MEASURED: 17824 by calls at n=2000 versus 2000 for this spelling – ~9x. The comprehension also INFERS its eltype, so a numeric column sorts a concrete Vector{Float64}.
  • alg = MergeSort IS SPELLED even though sortperm'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 = true REVERSES 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.
  • order is RESET to 1:n before every sort, so sort_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.

source
ManyUI.sort_directionMethod
sort_direction(w::DataTable) -> ManyUI.SortDir.T

The sort direction. NONE iff sort_column(w) == 0. Pure.

source
ManyUI.sort_indicatorMethod
sort_indicator(w::DataTable, j::Int64) -> String

TC_SORT_ASC, TC_SORT_DESC, or "" when column j is not the sorted one.

source
ManyUI.source_indexMethod
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.

source
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.

source