Skip to content

feat(flow editor): Format / Concat / IntToString / FloatToString palette nodes#207

Merged
apotema merged 2 commits into
mainfrom
feat/palette-string-nodes
Jun 8, 2026
Merged

feat(flow editor): Format / Concat / IntToString / FloatToString palette nodes#207
apotema merged 2 commits into
mainfrom
feat/palette-string-nodes

Conversation

@apotema

@apotema apotema commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Adds the four string-formatting reporter nodes (flow-codegen#26) to the flow editor palette. Closes #206. Follows the time-node palette work (#205).

What

  • Palette — a "String" cluster: + Format (seeds an empty template), + Concat, + IntToString, + FloatToString.
  • Reporter shape + pins — all four are reporters (rounded) with a value OUTPUT ([]const u8), no exec pins. IntToString/FloatToString take a single value input; Concat/Format take VARIADIC arg<N> inputs via a new varArgInputCount (data-input analogue of switchCaseOutputCount — renders arg0..arg<wired+1>, always one spare).
  • template fieldFormat gets an editable template (.text widget, same as Identifier.name), round-tripping to {"type":"Format","template":"hits={d}"}.

⚠️ Intentional behavior change to EXISTING nodes (please note)

There was no per-type reporter classifier before: all .other built-ins fell through nodeVisual's else arm to the rectangular command shape — so BinOp/Compare/Logic/Literal/Identifier/GetComponent rendered as commands despite being pure-value reporters, contradicting the RFC §6 intent the code states ("reporters rounded"). The new isReporterTypeName set includes those existing expression nodes as well as the four new ones, so they now render rounded. This is deliberate: rounding only the new string reporters would leave the existing expression reporters rectangular — a worse inconsistency. Net effect: all pure-value expression nodes now share the rounded reporter silhouette; control nodes (Branch/loops/Switch/time gates) stay rectangular. Purely cosmetic, no functional/serialization change.

Tests

zig build test463/463 (+7 new); zig build gui-test26/26; zig build clean.

Parent: #187.

…tte nodes (#206)

Surface the four built-in string reporter nodes in the flow editor so
they can be authored (codegen shipped in flow-codegen#26). All four are
`.other`-kind REPORTERS (rounded, data-only) producing a single `value`
([]const u8) output that wires into command data inputs (Log / Emit /
SetVariable, …). They carry no exec pins — they sit off the exec spine.

- flow_doc.isReporterTypeName: new type-name reporter classifier for the
  `.other` expression nodes (BinOp/Compare/Logic/Literal/Identifier/
  GetComponent + the four string reporters). `nodeVisual` routes `.other`
  nodes through it so the string nodes get the rounded silhouette +
  reporter border. They're not `isControlNode`, so `needs_exec_in` stays
  false and they never get an exec-in anchor.
- flow_doc render `.other` arms:
  - IntToString / FloatToString: one `value` data INPUT + one `value`
    data OUTPUT (same name, distinguished by pin direction in the id —
    matches codegen).
  - Concat / Format: a dynamic set of `arg<N>` data INPUT pins + one
    `value` data OUTPUT. The arg count is derived from the data edges
    already wired into the node plus one spare slot (`varArgInputCount`,
    mirroring `switchCaseOutputCount`), rendered from the static
    `var_arg_pin_names` table so `recordPin`'s borrowed names outlive the
    frame.
- flow_doc palette: new "Text" cluster — + Format / + Concat /
  + IntToString / + FloatToString. Format seeds an empty `template`
  string literal (`""`, like Identifier's name seed).
- flow_io.other_field_specs: Format exposes `template` (the std.fmt
  format string) as a `.text` field, edited + round-tripped as a quoted
  JSON string the same way Identifier.name is.
- tests: appendOtherNode shape + seeded template, reporter classification
  (rounded, no exec-in), varArgInputCount / argIndexOf derivation, and
  parse/render round-trip (template persists, load -> save byte-stable).
@cursor

cursor Bot commented Jun 8, 2026

Copy link
Copy Markdown

PR Summary

Low Risk
Editor-only UI and classification; intentional cosmetic rounding of existing reporter nodes with no functional or serialization impact.

Overview
Adds four string reporter nodes (Format, Concat, IntToString, FloatToString, flow-codegen#26) to the flow editor: a Text palette section, pin rendering (value in/out for scalars; dynamic arg<N> + value out for Concat/Format), and Format.template inspector editing via other_field_specs.

Variadic args mirror Switch case counting: new argIndexOf, comptime var_arg_pin_names, and varArgInputCount (wired highest arg<N> + one spare, capped at 64).

Reporter styling: new public isReporterTypeName drives rounded nodeVisual for the new types and existing expression reporters (BinOp, Literal, etc.) that previously rendered as rectangular commands — cosmetic only, no serialization change.

Exports NodeVisual, nodeVisual, and isReporterTypeName for tests. +7 unit tests cover palette seeding, pins, round-trip, and varArgInputCount.

Reviewed by Cursor Bugbot for commit 1997765. Bugbot is set up for automated code reviews on this repo. Configure here.

@apotema apotema requested a review from Copilot June 8, 2026 20:36

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements support for string and text reporter nodes (Format, Concat, IntToString, and FloatToString) in the flow editor. It introduces logic to dynamically calculate and render variadic input pins (arg<N>) based on active connections, styles these nodes as rounded reporters, and adds comprehensive unit tests. The review feedback suggests improving the robustness of pin name parsing in argIndexOf by strictly validating ASCII digits, and refactoring varArgInputCount to use @max for a more idiomatic Zig implementation.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/modules/flow_doc.zig
Comment on lines +828 to +833
pub fn argIndexOf(pin: []const u8) ?u32 {
if (!std.mem.startsWith(u8, pin, "arg")) return null;
const digits = pin[3..];
if (digits.len == 0) return null;
return std.fmt.parseInt(u32, digits, 10) catch null;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The argIndexOf function parses the pin name suffix using std.fmt.parseInt. However, parseInt allows a leading + sign (e.g., arg+1 would parse as 1). To ensure strict correctness and prevent malformed pin names from being parsed as valid indices, we should verify that all characters in digits are ASCII digits before parsing.

pub fn argIndexOf(pin: []const u8) ?u32 {
    if (!std.mem.startsWith(u8, pin, "arg")) return null;
    const digits = pin[3..];
    if (digits.len == 0) return null;
    for (digits) |c| {
        if (!std.ascii.isDigit(c)) return null;
    }
    return std.fmt.parseInt(u32, digits, 10) catch null;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — argIndexOf now rejects any non-digit suffix before parseInt, so arg+1 (and other malformed forms) no longer parse. Matches caseIndexOf/codegen's isCaseExecPin.

Comment thread src/modules/flow_doc.zig
Comment on lines +851 to +857
pub fn varArgInputCount(node_id: u32, edges: []const flow_io.Edge) u32 {
var highest: ?u32 = null;
for (edges) |e| {
if (e.to_node != node_id) continue;
const idx = argIndexOf(e.to_pin) orelse continue;
if (highest == null or idx > highest.?) highest = idx;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In Zig, we can write the tracking of the highest index more idiomatically by using @max and avoiding the force-unwrap highest.?.

pub fn varArgInputCount(node_id: u32, edges: []const flow_io.Edge) u32 {
    var highest: ?u32 = null;
    for (edges) |e| {
        if (e.to_node != node_id) continue;
        const idx = argIndexOf(e.to_pin) orelse continue;
        highest = if (highest) |h| @max(h, idx) else idx;
    }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — highest = if (highest) |h| @max(h, idx) else idx;, dropping the force-unwrap.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds the four string/text formatting reporter nodes to the flow editor palette and rendering logic, aligning editor visuals/pins with the existing flow-codegen contract (issue #206) and bringing .other expression nodes in line with the intended “reporters are rounded” UI rule.

Changes:

  • Adds a new “Text” palette section with Format, Concat, IntToString, and FloatToString nodes (including seeding Format.template).
  • Implements data-pin rendering for the new nodes, including derived variadic arg<N> inputs for Concat/Format via varArgInputCount.
  • Introduces reporter classification for .other expression nodes (including the new string reporters) so they render with the rounded reporter silhouette; adds corresponding unit tests and round-trip coverage.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
src/modules/flow_doc.zig Adds variadic arg-pin infrastructure, renders pins for new string reporter nodes, and classifies .other expression nodes as reporters for visuals.
src/flow_io.zig Adds Format.template to other_field_specs so it’s editable via the inspector .text widget and round-trips correctly.
src/tests.zig Adds tests covering creation defaults, reporter classification, variadic arg derivation, and parse/render round-tripping for the new nodes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/modules/flow_doc.zig
Comment on lines +2233 to +2237
/// flow-codegen treats as pure-value reporters. The string/text reporters
/// (`Concat`/`Format`/`IntToString`/`FloatToString`, flow-codegen#26) join
/// this set so `nodeVisual` gives them the rounded shape and the generic
/// classifier never awards them an exec-in anchor (they're not
/// `isControlNode`, so `needs_exec_in` stays false).
Comment thread src/tests.zig
Comment on lines +7297 to +7298
// Load → save is byte-stable (these carry only string / no extras,
// so there's no numeric normalisation to settle).
Comment thread src/tests.zig
Comment on lines +7210 to +7215
test "string reporters classify as rounded reporters with no exec-in" {
// All four render as REPORTERS (rounded silhouette) — `nodeVisual`
// returns the reporter rounding (14) via `isReporterTypeName`, and
// they carry no exec pins so they're never swept onto the exec
// spine (`isReporterTypeName` set is disjoint from the control set).
const reporter_round: f32 = 14.0;
argIndexOf rejects non-digit suffixes (parseInt accepted a leading '+');
varArgInputCount tracks the highest index via @max.
@apotema apotema merged commit bc9f3da into main Jun 8, 2026
4 checks passed
apotema added a commit that referenced this pull request Jun 9, 2026
…) (#212)

* feat(flow editor): input reporter palette nodes (labelle-gui#208)

Surface the nine built-in input reporter nodes in the flow editor so
they can be authored (codegen shipped in flow-codegen#51 + the parallel
more-reporters PR). All are `.other`-kind REPORTERS (rounded, data-only)
producing a single `value` output read inside per-frame flows — bool for
the key/mouse predicates, f32 for the mouse getters. They carry no exec
pins (they sit off the exec spine) and no data INPUT pins: the key/button
is an on-node FIELD, not a pin.

- IsKeyDown / IsKeyPressed / IsKeyReleased: a `key` field (a KeyboardKey
  tag, e.g. "space"), `value` output (bool).
- IsMouseButtonDown / IsMouseButtonPressed / IsMouseButtonReleased: a
  `button` field (a MouseButton tag, e.g. "left"), `value` output (bool).
- GetMouseX / GetMouseY / GetMouseWheel: no field, `value` output (f32).

Mirrors the #207 string-reporter palette path:
- flow_doc.isReporterTypeName: add the nine type names so nodeVisual
  rounds them and the generic classifier never awards them an exec-in
  anchor.
- flow_doc render `.other` arms: a single shared arm emits one `value`
  data OUTPUT for all nine (no data inputs).
- flow_io.other_field_specs: `key` (.text) for the three IsKey* nodes,
  `button` (.text) for the three IsMouseButton* nodes; GetMouse* need no
  spec. Seeds round-trip as quoted JSON strings codegen parses verbatim
  ({ "type": "IsKeyDown", "key": "space" }).
- flow_doc palette: new "Input" cluster near the String cluster, seeding
  key/button defaults "space" / "left"; GetMouse* seed no extras.
- tests: appendOtherNode shape + seeded field, reporter classification
  (rounded, no exec-in), parse/render round-trip (key/button persist,
  load -> save byte-stable).
- gui_tests: bump the hidden test window to 1280x1080 so the grown node
  palette's bottom "Add raw call…" escape hatch isn't clipped below the
  inspector child's scroll fold (TE can't auto-scroll to an item ImGui
  culled), keeping palette_has_plugin_section's wildcard click resolvable.

* refactor(palette): isInputReporter helper instead of a 9-way or chain (gemini #212)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

flow editor: add string nodes (Format/Concat/IntToString/FloatToString) to the palette

2 participants