Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
38ed8b6
Editor icons: all 304 of the Mono set, at four sizes, behind names
greenfire27 Jul 28, 2026
8916d58
Editor icons: add align top, middle and bottom
greenfire27 Jul 28, 2026
2a93b70
GuiControl: add rendersChildren() and applySizing() for the editor
greenfire27 Jul 29, 2026
5433672
GuiControl: name the sizing flags for the edge they hold, and give ev…
greenfire27 Jul 29, 2026
830a8b9
SimObject: stop persisting hidden and locked
greenfire27 Jul 29, 2026
aced3ac
EditorIconButton: keep the disabled look while the pointer is over it
greenfire27 Jul 29, 2026
8bc5612
Gui Editor: replace the inspector with a custom properties pane
greenfire27 Jul 29, 2026
7b35118
Gui Editor: let a GuiControl be told what it is, and give its text a …
greenfire27 Jul 29, 2026
3e020cf
Gui Editor: compact the properties pane, and tell each switch what it…
greenfire27 Jul 29, 2026
b03115b
Gui Editor: draw the controls, so the palette can stop naming them
greenfire27 Jul 29, 2026
d51f8e6
Gui Editor: let the shot harness make the folder it writes into
greenfire27 Jul 29, 2026
50f1d37
Gui Editor: make the control palette a palette
greenfire27 Jul 29, 2026
bfcf8c7
Gui Editor: undo and redo, on the machinery that was already there
greenfire27 Jul 29, 2026
afe11ca
Merge branch 'gui-editor-improvements' into gui-editor-palette
greenfire27 Jul 29, 2026
01d1f8f
Gui Editor: check that placing by click is undoable
greenfire27 Jul 29, 2026
5659bf3
Tests: let every harness that screenshots make the folder it writes into
greenfire27 Jul 29, 2026
f6127b5
SimObject: a deep clone that copies the data and runs no lifecycle
greenfire27 Jul 30, 2026
0c02d31
Gui Editor: copy, cut and paste
greenfire27 Jul 30, 2026
37f4a08
Gui Editor: a control arrives where it can be seen, and a delete says so
greenfire27 Jul 30, 2026
d2845d7
Gui Editor: a tab book makes its own pages
greenfire27 Jul 30, 2026
dc61382
Gui Editor: a menu bar makes its own items
greenfire27 Jul 31, 2026
6e888f9
Gui Editor: a list carries the rows it was authored with
greenfire27 Aug 1, 2026
9daf5b2
Gui Editor: one caret, on the line the return made
greenfire27 Aug 2, 2026
b3c4d43
Gui Editor: an icon takes its color from the theme it is worn in
greenfire27 Aug 2, 2026
44e14f0
Gui Editor: fixed a few sizing issues with Torque Suit Theme
greenfire27 Aug 2, 2026
089e4cb
Gui Editor: a control tile shows its name under the picture
greenfire27 Aug 2, 2026
e71a846
Gui: a scroll control leaves room for its own scroll bar
greenfire27 Aug 2, 2026
9a089e6
Gui: a tree row can carry more than a triangle and a name
greenfire27 Aug 3, 2026
e638819
Gui Editor: the control icons at 16px, for the Explorer tree
greenfire27 Aug 3, 2026
c7eb636
Gui Editor: hidden and locked leave the properties pane for the Explorer
greenfire27 Aug 3, 2026
713b22a
Gui Editor: the Explorer tree indents by twelve, not by a row
greenfire27 Aug 3, 2026
b783f03
Tests: one command for the C++ unit suite, and correct what the docs …
greenfire27 Aug 3, 2026
29ac819
Gui Editor: four defects a read-through of the editor turned up
greenfire27 Aug 3, 2026
e6e59b3
Gui Editor: ask before throwing away a Gui that has not been saved
greenfire27 Aug 3, 2026
5e06aa0
GuiEditCtrl: remove saveSelection and loadSelection
greenfire27 Aug 3, 2026
3aff6ba
README: describe the Gui Editor that is actually there
greenfire27 Aug 3, 2026
ca3635e
Gui Editor: Duplicate, and a Delete you can find
greenfire27 Aug 3, 2026
615c4f3
GuiProfileTheme: cursors, and an editor for aiming them
greenfire27 Aug 5, 2026
7dd89c0
Gui Editor: cursors in the Profile Editor, and slots that only appear…
greenfire27 Aug 5, 2026
f42c994
Gui Editor: ask before deleting a cursor's image, and only when it is…
greenfire27 Aug 5, 2026
c4fa947
PlanetX: the theme's cursors, and the art that goes with them
greenfire27 Aug 5, 2026
3e994df
PlanetX: a name is already a namespace, and a sound that was really a…
greenfire27 Aug 5, 2026
30884c5
GuiImageButtonCtrl: a profile's imageAsset was already the answer
greenfire27 Aug 6, 2026
8be9a90
KeyboardToy: the toy nobody had opened since the controls changed
greenfire27 Aug 6, 2026
8a71029
KeyboardToy: the dialogs as the Gui Editor laid them out
greenfire27 Aug 6, 2026
d6fab96
GuiButtonCtrl: a caption the author cleared stays cleared
greenfire27 Aug 6, 2026
3447653
GuiTextEditProfile: a field with focus is still a field
greenfire27 Aug 6, 2026
d021c74
KeyboardToy: the dialogs and the keyboard, as the Gui Editor laid the…
greenfire27 Aug 6, 2026
c6f6612
Remove the design docs
greenfire27 Aug 6, 2026
e1a1897
GuiControl: a control that changes parent keeps the size it had
greenfire27 Aug 7, 2026
64485f7
Gui Editor: a control a tree drag stranded comes back into view
greenfire27 Aug 7, 2026
cf7f9db
Gui Editor: a hidden control is out of the way, not just out of sight
greenfire27 Aug 7, 2026
176670a
GuiEditorCursorCtrl: the pane lets go of a cursor that is deleted
greenfire27 Aug 7, 2026
7e5899b
Tests: run.sh finds the input-driven suites instead of naming two of …
greenfire27 Aug 7, 2026
3468019
Platform (macOS): a file name with no directory names no directory
greenfire27 Aug 8, 2026
c58193f
Platform (macOS, iOS): a file's size counts the bytes not yet written…
greenfire27 Aug 8, 2026
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ engine/compilers/android-studio/app/.cxx/
/profileFormShotProject/
/profileFormSmokeProject/
/smokeThemeProject/
/unsavedSmokeProject/
# GuiDefaultProfile.fontDirectory is the unexpanded string "^EditorCore/gui/fonts"
# (guiProfiles.cs), and anything that bakes a font-cache miss recorded against it
# writes to a folder of that literal name. Nothing in the suite does, but it has
Expand Down
15 changes: 11 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,22 @@ There are two suites, and they test different things.

