@meonode/canvas 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,276 +2,364 @@
2
2
 
3
3
  var layout_canvas_util = require('./layout.canvas.util.js');
4
4
  var common_const = require('../constant/common.const.js');
5
+ var canvas_helper = require('./canvas.helper.js');
5
6
 
6
- // TODO: Add comprehensive unit tests for this file.
7
7
  /**
8
- * Grid layout node that arranges children in a configurable number of columns or rows.
9
- * Uses Yoga's flexbox capabilities with wrapping and gap properties to simulate a grid.
10
- * @extends RowNode
8
+ * Grid layout node that arranges children in a 2D grid.
9
+ * Implements a simplified version of the CSS Grid Layout algorithm.
11
10
  */
12
11
  class GridNode extends layout_canvas_util.RowNode {
13
- columns;
14
- columnGapValue;
15
- rowGapValue;
16
- isVertical; // True if the main axis is vertical (flexDirection: column or column-reverse)
17
12
  /**
18
13
  * Creates a new grid layout node
19
14
  * @param props Grid configuration properties
20
15
  */
21
16
  constructor(props) {
22
- const columns = Math.max(1, props.columns || 1);
23
- const direction = props.direction || 'row'; // Default to horizontal row
24
- const isVertical = direction === 'column' || direction === 'column-reverse';
25
- // Map direction string to Yoga FlexDirection
26
- let flexDirection;
27
- switch (direction) {
28
- case 'row':
29
- flexDirection = common_const.Style.FlexDirection.Row;
30
- break;
31
- case 'column':
32
- flexDirection = common_const.Style.FlexDirection.Column;
33
- break;
34
- case 'row-reverse':
35
- flexDirection = common_const.Style.FlexDirection.RowReverse;
36
- break;
37
- case 'column-reverse':
38
- flexDirection = common_const.Style.FlexDirection.ColumnReverse;
39
- break;
40
- default:
41
- console.warn(`[GridNode] Invalid direction "${direction}". Defaulting to "row".`);
42
- flexDirection = common_const.Style.FlexDirection.Row;
43
- }
44
- // Determine the column and row gap values from props
45
- let columnGap = 0;
46
- let rowGap = 0;
47
- if (typeof props.gap === 'number' || (typeof props.gap === 'string' && props.gap.trim() !== '')) {
48
- // Single value applies to both row and column gaps
49
- columnGap = props.gap;
50
- rowGap = props.gap;
51
- }
52
- else if (props.gap && typeof props.gap === 'object') {
53
- // Object format: prioritize a specific direction (Column/Row), then All
54
- columnGap = props.gap.Column ?? props.gap.All ?? 0;
55
- rowGap = props.gap.Row ?? props.gap.All ?? 0;
56
- }
57
17
  super({
58
- name: 'Grid',
59
- flexWrap: common_const.Style.Wrap.Wrap, // Essential for grid behavior
60
- flexDirection,
61
18
  ...props,
62
- // Explicitly remove the 'direction' prop passed to super, as it's handled by flexDirection
63
- direction: undefined,
64
- // Pass undefined for gap to prevent BoxNode from trying to parse it
65
- gap: undefined,
19
+ name: props.name || 'Grid',
20
+ flexWrap: common_const.Style.Wrap.Wrap,
66
21
  });
67
- this.columns = columns;
68
- this.columnGapValue = columnGap;
69
- this.rowGapValue = rowGap;
70
- this.isVertical = isVertical;
71
- // Explicitly set gaps on this.node after super() call
72
- // These will be updated again in updateLayoutBasedOnComputedSize, but this ensures initial setup
73
- if (typeof columnGap === 'number') {
74
- this.node.setGap(common_const.Style.Gutter.Column, columnGap);
75
- }
76
- else if (typeof columnGap === 'string' && columnGap.endsWith('%')) {
77
- this.node.setGapPercent(common_const.Style.Gutter.Column, parseFloat(columnGap));
22
+ }
23
+ /**
24
+ * Helper to parse a track size definition.
25
+ */
26
+ parseTrack(track, availableSpace) {
27
+ if (typeof track === 'number') {
28
+ return { type: 'px', value: track };
78
29
  }
79
- if (typeof rowGap === 'number') {
80
- this.node.setGap(common_const.Style.Gutter.Row, rowGap);
30
+ if (track === 'auto') {
31
+ return { type: 'auto', value: 0 };
81
32
  }
82
- else if (typeof rowGap === 'string' && rowGap.endsWith('%')) {
83
- this.node.setGapPercent(common_const.Style.Gutter.Row, parseFloat(rowGap));
33
+ if (typeof track === 'string') {
34
+ if (track.endsWith('fr')) {
35
+ return { type: 'fr', value: parseFloat(track) };
36
+ }
37
+ if (track.endsWith('%')) {
38
+ return { type: '%', value: canvas_helper.parsePercentage(track, availableSpace) };
39
+ }
40
+ // Try parsing as number (px) if just string "100"
41
+ const num = parseFloat(track);
42
+ if (!isNaN(num))
43
+ return { type: 'px', value: num };
84
44
  }
45
+ return { type: 'auto', value: 0 };
85
46
  }
86
47
  /**
87
- * Appends a child node to this grid.
88
- * Overridden primarily for documentation/clarity, functionality is inherited.
89
- * @param child Child node to append
90
- * @param index Index at which to insert the child
48
+ * Parses the gap property into pixels.
91
49
  */
92
- appendChild(child, index) {
93
- super.appendChild(child, index);
50
+ getGapPixels(gap, width, height) {
51
+ let rowGap = 0;
52
+ let colGap = 0;
53
+ if (typeof gap === 'number') {
54
+ rowGap = colGap = gap;
55
+ }
56
+ else if (typeof gap === 'string') {
57
+ const val = canvas_helper.parsePercentage(gap, width); // Use width as base for simplicity if %
58
+ rowGap = colGap = val;
59
+ }
60
+ else if (gap && typeof gap === 'object') {
61
+ const colVal = gap.Column ?? gap.All ?? 0;
62
+ const rowVal = gap.Row ?? gap.All ?? 0;
63
+ colGap = canvas_helper.parsePercentage(colVal, width);
64
+ rowGap = canvas_helper.parsePercentage(rowVal, height);
65
+ }
66
+ return { rowGap, colGap };
94
67
  }
95
68
  /**
96
69
  * Update layout calculations after the initial layout is computed.
97
- * This method calculates the appropriate flex-basis for children based on the
98
- * number of columns and gaps, respecting the container's padding,
99
- * and applies the gaps using Yoga's built-in properties.
100
70
  */
101
71
  updateLayoutBasedOnComputedSize() {
102
- // Step 1: Early return if the grid is empty or invalid
103
- if (this.columns <= 0 || this.children.length === 0) {
104
- return;
105
- }
106
- // Step 2: Get container dimensions and padding after the initial layout
72
+ // 1. Get Container Dimensions
107
73
  const width = this.node.getComputedWidth();
108
- const height = this.node.getComputedHeight();
109
74
  const paddingLeft = this.node.getComputedPadding(common_const.Style.Edge.Left);
110
75
  const paddingRight = this.node.getComputedPadding(common_const.Style.Edge.Right);
111
76
  const paddingTop = this.node.getComputedPadding(common_const.Style.Edge.Top);
112
77
  const paddingBottom = this.node.getComputedPadding(common_const.Style.Edge.Bottom);
113
- // Calculate content box dimensions
114
78
  const contentWidth = Math.max(0, width - paddingLeft - paddingRight);
115
- const contentHeight = Math.max(0, height - paddingTop - paddingBottom);
116
- // Step 3: Validate dimensions needed for calculations
117
- if (!this.isVertical && contentWidth <= 0 && width > 0) {
118
- console.warn(`[GridNode ${this.props.key} - Finalize] Grid content width (${contentWidth}) is zero or negative after accounting for padding (${paddingLeft}+${paddingRight}) on total width ${width}. Cannot calculate basis.`);
119
- if (this.columns > 1)
120
- return;
121
- }
122
- if (this.isVertical && contentHeight <= 0 && height > 0) {
123
- console.warn(`[GridNode ${this.props.key} - Finalize] Grid content height (${contentHeight}) is zero or negative after accounting for padding (${paddingTop}+${paddingBottom}) on total height ${height}. Cannot calculate basis.`);
124
- if (this.columns > 1)
125
- return;
79
+ const computedHeight = this.node.getComputedHeight();
80
+ const contentHeight = Math.max(0, computedHeight - paddingTop - paddingBottom);
81
+ const { templateColumns, templateRows, autoRows = 'auto', gap, columns } = this.props;
82
+ // 2. Resolve Gaps
83
+ const { rowGap, colGap } = this.getGapPixels(gap, contentWidth, contentHeight);
84
+ // 3. Resolve Columns (Tracks)
85
+ let explicitColTracks = templateColumns || [];
86
+ if (explicitColTracks.length === 0 && columns) {
87
+ explicitColTracks = Array(columns).fill('1fr');
126
88
  }
127
- // Step 4: Calculate Gap Values in Pixels
128
- let columnGapPixels = 0;
129
- if (typeof this.columnGapValue === 'number') {
130
- columnGapPixels = this.columnGapValue;
89
+ if (explicitColTracks.length === 0)
90
+ explicitColTracks = ['1fr'];
91
+ const resolvedColTracks = this.resolveTracks(explicitColTracks, contentWidth, colGap);
92
+ // Pre-calculate Col Offsets needed for placement/width
93
+ const colOffsetsValues = [0];
94
+ for (let i = 0; i < resolvedColTracks.length; i++) {
95
+ colOffsetsValues.push(colOffsetsValues[i] + resolvedColTracks[i] + colGap);
131
96
  }
132
- else if (typeof this.columnGapValue === 'string' && this.columnGapValue.trim().endsWith('%')) {
133
- try {
134
- const percent = parseFloat(this.columnGapValue);
135
- if (!isNaN(percent) && contentWidth > 0) {
136
- columnGapPixels = (percent / 100) * contentWidth;
137
- }
138
- else if (isNaN(percent)) {
139
- console.warn(`[GridNode ${this.props.key}] Invalid percentage column gap format: "${this.columnGapValue}". Using 0px.`);
97
+ // 4. Place Items & Resolve Explicit Row Tracks
98
+ const explicitRowTracks = templateRows || [];
99
+ const resolvedExplicitRowTracks = this.resolveTracks(explicitRowTracks, contentHeight, rowGap);
100
+ const cells = []; // true if occupied
101
+ const items = [];
102
+ const isOccupied = (r, c) => {
103
+ if (!cells[r])
104
+ return false;
105
+ return cells[r][c] === true;
106
+ };
107
+ const setOccupied = (r, c) => {
108
+ if (!cells[r])
109
+ cells[r] = [];
110
+ cells[r][c] = true;
111
+ };
112
+ let cursorRow = 0;
113
+ let cursorCol = 0;
114
+ for (const child of this.children) {
115
+ const childProps = child.props;
116
+ const { gridColumn, gridRow } = childProps;
117
+ let colStart;
118
+ let colEnd;
119
+ let colSpan = 1;
120
+ let rowStart;
121
+ let rowEnd;
122
+ let rowSpan = 1;
123
+ // ... Grid Placement Logic ...
124
+ if (gridColumn) {
125
+ const parts = gridColumn.split('/').map(s => s.trim());
126
+ if (parts[0]) {
127
+ if (parts[0].startsWith('span')) {
128
+ colSpan = parseInt(parts[0].replace('span', '')) || 1;
129
+ }
130
+ else {
131
+ colStart = parseInt(parts[0]) - 1;
132
+ }
140
133
  }
141
- else if (contentWidth <= 0) {
142
- console.warn(`[GridNode ${this.props.key}] Cannot calculate percentage column gap (${this.columnGapValue}) because content width is zero. Using 0px.`);
134
+ if (parts[1]) {
135
+ if (parts[1].startsWith('span')) {
136
+ const span = parseInt(parts[1].replace('span', '')) || 1;
137
+ if (colStart !== undefined) {
138
+ colEnd = colStart + span;
139
+ colSpan = span;
140
+ }
141
+ else {
142
+ // If start is undefined but end is span? Unusual. Treat as span.
143
+ colSpan = span;
144
+ }
145
+ }
146
+ else {
147
+ colEnd = parseInt(parts[1]) - 1;
148
+ if (colStart !== undefined) {
149
+ colSpan = colEnd - colStart;
150
+ }
151
+ }
143
152
  }
144
153
  }
145
- catch (e) {
146
- console.warn(`[GridNode ${this.props.key}] Error parsing percentage column gap: "${this.columnGapValue}". Using 0px.`, e);
154
+ if (gridRow) {
155
+ const parts = gridRow.split('/').map(s => s.trim());
156
+ if (parts[0]) {
157
+ if (parts[0].startsWith('span')) {
158
+ rowSpan = parseInt(parts[0].replace('span', '')) || 1;
159
+ }
160
+ else {
161
+ rowStart = parseInt(parts[0]) - 1;
162
+ }
163
+ }
164
+ if (parts[1]) {
165
+ if (parts[1].startsWith('span')) {
166
+ const span = parseInt(parts[1].replace('span', '')) || 1;
167
+ if (rowStart !== undefined) {
168
+ rowEnd = rowStart + span;
169
+ rowSpan = span;
170
+ }
171
+ else {
172
+ rowSpan = span;
173
+ }
174
+ }
175
+ else {
176
+ rowEnd = parseInt(parts[1]) - 1;
177
+ if (rowStart !== undefined) {
178
+ rowSpan = rowEnd - rowStart;
179
+ }
180
+ }
181
+ }
147
182
  }
148
- }
149
- else if (typeof this.columnGapValue === 'string' && this.columnGapValue.trim() !== '') {
150
- console.warn(`[GridNode ${this.props.key}] Unsupported string column gap format: "${this.columnGapValue}". Using 0px. Only numbers and percentages ('%') are supported.`);
151
- }
152
- let rowGapPixels = 0;
153
- if (typeof this.rowGapValue === 'number') {
154
- rowGapPixels = this.rowGapValue;
155
- }
156
- else if (typeof this.rowGapValue === 'string' && this.rowGapValue.trim().endsWith('%')) {
157
- try {
158
- const percent = parseFloat(this.rowGapValue);
159
- if (!isNaN(percent) && contentHeight > 0) {
160
- rowGapPixels = (percent / 100) * contentHeight;
183
+ if (colStart !== undefined && rowStart !== undefined) ;
184
+ else {
185
+ // Auto placement
186
+ let placed = false;
187
+ while (!placed) {
188
+ if (!cells[cursorRow])
189
+ cells[cursorRow] = [];
190
+ if (colStart !== undefined)
191
+ cursorCol = colStart;
192
+ let fits = true;
193
+ for (let r = 0; r < rowSpan; r++) {
194
+ for (let c = 0; c < colSpan; c++) {
195
+ if (isOccupied(cursorRow + r, cursorCol + c)) {
196
+ fits = false;
197
+ break;
198
+ }
199
+ }
200
+ if (!fits)
201
+ break;
202
+ }
203
+ if (fits) {
204
+ rowStart = cursorRow;
205
+ colStart = cursorCol;
206
+ placed = true;
207
+ }
208
+ else {
209
+ cursorCol++;
210
+ if (cursorCol + colSpan > resolvedColTracks.length) {
211
+ cursorCol = 0;
212
+ cursorRow++;
213
+ }
214
+ }
161
215
  }
162
- else if (isNaN(percent)) {
163
- console.warn(`[GridNode ${this.props.key}] Invalid percentage row gap format: "${this.rowGapValue}". Using 0px.`);
216
+ cursorCol += colSpan;
217
+ if (cursorCol >= resolvedColTracks.length) {
218
+ cursorCol = 0;
219
+ cursorRow++;
164
220
  }
165
- else if (contentHeight <= 0) {
166
- console.warn(`[GridNode ${this.props.key}] Cannot calculate percentage row gap (${this.rowGapValue}) because content height is zero. Using 0px.`);
221
+ }
222
+ rowEnd = (rowStart ?? 0) + rowSpan;
223
+ colEnd = (colStart ?? 0) + colSpan;
224
+ for (let r = rowStart; r < rowEnd; r++) {
225
+ for (let c = colStart; c < colEnd; c++) {
226
+ setOccupied(r, c);
167
227
  }
168
228
  }
169
- catch (e) {
170
- console.warn(`[GridNode ${this.props.key}] Error parsing percentage row gap: "${this.rowGapValue}". Using 0px.`, e);
229
+ // CRITICAL FIX: Pre-set width on item to ensure height calculation is accurate later
230
+ const itemColStart = colStart;
231
+ const itemColEnd = colEnd;
232
+ // Extend local offsets if needed for spanned columns beyond track count (rare but safe)
233
+ while (colOffsetsValues.length <= itemColEnd) {
234
+ colOffsetsValues.push(colOffsetsValues[colOffsetsValues.length - 1] + 0 + colGap);
171
235
  }
236
+ const cs = Math.min(itemColStart, colOffsetsValues.length - 1);
237
+ const ce = Math.min(itemColEnd, colOffsetsValues.length - 1);
238
+ const targetWidth = Math.max(0, colOffsetsValues[ce] - colOffsetsValues[cs] - colGap);
239
+ child.node.setWidth(targetWidth);
240
+ child.node.calculateLayout(targetWidth, Number.NaN, common_const.Style.Direction.LTR);
241
+ items.push({ node: child, rowStart: rowStart, rowEnd: rowEnd, colStart: itemColStart, colEnd: itemColEnd });
172
242
  }
173
- else if (typeof this.rowGapValue === 'string' && this.rowGapValue.trim() !== '') {
174
- console.warn(`[GridNode ${this.props.key}] Unsupported string row gap format: "${this.rowGapValue}". Using 0px. Only numbers and percentages ('%') are supported.`);
175
- }
176
- // Ensure gaps are not negative
177
- columnGapPixels = Math.max(0, columnGapPixels);
178
- rowGapPixels = Math.max(0, rowGapPixels);
179
- // Step 5: Calculate flex-basis percentage for children
180
- const mainAxisGapPixels = this.isVertical ? rowGapPixels : columnGapPixels;
181
- const mainAxisContentSize = this.isVertical ? contentHeight : contentWidth;
182
- let childWidth = 0;
183
- if (mainAxisContentSize > 0 && this.columns > 0) {
184
- // Total space taken up by gaps on the main axis
185
- const totalGapSpaceOnMainAxis = this.columns > 1 ? mainAxisGapPixels * (this.columns - 1) : 0;
186
- // Calculate the space available *only* for the items themselves
187
- const availableSpaceOnMainAxis = Math.max(0, mainAxisContentSize - totalGapSpaceOnMainAxis);
188
- // Calculate the exact pixel of the total content size that each item should occupy
189
- const exactItemWidth = availableSpaceOnMainAxis / this.columns;
190
- // Ensure it's not negative (shouldn't happen, but safety)
191
- childWidth = Math.max(0, exactItemWidth - 0.5); // Slightly reduce to avoid rounding issues
192
- }
193
- else if (this.columns === 1) {
194
- // If only one column, it takes up the full basis (gaps don't apply)
195
- childWidth = mainAxisContentSize;
196
- }
197
- // Clamp basis percentage between 0 and 100 (mostly redundant after floor/max(0) but safe)
198
- childWidth = Math.max(0, Math.min(mainAxisContentSize, childWidth));
199
- // Step 6: Apply layout properties to children
200
- let childrenNeedRecalculation = false;
201
- for (const child of this.children) {
202
- let childChanged = false;
203
- const currentLayoutWidth = child.node.getWidth();
204
- const currentWidthValue = currentLayoutWidth.value;
205
- const currentWidthUnit = currentLayoutWidth.unit;
206
- let widthNeedsUpdate = false;
207
- if (currentWidthUnit === common_const.Style.Unit.Point) {
208
- // If current width is in points, check if the value is significantly different
209
- if (Math.abs(currentWidthValue - childWidth) > 0.01) {
210
- widthNeedsUpdate = true;
243
+ // 6. Finalize Rows (Implicit)
244
+ const totalRowsNeeded = Math.max(resolvedExplicitRowTracks.length, ...items.map(i => i.rowEnd));
245
+ const resolvedRowTracks = [...resolvedExplicitRowTracks];
246
+ // Fill implicit rows
247
+ for (let r = resolvedExplicitRowTracks.length; r < totalRowsNeeded; r++) {
248
+ let rowSize = 0;
249
+ // Better 'auto' handling:
250
+ if (autoRows === 'auto') {
251
+ const rowItems = items.filter(i => i.rowStart === r && i.rowEnd - i.rowStart === 1);
252
+ for (const item of rowItems) {
253
+ rowSize = Math.max(rowSize, item.node.node.getComputedHeight());
211
254
  }
212
255
  }
213
256
  else {
214
- // If current width is not in points (e.g., Auto, Percent, Undefined), it needs to be set to points
215
- widthNeedsUpdate = true;
216
- }
217
- if (widthNeedsUpdate) {
218
- child.node.setWidth(childWidth);
219
- childChanged = true;
257
+ const parsed = this.parseTrack(autoRows, contentHeight);
258
+ rowSize = parsed.value;
220
259
  }
221
- // Ensure grow/shrink are set correctly for grid items
222
- if (child.node.getFlexGrow() !== 0) {
223
- child.node.setFlexGrow(0);
224
- childChanged = true;
260
+ resolvedRowTracks.push(rowSize);
261
+ }
262
+ // 6. Calculate Offsets (Rows) & Final Layout Application
263
+ const colOffsets = colOffsetsValues; // Re-use
264
+ const rowOffsets = [0];
265
+ for (let i = 0; i < resolvedRowTracks.length; i++) {
266
+ let size = resolvedRowTracks[i];
267
+ // Re-check auto-sized explicit rows (value 0)
268
+ if (size === 0) {
269
+ const rowItems = items.filter(it => it.rowStart === i && it.rowEnd - it.rowStart === 1);
270
+ for (const item of rowItems) {
271
+ size = Math.max(size, item.node.node.getComputedHeight());
272
+ }
273
+ resolvedRowTracks[i] = size;
225
274
  }
226
- if (child.node.getFlexShrink() !== 1) {
227
- child.node.setFlexShrink(1); // Allow shrinking
228
- childChanged = true;
275
+ rowOffsets.push(rowOffsets[i] + size + rowGap);
276
+ }
277
+ // 7. Apply Positions
278
+ let childrenChanged = false;
279
+ for (const item of items) {
280
+ const x = colOffsets[item.colStart] + paddingLeft;
281
+ while (colOffsets.length <= item.colEnd) {
282
+ colOffsets.push(colOffsets[colOffsets.length - 1] + 0 + colGap);
229
283
  }
230
- // Remove margins that might interfere with gap property
231
- if (child.node.getMargin(common_const.Style.Edge.Bottom).unit !== common_const.Style.Unit.Undefined) {
232
- child.node.setMargin(common_const.Style.Edge.Bottom, undefined);
233
- childChanged = true;
284
+ const widthStart = colOffsets[item.colStart];
285
+ const widthEnd = colOffsets[item.colEnd];
286
+ const totalWidth = Math.max(0, widthEnd - widthStart - colGap);
287
+ const y = rowOffsets[item.rowStart] + paddingTop;
288
+ const heightStart = rowOffsets[item.rowStart];
289
+ const heightEnd = rowOffsets[item.rowEnd];
290
+ const totalHeight = Math.max(0, heightEnd - heightStart - rowGap);
291
+ const childNode = item.node.node;
292
+ if (childNode.getPositionType() !== common_const.Style.PositionType.Absolute) {
293
+ childNode.setPositionType(common_const.Style.PositionType.Absolute);
294
+ childrenChanged = true;
234
295
  }
235
- if (child.node.getMargin(common_const.Style.Edge.Right).unit !== common_const.Style.Unit.Undefined) {
236
- child.node.setMargin(common_const.Style.Edge.Right, undefined);
237
- childChanged = true;
296
+ if (childNode.getPosition(common_const.Style.Edge.Left).value !== x) {
297
+ childNode.setPosition(common_const.Style.Edge.Left, x);
298
+ childrenChanged = true;
238
299
  }
239
- if (child.node.getMargin(common_const.Style.Edge.Top).unit !== common_const.Style.Unit.Undefined) {
240
- child.node.setMargin(common_const.Style.Edge.Top, undefined);
241
- childChanged = true;
300
+ if (childNode.getPosition(common_const.Style.Edge.Top).value !== y) {
301
+ childNode.setPosition(common_const.Style.Edge.Top, y);
302
+ childrenChanged = true;
242
303
  }
243
- if (child.node.getMargin(common_const.Style.Edge.Left).unit !== common_const.Style.Unit.Undefined) {
244
- child.node.setMargin(common_const.Style.Edge.Left, undefined);
245
- childChanged = true;
304
+ if (childNode.getWidth().unit !== common_const.Style.Unit.Point || Math.abs(childNode.getWidth().value - totalWidth) > 0.1) {
305
+ childNode.setWidth(totalWidth);
306
+ childrenChanged = true;
246
307
  }
247
- if (childChanged && !child.node.isDirty()) {
248
- child.node.markDirty();
249
- childrenNeedRecalculation = true;
308
+ if (childNode.getHeight().unit !== common_const.Style.Unit.Point || Math.abs(childNode.getHeight().value - totalHeight) > 0.1) {
309
+ childNode.setHeight(totalHeight);
310
+ childrenChanged = true;
250
311
  }
251
312
  }
252
- // Step 7: Apply gaps using Yoga's built-in gap properties
253
- const currentColumnGap = this.node.getGap(common_const.Style.Gutter.Column).value;
254
- const currentRowGap = this.node.getGap(common_const.Style.Gutter.Row).value;
255
- let gapsChanged = false;
256
- // Use a small tolerance for comparing gap pixels
257
- if (Math.abs(currentColumnGap - columnGapPixels) > 0.001) {
258
- this.node.setGap(common_const.Style.Gutter.Column, columnGapPixels);
259
- gapsChanged = true;
313
+ // 9. Update Grid Height
314
+ const totalGridHeight = Math.max(0, rowOffsets[rowOffsets.length - 1] - rowGap);
315
+ const currentHeightStyle = this.node.getHeight();
316
+ if (currentHeightStyle.unit === common_const.Style.Unit.Auto || currentHeightStyle.unit === common_const.Style.Unit.Undefined) {
317
+ const targetTotalHeight = totalGridHeight + paddingTop + paddingBottom;
318
+ this.node.setHeight(targetTotalHeight);
319
+ childrenChanged = true;
260
320
  }
261
- if (Math.abs(currentRowGap - rowGapPixels) > 0.001) {
262
- this.node.setGap(common_const.Style.Gutter.Row, rowGapPixels);
263
- gapsChanged = true;
264
- }
265
- // Step 8: Mark the grid node itself as dirty if gaps changed or children changed
266
- if ((gapsChanged || childrenNeedRecalculation) && !this.node.isDirty()) {
321
+ if (childrenChanged && !this.node.isDirty()) {
267
322
  this.node.markDirty();
268
323
  }
269
324
  }
325
+ /**
326
+ * Resolves track sizes to pixels.
327
+ */
328
+ resolveTracks(tracks, availableSpace, gap) {
329
+ const resolved = [];
330
+ let usedSpace = 0;
331
+ let totalFr = 0;
332
+ const frIndices = [];
333
+ tracks.forEach((t, i) => {
334
+ const parsed = this.parseTrack(t, availableSpace);
335
+ if (parsed.type === 'px' || parsed.type === '%') {
336
+ resolved[i] = parsed.value;
337
+ usedSpace += parsed.value;
338
+ }
339
+ else if (parsed.type === 'fr') {
340
+ totalFr += parsed.value;
341
+ resolved[i] = 0;
342
+ frIndices.push(i);
343
+ }
344
+ else {
345
+ resolved[i] = 0;
346
+ }
347
+ });
348
+ const totalGaps = Math.max(0, tracks.length - 1) * gap;
349
+ usedSpace += totalGaps;
350
+ const remainingSpace = Math.max(0, availableSpace - usedSpace);
351
+ if (totalFr > 0) {
352
+ frIndices.forEach(i => {
353
+ const parsed = this.parseTrack(tracks[i], availableSpace);
354
+ const share = (parsed.value / totalFr) * remainingSpace;
355
+ resolved[i] = share;
356
+ });
357
+ }
358
+ return resolved;
359
+ }
270
360
  }
271
361
  /**
272
362
  * Factory function to create a new GridNode instance.
273
- * @param props Grid configuration properties.
274
- * @returns A new GridNode instance.
275
363
  */
276
364
  const Grid = (props) => new GridNode(props);
277
365