Skip to content

Phase 7: GUI - #101

Draft
ejams1 wants to merge 18 commits into
mainfrom
phase-7-gui
Draft

ejams1 wants to merge 18 commits into
mainfrom
phase-7-gui

Conversation

@ejams1

@ejams1 ejams1 commented Oct 19, 2026 •

Copy link
Copy Markdown
Owner

Ports the upstream GUI (xeMainForm and 21 dialogs) onto the existing command layer with egui, per decision 1 of docs/PLAN.md. Step list and premises are in docs/PLAN.md Phase 7. Every action calls a session command; no new commands are added.

Landed so far

  • Step 1 and 1b: the crate, the main window, the command gaps
  • Step 2: the navigation tree
  • Step 3: the view tab's record grid, its folds, colours and edits
  • Step 4: the record panes and the element detail editor
  • Step 5: the selection dialogs
  • Step 6: the viewers and editors
  • Step 7: the settings and modal tools, the wait form and the job runner
  • Steps 8 (the menu and binding audit) and 9 (the gate) are open

Steps 3, 4 and 7 were parallel forks of main_form.rs from the same base and were integrated by hand; the merge notes are under step 3 in docs/PLAN.md. The integration also fixed the view tab's element detail panel, which was drawn inside the central panel and painted over the filter strip.

Validation

  • cargo check --workspace --all-targets, cargo clippy --workspace --all-targets, cargo fmt --check: clean
  • cargo test --workspace: 75 test binaries, 0 failures
  • cargo xtask check: the gui coverage has 661 entries and 0 pending
  • A live GUI run against the real Fallout 4 data loads Fallout4.esm and Fallout4.exe and shows the navigation tree, the view tab, the record panes and the element detail panel

🤖 Generated with Claude Code

ejams1 and others added 18 commits October 18, 2026 23:12
…form shell

New crate crates/xedit-gui and binary xedit-gui on eframe/egui 0.37 (added
to [workspace.dependencies]; the GUI draws through its own widgets). The
in-process CommandBridge mirrors xedit_cli::engine onto xedit_session's
Registry: call(name, json) and typed request<Req, Res> with the CLI,
serve and mcp request/response structs, and no new command. The initial
load is the CLI's startup step (Session::load in command::load_session,
on a stack-sized worker thread).

Ported forms: xeMainForm (main_form.rs) with pnlTop (bnMainMenu opening
pmuMain, bnBack/bnForward, lblPath, the pnlBtn project links with
pmuBtnMenu), pnlNav/vstNav with the splitter, pgMain's bottom tab strip
(View, Referenced By, Messages, Information, the three spreadsheets),
stbMain with the last message and the load gauge, FormShow/DoInit's
startup flow, AddMessage and the FormClose settings write; xeGameSelectForm
and xeModuleSelectForm as the minimal startup dialogs. 4.1.5q has no
TMainMenu, so the command surface is pmuMain and the nine context menus;
menu_model.rs holds all 191 items with their DFM Caption, ShortCut,
OnClick, Checked and RadioItem and the command of each covered ledger
binding, and a later step's item is drawn disabled with its step and
command in the tooltip.

Widgets: widgets/virtual_tree.rs and widgets/grid.rs (TreeSource/TreeRow/
TreeCell, GridSource/GridCell, pure selection/focus/edit state machines,
virtualised rows, column resizing), painted through widgets/conflict.rs
with wbColorConflictAll/wbColorConflictThis, RGBtoHSL/HSLtoRGB,
Lighter/Darker and wbLighter/wbDarker plus the ini overrides.
theme.rs applies UI.Theme, FontRecords and FontMessages; settings.rs
reads and writes the GUI keys of xeSettingsFileName through the port's
TMemIniFile (frmMain placement and pnlNavWidth, vstNavColumnWidth<i>,
UI fonts and theme, ColorConflictAll/ColorConflictThis, View/Nav keys),
keeping every other line. icon.rs decodes the DIB entries of the three
upstream .ico files per game family. startup.rs holds the game list
(GameModes order, tes4r refused with issue #80) and the module check
state with SimulateLoad.

Session: module_select::selectable_modules runs DoInitPath and
wbLoadModules for the dialog without a session (not a command), and
NoParams, Version, SessionInfo, SessionLoadRequest and SessionLoadResponse
gained the opposite serde derive for the typed bridge; no command or
schema changed. upstream-map.toml and README updated.

Verification: cargo fmt --all --check; cargo clippy --workspace
--all-targets -- -D warnings; cargo test --workspace green (47 GUI unit
tests, 6 tests/menu_model.rs tests that check every mni*:OnClick ledger
binding is modelled and carries the ledger's command); cargo xtask check
green (gui 661 entries, 0 pending). Smoke: the binary ran on a
non-interactive desktop (CreateDesktop + SW_HIDE) with --game sse and
--game sse --load Update.esm, alive 8 s and 25 s; the drawing is
unwitnessed until cargo xtask gui-shot lands.

Co-Authored-By: Claude Code <[email protected]>
`cargo xtask gui launch|windows|shot|keys|click|menus|close` drives a GUI
program on a private desktop for the gate's screen checks. A session is a
JSON file; a helper process (`gui session`, run from a copy of xtask) creates
the desktop, starts the program on it, holds the desktop open and serves
every request through files beside the session. Windows belong to their
desktop: a process that opens the desktop by name enumerates none of them,
so listing, PrintWindow capture (BitBlt fallback), key posting, clicking,
menu reading and closing all happen in the helper.

Verified against the 4.1.5q oracle `xFOEdit.exe -FO4` (launch, window list
showing TfrmMain and TfrmModuleSelect, screenshots of the main form, What's
New, the developer message and the module selection, `text:FO4` typed into
the module selection's filter with --control TLabeledEdit, caption clicks
for the DPI-clipped dialog buttons, close) and against dxdiag.exe. Findings:
4.1.5q has no Win32 menu (its File menu is pmuMain on the bnMainMenu speed
button, so `gui menus` reports no menu); notepad.exe on this Windows build is
the packaged app and exits with code 0 outside its package.

cargo fmt --all --check, cargo clippy --workspace --all-targets -D warnings,
cargo test --workspace and cargo xtask check are green (17 gui tests,
including the menu tree read from a window built for the test).

Co-Authored-By: Claude Code <[email protected]>
… need

The phase-7 reconnaissance found five holes below the GUI; this builds each as a
first-class typed command (schema, CLI route, edit gate, tests) with the upstream
semantics read from the Pascal.

modules.list (crates/xedit-session/src/module_select.rs, `xedit modules list
[--all] [--active] [--masters-present]`): `wbModulesByLoadOrder` as
`TfrmModuleSelect.ShowModal` and `TfrmModGroupEdit.vstModGroupItemsKeyDown` build
it, with every field `vstModulesGetText`/`vstModulesInitNode` read -- name and
original (`.ghost`) name, load order and FileID, path, size, `valid`, `loaded`,
`active`, `active_in_plugins_txt`, `masters_missing`, `has_index`, the
ESM/ESM-extension/light/medium/update/localized/blueprint/ghost/hardcoded flags,
the official/CC/plugins.txt/loadorder.txt indices, the masters and the CRC32.
`core/wbLoadOrder.pas`'s `TwbModuleInfo` gained those header flags
(`wbMastersForFile` through `MainRecordStructFlags`, incl. the `.esl` extension
rule of `wbLoadModules`) and `file_size`.

signatures.list (crates/xedit-session/src/filter.rs, `xedit signatures list`):
`TfrmFilterOptions.FormCreate`'s `wbRecordDefs` loop -- `DefaultSignature - GetName`
for every record definition, the base-record list only for
`ReferenceSignatureCount > 0`, both sorted by that text.

SessionLoadRequest.compare_to (`xedit session load --compare-to`): the "Compare
to..." load is a request field now, winning over the process's `--compare-to`
(consumed either way) and reported in the response. Found on the way: the CLI's
global `--compare-to` shadowed the subcommand flag through clap's global
propagation, so the startup flag is no longer global and both routes were run on
a synthetic folder.

settings.get/settings.set (crates/xedit-session/src/settings.rs, `xedit settings
get|set`): any `xEdit.ini` section and key read and written with the
`TMemIniFile` shapes (`ReadString`/`ReadInteger`/`ReadBool`/`ReadFloat`/
`ReadDouble`, `WriteBool` as 1/0, floats as `FloatToStr`, the file normalised as
`UpdateFile` does, the folder created as FormCreate's ForceDirectories does),
covering the GUI keys: `[frmMain]` placement, `pnlNavWidth` and
`vstNavColumnWidth<i>`, the per-form placement sections, `[ColorConflictThis]`/
`[ColorConflictAll]`, `[Nav] FilesSort`/`INFOSort` and `[View]`. `settings.set`
is mutating with a dry run.

Core callback seams (crates/xedit-core/src/interface/globals.rs):
`wbGetFormIDCallback` and `wbGetCellDetailsForWorldspaceCallback` with
`wb_get_form_id`/`wb_get_cell_details_for_worldspace`; `add.rs` asks them exactly
where `TwbGroupRecord.Add` does (`wbGetFormID(Self)` through
`LoadOrderFormIDtoFileFormID`, `Exit` on Null; the world-cell dialog's persistent
flag and grid cell, its cancellation adding nothing). With no callback installed
the headless default keeps today's behaviour (the world-cell parameter error, a
new FormID), which every existing front end relies on.

Ledger notes (the two dialogs' data sources, the compare load), upstream-map
notes (wbLoadOrder, wbInterface, the four forms, xeMainForm), PLAN step 1b,
using-xedit-cli rows.

Verification 2026-10-18/19: cargo fmt --all --check, cargo clippy --workspace
--all-targets -- -D warnings and cargo xtask check green (gui 661/0 pending,
switch 156/2, game_mode 14/4, tool_mode 17/0, script 3663/0, units 172/49);
cargo test --workspace green except the known environmental
copies_a_skyrim_npc_as_an_override, which fails identically with these changes
stashed; cargo xtask parity roundtrip --file Fallout4.esm: 2 cases (fo4, fo4vr)
both structure-edited (every record the input's, the OFST data dropped, groups
112381 (728 empty) -> 112367 (727 empty)), 0 different, 0 failed -- the
documented expected class without a cached oracle save. Live smokes on the SSE
install: modules list (Skyrim.esm 249752131 bytes, official index MIN, Update.esm
-1, Dawnguard 0, CC cc_index 2/17/27), signatures list (134 definitions, 30
base), settings get/set on a temp ini, both compare-to routes. Tests:
crates/xedit-session/tests/gui_commands_batch4.rs, 7 green (three runs).

Co-Authored-By: Claude Code <[email protected]>
Ported xeModuleSelectForm, xeModGroupSelectForm, xeModGroupEditForm,
xeFilterOptionsForm, xeFileSelectForm and xeGameSelectForm as one module
per form under crates/xedit-gui/src/dialogs/, over one shared
selection.rs (NameFilter with the PerlRegEx case-less mode and the red
box, PresetCombo with the acPreset*Update rules and disabled reasons,
Checklist with the selected-or-all bulk actions, the owner-drawn search,
Question/show_question, fit_size and DialogPlacement).

The module dialog draws the eight vstModules columns (Filename, File
Order from LoadOrderDescription, ESM, Light, Load Order, FormID Prefix,
Update, Med), the master children with CheckStateForModule, the regex
edFilter, Select All/None/Invert, the modules.presets/preset_* combo
with <plugins.txt> and SimulateLoad's miLoadOrder/miFileID slot
assignment; its tree is modules.list, served at startup by a session
over no files after selectable_modules primes the game globals, and OK
hands session.load the full paths. The mod group selection draws the
group/item/CRC32 tree with its presets and MinSelect/MaxSelect errors
and confirms through modgroups.select; its edit flow opens
modgroups.show in the edit form (CRC32 question, CheckState, the six
flag columns, CRC32 children, Delete/Insert questions, Ctrl+Up/Down,
Ctrl+C, Ctrl+N, Space) and sends modgroups.edit. The filter options
form holds the two conflict lists, the option check boxes and texts,
the signatures.list check lists, the preset combo over filter.presets,
settings.get and filter.preset.save|delete, writes View|LastUsedFilter,
asks the changed-preset question from the dry run and runs filter.apply.
The file selection is the searchable check list with Backup plugins; the
game selection gained NoEscape, placement and the highlight and keeps
the --game bypass. main_form intercepts filter.apply, modgroups.select
and modgroups.edit to open the dialogs, and the screen-check switches
--dialog/--dialog-arg plus the XEDIT_GUI_* env overrides open one
without the menus. The typed bridge needed additive Serialize/
Deserialize derives on the step-1b request/response structs; the
virtual tree gained the additive check/column/expander/header clicks
and focused_column. Placement is written through settings.get/set under
the upstream form names (PORT-EXTENSION: 4.1.5q keeps none for these
six forms), clamped to the desktop.

Verification: cargo fmt --all --check; cargo clippy --workspace
--all-targets -- -D warnings; cargo test --workspace green except the
known environmental copies_a_skyrim_npc_as_an_override; xedit-gui 87
unit tests plus 4 install-backed tests (FO4 modules.list and
modules.preset_* round trip, SSE signatures.list and the
filter.preset.save -> settings.get -> FilterLoadPreset round trip, a
private mod group file through modgroups.list/show/edit, placement);
cargo xtask check green (gui 661/0; units 172 tracked, 43 pending; the
six units are ported in upstream-map.toml). Screen checks with cargo
xtask gui on the private desktop: all six dialogs against the FO4
install in M:\projects\scratch\p7-step5-evidence, plus the 4.1.5q
module selection from xFOEdit.exe -FO4.

Co-Authored-By: Claude Code <[email protected]>
Port of xEdit/xeMainForm.pas's vstNav into crates/xedit-gui/src/nav_tree.rs:
the lazy node tree (AddFileInternal roots in load order, vstNavInitChildren
and vstNavInitNode's grouping: a parented group of types 1/6/7 and, under
wbVWDAsQuestChildren, 10 shows below its record and its own node carries
ivsHidden), vstNavGetText's cells for groups, main records, the file header
and files, vstNavChange's header titles and path, the conflict colours via
widgets::conflict, ReInitTree/NavUpdate, Container.GetAddList and the
action of every pmuNav, pmuNavHeaderPopup and pmuNavAdd item.

Conflict statuses are asked for the painted rows in batches of 200 per
frame through conflicts.list's new records field (conflict_statuses_of),
so expanding a large master does not scan it. filter.apply's response now
carries the surviving node element ids and the InheritStateFromChildren
statuses; the GUI installs them as the Conflicted and Filter views (a
filtered tree builds only the survivors, as the filter deleted the other
nodes), and filter.remove re-runs ReInitTree. lblFilterHint, edFormIDSearch,
edEditorIDSearch, edFileNameFilter and cbRegExFilter are ported with the
upstream colours and keyboard behaviour.

selection.rs is the shared GetSortedSelection(True) plus focused node
model (records with their file, signature, editor ID and name), refreshed
every frame for the view grid of step 3. Menu items dispatch the ledger
command with the upstream selection semantics; items whose handler opens
a dialog of step 5, 6 or 7 call a named Placeholder in dialogs/mod.rs with
its tooltip. mniNavAdd lists Container.GetAddList and each entry calls
elements.add.

Verification: cargo fmt --all --check; cargo clippy --workspace
--all-targets -- -D warnings; cargo test --workspace green except the known environmental failure
`copies_a_skyrim_npc_as_an_override` (`xedit-session/tests/structure.rs`,
the same on the phase branch before this step) (xedit-gui: 53
unit tests, including the schema check of every navigation command's
request and the dispatch of every covered menu item, 6 menu-model tests,
and 5 headless tests over SSE Update.esm: roots and load order, the
header/groups/records cells, a parented group below a CELL, a record
selection, the add list, and the filtered tree keeping only the survivor
CELL/WRLD records and groups); cargo xtask check (gui 661 entries,
0 pending). Witnessing: cargo xtask gui launch/windows/shot/click/close
finds the window (the title and icon are the app's viewport commands) but
the wgpu client area captures black on the private desktop, so the drawing
is unwitnessed.

Co-Authored-By: Claude Code <[email protected]>
The GUI now also carries step 5's dialog tests; the note says 53 tests of
this step and 99 at the rebased tip.

Co-Authored-By: Claude Code <[email protected]>
Port xeLogAnalyzerForm, xeScriptForm, xeViewElementsForm, xeRichEditForm,
xeLocalizationForm, xeDeveloperMessageForm, xeTipForm, xeScriptHost and
xeLocalizePluginForm as one module each under crates/xedit-gui/src/dialogs/,
with the shared split tree+memo viewer and the shared RTF/text viewer (the
RTF parser, BuildTOC, zoom, search and the virtualised paragraphs) in
dialogs/viewers.rs. The release's own developer messages, tips and What's
New text are decompressed from the 4.1.5q DFMs into resources/ (MPL 2.0);
the 200x200 developer bitmap is decoded by the form.

Command layer (CLI routes stay `xedit call`, no new command names): script.list
gains `subdirectories` (soAllDirectories with relative names), script.read
returns a script's text and unit name, script.save ports btnSaveClick (tabs
to two spaces, .backup.<timestamp> copy, CRLF lines), script.run takes the
editor's `source` over the file as ApplyScript(aScriptName, aScript) does,
and the log_analyzer/localization/settings/script/elements response and
request types gain the Deserialize/Serialize derives the typed GUI bridge
needs.

The main form hosts the dialogs, routes the four menu command names, opens
the developer message and tip after a load as DoInit does, persists the
forms' settings through settings.get/settings.set, and `--dialog <name>`
opens one dialog alone for the screen check (presentation only).

Landed on phase-7-gui with steps 5 and 2 kept. The CLI stays step 5's one
`--dialog <String>` (plus `--dialog-arg`): a name DialogKind knows opens its
viewer or editor alone, while filter|modgroups|modgroup-edit|files still open
through open_pending_dialog once the session is loaded; TfrmMain::new keeps
its three arguments and pending_dialog is an Option<String>. Both dialog
hosts, both menu route tables and both post-load paths are kept, and step 2's
tree, selection, run_session_command and confirm_window run beside the
step-6 dialogs. The screen-tool helper (`cargo xtask gui`) no longer inherits
the caller's stdio, records a panic in the session's .error file instead of
writing to the closed stderr, catches a panic in run_session and in each
request, and holds the desktop after one, so a later shot no longer finds a
dead desktop.

Tests: 92 GUI unit tests (script combo/filter, analyzer tree building and
sort, view-elements tabs/find/compare, localization table model, tip
rotation, RTF parser/TOC/search, script host) and 3 new session script
tests (subdirectory listing, read, save backup/CRLF); fmt, clippy
-D warnings and xtask check green; the workspace test run is green apart
from the recorded script elapsed-time flake under load
(the_string_list_surface_works printed 00:01 where it expects 00:00 and
passes alone). Screen checks captured seven dialogs on
private desktops under M:\projects\scratch\p7-step6-evidence, and rich-edit
is witnessed at the rebased tip (rich-edit-fixed2.png shows the What's New
document with its search, TOC and zoom strip); `cargo xtask gui
launch|windows|shot|close` also ran with the launching pipe closed as soon
as launch returned.

Co-Authored-By: Claude Code <[email protected]>
…ting

`crates/xedit-gui/src/screenshot.rs` adds `xedit-gui --screenshot <path>
[--screenshot-frames <n>] [--screenshot-delay <ms>] --exit`: the app takes its
own capture through egui/eframe's screenshot path
(`ViewportCommand::Screenshot` -> `ColorImage`) and writes it as 8-bit RGBA
PNG through the `png` crate (MIT OR Apache-2.0, already in the lock file via
epaint). The capture waits for the screen the switches name - the load's
completion with `--load`, the game or module selection without it, the
`Fatal:` screen of a failed load - and `--dialog <name>` opens its dialog
inside the same `finish_load` step, so the dialog is in the captured frame.
Two painted frames follow by default (`--screenshot-frames`),
`--screenshot-delay` adds a wall-clock wait, and `--exit` closes the window
once the PNG is written; a screen that is not ready after 180 s closes
without one, so a test cannot hang. egui 0.37 runs `App::ui` only on repaint
requests and `logic` alone while hidden, so the load result and the capture
state machine are polled from `logic` too, and
`ViewportCommand::RequestPaintWhileHidden` asks for the painted frames the
switch counts.

Idle efficiency: the loaded window had been repainting continuously (a full
core, hidden and idle) because `ViewportCommand::Title`/`Icon` were sent every
frame and every `send_viewport_cmd` requests a repaint of its viewport
(`Context::send_viewport_cmd_to`). They are now sent once (`window_titled`),
and the repaint policy is documented on the `eframe::App` impl. Measured on
the private desktop, hidden, idle after `--game sse --load Update.esm`, plain
`Get-Process.TotalProcessorTime` samples over 10 s: before 11.125
CPU-seconds (1.113 cores), after 0.000 CPU-seconds (0.000 cores).

Witnessed by the app's own captures on the private desktop (SSE and FO4
installs via `M:/projects/parity-env.sh`), all in
`M:\projects\scratch\p7-shot-evidence` and none blank: `sse-main.png`
(Update.esm, bare shell; an unknown `--dialog main` suppresses step 6's
first-run developer message), `sse-main-developer-message.png` (the same load
with that message), `sse-dialog-filter.png`, `sse-dialog-modgroups.png`,
`sse-dialog-files.png`, `sse-module-select.png`, `game-select.png` (captured
with `--screenshot-frames 5 --screenshot-delay 2000`), `fo4-main.png`
(Fallout4.esm), `fo4-main-developer-message.png` and
`fo4-dialog-filter.png`. A step-6 `--dialog` name is not capturable alone
yet: the dialog-only path's `show_dialogs` consumes the pending name on a
Loading pass, so `finish_load` sees none and `startup_dialogs` opens the
developer message over it (`--dialog tip` proved it); that is a step-6
ordering bug, recorded in the skill and PLAN notes. The capture recipe and
the idle-repaint rule are in `.claude/skills/checking-parity/SKILL.md`.

Verification: `cargo fmt --all --check`; `cargo clippy --workspace
--all-targets -- -D warnings`; `cargo test --workspace` (the only failure was
the documented timing-sensitive
`a_child_group_is_walked_through_its_record_once`, which passes in isolation
in 0.43 s; the known environmental
`copies_a_skyrim_npc_as_an_override` was not reached under fail-fast);
`cargo xtask check` (gui 661/0, switch 156/2, game_mode 14/4, tool_mode 17/0,
script 3663/0, units 172/43).

Co-Authored-By: Claude Code <[email protected]>
Ports the View tab of xeMainForm.pas: DoSetActiveRecord (one column per
version of a record, headed by the file name), DoSetActiveRecord(aMainRecords)
(one column per selected record, headed EditorID [FileName]), the filler
column when more than one record or none is shown, InitNodes/InitChildren
rows (MemberCount + AdditionalElementCount and their children), ExpandView
with per-node folds, InitConflictStatus colours through widgets::conflict,
vstViewBeforeCellPaint's per-cell fill, vstViewPaintText (including the
sibling comparison's ctMaster/ctOverride/ctConflictLoses painting),
vstViewGetText (value, summary of a collapsed row, focus-dependent label),
vstViewNewText/vstViewEditing (in-place edits of editable cells),
vstViewHeaderClick/UpdateColumnWidths (standard and fit-all widths) and the
bnBack/bnForward selection history (History, CheckHistoryAdd).

The model is built from the command layer: records.compare for one record,
records.compare_selected for the sibling comparison, whose CompareCell now
also reports the element path elements.set takes (the names the view shows,
such as CNAM - Author, resolved back to the element before they are
reported), EditValue, DisplayName, Summary, DisplaySortKey, IsEditable,
Modified, Name and the cell's own ConflictAll, and whose CompareRow reports
Node.Index and the definition member name elements.add takes. The view never
reads the session directly. A local FormID of zero now resolves to the file
header (File.Elements[0]) in record_by_load_order_form_id (with session.info reporting each file's
load order file id as file_ids, -1 for the hardcoded executable), the record
a selected file row shows and the only record the group index does not hold.

The context menus pmuView and pmuViewHeader are drawn per cell and per
header column with the pmuViewPopup applicability rules; the items run the
commands their ledger entries name: elements.set (Edit and Reset structure),
elements.add, elements.remove, elements.clear, elements.remove_from_records,
elements.change_member (Next/Previous Member), elements.copy_to_records
(both copy items), elements.move (Move Up/Down), records.copy (the six
copy-into items, through a small target-file dialog until step 5's
xeFileSelectForm), records.delete (Remove), records.set_hidden (Hide and
Unhide all), modgroups.create (Create ModGroup, through a small name dialog
until step 5's xeModGroupEditForm), modgroups.select (Reload ModGroups),
records.compare_selected (Compare referenced records and Sort by this row of
a sibling view), refs.get for the referenced records, records.compare for
Hide no conflict and Master and Leafs, and elements.get for the clipboard
items that need a full path or a signature. After every edit the view is
built again (PostResetActiveTree) and the focus follows the edited element.

fpnlViewFilter is wired: the name and value texts, and/or and the three keep
boxes go to the command's ApplyViewFilter. A navigation tree file row shows
its header record (vstNavSelectionChanged's _File.Elements[0]); the tree takes
the keyboard as vstNav does (Tab reaches the tree and its rows, the arrows walk
the rows, Enter activates -- vstNavKeyDown); a panic in the GUI thread writes
xedit-gui-panic.log beside the binary. The shared Selection model of step 2 drives the view: show_nav_selection maps
Selection::active_records (and a focused file node to its header record) into
set_active_records, the only interface the view consumes, so no selection
state is duplicated.

Verification: `cargo fmt --all --check`, `cargo clippy --workspace --all-targets -- -D warnings`, `cargo test --workspace` (the GUI crate: 114 unit tests after this branch's rebase over steps 1b, 2 and 5 (step 3 contributes 55: 10 for the view model, 9 for the grid, 7 for the tree including one that Tabs onto a row), 6 menu-model and 4 selection-dialog integration tests, the conflicts and filter session tests, all green) and `cargo xtask check` (gui 661 entries, 0 pending). Integration (`crates/xedit-gui/tests/view_grid.rs`, skipped without `XEDIT_SSE_DATA`/`XEDIT_FO4_DATA`): an Update.esm override (MISC 0000000A Lockpick) gives two columns headed by file name with the Update.esm column active and definition rows with element paths; the Update.esm header's `CNAM - Author` cell goes through `ViewSource::set_cell` and `elements.set` and reads back with `elements.get`; a navigation `Update.esm` row shows the header's rows through `TfrmMain::activate_nav_file` and the hardcoded `SkyrimSE.exe` row refuses; the Fallout4.esm header shows its Record Header/HEDR/member rows in one column without a filler. Screen check (`cargo xtask gui`, 2026-10-19, evidence in `M:\projects\scratch\p7-screen-evidence\view-step3`): the main window over Skyrim.esm + Update.esm (title `SSEEdit 0.1.0`), the `pmuMain` popup opened with Tab+Enter, the Messages tab, and the View tab with its filter strip. The tooling's posted mouse clicks are not delivered to eframe/winit (a click on `bnMainMenu` does nothing while Tab+Enter opens `pmuMain`), so a navigation row could not be clicked and the grid's cells are proven by the headless tests and the CLI dump rather than by a screenshot. The screen check found two real bugs, both fixed: `then_some(column - 1)` and `then_some(widths.len() - 1)` evaluate eagerly and underflowed at the label column and at an empty header (a panic the first time the View tab drew), and the navigation tree now gives a Tab-focused row the tree's arrows and Enter (`vstNavKeyDown`).

Co-Authored-By: Claude Code <[email protected]>
New crates/xedit-gui/src/panes.rs and element_detail.rs, wired into
main_form.rs over step 2's shared Selection. Referenced By: lvReferencedBy
with fpnlReferencedByFilter and pmuRefBy (ApplyReferencedByFilter through
refs.get's filter fields, ApplyReferencedByAlphaSort with the
Record->FormID and File->RawFileName click cycle and the upstream captions,
UpdateReferencedByTabCaption, pgMainChange's lazy population,
Ctrl+A/Ctrl+C/Delete, double-click jump and the covered commands
records.compare_selected, records.copy (with disable), records.delete,
elements.mark_modified, records.set_visible_when_distant and script.run).
Messages: AddMessage, mmoMessagesDblClick's FormID parse and pmuMessages
Clear/Save selected/Autoscroll. Information: the Memo1 text verbatim from
the DFM, read only and wrapped. The localization view of the selected
record through localization.files/localization.get. The three
spreadsheets: the DFM columns in Position order, the Skyrim column tweaks,
vstSpreadSheet*InitNode's per-game element mapping, header-click sorting,
every row of the group, shown only for Oblivion and Skyrim. Element detail:
the rules xeElementDetailForm leaves to the view (vstViewEditing,
vstViewGetEditText/NewText/DblClick, vstCreateEditor, mniViewEditClick,
wbConvertIntFormID) as an inspector whose every commit is elements.set.
The navigation selection's focused (else first) main record becomes the
active record, and a pane jump reveals the record in the tree.

Grid gained header clicks and cell-click focus; menu_model::draw_menu is the
shared popup drawer and the form's presentation items run when clicked.
Session::file falls back to the file registry so a command can name a master
the plugins loaded with (records.list --file Skyrim.esm answered "not
loaded" although files.list shows it; the spreadsheet lists the masters'
records now); RecordRef gained name, and ElementNode's optional fields
deserialize without them. XEDIT_GUI_TAB opens the window on a tab for the
screen check (the private desktop delivers no mouse input to the egui
window), alongside the existing XEDIT_GUI_SCALE/DPI_UNAWARE/WINDOW/LOG.

Verification: cargo fmt --all --check; cargo clippy --workspace
--all-targets -- -D warnings; cargo test --workspace (1029 passed, 0 failed;
the GUI crate 162 unit tests, 13 of them this step's); cargo xtask check
(gui 661 entries 0 pending, units 172 tracked 42 pending); tests/panes.rs
5/5 over DLCworkshop01.esm read from its bytes with a stub master and a
synthetic Skyrim SE plugin (referenced-by filter/sort, the element editor's
dry run and commit, the spreadsheet rows, the localization view, a master
named as a file); cargo xtask gui screenshots on the private desktop of the
Messages log, the Information memo and the Weapon Spreadsheet (2509 records
of Skyrim.esm and Update.esm).

Co-Authored-By: Claude Code <[email protected]>
…ob runner

Ports xeWaitForm (IwbWaitForm/IwbProgress, TwbNullWaitForm/TwbNullProgress,
NullCreateWaitForm, the fwDoUpdate delays, GetIsCanceled, TwbProgress's
UpdateStatus), xeOptionsForm (the General/View/Cleaning/UI Settings/UI
Theme/Experts tabs), xeLODGenForm (the worldspace checklist and option
controls), xeLegendForm (the conflict colour grid), xePushLikeButton,
xeEditWarningForm and xeWorldspaceCellDetailsForm, with the GetFormID
prompt beside it.

Core: crates/xedit-core/src/interface/wait_form.rs is the progress seam.
wbCreateWaitForm answers the null form headless (the CLI is unchanged);
GeneralProgress/poll_cancel is the `if wbForceTerminate then Abort` check
and Cancelled is EAbort. The commands poll at upstream's points:
filter.apply (status pre-walk, pass 1, pass 2 -> FilterReport.cancelled),
files.clean (between passes and the cancelled cleaning filter), refs.build
(per file), lodgen.generate (per worldspace, and per reference in the FO4
and Oblivion generators), session.load (per plugin, per module's archives
and around the resources cache). A stop is the `cancelled` error.

GUI: job_runner.rs runs a command on a worker thread with the session
moved to it and handed back; dialogs/wait_form.rs is TfrmWait with the
progress list and a working Cancel. main_form routes filter.apply,
refs.build, files.clean (the UDR/ITM items, which widgets::menu_command
now reports by item id), the LODGen dry run and run and the startup
session.load through it, keeps the window repainting and disables the
menus while one runs. dialogs/options_form.rs is every tab with its keys
read and written through settings.get/settings.set, apply_globals for the
wb* switches, and the live main form taking theme, fonts, colours,
column/row sizes and the shrink flag. dialogs/lodgen_form.rs is the
checklist and options from a lodgen.generate dry run with the per-game
enable rules. dialogs/legend_form.rs is the 7x12 colour grid with
[frmLegend] placement. widgets/push_like_button.rs is the checkbox-style
button state machine. dialogs/edit_warning_form.rs counts down with
[Usage] EditWarnCount. dialogs/prompts.rs installs both core callbacks and
hands the synchronous call to the frame loop (NextObjectID is restored on
a cancelled FormID prompt); dialogs/worldspace_cell_details_form.rs and
dialogs/form_id_prompt.rs are the dialogs.

Verification: cargo fmt --all --check, cargo clippy --workspace
--all-targets -- -D warnings and cargo xtask check green (gui 661/0,
units 172 tracked 36 pending; the seven units are ported in
upstream-map.toml), cargo test --workspace green. GUI 133 unit tests plus
tests/step7.rs 9 tests: wbForceTerminate and a wait form cancel stop
filter.apply at their poll points, files.clean/refs.build/lodgen.generate
return cancelled, the options round trip through settings.set ->
settings.get -> state_from_values, both prompt seams answered. Screen
checks on the private desktop (FO4): options-{general,view,cleaning,ui,
theme,experts}.png, lodgen.png, legend.png, edit-warning.png,
worldspace-cell.png, formid.png, wait-form.png, cancel-early.png
("Loading cancelled" and the module selection back), logs beside them in
M:\projects\scratch\p7-step7-evidence.

Co-Authored-By: Claude Code <[email protected]>
Steps 3, 4 and 7 were three parallel forks of `crates/xedit-gui/src/main_form.rs`
from the same base (the step-6 tip), so they could not be fast-forwarded onto
each other. This merges them on the phase branch.

Step 3's view grid wins the conflicts: its grid carries the tree rows, the
per-cell conflict fill, the header context menu and the `(column, shift)`
header click, which step 4's simpler grid did not have. Step 4's record panes
(`panes.rs`, `element_detail.rs`) are grafted onto it and adapted to the
step-3 grid API; step 7's job runner, wait form and core-callback prompts go
on top of both.

`DoSetActiveRecord` had two halves by then, so the names are split: the view
grid keeps `set_active_record` (`ViewRecord`) and the panes get
`set_pane_record` (`RecordSelection`). The message log is now the step-4
`MessagesPane`, so the view's own message items moved with it.

`cargo check --workspace --all-targets`, clippy, `cargo fmt --check`,
`cargo test --workspace` (75 test binaries, no failures) and `cargo xtask
check` are green.

Co-Authored-By: Claude Code <[email protected]>
The element detail editor was shown as a right panel inside the central
panel's closure, so egui gave the central area no room for it and the
panel painted over the view's filter strip. It is now a side panel of
the tab area, beside the central panel.

The filter strip is a wrapped row, like upstream's `fpnlViewFilter`
flow panel, so the filter controls wrap instead of running under the
detail panel.

Co-Authored-By: Claude Code <[email protected]>
The GUI is at step 7 of 9, not step 1: the selection dialogs, the
viewers and editors and the modal tools are all in place, and the
screenshot of each dialog confirms that it renders.

Co-Authored-By: Claude Code <[email protected]>
@ejams1
ejams1 added this pull request to stack #106 October 20, 2026 22:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants