The two caveats from the dependency investigation. ## The capabilities feature Camera and location attachments are now available behind `features = ["capabilities"]`, which pulls `nigig-uikit` and supplies `UikitAttachmentProvider`. Measured: 89 crates by default, 275 with the feature on. That cost is real and it is inherent, not packaging waste. `camera_widget` imports `send_geocode_request` and `request_map_tile` from `nigig-core`, both of which call `spawn_async` — the shared Tokio runtime — and the first makes an HTTPS call to Nominatim. A camera that geocodes needs an async runtime and an HTTP client; there is no lighter honest version. It is affordable because it is opt-in, and because any app enabling it already depends on `nigig-core`, so that app's own tree grows by nothing. Everything touching `nigig-uikit` is in one module, so the boundary is a file rather than `#[cfg]` scattered through the widget. The provider holds no widgets of its own: the host owns the `CameraWidget` already in its tree and this asks it to open, because a provider that instantiated a second camera would fight the first for the device. A second request while one is outstanding is refused rather than overwriting. The table turns that refusal into `AttachmentUnavailable`, so the user is told the camera is busy instead of watching their first request vanish. File picking is deliberately declined here — `robius-file-picker` already ships unconditionally and costs nothing, and two paths for one job is one too many. Two CI gates, both verified to fail when they should: the opt-in build must keep compiling, and the default build must pull none of `tokio`, `reqwest`, `hyper`, `clap`, `csv`, `image`, `nigig-uikit` or `nigig-core`. The second checks the resolved `cargo tree` rather than the manifest, because feature unification can switch an optional dependency on from a sibling crate. Tests 99 default, 105 with the feature. Both clippy-clean. ## The matrix_client defect Raised in REVIEWS/MATRIX_CLIENT_FEATURE_GATE.md rather than fixed. It is not my crate, nothing depends on the broken combination, and a blind fix could change behaviour someone relies on. `matrix_client` declares `native = ["dep:tokio", "dep:reqwest", "dep:rusqlite"]` but its source gates on `#[cfg(not(target_arch = "wasm32"))]`. Two switches for the same modules, so on a native target with the feature off the modules compile and their dependencies do not — 19 errors, 26 ungated uses across 7 files. There is no CI job for the crate, which is why it rotted unnoticed. The note corrects an overstatement I made while arguing for the trait hook. I said fixing this would unblock wasm. It would not: `matrix_client` already builds clean for wasm32 with `--no-default-features`, and `nigig-core` has 8 wasm errors of its own (`crate::platform::spawn` missing) that have nothing to do with it. The only broken combination is native-target-with-feature-off, which nothing builds. I also said earlier that `matrix_client` was heavy — it is a 7-dependency local crate, not matrix-sdk. That was wrong and it inflated the case for the trait hook; the note records the measured numbers instead.
511 lines
23 KiB
Markdown
511 lines
23 KiB
Markdown
# makepad-table + invoicer platform
|
||
|
||
A Notion-style manipulable table widget for [Makepad](https://github.com/makepad/makepad) **plus** a receipt/quote/invoice editing platform that exports to PDF.
|
||
|
||
The project follows a **parallel data model architecture**:
|
||
|
||
```
|
||
┌──→ Makepad UI (apps/invoicer) — editing + live preview
|
||
Document (struct) ──┤
|
||
└──→ PDF Exporter (crates/pdf-export) — vector PDF bytes
|
||
```
|
||
|
||
The Makepad UI and PDF exporter never talk to each other directly. They both speak the language of `makepad-doc-model`. This means:
|
||
|
||
- You can edit a document in Makepad and the same struct renders identically to PDF.
|
||
- You can write a CLI tool that converts JSON → PDF without ever launching the GUI.
|
||
- You can swap out the Makepad UI for a web UI later without touching the model or exporter.
|
||
|
||
## Workspace layout
|
||
|
||
```
|
||
makepad_table/
|
||
├── Cargo.toml ← workspace manifest
|
||
├── src/ ← the Table widget itself (Phase 1+2)
|
||
│ ├── lib.rs
|
||
│ └── table.rs ← the Table widget (3260 lines, all 6 phases)
|
||
├── crates/
|
||
│ ├── doc-model/ ← shared data types
|
||
│ │ ├── Cargo.toml
|
||
│ │ └── src/lib.rs ← Document, Invoice, Quote, Receipt, Party, LineItem, Money, TaxRate, Currency + JSON helpers + tests
|
||
│ └── pdf-export/ ← pdf-writer based renderer
|
||
│ ├── Cargo.toml
|
||
│ ├── src/lib.rs ← render_pdf(doc) → Vec<u8>
|
||
│ └── resources/
|
||
│ ├── Inter-Regular.ttf ← embedded as F1 (body)
|
||
│ └── JetBrainsMono-Regular.ttf ← embedded as F2 (numbers)
|
||
├── apps/
|
||
│ └── invoicer/ ← makepad UI app
|
||
│ ├── Cargo.toml
|
||
│ ├── src/main.rs ← sidebar + editor + Table + Export button
|
||
│ ├── examples/
|
||
│ │ └── table_demo.rs ← standalone Table widget demo (Cargo example)
|
||
│ └── resources/
|
||
│ ├── invoice.json ← sample data
|
||
│ ├── quote.json
|
||
│ └── receipt.json
|
||
└── examples/
|
||
└── table_demo/ ← standalone demo; the template
|
||
├── Cargo.toml the "drop into makepad" section
|
||
└── src/main.rs below refers to
|
||
```
|
||
|
||
All five packages belong to the nested workspace declared in the top-level
|
||
`Cargo.toml` here. `examples/table_demo` was previously a member of no
|
||
workspace at all, which meant `cargo` refused to operate in that directory
|
||
and nothing ever compiled it — it had drifted to a removed makepad API.
|
||
|
||
## Quick start
|
||
|
||
### Option A — Run the invoicer app
|
||
|
||
```bash
|
||
cd /path/to/makepad_table
|
||
cargo run -p makepad-invoicer --release
|
||
```
|
||
|
||
On startup it loads 3 sample documents (invoice, quote, receipt). Use the
|
||
sidebar buttons to switch between them, edit the header fields or any line-item
|
||
cell directly, and click "Export PDF" to write `~/invoicer/<doc_number>.pdf`.
|
||
|
||
### Option B — Use the model + PDF exporter as a library
|
||
|
||
```rust
|
||
use makepad_doc_model::{Document, sample_invoice};
|
||
use makepad_pdf_export::render_pdf_to_file;
|
||
use std::path::Path;
|
||
|
||
let doc = sample_invoice();
|
||
render_pdf_to_file(&doc, Path::new("invoice.pdf")).unwrap();
|
||
```
|
||
|
||
### Option C — Run the doc-model unit tests
|
||
|
||
```bash
|
||
cd /path/to/makepad_table
|
||
cargo test -p makepad-doc-model
|
||
```
|
||
|
||
Tests cover: invoice arithmetic (subtotal/discount/tax/total), currency
|
||
formatting including negatives and `i64` extremes, thousands separators at
|
||
every digit width, JSON round-trips for all 3 doc types, tax override
|
||
precedence, payment-method labels, the Phase 2 setters and their trimming,
|
||
currency lookup, percentage validation, and the Phase 3 document library
|
||
(filename sanitising, search, recents, save/reopen round-trip).
|
||
|
||
## Data model highlights
|
||
|
||
**Money** is stored as integer minor units (cents/pence) — never f64 — to avoid rounding errors in totals. `i64` is used because amounts can exceed `i32` range (e.g. `i32::MAX` cents = ~$21M).
|
||
|
||
**Tax rates** are basis points (8.5% = 850 bps). Same fixed-point arithmetic reasoning.
|
||
|
||
**Multi-tax** is supported per-line-item: each `LineItem` has an optional `tax_override: Option<TaxRate>`. When `None`, the document's `default_tax` applies. This handles VAT/GST/sales tax mixes where some items are zero-rated (e.g. resale of stock) or taxed differently (e.g. services vs goods).
|
||
|
||
**Multi-currency** via the `Currency` enum: 30+ preset currencies with correct symbols and decimal counts (`JPY`/`KRW`/`VND` have 0 decimals, everything else 2). User-defined currencies via `Currency::Other { code, symbol, decimals }`.
|
||
|
||
**JSON** round-trips work via `#[serde(tag = "kind", rename_all = "lowercase")]` on the `Document` enum, so:
|
||
|
||
```json
|
||
{ "kind": "invoice", "number": "INV-2024-001", ... }
|
||
```
|
||
|
||
deserializes to `Document::Invoice(Invoice { ... })` and re-serializes identically.
|
||
|
||
## PDF output (Minimal B&W)
|
||
|
||
- A4 portrait (595 × 842 pt), 50pt margins
|
||
- Black text on white, thin 0.5pt rules, faint zebra striping on alternating line items
|
||
- Inter for body text (12pt headers, 9-10pt body, 28pt title)
|
||
- JetBrains Mono for doc numbers, dates, qty, prices, totals
|
||
- Three document types share most layout; differences:
|
||
- **Invoice**: title "Invoice", shows issue + due dates, status label
|
||
- **Quote**: title "Quote", shows issue + valid-until dates, status label
|
||
- **Receipt**: title "Receipt", shows date-paid + payment method, "PAID" badge
|
||
- Per-line tax rate displayed as a percentage
|
||
- Totals block: Subtotal → Discount (if > 0) → Tax → bold rule → Total
|
||
- Footer: transaction ID (receipts only), notes, terms (invoice/quote only)
|
||
|
||
## Phase status
|
||
|
||
| Phase | Status | What |
|
||
|---|---|---|
|
||
| Table Phase 1 | ✅ Done | Static rendering: bg, grid, header, cells |
|
||
| Table Phase 2 | ✅ Done | Hover + cell editing via TextInput overlay (mouse + touch) |
|
||
| Table Phase 3 | ✅ Done | Row/column context menus — own overlay, hover, Escape, 15 actions |
|
||
| Table Phase 4 | ✅ Done | Drag-reorder columns with ghost + insert marker |
|
||
| Table Phase 5 | ✅ Done | LaTeX cells — set `TableColumn::kind = CellKind::Latex` |
|
||
| Table Phase 6 | ✅ Done | 3D cells — isometric wireframe preview from a text spec |
|
||
| **Invoicer doc-model** | ✅ Done | Document enum, all 3 doc types, multi-tax, multi-currency, JSON |
|
||
| **Invoicer PDF exporter** | ✅ Done | Minimal B&W, embedded Inter + JetBrains Mono, all 3 doc types |
|
||
| **Invoicer Makepad UI** | ✅ Phase 1 | Sidebar + header + totals + Table for line items + Export PDF |
|
||
| **Invoicer UI Phase 2** | ✅ Done | Editable header fields, currency + tax entry, doc switcher in sidebar |
|
||
| **Invoicer UI Phase 3** | ✅ Done | Search box, filtered document list, recents, platform file picker, New/Delete |
|
||
|
||
## Interaction model
|
||
|
||
| Gesture | Target | Result |
|
||
|---|---|---|
|
||
| Single click / tap | cell | selects it |
|
||
| Double click / double tap | cell | edits it |
|
||
| Single click / tap | row or column header | selects it and shows the resize handle |
|
||
| Long press (500ms) | row or column header | opens its context menu |
|
||
| Long press (500ms) | cell | opens the cell menu: copy, paste, clear, attach |
|
||
| Drag the handle | selected header | resizes that column or row |
|
||
| Drag the corner anchor | attached image | resizes the image; the row follows |
|
||
| Drag a column header | column | reorders it |
|
||
|
||
Mouse and touch run the **same** state machine — one `PressTracker`, one
|
||
long-press timer, one click handler. Only gesture recognition differs, because
|
||
it must: a finger reports Start/Move/Stop with no tap count and no hover, so
|
||
double-tap is derived from the interval between taps. They had previously
|
||
drifted, with long press working under a finger and not under a mouse.
|
||
|
||
A held press schedules its own frame. Neither a mouse nor a finger emits
|
||
anything while held still, so without that the long press would only fire if
|
||
the user happened to jiggle the pointer.
|
||
|
||
### The row gutter
|
||
|
||
A leading gutter numbers the rows 1..n and acts as the row header. It is not a
|
||
data column: it has no entry in `columns`, and cannot be reordered, renamed or
|
||
deleted. Numbers are derived from the row index every frame rather than
|
||
stored, so an insert or a reorder cannot leave them stale.
|
||
|
||
Every grid position is drawn whether or not it holds text — a row shorter than
|
||
the column list used to simply stop, leaving the remaining columns with no
|
||
background and nothing to click.
|
||
|
||
## Cell attachments
|
||
|
||
A long press on a cell offers copy, paste, clear, and three attachment
|
||
sources. A cell holding an image grows to fit it, capped so one tall photo
|
||
cannot make a row taller than the viewport.
|
||
|
||
Cells stay `String`. Attachments are a side-table on `TableData` keyed by
|
||
`(row, col)`, so a table without them costs nothing and still round-trips as
|
||
text. The keys are positional, so inserting a row or reordering a column
|
||
shifts them too — otherwise the data moves and the image stays behind.
|
||
|
||
```rust
|
||
use makepad_table::{CellAttachment, CellAttachmentProvider};
|
||
|
||
// The host supplies whatever the widget cannot reach on its own.
|
||
struct MyProvider;
|
||
impl CellAttachmentProvider for MyProvider {
|
||
fn capture_photo(&mut self, cx: &mut Cx, row: usize, col: usize) -> bool {
|
||
// open the camera; later call table.set_attachment(..)
|
||
true
|
||
}
|
||
}
|
||
table.set_attachment_provider(Box::new(MyProvider));
|
||
```
|
||
|
||
### Images
|
||
|
||
An attached PNG or JPEG is decoded and drawn for real, not shown as a
|
||
placeholder. Decoding reuses `pageflipnav/src/utils.rs::load_png_or_jpg` — the
|
||
Robrix-derived app in this repo — rather than a reinvention: sniff the header
|
||
with `imghdr` and call the matching loader, falling back to trying both when
|
||
the sniff says something unexpected. The sniff avoids a decode-and-fail on
|
||
the common path; the fallback means a mislabelled file is still decoded
|
||
rather than refused. `imghdr` has no transitive dependencies. The result is cached per cell — `draw_walk` runs every
|
||
frame and re-reading a photo from disk at 60Hz would dominate the widget.
|
||
|
||
The row grows to whatever the image will actually draw at, so the two can
|
||
never disagree and clip. Drag the corner anchor to resize: that pins an
|
||
explicit height which the aspect-ratio default no longer overrules, and the
|
||
row follows it. The floor is checked at compile time against the anchor size,
|
||
because an image dragged smaller than its own grab handle could not be
|
||
grabbed again.
|
||
|
||
Row heights are per row. `TableRow::height` holds a dragged override and
|
||
`RowGeometry` accumulates the boundaries, so resizing one row shifts the rows
|
||
below it rather than resizing all of them — which is what a single shared
|
||
`row_height` did.
|
||
|
||
PDFs and locations stay as labelled chips. A PDF page needs a rasteriser this
|
||
widget deliberately does not carry.
|
||
|
||
### The `capabilities` feature
|
||
|
||
Camera and location are available through an opt-in feature:
|
||
|
||
```toml
|
||
makepad-table = { path = "...", features = ["capabilities"] }
|
||
```
|
||
|
||
It supplies `UikitAttachmentProvider`, which drives the `nigig-uikit` camera
|
||
and location widgets the host already has in its tree.
|
||
|
||
**Measured cost:** 89 crates by default, 275 with the feature on. That is
|
||
real, and it is inherent rather than packaging waste — `camera_widget`
|
||
imports `send_geocode_request` and `request_map_tile` from `nigig-core`, both
|
||
of which call `spawn_async` (the shared Tokio runtime), and the first makes
|
||
an HTTPS call to Nominatim. A camera that geocodes needs an async runtime and
|
||
an HTTP client.
|
||
|
||
It is affordable because it is opt-in and because any app enabling it already
|
||
depends on `nigig-core`, so that app's own tree grows by nothing. CI checks
|
||
both halves: that the opt-in build still compiles, and that the default build
|
||
pulls none of `tokio`, `reqwest`, `hyper`, `clap`, `csv`, `image`,
|
||
`nigig-uikit` or `nigig-core`.
|
||
|
||
File picking is deliberately *not* part of it. `robius-file-picker` already
|
||
ships unconditionally and costs nothing; two paths for one job is one too
|
||
many.
|
||
|
||
**Why a trait underneath.** Camera and location live in `nigig-uikit`, which pulls in
|
||
`nigig-core` and with it tokio, reqwest, matrix-sdk and clap. That is the
|
||
wrong dependency for a table widget, so the widget names what it needs and a
|
||
host that already has those crates supplies them. A provider returning
|
||
`false` — including the default, which declines everything — makes the table
|
||
emit `AttachmentUnavailable` rather than leaving a menu entry that appears to
|
||
do nothing.
|
||
|
||
File picking is the exception and ships here: `robius-file-picker` is already
|
||
used by three crates in this repo, works through `rfd` on desktop and the
|
||
platform picker on Android, and brings none of that weight.
|
||
|
||
## LaTeX cells (Phase 5)
|
||
|
||
Set a column's `kind` and its cells render as maths instead of text:
|
||
|
||
```rust
|
||
use makepad_table::{CellKind, TableColumn};
|
||
|
||
let col = TableColumn {
|
||
id: "formula".into(),
|
||
title: "Formula".into(),
|
||
width: 200.0,
|
||
kind: CellKind::Latex,
|
||
..Default::default()
|
||
};
|
||
```
|
||
|
||
Cells stay `String`; the kind lives on the column, so tables written before
|
||
this existed are unaffected and a table still round-trips through plain text.
|
||
Rendering goes through makepad's `MathView`, which is already registered with
|
||
the script VM, rather than a second LaTeX layout path.
|
||
|
||
An expression that fails to parse is **not** blanked. `MathView` draws its own
|
||
`[reason]` marker, so a typo shows up in the cell rather than silently erasing
|
||
the content.
|
||
|
||
## 3D cells (Phase 6)
|
||
|
||
Set `kind: CellKind::Solid3d` and the cell text describes a solid, drawn as an
|
||
isometric wireframe:
|
||
|
||
```text
|
||
cube 10 20 30 a box with those extents
|
||
cube 10 a uniform cube
|
||
sphere 5 radius
|
||
cylinder 3 12 radius, height
|
||
```
|
||
|
||
Separators may be spaces or commas, shape names are case-insensitive, and
|
||
`box` and `cyl` are accepted as aliases. Every dimension must be finite and
|
||
greater than zero.
|
||
|
||
The cell stays a string, so a 3D column saves, loads and round-trips like any
|
||
other. A spec that cannot be parsed draws the reason — `[unknown shape:
|
||
torus]`, `[cube wants 3 args, got 2]` — rather than an empty cell, so a typo
|
||
is visible instead of looking like a rendering fault.
|
||
|
||
**Why a wireframe.** Shading needs a 3D pass with its own camera, depth buffer
|
||
and lighting shader; the CAD viewport elsewhere in this repo spends about
|
||
2,500 lines on exactly that. A cell forty pixels tall gains nothing from it.
|
||
Edges are projected isometrically and stroked with `DrawVector`, which needs
|
||
no pass of its own, and the drawing is scaled to fit and centred so a 1-unit
|
||
and a 1000-unit cube look identical in the cell.
|
||
|
||
Curved solids are drawn as rings rather than their full triangulation — a
|
||
40px cell cannot resolve hundreds of triangles.
|
||
|
||
## Document library and file handling (Invoicer UI Phase 3)
|
||
|
||
The sidebar has a search box, a filtered document list, a recents list, and
|
||
New / Open / Save As / Delete in the toolbar.
|
||
|
||
`DocumentLibrary` in `makepad-doc-model` is the layer underneath it —
|
||
browse, save, load, search — kept in the model so it is testable without a
|
||
window.
|
||
|
||
```rust
|
||
use makepad_doc_model::DocumentLibrary;
|
||
|
||
let mut lib = DocumentLibrary::new(8); // keep 8 recent paths
|
||
lib.push(makepad_doc_model::sample_invoice());
|
||
let path = lib.save_entry_to_dir(0, &dir)?; // writes <number>.json
|
||
let index = lib.open_path(&path)?; // and records it as recent
|
||
let hits = lib.search("globex"); // indices, in library order
|
||
```
|
||
|
||
### The file picker
|
||
|
||
File opening and saving go through **`robius-file-picker`**, the same crate
|
||
`nigig-build`, `nigig-pay-ui` and `nigig-sms` already use, rather than
|
||
makepad's own `FileDialog`.
|
||
|
||
That is not a preference. Makepad's `open_system_openfile_dialog` is
|
||
implemented **on macOS only** — the Linux and Android backends never handle
|
||
`CxOsOp::SelectFileDialog`, so the op is queued and dropped. The button
|
||
compiles, runs, and silently does nothing on the platform this repo targets.
|
||
CI gates against it regressing.
|
||
|
||
The picker's callback runs off the UI thread with no `Cx`, so it parks its
|
||
result and signals; `drain_file_picker` applies it on the next
|
||
`Event::Signal`. Same shape as the SMS bulk CSV import.
|
||
|
||
Two deliberate choices worth knowing:
|
||
|
||
- **Save says "Choose where to save…", not "Saved".** The dialog being open
|
||
is not the file being written, and claiming success before the write is a
|
||
lie the user discovers later.
|
||
- **Delete removes the row, not the file.** Removing an entry from a list is
|
||
not consent to delete a document off disk, and there is no undo here. The
|
||
status line says so.
|
||
|
||
### The document list
|
||
|
||
A fixed pool of 12 button slots rather than a `PortalList`, because this app
|
||
opens documents one at a time and the count stays small. The pool is honest
|
||
about its limit: anything past it shows as `+n more — narrow the search to
|
||
reach them` rather than being silently dropped.
|
||
|
||
Three things the library gets right that are easy to get wrong:
|
||
|
||
- **A document number becomes a filename**, and it is user-controlled text.
|
||
`safe_file_stem` replaces anything outside `[A-Za-z0-9._-]`, so
|
||
`../../etc/passwd` cannot steer a write out of its directory. Leading dots
|
||
are replaced too, and a fully-stripped name falls back to `untitled`.
|
||
- **An empty search matches everything**, so clearing the box restores the
|
||
list instead of emptying it. All terms must match, so each word narrows.
|
||
The haystack is type, number, both party names and every line description.
|
||
- **Recents de-duplicate and move to the front.** Without that, re-opening
|
||
one file fills the list with it and pushes everything else out.
|
||
|
||
## Building and testing
|
||
|
||
This is a **nested workspace**: it is named in the root manifest's
|
||
`workspace.exclude`, so `cargo build` at the repo root does not touch it.
|
||
Build and test it on its own:
|
||
|
||
```bash
|
||
cd crates/apps/makepad_table
|
||
cargo test # 145 tests: 99 widget, 36 doc-model, 6 invoicer, 4 pdf-export
|
||
cargo test --features capabilities -p makepad-table # 105: +6 provider tests
|
||
cargo clippy --all-targets -- -D warnings
|
||
cargo fmt -- --check
|
||
```
|
||
|
||
`makepad-widgets` is pinned to the same fork revision the rest of the repo
|
||
uses. It previously tracked upstream branch `dev`, which meant the same commit
|
||
of this repo could build against a different makepad from one day to the next.
|
||
|
||
### What the tests cover
|
||
|
||
The widget's geometry and index arithmetic are unit-tested without a GPU.
|
||
`Table` derives `Script` and `Widget` and cannot be constructed outside a live
|
||
`Cx`, so the logic worth testing lives in plain types alongside it —
|
||
`ColumnGeometry` for boundary and drop-position maths, `reorder_columns` for
|
||
the move itself. The widget forwards to both.
|
||
|
||
Focus handling, action emission and drawing are **not** covered: they need the
|
||
makepad test runtime and a display.
|
||
|
||
## Run the standalone Table widget demo
|
||
|
||
The Table widget has its own demo as a Cargo *example* of the makepad-invoicer package. It inherits invoicer's dependencies, so no separate Cargo.toml is needed — just run:
|
||
|
||
```bash
|
||
cargo run -p makepad-invoicer --example table_demo --release
|
||
```
|
||
|
||
Or from anywhere in the workspace:
|
||
|
||
```bash
|
||
cargo run --example table_demo --release
|
||
```
|
||
|
||
This loads a single Table widget seeded with the "Random table" data from the reference video (Role / Current Task / Deadline, 6 rows). Click any cell to edit, hover a row for the green handle, tap a row handle or column header to open its context menu, and drag a column header sideways to reorder it.
|
||
|
||
## Drop into makepad itself
|
||
|
||
To move the Table widget from this crate into the makepad repo:
|
||
|
||
1. **Copy** `src/table.rs` → `/path/to/makepad/widgets/src/table.rs`
|
||
2. **Change the import** at the top of `table.rs` from `use makepad_widgets::*;` to `use crate::{makepad_derive_widget::*, makepad_draw::*, widget::*};`
|
||
3. **Register** the module in `widgets/src/lib.rs`:
|
||
```rust
|
||
pub mod table; // near line 121 (chart module)
|
||
pub use crate::table::*; // near line 238
|
||
crate::table::script_mod(vm); // AFTER math_view, see below
|
||
```
|
||
4. **Run**:
|
||
```bash
|
||
cd /path/to/makepad
|
||
cargo run -p makepad-example-table --release
|
||
```
|
||
(`makepad-example-table` is the package in `examples/table_demo` here;
|
||
copy it across alongside the widget.)
|
||
|
||
Two things the widget now depends on from inside `widgets`, which matter if
|
||
you move it:
|
||
|
||
- **`MathView`** (Phase 5, LaTeX cells). It is already part of
|
||
`makepad-widgets` and registered by `widgets/src/lib.rs`, so inside the
|
||
makepad tree the `math_cell` field resolves without extra work — but the
|
||
registration order matters, because `Table`'s DSL body names
|
||
`mod.widgets.MathView` and the VM has to know that type first.
|
||
|
||
This is a real trap rather than a theoretical one: at the pinned revision
|
||
`crate::chart::script_mod(vm)` is line 611 and
|
||
`crate::math_view::script_mod(vm)` is line 617, so following step 3's
|
||
"put it next to chart" advice literally would register `Table` six lines
|
||
too early. Put `crate::table::script_mod(vm)` *after* the `math_view` call.
|
||
- **`DrawVector`** (Phase 6, 3D cells) from `makepad_draw`, which step 2's
|
||
import line already brings in.
|
||
|
||
Neither needs a Cargo feature: both are unconditional parts of
|
||
`makepad-widgets` at the pinned revision.
|
||
|
||
## Known limitations
|
||
|
||
This section used to list five things to watch for on "the first compile on
|
||
your machine", because the code had been written without a toolchain and had
|
||
never been built. It has been built since, and it is now in CI, so the three
|
||
predictions are settled: `KeyCode::Tab` is the correct spelling (not
|
||
`TabKey`), `#[live] cell_editor: TextInput` works as written without needing
|
||
`ComponentRef`, and the pdf-writer 0.15 calls compile as they stand.
|
||
|
||
What that first compile *did* find were three defects in the money code, all
|
||
fixed: a `usize` underflow in the thousands separator that panicked on most
|
||
numbers, a sign placed inside the currency symbol with a mismatched
|
||
fractional part, and test expectations that were a factor of ten low. See the
|
||
git history for the detail.
|
||
|
||
The genuine limitations that remain:
|
||
|
||
1. **Font metrics in the embedded PDF font descriptor** — the ascent, descent
|
||
and bbox values (`950.0`, `-250.0`, and so on) are rough. Real ones would
|
||
come from parsing the TTF's `head`/`hhea`/`OS/2` tables. Most PDF viewers
|
||
render correctly despite this; a pedantic one may warn.
|
||
2. **ToUnicode CMap** — the embedded CMap is a minimal identity mapping for
|
||
BMP codepoints. Full Unicode (emoji, supplementary planes) needs a
|
||
complete CMap generated from the font's `cmap` table, so copy-paste out of
|
||
a generated PDF will not round-trip those characters.
|
||
3. **Cell text width is approximated** at 7px per character for centre and
|
||
right alignment. It is wrong for anything but a monospace-ish Latin
|
||
string. Fixing it needs a real text measurer rather than a better guess;
|
||
the approximation is isolated in `align_text_x` and covered by tests, so
|
||
replacing it will produce a visible diff rather than a silent shift.
|
||
4. **3D cells are a wireframe preview, not a render.** No shading, no depth
|
||
sorting, no camera. See the Phase 6 section above for why.
|
||
5. **The widget's drawing and focus behaviour is not unit-tested** — only its
|
||
geometry and index arithmetic are. Anything needing a live `Cx` needs the
|
||
makepad test runtime and a display.
|
||
|
||
## License
|
||
|
||
MIT OR Apache-2.0. The bundled Inter and JetBrains Mono TTFs are under the SIL Open Font License 1.1.
|