Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions Coder.Editor/CoderEditorApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,8 @@
{
ClassDeclaration declaration = new("Counter");

declaration.Members.Add(new VariableDeclaration("count", "int", new LiteralExpression<int>(0)));
declaration.Members.Add(new VariableDeclaration("step", "int", new LiteralExpression<int>(1)));
declaration.Members.Add(new FieldDeclaration("count", "int") { InitialValue = new LiteralExpression<int>(0) });

Check warning on line 144 in Coder.Editor/CoderEditorApp.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Define a constant instead of using this literal 'count' 5 times.

Check warning on line 144 in Coder.Editor/CoderEditorApp.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Define a constant instead of using this literal 'count' 5 times.

Check warning on line 144 in Coder.Editor/CoderEditorApp.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Define a constant instead of using this literal 'count' 5 times.

Check warning on line 144 in Coder.Editor/CoderEditorApp.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Define a constant instead of using this literal 'count' 5 times.
declaration.Members.Add(new FieldDeclaration("step", "int") { InitialValue = new LiteralExpression<int>(1) });

FunctionDeclaration add = new("Add") { ReturnType = "int" };
add.Parameters.Add(new Parameter("amount", "int"));
Expand Down Expand Up @@ -323,7 +323,7 @@
/// Draws the application's File menu.
/// </summary>
/// <remarks>Called from inside the application's main menu bar, so it opens no bar of its own.</remarks>
public void DrawMenu()

Check warning on line 326 in Coder.Editor/CoderEditorApp.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 24 to the 15 allowed.

Check warning on line 326 in Coder.Editor/CoderEditorApp.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 24 to the 15 allowed.

Check warning on line 326 in Coder.Editor/CoderEditorApp.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 24 to the 15 allowed.
{
if (ImGui.BeginMenu("File"))
{
Expand All @@ -340,8 +340,13 @@
}

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)
{
Expand All @@ -360,7 +365,7 @@

if (Settings.RecentFiles.Count > 0 && ImGui.BeginMenu("Recent"))
{
foreach (string recent in Settings.RecentFiles.ToArray())

Check warning on line 368 in Coder.Editor/CoderEditorApp.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Loops should be simplified using the "Where" LINQ method

Check warning on line 368 in Coder.Editor/CoderEditorApp.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Loops should be simplified using the "Where" LINQ method

Check warning on line 368 in Coder.Editor/CoderEditorApp.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Loops should be simplified using the "Where" LINQ method
{
if (ImGui.MenuItem(recent))
{
Expand Down
2 changes: 1 addition & 1 deletion Coder.Test/Editor/CoderEditorAppTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ public void NewDocument_IsAClassWithFieldsAndMethodsThatDoSomething()

Assert.AreEqual("Counter", document.Name);

List<VariableDeclaration> fields = [.. document.Members.OfType<VariableDeclaration>()];
List<FieldDeclaration> fields = [.. document.Members.OfType<FieldDeclaration>()];
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");

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion tests/Coder.Editor.UITests/Gallery/EditorGallery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
37 changes: 34 additions & 3 deletions tests/Coder.Editor.UITests/Gallery/EditorUnderTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,43 @@ internal void Preview(string languageId)
/// <summary>The height of the status bar, which a column picture leaves out.</summary>
private const int StatusBarHeight = 34;

/// <summary>
/// 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.
/// </summary>
private const int ColumnInset = 2;

/// <summary>Where the side column's panes start, below the menu bar and the column's own border.</summary>
private const int PanesTop = 36;

/// <summary>The chrome above and below the panes that is not shared out between them.</summary>
private const int PanesChrome = 84;

/// <summary>Gets the right-hand column: properties, generated code and layout tuning.</summary>
/// <returns>The column, from the divider to the window's right edge, between the menu bar and the status bar.</returns>
internal Rectangle SideColumn()
internal Rectangle SideColumn() => SideColumn(0f, 1f);

/// <summary>Gets part of the right-hand column, between two of the boundaries its panes are split at.</summary>
/// <param name="from">
/// 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.
/// </param>
/// <param name="to">Where to stop, in the same terms: 1 for the bottom.</param>
/// <returns>The part of the column, cropped to the borders of the panes it keeps.</returns>
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);
}

/// <summary>Finds where the column's panes are divided, for a share of the column above the division.</summary>
/// <param name="share">The sum of the splits of the panes above the division.</param>
/// <returns>The division's distance from the top of the window.</returns>
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);
}
54 changes: 42 additions & 12 deletions tests/Coder.Editor.UITests/Gallery/GalleryCatalog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,41 @@ namespace ktsu.Coder.Editor.UITests.Gallery;
/// </remarks>
internal static class GalleryCatalog
{
/// <summary>
/// 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.
/// </summary>
private static readonly (int Width, int Height) GraphDisplay = (2560, 1440);

/// <summary>How much of the window the graph is given in the pictures of the whole graph.</summary>
private const float GraphSplit = 0.66f;

/// <summary>Gets the entries.</summary>
internal static IReadOnlyList<GalleryEntry> 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<ClassDeclaration>(declaration => declaration.Name == "Counter")),
editor => editor.Select<ClassDeclaration>(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<FunctionDeclaration>(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."),
Expand All @@ -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(),
},
Expand All @@ -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),
},
];

Expand All @@ -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),
};

/// <summary>
Expand Down
7 changes: 7 additions & 0 deletions tests/Coder.Editor.UITests/Gallery/GalleryEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ internal sealed record GalleryEntry(string Name, string Description, Action<Edit
/// </summary>
public Func<EditorUnderTest, Rectangle?>? Crop { get; init; }

/// <summary>
/// Gets the display this entry is drawn at, or null for <see cref="EditorGallery.Display"/>. 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.
/// </summary>
public (int Width, int Height)? Display { get; init; }

/// <summary>Gets the file name the picture is written under, without its extension.</summary>
public string Slug => MakeSlug(Name);

Expand Down
Loading