### C++ unit tests (GoogleTest)

Vendored at `engine/source/testing/googleTest`. Tests live in `engine/source/testing/tests/` (e.g. `platformFileIoTests.cc`, `platformStringTests.cc`) and as `TEST(...)` blocks throughout the engine.
Vendored at `engine/source/testing/googleTest`. **Every test lives in `engine/source/testing/tests/`** (e.g. `guiTreeRowLayoutTests.cc`, `platformStringTests.cc`); each file is listed explicitly in `cmake/EngineSources.cmake`, so a new one needs a CMake edit and a re-configure to be compiled at all.

- Run **all** tests by launching the engine with the alternate boot script: `main.runAllUnitTests.cs`, which calls the `runAllUnitTests()` console function and quits. Point the executable at this script (or `exec` it) instead of the default `main.cs`.
- From the in-engine console you can invoke `runAllUnitTests()` directly, or run a subset via the test-name filter argument (forwarded to GoogleTest).
```
tests\run-unit.ps1 all of them
tests\run-unit.ps1 GuiTreeRowLayoutTests.* one suite (a GoogleTest filter)
```

- Under the hood that launches the engine with the alternate boot script `main.runAllUnitTests.cs`, which calls `runAllUnitTests()` and quits. You can also invoke `runAllUnitTests()` from the in-engine console.
- **`runAllUnitTests()` takes no arguments.** It hands `InitGoogleTest` an empty argv, so a subset is selected with the `GTEST_FILTER` environment variable — which is what `run-unit.ps1`'s parameter sets.
- **What a unit test can reach.** The engine boots far enough to give it `Con`, `Sim`, the string table, the resource manager and `GuiDefaultProfile`, so it can `new` and `registerObject()` a control, read and write fields, run script via `Con::evaluate`, and round-trip TAML. It has **no canvas and no GL context**, so it must never wake a control or measure text: a font registers a texture and `TextureManager::refresh` asserts — which in a debug build is a modal box, so the failure arrives as a *hang*. Note this rules out adding rows to a list box or tree, since that calls `updateSize()` → `getFont()`.
- The established move when GUI logic is worth testing is to extract the arithmetic into a `static` that takes everything it uses, then test the static — see `GuiScrollCtrl::subtractScrollBars`, `GuiControl::splitParagraphs`, `GuiTreeViewCtrl::resolveIndent`.
- Tests are compiled out of shipping builds (`TORQUE_SHIPPING` guards `unitTesting.h`).

### TorqueScript integration tests

`tests/` drives the real engine — a real canvas, the real editor, real posted mouse and keyboard input — and checks that it behaves. This is what covers the editors, which the unit tests do not reach.
`tests/` drives the real engine — a real canvas, the real editor, real posted mouse and keyboard input — and checks that it behaves. This is what covers the editors, which the unit tests do not reach. It is also much slower: one process per suite with a 90 second timeout each, against seconds for the whole unit run. **Prefer a unit test where the thing under test can be reached without a canvas**, and keep these for what genuinely needs one.

```
tests\run.ps1 every pass/fail suite (exits non-zero on a change)
Expand Down
4 changes: 3 additions & 1 deletion PlanetX/AppCore/1/appCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@
exec("./scripts/constants.cs");
exec("./scripts/defaultPreferences.cs");
exec("./gui/guiCursors.cs");
%this.createGuiCursors();
exec("./scripts/themes.cs");
%this.loadThemes();
// After the themes, not before: the cursors a project uses are its theme's,
// and this installs them under the names the engine looks up.
%this.installThemeCursors(%this.cursorTheme());
exec("./scripts/canvas.cs");

// Initialize the canvas
Expand Down
149 changes: 105 additions & 44 deletions PlanetX/AppCore/1/gui/guiCursors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,24 @@
// IN THE SOFTWARE.
//-----------------------------------------------------------------------------

/// The mouse cursors a GUI names by convention: a text field asks for EditCursor,
/// a window's edges for LeftRightCursor and friends, and a control with none of
/// its own gets DefaultCursor.
/// The mouse cursors a GUI names by convention: a text field asks for
/// EditCursor, a window's edges for LeftRightCursor and friends, and a control
/// with none of its own gets DefaultCursor. Those names are hard-coded in the
/// engine (guiTextEditCtrl.cc, guiWindowCtrl.cc, guiFrameSetCtrl.cc,
/// guiEditCtrl.cc), so something has to answer to them.
///
/// This file used to build a project's ~70 GUI profiles as well. Those are now a
/// GuiProfileTheme (see scripts/themes.cs), which derives the whole set from six
/// colors and is editable in the GUI Profile Editor - so a project skins itself
/// by editing a theme rather than by forking a thousand lines of script. Cursors
/// have not moved into the theme yet, so they stay here.
/// This file used to answer by building seven cursors out of literals, the last
/// of the hand-written GUI furniture after the ~70 profiles became a
/// GuiProfileTheme. Now the theme owns cursors too - each one its own art,
/// tinted from the theme's palette - and this installs a chosen theme's set
/// under the canonical names. A control that names a cursor outright still wins;
/// this is only what everything else falls back to, including the canvas arrow.
///
/// It is also callable at any time, which is how a game swaps between themes
/// that look nothing alike:
///
/// AppCore.installThemeCursors(Combat);
/// Canvas.setCursor(DefaultCursor);

/// Registers %object under %name, or - if something already holds the name -
/// copies the new object's fields onto the existing one and throws the new one
Expand All @@ -55,54 +64,106 @@
}
}

function AppCore::createGuiCursors(%this)
/// Every theme the project loaded, as a space-separated list of ids. They live
/// in the Gui data group, which is where GuiProfileTheme::onAdd puts them.
function AppCore::getThemes(%this)
{
%this.SafeCreateNamedObject("DefaultCursor", new GuiCursor()
%themes = "";
if(!isObject(GuiDataGroup))
{
hotSpot = "1 1";
renderOffset = "0 0";
bitmapName = "^AppCore/gui/images/cursors/defaultCursor";
});
return %themes;
}

%this.SafeCreateNamedObject("LeftRightCursor", new GuiCursor()
for(%i = 0; %i < GuiDataGroup.getCount(); %i++)
{
hotSpot = "0.5 0";
renderOffset = "0.5 0.4";
bitmapName = "^AppCore/gui/images/cursors/leftRight";
});
%object = GuiDataGroup.getObject(%i);
if(%object.getClassName() $= "GuiProfileTheme")
{
%themes = (%themes $= "") ? %object.getId() : (%themes SPC %object.getId());
}
}

return %themes;
}

%this.SafeCreateNamedObject("UpDownCursor", new GuiCursor()
/// Which theme's cursors become the canonical ones. A project with one theme
/// never has to think about this; a project with several says so by setting
/// $pref::AppCore::cursorTheme, and gets told when it hasn't.
function AppCore::cursorTheme(%this)
{
%themes = %this.getThemes();
%count = getWordCount(%themes);
if(%count == 0)
{
hotSpot = "1 1";
renderOffset = "0.5 0.5";
bitmapName = "^AppCore/gui/images/cursors/upDown";
});
return 0;
}

%this.SafeCreateNamedObject("NWSECursor", new GuiCursor()
if($pref::AppCore::cursorTheme !$= "")
{
hotSpot = "1 1";
renderOffset = "0.5 0.5";
bitmapName = "^AppCore/gui/images/cursors/NWSE";
});
for(%i = 0; %i < %count; %i++)
{
%theme = getWord(%themes, %i);
if(%theme.getName() $= $pref::AppCore::cursorTheme)
{
return %theme;
}
}
warn("AppCore::cursorTheme: $pref::AppCore::cursorTheme names '" @ $pref::AppCore::cursorTheme @ "', which is not a loaded theme.");
}

%this.SafeCreateNamedObject("NESWCursor", new GuiCursor()
if(%count == 1)
{
hotSpot = "1 1";
renderOffset = "0.5 0.5";
bitmapName = "^AppCore/gui/images/cursors/NESW";
});
return getWord(%themes, 0);
}

%this.SafeCreateNamedObject("MoveCursor", new GuiCursor()
for(%i = 0; %i < %count; %i++)
{
hotSpot = "1 1";
renderOffset = "0.5 0.5";
bitmapName = "^AppCore/gui/images/cursors/move";
});
%theme = getWord(%themes, %i);
if(%theme.getName() $= "Base")
{
return %theme;
}
}

%first = getWord(%themes, 0);
warn("AppCore::cursorTheme: " @ %count @ " themes are loaded and none is named 'Base', so the cursors come from '" @
%first.getName() @ "'. Set $pref::AppCore::cursorTheme to choose.");
return %first;
}

/// Point the canonical cursor names at %theme's cursors. The names are copies
/// rather than the members themselves: a name can only belong to one object,
/// and a theme's members have to keep their own names for the Guis that
/// reference them.
function AppCore::installThemeCursors(%this, %theme)
{
if(!isObject(%theme))
{
warn("AppCore::installThemeCursors: no theme to install cursors from.");
return false;
}

%this.SafeCreateNamedObject("EditCursor", new GuiCursor()
%categories = %theme.getCursorCategoryNames();
%count = getWordCount(%categories);
for(%i = 0; %i < %count; %i++)
{
hotSpot = "0 0";
renderOffset = "0.5 0.5";
bitmapName = "^AppCore/gui/images/cursors/ibeam";
});
%category = getWord(%categories, %i);
%cursor = %theme.getCursor(%category);
if(!isObject(%cursor))
{
continue;
}

// The category's canonical name comes from the engine's own table, so
// this never has to take a theme name apart to find it.
%this.SafeCreateNamedObject(%theme.getCursorCanonicalName(%category), new GuiCursor()
{
bitmapName = %cursor.bitmapName;
hotSpot = %cursor.hotSpot;
renderOffset = %cursor.renderOffset;
color = %cursor.color;
});
}

return true;
}
6 changes: 6 additions & 0 deletions PlanetX/AppCore/1/scripts/defaultPreferences.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@
$pref::iOS::EnableOtherOrientationRotation = 1;
$pref::iOS::StatusBarType = 0;

/// AppCore. Which theme's cursors are installed under the names the engine
/// looks up when a control names none of its own - DefaultCursor, EditCursor
/// and the rest (see gui/guiCursors.cs). Empty is the usual answer: a project
/// with one theme uses it, and one with several is asked to say which.
$pref::AppCore::cursorTheme = "";

/// T2D
$pref::T2D::ParticlePlayerEmissionRateScale = 1.0;
$pref::T2D::ParticlePlayerSizeScale = 1.0;
Expand Down
91 changes: 87 additions & 4 deletions PlanetX/AppCore/1/scripts/themes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,83 @@
return pathConcat(filePath(filePath(makeFullPath(%module.getModulePath(), getMainDotCsDir()))), "themes");
}

/// Where one theme keeps its cursor art. A folder per theme, because two themes
/// in the same project may want cursors that look nothing alike - a menu
/// pointer and a combat reticle - and sharing one folder would mean one
/// overwriting the other.
function AppCore::getThemeCursorsPath(%this, %theme)
{
%path = %this.getThemesPath();
if(%path $= "" || !isObject(%theme) || %theme.getName() $= "")
{
return "";
}
return pathConcat(%path, "cursors", %theme.getName());
}

/// The stock cursor art every theme starts from, inside AppCore itself. It is
/// grayscale on purpose so a theme's tint colors it.
function AppCore::getStockCursorsPath(%this)
{
%module = ModuleDatabase.findModule("AppCore", 1);
if(!isObject(%module))
{
return "";
}
return pathConcat(makeFullPath(%module.getModulePath(), getMainDotCsDir()), "gui/images/cursors");
}

/// Give %theme its own copy of the stock cursor art and point it at the folder.
/// Idempotent: pathCopy is asked not to overwrite, so a theme whose art is
/// already there (or has been edited) is left exactly as it is.
///
/// Only the folder being absent triggers the copy, which keeps boot down to one
/// directory test per theme - and means Android, where pathCopy is unsupported,
/// never reaches it in a project that shipped its art.
function AppCore::seedThemeCursors(%this, %theme)
{
%target = %this.getThemeCursorsPath(%theme);
if(%target $= "")
{
return false;
}

// isDirectory rather than isFile: isFile answers out of the resource
// manager, which knows nothing about files written after the last scan.
if(!isDirectory(%target))
{
%source = %this.getStockCursorsPath();
if(%source $= "" || !isDirectory(%source))
{
warn("AppCore::seedThemeCursors: no stock cursor art at " @ %source @ ".");
return false;
}

createPath(%target @ "/");

%categories = %theme.getCursorCategoryNames();
for(%i = 0; %i < getWordCount(%categories); %i++)
{
%file = %theme.getCursorStockFile(getWord(%categories, %i));
if(%file $= "")
{
continue;
}
pathCopy(pathConcat(%source, %file), pathConcat(%target, %file));
}
}

// Assigning the folder restamps the theme, which fills in the bitmap of any
// cursor that has none yet. A cursor already pointing at art keeps it.
%directory = makeRelativePath(%target, getMainDotCsDir());
if(%theme.cursorDirectory !$= %directory)
{
%theme.cursorDirectory = %directory;
}

return true;
}

function AppCore::loadThemes(%this)
{
%path = %this.getThemesPath();
Expand Down Expand Up @@ -95,9 +172,10 @@
}

/// Reads one file from the themes folder. Alongside themes it may hold stand-alone
/// profiles, which the Profile Editor writes as a one-profile bundle (and, from
/// older versions, as a bare profile). Loading is all these need: a profile puts
/// itself in the Gui data group under its own name, which is how a Gui finds it.
/// profiles, which the Profile Editor writes as a one-profile bundle - a SimSet,
/// or a SimGroup from older versions (and, older still, a bare profile). Loading
/// is all these need: a profile registers under its own name, which is how a Gui
/// finds it.
function AppCore::loadTheme(%this, %file)
{
%object = TAMLRead(%file);
Expand All @@ -118,10 +196,12 @@
}

%this.repairFontDirectory(%object);
%this.seedThemeCursors(%object);
return true;
}

if(%class $= "ScriptGroup" || %class $= "GuiControlProfile")
// A bundle is a SimSet; SimGroup, which the older ones are, derives from it.
if(%object.isMemberOfClass("SimSet") || %class $= "GuiControlProfile")
{
return false;
}
Expand Down Expand Up @@ -171,6 +251,9 @@
borderSize = 1;
};

// Before the write, so the file records where the art went.
%this.seedThemeCursors(%theme);

%file = pathConcat(%path, "Base.taml");
TAMLWrite(%theme, %file);

Expand Down
10 changes: 6 additions & 4 deletions PlanetX/PlanetXGame/game.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,10 @@

// The weapon-upgrade catalog and this run's upgrade state. A named session
// singleton (like PlanetXWindow/PlanetXScene) so any file can reach it; reset()
// at the start of each run clears it back to the stock blaster.
new ScriptObject(PlanetXUpgrades) { class = "PlanetXUpgrades"; };
// at the start of each run clears it back to the stock blaster. Naming it is
// all it needs - the name is already the namespace upgrades.cs writes its
// methods in, so a class saying the same word again would say nothing.
new ScriptObject(PlanetXUpgrades);

// Two-player co-op starts off; the title's "START 2 PLAYERS" turns it on.
$PlanetX::twoPlayer = false;
Expand Down Expand Up @@ -362,7 +364,7 @@ class = "PlanetXUpgradeScreen";
}

// Co-op: take this player out of play.
Audio.PlaySound("PlanetXGame:playerDeath");
Audio.PlaySound("PlanetXGame:playerDeathBurst");
%player.playDeathFx();
%player.goDown();

Expand Down Expand Up @@ -392,7 +394,7 @@ class = "PlanetXUpgradeScreen";
// ghost bolts off-screen during the hold before the dialog.
if (isObject(%player) && !%player.downed)
{
Audio.PlaySound("PlanetXGame:playerDeath");
Audio.PlaySound("PlanetXGame:playerDeathBurst");
%player.playDeathFx();
%player.stopFiring();
%player.setLinearVelocity(0, 0);
Expand Down
2 changes: 1 addition & 1 deletion PlanetX/PlanetXGame/sound/playerDeath.audio.taml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<AudioAsset
AssetName="playerDeath"
AssetName="playerDeathBurst"
AudioFile="playerDeath.wav"
VolumeChannel="1" />
Loading
Loading