Skip to content

feat: Implement Dynamic Flexible Grid Layout Engine (#3345) - #3361

Open
knoxiboy wants to merge 1 commit into
Karanjot786:mainfrom
knoxiboy:3345-feat-dynamic-flexible-grid-layout-engine
Open

feat: Implement Dynamic Flexible Grid Layout Engine (#3345)#3361
knoxiboy wants to merge 1 commit into
Karanjot786:mainfrom
knoxiboy:3345-feat-dynamic-flexible-grid-layout-engine

Conversation

@knoxiboy

@knoxiboy knoxiboy commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Description

Implements a dynamic flexible grid layout calculation engine supporting fractional (fr), percentage, and fixed unit tracks to dynamically calculate terminal UI widget bounding boxes.

Related Issue

Closes #3345

Which package(s)?

@termuijs/core

Type of Change

  • 🐛 Bug fix (type:bug)
  • ✨ Feature (type:feature)
  • 📝 Docs (type:docs)
  • 🧪 Tests (type:testing)
  • ♻️ Refactor (type:refactor)
  • 🎨 Design / UX (type:design)
  • ♿ Accessibility (type:accessibility)
  • ⚡ Performance (type:performance)
  • 🔧 DevOps / CI (type:devops)
  • 🔒 Security (type:security)

Checklist

  • ⭐ You starred the repo. The needs-star check blocks your merge otherwise.
  • Tests pass locally: bun vitest run
  • Build passes: bun run build
  • Typecheck passes: bun run typecheck
  • You read CONTRIBUTING.md.
  • Your PR title follows type: short description.
  • Widget state mutators call markDirty() (if your change affects rendering).
  • No new any types without an inline comment explaining why.
  • No unrelated refactors bundled into this PR.

GSSoC 2026 Participation

  • You are a GSSoC 2026 contributor.
  • Your GSSoC profile: https://gssoc.girlscript.org/profile/knoxiboy

Screenshots / Recordings (UI changes)

Calculates dynamic grid cell dimensions across fixed and fractional tracks.

Notes for the Reviewer

Zero external dependencies; integrates cleanly into the core layout pipeline.

Summary by CodeRabbit

  • New Features

    • Added grid layout support with flexible row and column sizing.
    • Supports automatic, pixel, percentage, and fractional track units.
    • Added calculation of grid cell positions and dimensions, including custom origins.
  • Tests

    • Added coverage for track parsing, mixed sizing strategies, fractional sizing, and cell rectangle calculations.

@knoxiboy
knoxiboy requested a review from Karanjot786 as a code owner August 1, 2026 13:56
Copilot AI review requested due to automatic review settings August 1, 2026 13:56

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions github-actions Bot added area:core @termuijs/core type:testing +10 pts. Tests. type:feature +10 pts. New feature. labels Aug 1, 2026
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Added GridLayoutManager with track parsing, fixed and fractional size allocation, and cell-rectangle calculation. Added public track-related types and unit tests covering parsing, sizing, proportional fractions, and cell coordinates.

Changes

Grid layout engine

Layer / File(s) Summary
Track contracts and parsing
packages/core/src/layout/gridLayout.ts
Added TrackUnit, TrackSpec, and GridCellRect. Added parsing for auto, pixels, percentages, fractional units, numeric values, and invalid-token fallbacks.
Track-size allocation
packages/core/src/layout/gridLayout.ts
Added allocation for pixel and percentage tracks, followed by distribution of remaining space across fractional tracks.
Cell geometry and validation
packages/core/src/layout/gridLayout.ts, packages/core/test/gridLayout.test.ts
Added cell-rectangle calculation with optional origins and tests for parsing, mixed sizing, proportional fractions, and coordinates.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

  • Karanjot786/TermUI#1360: Implements overlapping CSS Grid track resolution and placement logic related to these grid utilities.

Suggested labels: level:advanced

Suggested reviewers: karanjot786, copilot

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The implementation covers track parsing, sizing, and cell rectangles, but it does not show row spanning, margin or padding handling, the dashboard example, or the requested export. Add row spanning, automatic margins, and padding distribution, create examples/dashboard-grid.ts, and export the engine through the package entry point.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the feature and follows the required type-and-description format.
Description check ✅ Passed The description includes the required sections, issue link, package, feature classification, checklist, contributor data, and reviewer notes.
Out of Scope Changes check ✅ Passed The changed files contain grid layout implementation and focused tests, which align with the linked feature and show no unrelated refactors.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

🧹 Nitpick comments (1)
packages/core/test/gridLayout.test.ts (1)

1-2: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Exercise the public export in this test.

Line 2 imports the implementation module directly. A broken src/index.ts export could still pass this suite. Import from ../src, or add a separate smoke test through the public entry point.

The PR objective defines src/index.ts as the public export.

Suggested import change
-import { GridLayoutManager } from '../src/layout/gridLayout';
+import { GridLayoutManager } from '../src';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/test/gridLayout.test.ts` around lines 1 - 2, Update the
GridLayoutManager import in gridLayout.test.ts to use the public ../src entry
point instead of the implementation module, so the test exercises the
src/index.ts export while preserving the existing test behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/core/src/layout/gridLayout.ts`:
- Around line 19-33: Update parseTrackSpec to use complete-token numeric
parsing, preserve explicit 0fr, and default only the bare fr token to value 1;
malformed or invalid factors must not be accepted from numeric prefixes. In the
track allocation logic that accumulates totalFr, validate each public TrackSpec
value and ignore or safely handle negative and non-finite fr values before
adding them to totalFr.
- Around line 61-68: Update the second-pass fractional allocation in the grid
layout calculation so integer rounding distributes all leftover units among the
`fr` tracks, ensuring their sizes sum to `remainingSize` (for example, three 1fr
tracks over 10 units must total 10). Add a test covering fractional tracks and
assert that the computed sizes sum to the available remaining space.

In `@packages/core/test/gridLayout.test.ts`:
- Around line 22-26: Update computeTrackSizes and its related fr-allocation
logic to handle non-divisible totals without leaving uncovered cells; for a
total of 100 with three 1fr tracks, ensure the implementation follows an
explicit remainder policy and the resulting sizes cover all 100 cells. Add a
regression test alongside the existing GridLayoutManager track-size tests for
this case.

---

Nitpick comments:
In `@packages/core/test/gridLayout.test.ts`:
- Around line 1-2: Update the GridLayoutManager import in gridLayout.test.ts to
use the public ../src entry point instead of the implementation module, so the
test exercises the src/index.ts export while preserving the existing test
behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: dabcaec0-9c52-4c26-911e-2ecc8aabba3b

📥 Commits

Reviewing files that changed from the base of the PR and between 48f63a1 and 4a6a763.

📒 Files selected for processing (2)
  • packages/core/src/layout/gridLayout.ts
  • packages/core/test/gridLayout.test.ts

Comment on lines +19 to +33
static parseTrackSpec(specStr: string): TrackSpec[] {
const tokens = specStr.trim().split(/\s+/).filter(Boolean);
return tokens.map((token) => {
if (token === 'auto') return { value: 0, unit: 'auto' };
if (token.endsWith('px')) {
return { value: parseFloat(token.slice(0, -2)) || 0, unit: 'px' };
}
if (token.endsWith('%')) {
return { value: parseFloat(token.slice(0, -1)) || 0, unit: 'percent' };
}
if (token.endsWith('fr')) {
return { value: parseFloat(token.slice(0, -2)) || 1, unit: 'fr' };
}
const num = parseFloat(token);
return isNaN(num) ? { value: 0, unit: 'auto' } : { value: num, unit: 'px' };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve zero fr values and reject invalid factors.

parseTrackSpec('0fr') currently returns { value: 1, unit: 'fr' } because 0 || 1 is truthy fallback logic. This changes an explicit zero-flex track into a flexible track.

parseFloat also accepts numeric prefixes for malformed tokens. Since TrackSpec is public, callers can bypass the parser and pass negative or non-finite fr values. The allocation formula can then produce negative or NaN track sizes.

Use complete-token numeric parsing. Treat only the bare fr token as the default value 1. Validate TrackSpec.value before adding it to totalFr.

Proposed fix
-      if (token.endsWith('fr')) {
-        return { value: parseFloat(token.slice(0, -2)) || 1, unit: 'fr' };
-      }
+      if (token === 'fr') return { value: 1, unit: 'fr' };
+      if (token.endsWith('fr')) {
+        const value = Number(token.slice(0, -2));
+        if (!Number.isFinite(value) || value < 0) {
+          return { value: 0, unit: 'auto' };
+        }
+        return { value, unit: 'fr' };
+      }
...
       } else if (track.unit === 'fr') {
+        if (!Number.isFinite(track.value) || track.value < 0) {
+          throw new RangeError('fr track values must be finite and non-negative');
+        }
         totalFr += track.value;

Also applies to: 56-65

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/layout/gridLayout.ts` around lines 19 - 33, Update
parseTrackSpec to use complete-token numeric parsing, preserve explicit 0fr, and
default only the bare fr token to value 1; malformed or invalid factors must not
be accepted from numeric prefixes. In the track allocation logic that
accumulates totalFr, validate each public TrackSpec value and ignore or safely
handle negative and non-finite fr values before adding them to totalFr.

Comment on lines +61 to +68
// Second pass: allocate fr units from remaining size
if (totalFr > 0 && remainingSize > 0) {
tracks.forEach((track, i) => {
if (track.unit === 'fr') {
sizes[i] = Math.floor((remainingSize * track.value) / totalFr);
}
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Distribute the fractional rounding remainder.

Independent Math.floor calls can leave part of the available space unused. For example, three 1fr tracks in a size of 10 return [3, 3, 3]. The cell rectangles then cover only 9 cells.

Allocate the remainder to one fractional track or use a largest-remainder distribution. Add a test that verifies the fractional sizes sum to the remaining space.

Proposed fix
+      const frIndexes = tracks.flatMap((track, index) =>
+        track.unit === 'fr' && track.value > 0 ? [index] : [],
+      );
+      let allocatedFrSize = 0;
       tracks.forEach((track, i) => {
         if (track.unit === 'fr') {
-          sizes[i] = Math.floor((remainingSize * track.value) / totalFr);
+          sizes[i] =
+            i === frIndexes[frIndexes.length - 1]
+              ? remainingSize - allocatedFrSize
+              : Math.floor((remainingSize * track.value) / totalFr);
+          allocatedFrSize += sizes[i];
         }
       });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Second pass: allocate fr units from remaining size
if (totalFr > 0 && remainingSize > 0) {
tracks.forEach((track, i) => {
if (track.unit === 'fr') {
sizes[i] = Math.floor((remainingSize * track.value) / totalFr);
}
});
}
// Second pass: allocate fr units from remaining size
if (totalFr > 0 && remainingSize > 0) {
const frIndexes = tracks.flatMap((track, index) =>
track.unit === 'fr' && track.value > 0 ? [index] : [],
);
let allocatedFrSize = 0;
tracks.forEach((track, i) => {
if (track.unit === 'fr') {
sizes[i] =
i === frIndexes[frIndexes.length - 1]
? remainingSize - allocatedFrSize
: Math.floor((remainingSize * track.value) / totalFr);
allocatedFrSize += sizes[i];
}
});
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/layout/gridLayout.ts` around lines 61 - 68, Update the
second-pass fractional allocation in the grid layout calculation so integer
rounding distributes all leftover units among the `fr` tracks, ensuring their
sizes sum to `remainingSize` (for example, three 1fr tracks over 10 units must
total 10). Add a test covering fractional tracks and assert that the computed
sizes sum to the available remaining space.

Comment on lines +22 to +26
it('should compute equal track sizes for multiple fr tracks', () => {
const tracks = GridLayoutManager.parseTrackSpec('1fr 2fr');
const sizes = GridLayoutManager.computeTrackSizes(90, tracks);
expect(sizes).toEqual([30, 60]);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Cover non-divisible fr allocations.

Lines 22-26 use a divisible total, so the test does not detect rounding gaps. For a total size of 100 with 1fr 1fr 1fr, computeTrackSizes returns [33, 33, 33], and the tracks cover only 99 cells.

Add a regression case and distribute the remainder, or define an explicit remainder policy, before relying on this test for arbitrary terminal sizes.

Suggested regression test
+  it('should fill the container when fr tracks require rounding', () => {
+    const tracks = GridLayoutManager.parseTrackSpec('1fr 1fr 1fr');
+    const sizes = GridLayoutManager.computeTrackSizes(100, tracks);
+
+    expect(sizes.reduce((total, size) => total + size, 0)).toBe(100);
+  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/test/gridLayout.test.ts` around lines 22 - 26, Update
computeTrackSizes and its related fr-allocation logic to handle non-divisible
totals without leaving uncovered cells; for a total of 100 with three 1fr
tracks, ensure the implementation follows an explicit remainder policy and the
resulting sizes cover all 100 cells. Add a regression test alongside the
existing GridLayoutManager track-size tests for this case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:core @termuijs/core type:feature +10 pts. New feature. type:testing +10 pts. Tests.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Implement Dynamic Flexible Grid Layout Engine

2 participants