colwidth

package
v0.0.22 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 24, 2026 License: MIT Imports: 9 Imported by: 0

Documentation

Overview

Package colwidth resolves table column widths from user-set overrides, per ADR-0151. It owns the four things the ADR's §SD5 names: tier resolution, capture detection, the per-table apply epoch, and the mapping to and from stored override rows.

The model is that the highest-signal width is the one the user chose, so resolution runs override → app-supplied default → crate autofit, and a drag the user performs is captured back as an override. Overrides are keyed by what a column *is* — its name and render type — rather than where it sits, so they survive column reordering, a table moving to a different pane, and the same field recurring in a differently-shaped query result.

Nothing here talks to egui. The package is pure Go over a store port so its state machine can be tested without a render loop; the binding that applies widths and reads them back is a separate milestone.

Index

Constants

View Source
const DefaultDebounce = 700 * time.Millisecond

DefaultDebounce is how long a captured width is held before it is written. A drag emits a new width every frame it moves, so writing on each observation would put a row per frame into the facts table; waiting for the motion to stop collapses one drag into one row.

View Source
const DefaultMaxEntries = 512

DefaultMaxEntries bounds the in-memory override set. It is not a retention policy: rows already written stay written, and pruning the durable trail is a retention question over the facts table (the ADR's Update moved it there when the storage stopped being one document). The cap only stops a very long-lived process from growing its working set without limit.

Variables

View Source
var PackageProps = packageprops.Props{
	WASMWASI:         packageprops.WASMCompiles,
	WASMJS:           packageprops.WASMCompiles,
	WASMFreestanding: packageprops.WASMCompiles,
}

PackageProps records this package's curated properties (ADR-0080). Seeded by `boxer code analysis golang wasmsurvey props generate`; curate by hand. The same group's `props verify` reconciles it.

Functions

func ShapeHash

func ShapeHash(cols []Column) (hash string)

ShapeHash identifies "the same logical table" — the sorted set of the columns' keys. Sorting makes it order-independent, so reordering columns does not change the shape; de-duplication makes a repeated column key contribute once, so the hash describes a set as the ADR says it does.

Types

type Column

type Column struct {
	Name string
	Type string
}

Column is a column's semantic identity as the call site knows it.

Name is the rendered header text and Type a short discriminator for the render type — the leeway canonical type where the data has one, else an app-chosen format tag. Type participates in the key deliberately: when a column's type changes the old width is no longer meaningful, and keying on the pair invalidates it without needing a rule anyone has to remember.

func (Column) Key

func (inst Column) Key() (key string)

Key returns the column's stable identity, hex-encoded.

The two fields are length-prefixed rather than delimiter-joined so that no pair of (name, type) values can collide by moving the boundary — a column named "a" of type "b|c" and one named "a|b" of type "c" are different columns and must not share a width.

type HostI

type HostI interface {
	ColumnWidthStore() (store StoreI)
}

HostI is the optional frame-context capability a host with a facts store provides so an app can persist column widths. It takes the shape [ADR-0155] §SD1 settled for reaching host-held collaborators: an optional capability type-asserted off the context, exactly as app.WindowFocusI is, so the four-method app contract stays frozen and hosts without a facts store owe nothing.

It is declared here rather than in `app` because its store speaks in facts rows, and `factsstore` imports `app` — declaring it there would close that loop.

Absence means no durable widths, not an error. An app that cannot acquire a store renders with its own defaults; every width affordance stays usable and nothing persists:

var res *colwidth.Resolver
if h, ok := ctx.(colwidth.HostI); ok {
	if st := h.ColumnWidthStore(); st != nil {
		res, _ = colwidth.New(st, colwidth.Opts{AppId: ctx.AppId()})
	}
}

Construct with the mount context's AppId, never one the app composes: ADR-0155 §SD3 makes that the keying identity for embedded and windowed instances alike, so a column dragged in one follows the content to the other.

type Opts

type Opts struct {
	// AppId scopes every read and write. Overrides never cross apps.
	AppId app.AppIdT
	// InstanceKey is the window a captured width is attributed to on the
	// trail (ADR-0191 §SD4). It is recorded, never keyed on: an override is
	// the app's, so a drag in a second window overwrites the first's entry
	// rather than forking it. Zero — a host that does not mint window keys —
	// writes an unattributed row, exactly as before.
	InstanceKey uint64
	// Debounce overrides DefaultDebounce. Negative is rejected; zero
	// takes the default.
	Debounce time.Duration
	// MaxEntries overrides DefaultMaxEntries.
	MaxEntries int
	// MinPoints and MaxPoints clamp both resolved and captured widths.
	// A zero MaxPoints means unbounded. They exist so a corrupt or absurd
	// stored value cannot render a table unusable — the user would have no
	// way to drag a 100000pt column back.
	MinPoints float64
	MaxPoints float64
}

Opts configures a Resolver. Only AppId is required.

type Resolver

type Resolver struct {
	// contains filtered or unexported fields
}

Resolver resolves and captures column widths for one app.

It is not safe for concurrent use: it is designed to be owned by a single app instance and driven from its render loop, the same single-threaded discipline every other imzero2 widget state follows.

func New

func New(store StoreI, opts Opts) (inst *Resolver, err error)

New constructs a Resolver. It does not read the store — call Load once storage is available, which for an app is after Mount rather than at construction.

func (*Resolver) Clear

func (inst *Resolver) Clear(tableTag string, col Column) (err error)

Clear removes the instance- and column-tier overrides for one column and returns it to defaults (§SD5's "clear override" affordance). The next Resolve for the table sees different widths and bumps its epoch, so the crate is re-seeded without the caller doing anything.

The pending capture is dropped too: clearing an override that a drag has just set, but that has not been flushed yet, must not have the drag write it back a moment later.

func (*Resolver) ClearAll

func (inst *Resolver) ClearAll(tableTag string, cols []Column) (err error)

ClearAll returns every column of one table to defaults — the "reset this table's widths" gesture that pairs with per-column Clear.

Unlike Clear it does not stop at the first failure. A partial clear is the worst outcome for this gesture: the user asked for a uniform table and would get some columns reset and others not, with no way to tell which. So every column is attempted and the first error is returned once all of them have been.

func (*Resolver) Epoch

func (inst *Resolver) Epoch(tableTag string) (epoch uint32)

Epoch is the table's apply generation. The binding writes Go's widths into the crate's state only when this changes; between bumps the crate's own state — the user's live drag — wins.

func (*Resolver) Flush

func (inst *Resolver) Flush(now time.Time) (written int, err error)

Flush writes captures whose motion stopped at least Debounce ago. It is safe to call every frame; entries still moving are left pending.

A write failure leaves the entry dirty so the next Flush retries: losing a width the user set because one insert failed is worse than writing a second row for it.

func (*Resolver) Len

func (inst *Resolver) Len() (n int)

Len reports the number of overrides held in memory.

func (*Resolver) Load

func (inst *Resolver) Load() (err error)

Load reads the app's override set. Pending unwritten captures are kept: a Load racing a drag must not silently discard the user's adjustment.

func (*Resolver) MarkReseed

func (inst *Resolver) MarkReseed(tableTag string)

MarkReseed opens the same settle window Resolve opens when it bumps an epoch, for a frame on which the *call site* hands the crate authority over the widths — asking egui_table to auto-size this frame is the case it was added for.

Without it a call site has no way to say so that survives the read-back's lag. Passing firstShow on the frame it asks for the fit covers that frame's report, which describes the columns as they were *before* the fit; the fit's own result arrives one report later, with the flag already back to false, and is read as a gesture. Measured on play's per-DB-row grid: a re-fit on first show wrote eight override rows for a table nobody had touched.

It is deliberately the same two-frame window and the same field, not a parallel mechanism: "the crate chose these widths, not the user" is one idea, and a second counter would be a second thing to get right.

func (*Resolver) Observe

func (inst *Resolver) Observe(tableTag string, cols []Column, fetched []float64, fontSize float64, firstShow bool, now time.Time)

Observe reports the widths the binding read back after a frame.

A fetched width that differs from what the crate last settled on for that column is a user adjustment, and is captured as an override on the instance and column tiers (§SD1). Two things are deliberately not captures: the first-show frame, where the crate force-autofits and the widths are its idea rather than the user's, and a value equal to the settled one, which is the crate echoing itself back.

A capture updates the sent width too, without bumping the epoch. That is the echo suppression the ADR calls for: the crate already holds the width, so re-applying it would fight the very drag that produced it.

firstShow *adopts* rather than ignores, and this is the whole of its effect. Skipping the frame outright left the settled width holding the default the call site supplied, so the very widths this is meant to disown read as a change on the next frame and were captured then — a one-frame delay, not a suppression, and enough for every column of a table nobody touched to acquire a durable override. Taking them as the baseline instead means only what moves *after* the crate settled counts as the user's. Since Resolve now opens the same window itself whenever it re-seeds, a call site that cannot tell a first show from any other frame may pass false throughout.

A report is matched to cols by position, so one whose length differs is not about these columns at all — it was produced under a different set, or arrived truncated — and is dropped rather than lined up anyway.

func (*Resolver) PendingCount

func (inst *Resolver) PendingCount() (n int)

PendingCount reports how many captures are waiting to be written. Tests and diagnostics use it; the render path does not.

func (*Resolver) Resolve

func (inst *Resolver) Resolve(tableTag string, cols []Column, fontSize float64, defaults []float64) (widths []float64)

Resolve returns one width per column: the most specific override that matches, else the caller's default. defaults may be nil or shorter than cols, in which case the missing entries resolve to 0 — the call site's signal to let the crate autofit.

Resolve also records what it returned as the applied width for the table and bumps the table's epoch when that differs from last time, so the binding knows a re-apply is due. Calling it repeatedly with an unchanged result is cheap and does not bump the epoch.

type StoreI

type StoreI interface {
	ListColumnWidths(appId app.AppIdT) (rows []factsstore.ColumnWidthRow, err error)
	WriteColumnWidth(row factsstore.ColumnWidthRow) (id uint64, err error)
	DeleteColumnWidth(appId app.AppIdT, tier string, scope string, columnKey string) (err error)
}

StoreI is the resolver's view of durable storage. It is exactly the column-width subset of factsstore.FactsStoreI, so a facts store satisfies it structurally and no adapter is needed; a test can supply a map instead.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL