Resizable panes, zoom, and a window that opens where it was left - #19
Merged
Merged
Conversation
…t [minor] Four changes to how the editor is arranged and looked at. AstGraph.SeparateOverlaps is gone. ktsu.ForceDirectedLayout 3.18.0 does that job in the simulation itself, so the application was undoing overlaps the library had already resolved. The editor test that covered it stays, asserting the rectangles directly: the separation only works when a node's measured size reaches the simulation, and that wiring is still this application's. The panes are divider containers, so the user sizes them rather than the application. Properties and the code preview are stacked in one column with properties on top: they are read at different times and want different shapes, and sharing a column gives each the full width and lets the user decide the split. The editor no longer draws an inspector of its own here — it still can, for a host that wants one — so the toolbar's Inspector checkbox goes, the divider being a better way to do the same thing. The graph can be zoomed, and "Fit to canvas" now zooms as well as centres. The node editor underneath has no zoom, so the scaling is applied to the engine's positions and the font together, put on before the frame is drawn and taken off after: the simulation is never asked to work in a space that changes under it, and a node dragged while zoomed lands where the pointer was. Fitting picks the largest zoom the whole arrangement still fits at, and never magnifies — a graph that already fits is shown at its own size. The window and the pane splits are remembered between runs, alongside the recent files and the preview language, since they are all things the user arranged rather than settings the application should keep choosing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QwzCFb8zhd263sZeRbF7ba
KTSU0006: the maximized state is Silk.NET.Windowing.Common's, not Silk.NET.Windowing's — the facade beside it. The reference was on the wrong one of the two, so the type was still being used transitively. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QwzCFb8zhd263sZeRbF7ba
|
matt-edmondson
deleted the
claude/auto-layout-expression-editor-ugcdvj
branch
September 8, 2026 05:04
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Four changes to how the editor is arranged and looked at.
AstGraph.SeparateOverlapsis gonektsu.ForceDirectedLayout3.18.0 resolves overlapping node boxes in the simulation itself (ktsu-dev/ImGuiApp#351), so the application was undoing overlaps the library had already undone. The method, its four constants, the editor's call to it and its two unit tests all go.Editor_PullsOverlappingNodesApartAsItRunsstays, with its assertion rewritten to check the rectangles directly rather than to call the removed method. It is still worth having here: the separation only works when a node's measured size reaches the simulation, and that path — the renderer measuring, the editor writing the measurement into the engine — is this application's wiring, not the library's.One comment was corrected rather than left:
Separated()'s remark said two coincident nodes stay coincident for ever because repulsion has no direction between them. That was true when it was written and is not any more. The nudge is still worth doing — a node that appears exactly on top of the last one and then slides out is worse than one placed clear — so the code stays and the reason is now the real one.The panes are divider containers
The layout is
ktsu.ImGui.Widgets'DividerContainer, so the user drags the splits rather than living with the application's. A columns container holds the graph and a side column; the side column is a rows container holding properties on top and the code preview below.Stacked rather than side by side because they are read at different times and want different shapes: properties are a short column of labelled rows, generated source is lines read downwards. Sharing one column gives each of them the full width and lets the user decide the height split.
AstGraphEditor.DrawInspectoris now public so a host can place the panel itself;ShowInspectorstays for a host that just wants an editor, and this application turns it off. The toolbar's Inspector checkbox is removed — with the panel a draggable pane, a checkbox is a second and worse way to do the same thing.Zoom, and "Fit to canvas" that fits
The node editor underneath has no zoom of its own and reads each node's position straight out of the engine, so the only place a zoom can be applied is the engine. Applying it permanently would put the simulation in a space that changes whenever the slider moves — rest length, repulsion distance and overlap margin are all lengths, and none would mean the same thing afterwards.
So the scaling goes on before the frame is drawn and comes off after. Between those two calls the engine holds view positions, which is what gets drawn and what a drag is read back in; outside them it holds the graph's own, which is what the layout runs on. The font is scaled to match, because a node's box is sized from the text inside it — without that, zooming out would only pack the nodes closer while they stayed the same size.
Taking the transform off is not simply the inverse of putting it on, and getting that wrong is what the first attempt did: a size only arrives from the renderer when it has measured a new one, so dividing sizes unconditionally divided the same value again every frame, and within a second the nodes were thousands of times their real size — enough geometry to trip ImGui's 16-bit vertex-index assertion. A node the frame did not touch is now restored to exactly the value it had rather than divided.
Fit to canvas (the old "Fit") now picks the largest zoom the whole arrangement still fits at, with a margin, and never magnifies: a graph that already fits is shown at its own size, because magnifying it is not what "fit" means to someone who asked to see all of it. The first-frame fit also repeats until the nodes have been measured — the zoom it chooses depends on their sizes, and on frame one those are all still zero.
This part belongs in the library, and is on its way there. ktsu-dev/ImGuiApp#359 puts zoom and fit-to-view inside
NodeEditorRenderer, where the transform can live at the seam between the engine and ImNodes instead of being wrapped around the whole frame from outside — and where the node editor's own padding and pin sizes can be scaled too, which from here they cannot. Once that ships,EnterViewSpace,LeaveViewSpace,ZoomandFitViewcome out of this application in favour ofrenderer.Zoomandrenderer.FitToView. What is here is what can be done from outside the library today.The window and the splits are remembered
Size, position and whether it was maximized go into
EditorSettingsalongside the recent files and the preview language, restored throughImGuiAppConfig.InitialWindowStateand kept current byOnMoveOrResize. A first run leaves the position at the windowing layer's own "no position yet" sentinel, so the platform places the window rather than this application putting it in a corner.The size and position stored are the ones the window has when it is not maximized, which is what should come back when a maximized window is restored.
This adds a
Silk.NET.Windowing.Commonreference, to name the maximized state —WindowStatelives there rather than in theSilk.NET.Windowingfacade beside it, which the second commit corrects after CI caught the first attempt with KTSU0006. That is the one thing here that costs a dependency; without it the size and position could persist but "was it maximized" could not.Testing
329 tests, all passing (325 before, with 2 removed and 6 added). New coverage: fitting zooms out until an oversized graph fits, fitting does not magnify one that already fits, the zoom stays in range however it is set, the window and pane splits survive the settings round trip, and a first run leaves its position to the platform.
Verified visually through the headless harness: the three panes with their handles, the graph at 50% and 200%, and the graph returning to exactly its original size and place after zooming out, in, and back to 100%.
🤖 Generated with Claude Code
https://claude.ai/code/session_01QwzCFb8zhd263sZeRbF7ba