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
35 changes: 28 additions & 7 deletions Coder.Editor/CoderEditorApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@
{
ClassDeclaration declaration = new("Counter");

declaration.Members.Add(new VariableDeclaration("count", "int", new LiteralExpression<int>(0)));

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

View workflow job for this annotation

GitHub Actions / 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 / Analyze & Release

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

FunctionDeclaration add = new("Add") { ReturnType = "int" };
Expand Down Expand Up @@ -212,11 +212,17 @@
/// </summary>
/// <returns>The container to tick each frame.</returns>
/// <remarks>
/// Properties and code are stacked rather than placed side by side because they are read at
/// different times and want different shapes: properties are a short column of labelled rows,
/// while generated source is lines that want to be read down. Sharing one column gives each of
/// them the full width and lets the user decide how the height is split between them — which is
/// the point of making these panes rather than fixed regions.
/// Properties, code and layout tuning are stacked rather than placed 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 that want to be read down, and the tuning is a long list of
/// sliders. Sharing one column gives each of them the full width and lets the user decide how the
/// height is split between them — which is the point of making these panes rather than fixed
/// regions.
/// <para>
/// The tuning gets a pane of its own rather than a section inside the properties, because it is
/// read while watching the graph move: it has to be able to be tall while the properties are
/// short, and it must not close itself every time a different node is selected.
/// </para>
/// <para>
/// The sizes are remembered between runs, so an arrangement the user settled on is the one they
/// come back to.
Expand All @@ -226,11 +232,16 @@
{
ImGuiWidgets.DividerContainer side = new(
"coder-side",
container => Settings.PropertiesSplit = container.GetSizes()[0],
container =>
{
Settings.PropertiesSplit = container.GetSizes()[0];
Settings.CodeSplit = container.GetSizes()[1];
},
ImGuiWidgets.DividerLayout.Rows,
[
new ImGuiWidgets.DividerZone("properties", Settings.PropertiesSplit, DrawPropertiesPane),
new ImGuiWidgets.DividerZone("code", 1f - Settings.PropertiesSplit, _ => DrawCodePane()),
new ImGuiWidgets.DividerZone("code", Settings.CodeSplit, _ => DrawCodePane()),
new ImGuiWidgets.DividerZone("layout", 1f - Settings.PropertiesSplit - Settings.CodeSplit, DrawLayoutPane),
]);

return new ImGuiWidgets.DividerContainer(
Expand All @@ -254,6 +265,16 @@
Editor.DrawInspector(ImGui.GetContentRegionAvail());
}

/// <summary>
/// Draws the layout tuning, which the editor supplies but does not place.
/// </summary>
/// <param name="deltaTime">Seconds since the last frame; the panel does not animate, so unused.</param>
private void DrawLayoutPane(float deltaTime)
{
ImGui.TextUnformatted("Layout");
Editor.DrawLayoutSettings(ImGui.GetContentRegionAvail());
}

/// <summary>
/// Applies the keyboard shortcuts the menu also offers.
/// </summary>
Expand Down Expand Up @@ -302,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 / 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 / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 24 to the 15 allowed.
{
if (ImGui.BeginMenu("File"))
{
Expand Down Expand Up @@ -339,7 +360,7 @@

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

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

View workflow job for this annotation

GitHub Actions / Analyze & Release

Loops should be simplified using the "Where" LINQ method

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

View workflow job for this annotation

GitHub Actions / Analyze & Release

Loops should be simplified using the "Where" LINQ method
{
if (ImGui.MenuItem(recent))
{
Expand Down
11 changes: 8 additions & 3 deletions Coder.Editor/EditorSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,15 @@ public sealed class EditorSettings
public float GraphSplit { get; set; } = 0.62f;

/// <summary>
/// Gets or sets the share of that panel's height the properties take, the rest going to the code
/// preview under them.
/// Gets or sets the share of that panel's height the properties take.
/// </summary>
public float PropertiesSplit { get; set; } = 0.4f;
public float PropertiesSplit { get; set; } = 0.3f;

/// <summary>
/// Gets or sets the share of that panel's height the generated code takes, the rest going to the
/// layout tuning under it.
/// </summary>
public float CodeSplit { get; set; } = 0.4f;

/// <summary>
/// Records a file as the most recently opened, without letting the list grow or repeat.
Expand Down
36 changes: 36 additions & 0 deletions Coder.Graph/AstGraphEditor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Hexa.NET.ImGui;
using Hexa.NET.ImNodes;
using ktsu.Coder.Ast;
using ktsu.ForceDirectedLayout;
using ktsu.ImGui.NodeEditor;
using ktsu.UndoRedo;
using ktsu.UndoRedo.Contracts;
Expand Down Expand Up @@ -336,6 +337,41 @@
ImGui.EndChild();
}

/// <summary>
/// Draws the panel that tunes the force-directed layout, so the graph can be arranged while it is
/// on screen rather than by rebuilding.
/// </summary>
/// <remarks>
/// The whole tuning surface comes from <see cref="PhysicsSettingsPanel"/>, which the node editor
/// library supplies: every setting the simulation has, grouped by the force it belongs to. It is
/// drawn beside the graph deliberately - the forces interact, so a change to any one of them is
/// only judgeable by watching what the graph does about it.
/// <para>
/// The panel's run toggle is shown and written as <see cref="LayoutRunning"/>, the same flag the
/// toolbar's checkbox holds, so the two agree whichever the user reaches for. This editor stops
/// the layout by not advancing it rather than by the engine's own <c>Enabled</c>, which stays on:
/// a step the engine is never given cannot run either way, and keeping one flag rather than two
/// means there is no arrangement where the graph is stopped for a reason the user cannot see.
/// </para>
/// </remarks>
/// <param name="size">The area to draw it in.</param>
public void DrawLayoutSettings(Vector2 size)
{
ImGui.BeginChild("ast-layout-settings", size, ImGuiChildFlags.Borders, ImGuiWindowFlags.HorizontalScrollbar);

PhysicsSettings settings = Graph.Engine.PhysicsSettings with { Enabled = LayoutRunning };
if (PhysicsSettingsPanel.Draw(ref settings))
{
LayoutRunning = settings.Enabled;
Graph.Engine.UpdatePhysicsSettings(settings with { Enabled = true });
}

ImGui.Separator();
PhysicsSettingsPanel.DrawDiagnostics(Graph.Engine);

ImGui.EndChild();
}

/// <summary>
/// Draws the menu of kinds the selected node could be turned into.
/// </summary>
Expand Down Expand Up @@ -777,7 +813,7 @@
{
// Undone in the order the replacement was made: the original goes back into its place,
// then each child that moved goes back into the slot it came from.
Graph.Replace(replacement, existing);

Check warning on line 816 in Coder.Graph/AstGraphEditor.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Parameters to 'Replace' have the same names but not the same order as the method arguments.

Check warning on line 816 in Coder.Graph/AstGraphEditor.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Parameters to 'Replace' have the same names but not the same order as the method arguments.
Graph.MoveTo(existing, from);
foreach ((AstNode child, AstLocation origin) in moved)
{
Expand Down Expand Up @@ -1039,7 +1075,7 @@

// A category's operator entries are one submenu deep, so eighteen binary operators do not
// bury the four literals.
foreach (string group in AstNodeCatalog.GroupsIn(category))

Check warning on line 1078 in Coder.Graph/AstGraphEditor.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Loops should be simplified using the "Where" LINQ method

Check warning on line 1078 in Coder.Graph/AstGraphEditor.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Loops should be simplified using the "Where" LINQ method
{
if (ImGui.BeginMenu(group))
{
Expand Down
2 changes: 2 additions & 0 deletions Coder.Test/Editor/EditorWiringTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ public async Task RunAsync_ReadsSettingsBeforeStartingAndWritesThemAfter()
WindowMaximized = true,
GraphSplit = 0.45f,
PropertiesSplit = 0.7f,
CodeSplit = 0.2f,
};
Assert.IsTrue(await store.SaveAsync(stored).ConfigureAwait(false));

Expand Down Expand Up @@ -155,6 +156,7 @@ public async Task RunAsync_ReadsSettingsBeforeStartingAndWritesThemAfter()
Assert.IsTrue(reread.WindowMaximized);
Assert.AreEqual(0.45f, reread.GraphSplit, "the pane split should have been written back on exit");
Assert.AreEqual(0.7f, reread.PropertiesSplit);
Assert.AreEqual(0.2f, reread.CodeSplit, "the layout pane's split should round-trip too");
}

/// <summary>
Expand Down
124 changes: 124 additions & 0 deletions Coder.Test/Graph/AstGraphEditorLayoutPanelTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace ktsu.Coder.Test.Graph;

using System.Numerics;

using Hexa.NET.ImGui;

using ktsu.Coder.Ast;
using ktsu.Coder.Graph;
using ktsu.ImGui.App;
using ktsu.ImGui.App.Testing;

using Microsoft.VisualStudio.TestTools.UnitTesting;

/// <summary>
/// Covers the panel that tunes the layout while the graph is on screen.
/// </summary>
/// <remarks>
/// The controls themselves belong to ktsu.ImGui.NodeEditor and are covered there. What is this
/// editor's own is the wiring: that the panel reaches the graph's engine, that its run toggle is the
/// same flag the toolbar's is, and that a change made on it survives into the simulation rather than
/// being discarded when the frame ends.
/// <para>
/// ImGui contexts are process-global, so only one harness can be live at a time and this class must
/// not run its methods in parallel.
/// </para>
/// </remarks>
[TestClass]
[DoNotParallelize]
public sealed class AstGraphEditorLayoutPanelTests
{
private static readonly HarnessOptions Options = new() { Width = 900, Height = 1100 };

private static FunctionDeclaration SampleFunction()
{
FunctionDeclaration function = new("total") { ReturnType = "int" };
function.Parameters.Add(new Parameter("a", "int"));
function.Body.Add(new ReturnStatement(
new BinaryExpression(new VariableReference("a"), BinaryOperator.Add, Literal.Number(1))));
return function;
}

/// <summary>Draws only the tuning panel, so nothing else can be what a probe finds.</summary>
private static ImGuiAppConfig ConfigFor(AstGraphEditor editor) => new()
{
Title = "Layout tuning",
OnRender = _ =>
{
ImGui.Begin("layout");
editor.DrawLayoutSettings(new Vector2(880, 1050));
ImGui.End();
},
};

private static bool IsVisible(ImGuiAppHarness harness, string name) =>
harness.Probe.WasSeenInFrame(name, harness.FrameCount - 1);

[TestMethod]
public void Panel_OffersEveryGroupOfSettings()
{
AstGraphEditor editor = new(SampleFunction());

using ImGuiAppHarness harness = ImGuiAppHarness.Start(ConfigFor(editor), Options);
harness.Step(3);

foreach (string group in new[] { "Repulsion", "Link springs", "Link shaping", "Gravity", "Overlap", "Motion and limits" })
{
Assert.IsTrue(IsVisible(harness, group), $"The layout pane is missing its '{group}' group.");
}

Assert.IsTrue(IsVisible(harness, "Run simulation"), "the run toggle");
Assert.IsTrue(IsVisible(harness, "Energy"), "the diagnostics readout");
}

[TestMethod]
public void Panel_EditsTheGraphsOwnSimulation()
{
AstGraphEditor editor = new(SampleFunction());

using ImGuiAppHarness harness = ImGuiAppHarness.Start(ConfigFor(editor), Options);
harness.Step(3);

// Reached through the panel's own group rather than set directly, so what is covered is that
// the pane is wired to this graph's engine and not to a copy of its settings.
harness.Click("Link shaping");
harness.Step(2);

Rectangle slider = harness.Probe.Rect("Untwisting")
?? throw new AssertFailedException("The link shaping group should offer an untwisting slider.");

double before = editor.Graph.Engine.PhysicsSettings.LinkUntwistStrength;
float y = slider.MinY + (slider.Height / 2f);

// A slider's item rectangle spans the track and the label beside it, so the drag stays in the
// left portion to be sure it lands on the track.
harness.Mouse.Drag(slider.MinX + (slider.Width * 0.1f), y, slider.MinX + (slider.Width * 0.45f), y);
harness.Step(2);

Assert.AreNotEqual(before, editor.Graph.Engine.PhysicsSettings.LinkUntwistStrength, 0.0001,
"Dragging the panel's slider should have reached the graph's own simulation.");
}

[TestMethod]
public void Panel_RunToggleIsTheSameFlagTheToolbarHolds()
{
AstGraphEditor editor = new(SampleFunction()) { LayoutRunning = true };

using ImGuiAppHarness harness = ImGuiAppHarness.Start(ConfigFor(editor), Options);
harness.Step(3);

harness.Click("Run simulation");
harness.Step(2);

Assert.IsFalse(editor.LayoutRunning,
"The panel's run toggle should stop the layout the same way the toolbar's checkbox does.");

// The engine's own flag stays on: this editor stops the layout by not advancing it, and two
// flags for one visible behaviour is the arrangement that leaves a graph stopped for a reason
// the user cannot see.
Assert.IsTrue(editor.Graph.Engine.PhysicsSettings.Enabled,
"the engine's own Enabled should be left on");
}
}
12 changes: 6 additions & 6 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@
<PackageVersion Include="ktsu.Essentials.FileSystemProviders.Native" Version="2.3.2" />
<PackageVersion Include="ktsu.Essentials.PersistenceProviders.ConfigHome" Version="2.3.2" />
<PackageVersion Include="ktsu.Essentials.SerializationProviders.Yaml" Version="2.3.2" />
<PackageVersion Include="ktsu.ImGui.App" Version="3.25.0" />
<PackageVersion Include="ktsu.ImGui.App" Version="3.26.0" />
<PackageVersion Include="ktsu.UndoRedo.Core" Version="1.0.19" />
<PackageVersion Include="ktsu.ImGui.App.Testing" Version="3.25.0" />
<PackageVersion Include="ktsu.ImGui.SyntaxHighlighting" Version="3.25.0" />
<PackageVersion Include="ktsu.ImGui.Widgets" Version="3.25.0" />
<PackageVersion Include="ktsu.ImGui.App.Testing" Version="3.26.0" />
<PackageVersion Include="ktsu.ImGui.SyntaxHighlighting" Version="3.26.0" />
<PackageVersion Include="ktsu.ImGui.Widgets" Version="3.26.0" />
<PackageVersion Include="Silk.NET.Windowing.Common" Version="2.23.0" />
<PackageVersion Include="ktsu.ImGui.NodeEditor" Version="3.25.0" />
<PackageVersion Include="ktsu.ForceDirectedLayout" Version="3.25.0" />
<PackageVersion Include="ktsu.ImGui.NodeEditor" Version="3.26.0" />
<PackageVersion Include="ktsu.ForceDirectedLayout" Version="3.26.0" />
<PackageVersion Include="ktsu.CodeBlocker" Version="2.0.4" />
<PackageVersion Include="ktsu.DeepClone" Version="2.0.2" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.11" />
Expand Down