From 0ca6aed0b04423567d418fe6c02fa6c701f1535f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 10 Oct 2026 00:27:07 +0000 Subject: [PATCH] Make the editor gallery readable and the sample document honest Render the graph pictures on a 2560x1440 display with a wider canvas so node text is legible, crop the side-column pictures to pane boundaries (no border sliver, no clipped Members/Settled panes, no empty space under the code), keep the layout-tuning pane whole, and crop the file menu to its own edge. Label the File menu's Path box ahead of the field, with a hint, instead of a trailing "Path" that read as nothing. Declare the sample document's fields as FieldDeclaration rather than VariableDeclaration, so Go and C note their defaults and Python writes annotated class fields; the bare member references in the method bodies are #141, which the Python caption now says. Show editing-a-document.png as the README's preview. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01WWRBkw7bXdWGzuLpqnCvrB --- Coder.Editor/CoderEditorApp.cs | 11 ++-- Coder.Test/Editor/CoderEditorAppTests.cs | 2 +- README.md | 2 + .../Gallery/EditorGallery.cs | 3 +- .../Gallery/EditorUnderTest.cs | 37 +++++++++++-- .../Gallery/GalleryCatalog.cs | 54 ++++++++++++++----- .../Gallery/GalleryEntry.cs | 7 +++ 7 files changed, 96 insertions(+), 20 deletions(-) diff --git a/Coder.Editor/CoderEditorApp.cs b/Coder.Editor/CoderEditorApp.cs index 5ae1f00..eca86e5 100644 --- a/Coder.Editor/CoderEditorApp.cs +++ b/Coder.Editor/CoderEditorApp.cs @@ -141,8 +141,8 @@ public static ClassDeclaration NewDocument() { ClassDeclaration declaration = new("Counter"); - declaration.Members.Add(new VariableDeclaration("count", "int", new LiteralExpression(0))); - declaration.Members.Add(new VariableDeclaration("step", "int", new LiteralExpression(1))); + declaration.Members.Add(new FieldDeclaration("count", "int") { InitialValue = new LiteralExpression(0) }); + declaration.Members.Add(new FieldDeclaration("step", "int") { InitialValue = new LiteralExpression(1) }); FunctionDeclaration add = new("Add") { ReturnType = "int" }; add.Parameters.Add(new Parameter("amount", "int")); @@ -340,8 +340,13 @@ public void DrawMenu() } ImGui.Separator(); + + // Labelled ahead of the box, and hinted while empty: Open, Save and Export below all read + // it, and an empty field with its name trailing off to the right says none of that. + ImGui.TextUnformatted("Path"); + ImGui.SameLine(); ImGui.SetNextItemWidth(320f); - ImGui.InputText("Path", ref pathBuffer, 512); + ImGui.InputTextWithHint("##path", "file to open, save or export to", ref pathBuffer, 512); if (ImGui.MenuItem("Open") && pathBuffer.Length > 0) { diff --git a/Coder.Test/Editor/CoderEditorAppTests.cs b/Coder.Test/Editor/CoderEditorAppTests.cs index e8f32fd..1ca898c 100644 --- a/Coder.Test/Editor/CoderEditorAppTests.cs +++ b/Coder.Test/Editor/CoderEditorAppTests.cs @@ -101,7 +101,7 @@ public void NewDocument_IsAClassWithFieldsAndMethodsThatDoSomething() Assert.AreEqual("Counter", document.Name); - List fields = [.. document.Members.OfType()]; + List fields = [.. document.Members.OfType()]; Assert.AreEqual(2, fields.Count, "the class should carry a couple of fields"); Assert.IsTrue(fields.TrueForAll(f => f.InitialValue is not null), "each field should be initialised"); diff --git a/README.md b/README.md index e07a4e4..93e6937 100644 --- a/README.md +++ b/README.md @@ -191,6 +191,8 @@ keyboard focus is. dotnet run --project Coder.Editor ``` +[![The Coder editor: a document as a node graph, with its properties and generated code beside it](docs/gallery/editing-a-document.png)](docs/gallery/README.md) + Pictures of the editor as it draws today are in the [editor gallery](docs/gallery/README.md). Documents are `.coder.yaml` files — the same YAML the serializer already round-trips, so anything the diff --git a/tests/Coder.Editor.UITests/Gallery/EditorGallery.cs b/tests/Coder.Editor.UITests/Gallery/EditorGallery.cs index 1de843a..046a72b 100644 --- a/tests/Coder.Editor.UITests/Gallery/EditorGallery.cs +++ b/tests/Coder.Editor.UITests/Gallery/EditorGallery.cs @@ -80,7 +80,8 @@ public void Photograph(string name) new EditorSettings()); entry.Prepare?.Invoke(app); - using ImGuiAppHarness harness = ImGuiAppHarness.Start(app.BuildConfig(), new HarnessOptions { Width = Display.Width, Height = Display.Height }); + (int width, int height) = entry.Display ?? Display; + using ImGuiAppHarness harness = ImGuiAppHarness.Start(app.BuildConfig(), new HarnessOptions { Width = width, Height = height }); Assert.IsTrue(GalleryFonts.Load(), "ImGuiApp's own font could not be found, so the pictures would not look like the application."); harness.Mouse.MoveTo(-100f, -100f); harness.Step(SettleFrames); diff --git a/tests/Coder.Editor.UITests/Gallery/EditorUnderTest.cs b/tests/Coder.Editor.UITests/Gallery/EditorUnderTest.cs index 7d847e2..dde1092 100644 --- a/tests/Coder.Editor.UITests/Gallery/EditorUnderTest.cs +++ b/tests/Coder.Editor.UITests/Gallery/EditorUnderTest.cs @@ -35,12 +35,43 @@ internal void Preview(string languageId) /// The height of the status bar, which a column picture leaves out. private const int StatusBarHeight = 34; + /// + /// How far into the column its own border is drawn. Cropping to the border, rather than to the + /// divider the column is measured from, keeps a sliver of the graph pane's edge out of the picture. + /// + private const int ColumnInset = 2; + + /// Where the side column's panes start, below the menu bar and the column's own border. + private const int PanesTop = 36; + + /// The chrome above and below the panes that is not shared out between them. + private const int PanesChrome = 84; + /// Gets the right-hand column: properties, generated code and layout tuning. /// The column, from the divider to the window's right edge, between the menu bar and the status bar. - internal Rectangle SideColumn() + internal Rectangle SideColumn() => SideColumn(0f, 1f); + + /// Gets part of the right-hand column, between two of the boundaries its panes are split at. + /// + /// Where to start, as the share of the column above it: 0 for the top, or the sum of the splits of + /// the panes to leave out above. + /// + /// Where to stop, in the same terms: 1 for the bottom. + /// The part of the column, cropped to the borders of the panes it keeps. + internal Rectangle SideColumn(float from, float to) { Bitmap32 frame = Harness.Target; - int left = (int)(frame.Width * App.Settings.GraphSplit); - return new Rectangle(left, MenuBarHeight, frame.Width, frame.Height - StatusBarHeight); + int left = (int)(frame.Width * App.Settings.GraphSplit) + ColumnInset; + int top = from <= 0f ? MenuBarHeight + ColumnInset + 2 : PaneBoundary(frame, from) + 6; + int bottom = to >= 1f ? frame.Height - StatusBarHeight - ColumnInset : PaneBoundary(frame, to) - 2; + return new Rectangle(left, top, frame.Width, bottom); } + + /// Finds where the column's panes are divided, for a share of the column above the division. + /// The sum of the splits of the panes above the division. + /// The division's distance from the top of the window. + internal int PaneBoundary(float share) => PaneBoundary(Harness.Target, share); + + private static int PaneBoundary(Bitmap32 frame, float share) => + PanesTop + (int)MathF.Round((frame.Height - PanesChrome) * share); } diff --git a/tests/Coder.Editor.UITests/Gallery/GalleryCatalog.cs b/tests/Coder.Editor.UITests/Gallery/GalleryCatalog.cs index 4225bdf..b9670cd 100644 --- a/tests/Coder.Editor.UITests/Gallery/GalleryCatalog.cs +++ b/tests/Coder.Editor.UITests/Gallery/GalleryCatalog.cs @@ -17,22 +17,41 @@ namespace ktsu.Coder.Editor.UITests.Gallery; /// internal static class GalleryCatalog { + /// + /// The display the pictures of the whole graph are drawn at. The graph is fitted to its canvas, + /// and on the usual display that is about half size, where node text cannot be read. + /// + private static readonly (int Width, int Height) GraphDisplay = (2560, 1440); + + /// How much of the window the graph is given in the pictures of the whole graph. + private const float GraphSplit = 0.66f; + /// Gets the entries. internal static IReadOnlyList Entries { get; } = [ new( "Editing a document", "The document is an abstract syntax tree drawn as a node graph, laid out by a force-directed simulation. Beside it, the selected node's properties and the source the document generates, here in C#.", - editor => editor.Select(declaration => declaration.Name == "Counter")), + editor => editor.Select(declaration => declaration.Name == "Counter")) + { + Display = GraphDisplay, + Prepare = app => app.Settings.GraphSplit = GraphSplit, + }, new( "Inspecting a node", "Selecting a node shows its own properties in the inspector, each with the control that suits it: a text field, a checkbox or a picker. Every edit is one step on the undo stack.", editor => editor.Select(function => function.Name == "Add")) { - Prepare = app => app.Settings.PropertiesSplit = 0.6f, - Crop = editor => editor.SideColumn(), + // The layout pane is left out: squeezed under an inspector this tall it is a sliver of + // sliders, and it has a picture of its own. + Prepare = app => + { + app.Settings.PropertiesSplit = 0.5f; + app.Settings.CodeSplit = 0.45f; + }, + Crop = editor => editor.SideColumn(0f, 0.87f), }, - Language("python", "Python", "One document, seven languages. The preview regenerates as the graph is edited; Python writes the receiver as `self` and the types as annotations."), + Language("python", "Python", "One document, seven languages. The preview regenerates as the graph is edited; Python writes the receiver as `self` and the types as annotations. The method bodies still name the fields bare, because the AST has no way yet to say \"this instance's `count`\" ([#141](https://github.com/ktsu-dev/Coder/issues/141))."), Language("cpp", "C++", "C++ gathers the members under an access section and closes the class with the semicolon the language requires."), Language("rust", "Rust", "Rust puts the data in a `struct` and the behaviour in an `impl` block beside it."), Language("go", "Go", "Go writes a struct with its methods beside it, laid out exactly as `gofmt` would."), @@ -41,23 +60,29 @@ internal static class GalleryCatalog "A document with an operand nobody has filled in, and a node left detached from the tree, does not generate. The code pane lists what is outstanding instead, and clicking a problem selects the node it is about.", _ => { }) { - Prepare = Unfinished, + Display = GraphDisplay, + Prepare = app => + { + app.Settings.GraphSplit = GraphSplit; + Unfinished(app); + }, }, new( "Layout tuning", "Every force in the layout simulation can be tuned while watching the graph move, grouped by what it does and captioned with its effect, with the simulation's own state below.", editor => { - // The Repulsion header, the first of the layout panel's groups. - editor.Harness.Mouse.Click(1092f, 354f); + // The Repulsion header, the first of the layout panel's groups, sits a fixed distance + // below the top of its pane. + editor.Harness.Mouse.Click(1092f, editor.PaneBoundary(0.55f) + 73f); editor.Harness.Step(2); editor.Harness.Mouse.MoveTo(-100f, -100f); }) { Prepare = app => { - app.Settings.PropertiesSplit = 0.12f; - app.Settings.CodeSplit = 0.18f; + app.Settings.PropertiesSplit = 0.10f; + app.Settings.CodeSplit = 0.45f; }, Crop = editor => editor.SideColumn(), }, @@ -71,7 +96,8 @@ internal static class GalleryCatalog editor.Harness.Mouse.MoveTo(-100f, -100f); }) { - Crop = _ => new Rectangle(0, 0, 400, 160), + // Wide enough to end in the gap after the graph's "Fit to canvas" button rather than through it. + Crop = _ => new Rectangle(0, 0, 446, 160), }, ]; @@ -84,10 +110,14 @@ private static GalleryEntry Language(string languageId, string displayName, stri { Prepare = app => { + // Wide enough for Go's notes on its field defaults, the longest line any of them writes. + app.Settings.GraphSplit = 0.5f; app.Settings.PropertiesSplit = 0.12f; - app.Settings.CodeSplit = 0.55f; + app.Settings.CodeSplit = 0.42f; }, - Crop = editor => editor.SideColumn(), + + // Down to just below the longest listing, C++'s: the layout pane below has a picture of its own. + Crop = editor => editor.SideColumn(0f, 0.525f), }; /// diff --git a/tests/Coder.Editor.UITests/Gallery/GalleryEntry.cs b/tests/Coder.Editor.UITests/Gallery/GalleryEntry.cs index 8cb1ef4..2a821ef 100644 --- a/tests/Coder.Editor.UITests/Gallery/GalleryEntry.cs +++ b/tests/Coder.Editor.UITests/Gallery/GalleryEntry.cs @@ -27,6 +27,13 @@ internal sealed record GalleryEntry(string Name, string Description, Action public Func? Crop { get; init; } + /// + /// Gets the display this entry is drawn at, or null for . A + /// picture of the whole graph is drawn on a larger one, because the graph is fitted to its canvas + /// and on the usual display that leaves node text too small to read. + /// + public (int Width, int Height)? Display { get; init; } + /// Gets the file name the picture is written under, without its extension. public string Slug => MakeSlug(Name);