@aiquants/resize-panels 1.7.2 → 1.8.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.
- package/LICENSE +21 -0
- package/README.md +21 -2
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/styles/resize-panels.css +1 -1
- package/dist/styles/resize-panels.standalone.css +3 -0
- package/package.json +10 -7
- package/src/GlobalDebugOverlay.tsx +284 -0
- package/src/Panel.tsx +635 -0
- package/src/PanelDebugInfo.tsx +148 -0
- package/src/PanelGroup.tsx +260 -0
- package/src/PanelResizeHandle.tsx +718 -0
- package/src/context.ts +25 -0
- package/src/debugOverlayStore.ts +351 -0
- package/src/hooks.ts +968 -0
- package/src/index.ts +62 -0
- package/src/reducer.ts +1234 -0
- package/src/roundHalfToEven.ts +40 -0
- package/src/styles/components.entry.css +8 -0
- package/src/styles/resize-panels.css +3 -0
- package/src/styles/standalone.entry.css +12 -0
- package/src/types.ts +288 -0
- package/src/utils/simple-logger.ts +186 -0
- package/src/utils.ts +279 -0
- package/dist/resize-panels.css +0 -1
package/src/reducer.ts
ADDED
|
@@ -0,0 +1,1234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Reducer for managing panel state in PanelGroup
|
|
3
|
+
* PanelGroup のパネル状態を管理するための Reducer
|
|
4
|
+
*/
|
|
5
|
+
import { roundHalfToEven } from "./roundHalfToEven"
|
|
6
|
+
import type { PanelCollapseDirection, PanelLayoutData } from "./types"
|
|
7
|
+
import { adjustPixelPanelsByPriority, clamp, getConstraintInPixels, PERCENTAGE_DIGITS, percentageToPixels, toRoundedPercentage } from "./utils"
|
|
8
|
+
import { Logger, LogLevel } from "./utils/simple-logger"
|
|
9
|
+
|
|
10
|
+
const logger = new Logger(LogLevel.INFO, "[resize-panels]")
|
|
11
|
+
|
|
12
|
+
// px 値同士の比較で浮動小数点の桁落ち (数 ulp 程度) を許容する誤差。
|
|
13
|
+
// 厳密比較だと T - (T - min) が min を 1ulp 下回っただけで境界へのリサイズが棄却され、
|
|
14
|
+
// 小数コンテナ幅では最小サイズに到達できなくなる。
|
|
15
|
+
const SIZE_EPSILON = 1e-9
|
|
16
|
+
|
|
17
|
+
const snapshotPanelState = (panels: PanelLayoutData[]) =>
|
|
18
|
+
panels.map((panel) => ({
|
|
19
|
+
id: panel.id,
|
|
20
|
+
size: panel.size,
|
|
21
|
+
percentageSize: panel.percentageSize,
|
|
22
|
+
preferredPercentageSize: panel.preferredPercentageSize,
|
|
23
|
+
sizeBeforeCollapse: panel.sizeBeforeCollapse,
|
|
24
|
+
percentageSizeBeforeCollapse: panel.percentageSizeBeforeCollapse,
|
|
25
|
+
preferredPercentageSizeBeforeCollapse: panel.preferredPercentageSizeBeforeCollapse,
|
|
26
|
+
measuredPercentageSize: panel.measuredPercentageSize,
|
|
27
|
+
excludeFromAutoGrowth: panel.excludeFromAutoGrowth ?? false,
|
|
28
|
+
collapsed: panel.collapsed ?? false,
|
|
29
|
+
collapseFromStart: panel.collapseFromStart,
|
|
30
|
+
collapseFromEnd: panel.collapseFromEnd,
|
|
31
|
+
collapsedByDirection: panel.collapsedByDirection ?? null,
|
|
32
|
+
}))
|
|
33
|
+
|
|
34
|
+
const snapshotSinglePanel = (panel: PanelLayoutData | undefined | null) => {
|
|
35
|
+
if (!panel) {
|
|
36
|
+
return null
|
|
37
|
+
}
|
|
38
|
+
return snapshotPanelState([panel])[0]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const panelCanCollapse = (panel: PanelLayoutData | undefined | null): boolean => {
|
|
42
|
+
if (!panel) {
|
|
43
|
+
return false
|
|
44
|
+
}
|
|
45
|
+
return Boolean(panel.collapseFromStart || panel.collapseFromEnd)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Layout calculation logic
|
|
49
|
+
const recalculateFlexiblePanels = (panels: PanelLayoutData[], containerSize: number): PanelLayoutData[] => {
|
|
50
|
+
if (!Number.isFinite(containerSize) || containerSize <= 0) {
|
|
51
|
+
return panels
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type Descriptor = {
|
|
55
|
+
index: number
|
|
56
|
+
isPixelPanel: boolean
|
|
57
|
+
minSize: number
|
|
58
|
+
maxSize: number
|
|
59
|
+
preferredSize: number
|
|
60
|
+
priority: number
|
|
61
|
+
flexPriority?: number
|
|
62
|
+
excludeFromGrowth: boolean
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const nextPanels = panels.map((panel) => ({ ...panel }))
|
|
66
|
+
const descriptors: Descriptor[] = []
|
|
67
|
+
|
|
68
|
+
for (let index = 0; index < nextPanels.length; index += 1) {
|
|
69
|
+
const panel = nextPanels[index]
|
|
70
|
+
if (panel.collapsed) {
|
|
71
|
+
nextPanels[index] = {
|
|
72
|
+
...panel,
|
|
73
|
+
size: 0,
|
|
74
|
+
percentageSize: 0,
|
|
75
|
+
}
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const minFromConstraint = Math.max(0, getConstraintInPixels(panel.minSize, 0, containerSize))
|
|
80
|
+
const autoMinSize = Math.max(0, getConstraintInPixels(panel.autoMinSize, 0, containerSize))
|
|
81
|
+
const minSize = Math.max(minFromConstraint, autoMinSize)
|
|
82
|
+
const zeroThreshold = Math.max(1e-3, minSize)
|
|
83
|
+
const rawMaxSize = getConstraintInPixels(panel.maxSize, containerSize, containerSize)
|
|
84
|
+
const maxSize = Math.max(minSize, rawMaxSize)
|
|
85
|
+
const isPixelPanel = panel.sizeUnit === "pixels" || panel.originalPixelSize !== undefined
|
|
86
|
+
const currentSize = Math.max(0, panel.size)
|
|
87
|
+
let preferredSize = minSize
|
|
88
|
+
|
|
89
|
+
if (isPixelPanel) {
|
|
90
|
+
const preferredPixel = panel.preferredPixelSize ?? panel.size ?? panel.originalPixelSize ?? minSize
|
|
91
|
+
preferredSize = clamp(preferredPixel, minSize, maxSize)
|
|
92
|
+
} else {
|
|
93
|
+
const fallbackPercentage = panel.preferredPercentageSize ?? panel.percentageSize ?? toRoundedPercentage(panel.size, containerSize)
|
|
94
|
+
const preferredPixels = percentageToPixels(fallbackPercentage, containerSize)
|
|
95
|
+
preferredSize = clamp(preferredPixels, minSize, maxSize)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const excludeFromGrowth = !isPixelPanel && (panel.excludeFromAutoGrowth ?? false) && currentSize <= zeroThreshold
|
|
99
|
+
descriptors.push({
|
|
100
|
+
index,
|
|
101
|
+
isPixelPanel,
|
|
102
|
+
minSize,
|
|
103
|
+
maxSize,
|
|
104
|
+
preferredSize,
|
|
105
|
+
priority: panel.pixelAdjustPriority ?? index + 1,
|
|
106
|
+
flexPriority: !isPixelPanel ? panel.flexAdjustPriority : undefined,
|
|
107
|
+
excludeFromGrowth,
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (descriptors.length === 0) {
|
|
112
|
+
return nextPanels
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const assignments = new Map<number, number>()
|
|
116
|
+
let baseTotal = 0
|
|
117
|
+
for (const descriptor of descriptors) {
|
|
118
|
+
assignments.set(descriptor.index, descriptor.minSize)
|
|
119
|
+
baseTotal += descriptor.minSize
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let remaining = containerSize - baseTotal
|
|
123
|
+
|
|
124
|
+
const allocate = (descriptor: Descriptor, amount: number): number => {
|
|
125
|
+
if (amount <= 0) {
|
|
126
|
+
return 0
|
|
127
|
+
}
|
|
128
|
+
const currentSize = assignments.get(descriptor.index) ?? descriptor.minSize
|
|
129
|
+
const capacity = Math.max(0, descriptor.maxSize - currentSize)
|
|
130
|
+
if (capacity <= 0) {
|
|
131
|
+
return 0
|
|
132
|
+
}
|
|
133
|
+
const applied = Math.min(amount, capacity)
|
|
134
|
+
assignments.set(descriptor.index, currentSize + applied)
|
|
135
|
+
return applied
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (remaining > 0) {
|
|
139
|
+
const distributeFlexibleByRatio = (targetDescriptors: Descriptor[]) => {
|
|
140
|
+
const epsilon = 1e-6
|
|
141
|
+
while (remaining > epsilon) {
|
|
142
|
+
let totalWeight = 0
|
|
143
|
+
const weights: Array<{ descriptor: Descriptor; capacity: number }> = []
|
|
144
|
+
for (const descriptor of targetDescriptors) {
|
|
145
|
+
if (!descriptor.isPixelPanel && descriptor.excludeFromGrowth) {
|
|
146
|
+
continue
|
|
147
|
+
}
|
|
148
|
+
const currentSize = assignments.get(descriptor.index) ?? descriptor.minSize
|
|
149
|
+
const targetSize = Math.min(descriptor.preferredSize, descriptor.maxSize)
|
|
150
|
+
const capacity = Math.max(0, targetSize - currentSize)
|
|
151
|
+
if (capacity > epsilon) {
|
|
152
|
+
totalWeight += capacity
|
|
153
|
+
weights.push({ descriptor, capacity })
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (weights.length === 0 || totalWeight <= epsilon) {
|
|
157
|
+
break
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let progress = 0
|
|
161
|
+
const iterationRemaining = remaining
|
|
162
|
+
for (const entry of weights) {
|
|
163
|
+
if (remaining <= epsilon) {
|
|
164
|
+
break
|
|
165
|
+
}
|
|
166
|
+
if (!entry.descriptor.isPixelPanel && entry.descriptor.excludeFromGrowth) {
|
|
167
|
+
continue
|
|
168
|
+
}
|
|
169
|
+
const share = (iterationRemaining * entry.capacity) / totalWeight
|
|
170
|
+
const amount = Math.min(entry.capacity, share)
|
|
171
|
+
if (amount <= 0) {
|
|
172
|
+
continue
|
|
173
|
+
}
|
|
174
|
+
const applied = allocate(entry.descriptor, amount)
|
|
175
|
+
if (applied > 0) {
|
|
176
|
+
remaining -= applied
|
|
177
|
+
progress += applied
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (progress <= epsilon) {
|
|
182
|
+
break
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const pixelDescriptors = descriptors.filter((descriptor) => descriptor.isPixelPanel)
|
|
188
|
+
pixelDescriptors.sort((a, b) => b.priority - a.priority)
|
|
189
|
+
for (const descriptor of pixelDescriptors) {
|
|
190
|
+
if (remaining <= 0) {
|
|
191
|
+
break
|
|
192
|
+
}
|
|
193
|
+
const currentSize = assignments.get(descriptor.index) ?? descriptor.minSize
|
|
194
|
+
const targetSize = Math.min(descriptor.preferredSize, descriptor.maxSize)
|
|
195
|
+
const needed = Math.max(0, targetSize - currentSize)
|
|
196
|
+
if (needed <= 0) {
|
|
197
|
+
continue
|
|
198
|
+
}
|
|
199
|
+
const applied = allocate(descriptor, Math.min(needed, remaining))
|
|
200
|
+
if (applied > 0) {
|
|
201
|
+
remaining -= applied
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (remaining > 0) {
|
|
206
|
+
const flexibleDescriptors = descriptors.filter((descriptor) => !descriptor.isPixelPanel)
|
|
207
|
+
if (flexibleDescriptors.length > 0) {
|
|
208
|
+
const prioritizedDescriptors = flexibleDescriptors.filter((descriptor) => descriptor.flexPriority !== undefined && !descriptor.excludeFromGrowth)
|
|
209
|
+
if (prioritizedDescriptors.length > 0) {
|
|
210
|
+
prioritizedDescriptors.sort((a, b) => (b.flexPriority ?? 0) - (a.flexPriority ?? 0))
|
|
211
|
+
for (const descriptor of prioritizedDescriptors) {
|
|
212
|
+
if (remaining <= 0) {
|
|
213
|
+
break
|
|
214
|
+
}
|
|
215
|
+
if (descriptor.excludeFromGrowth) {
|
|
216
|
+
continue
|
|
217
|
+
}
|
|
218
|
+
const currentSize = assignments.get(descriptor.index) ?? descriptor.minSize
|
|
219
|
+
const targetSize = Math.min(descriptor.preferredSize, descriptor.maxSize)
|
|
220
|
+
const needed = Math.max(0, targetSize - currentSize)
|
|
221
|
+
if (needed <= 0) {
|
|
222
|
+
continue
|
|
223
|
+
}
|
|
224
|
+
const applied = allocate(descriptor, Math.min(needed, remaining))
|
|
225
|
+
if (applied > 0) {
|
|
226
|
+
remaining -= applied
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (remaining > 0) {
|
|
232
|
+
const ratioTargets = prioritizedDescriptors.length > 0 ? flexibleDescriptors.filter((descriptor) => descriptor.flexPriority === undefined && !descriptor.excludeFromGrowth) : flexibleDescriptors.filter((descriptor) => !descriptor.excludeFromGrowth)
|
|
233
|
+
if (ratioTargets.length > 0) {
|
|
234
|
+
distributeFlexibleByRatio(ratioTargets)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (remaining > 0) {
|
|
242
|
+
let previousRemaining = remaining
|
|
243
|
+
for (;;) {
|
|
244
|
+
let totalCapacity = 0
|
|
245
|
+
const candidates: Descriptor[] = []
|
|
246
|
+
for (const descriptor of descriptors) {
|
|
247
|
+
if (!descriptor.isPixelPanel && descriptor.excludeFromGrowth) {
|
|
248
|
+
continue
|
|
249
|
+
}
|
|
250
|
+
const currentSize = assignments.get(descriptor.index) ?? descriptor.minSize
|
|
251
|
+
const descriptorMax = descriptor.isPixelPanel ? Math.min(descriptor.maxSize, descriptor.preferredSize) : descriptor.maxSize
|
|
252
|
+
const capacity = Math.max(0, descriptorMax - currentSize)
|
|
253
|
+
if (capacity > 0) {
|
|
254
|
+
candidates.push(descriptor)
|
|
255
|
+
totalCapacity += capacity
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (totalCapacity <= 0) {
|
|
259
|
+
break
|
|
260
|
+
}
|
|
261
|
+
const iterationRemaining = remaining
|
|
262
|
+
for (const descriptor of candidates) {
|
|
263
|
+
if (remaining <= 0) {
|
|
264
|
+
break
|
|
265
|
+
}
|
|
266
|
+
if (!descriptor.isPixelPanel && descriptor.excludeFromGrowth) {
|
|
267
|
+
continue
|
|
268
|
+
}
|
|
269
|
+
const currentSize = assignments.get(descriptor.index) ?? descriptor.minSize
|
|
270
|
+
const descriptorMax = descriptor.isPixelPanel ? Math.min(descriptor.maxSize, descriptor.preferredSize) : descriptor.maxSize
|
|
271
|
+
const capacity = Math.max(0, descriptorMax - currentSize)
|
|
272
|
+
if (capacity <= 0) {
|
|
273
|
+
continue
|
|
274
|
+
}
|
|
275
|
+
const share = (iterationRemaining * capacity) / totalCapacity
|
|
276
|
+
const applied = allocate(descriptor, Math.min(capacity, share))
|
|
277
|
+
if (applied > 0) {
|
|
278
|
+
remaining -= applied
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (remaining <= 1e-6 || Math.abs(previousRemaining - remaining) <= 1e-6) {
|
|
282
|
+
break
|
|
283
|
+
}
|
|
284
|
+
previousRemaining = remaining
|
|
285
|
+
}
|
|
286
|
+
remaining = Math.max(0, remaining)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// remaining < 0 (最小サイズ合計がコンテナを超える不能構成) は意図的にここで縮小しない。
|
|
290
|
+
// 配分は各パネルの minSize を下限として維持し、不能状態は hooks 側の layoutConstraintViolation が
|
|
291
|
+
// 検出・通知してハンドル無効化とプレースホルダー表示で対処する。
|
|
292
|
+
// (かつて存在した縮小ループは、割当が常に minSize 以上・縮小下限も minSize のため一切動作しないデッドコードだった)
|
|
293
|
+
|
|
294
|
+
for (const descriptor of descriptors) {
|
|
295
|
+
const assignedSize = assignments.get(descriptor.index) ?? descriptor.minSize
|
|
296
|
+
const panel = nextPanels[descriptor.index]
|
|
297
|
+
const panelIsCollapsible = panelCanCollapse(panel)
|
|
298
|
+
const nextSizeBeforeCollapse = panelIsCollapsible ? assignedSize : panel.sizeBeforeCollapse
|
|
299
|
+
const nextMeasuredBeforeCollapse = panelIsCollapsible ? (panel.measuredPixelSizeBeforeCollapse ?? panel.measuredPixelSize ?? assignedSize) : panel.measuredPixelSizeBeforeCollapse
|
|
300
|
+
const nextPercentage = containerSize > 0 ? toRoundedPercentage(assignedSize, containerSize) : 0
|
|
301
|
+
|
|
302
|
+
nextPanels[descriptor.index] = {
|
|
303
|
+
...panel,
|
|
304
|
+
size: assignedSize,
|
|
305
|
+
percentageSize: nextPercentage,
|
|
306
|
+
preferredPercentageSize: panel.preferredPercentageSize,
|
|
307
|
+
preferredPixelSize: descriptor.isPixelPanel ? (panel.preferredPixelSize ?? assignedSize) : panel.preferredPixelSize,
|
|
308
|
+
sizeBeforeCollapse: nextSizeBeforeCollapse,
|
|
309
|
+
measuredPixelSizeBeforeCollapse: nextMeasuredBeforeCollapse,
|
|
310
|
+
measuredPixelSize: undefined,
|
|
311
|
+
measuredPercentageSize: undefined,
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return normalizePanelPercentages(nextPanels)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Normalize panel percentage snapshots so active panels sum to 100%.
|
|
320
|
+
* アクティブなパネルの割合が 100% になるようにスナップショットを正規化する処理。
|
|
321
|
+
*/
|
|
322
|
+
const normalizePanelPercentages = (panels: PanelLayoutData[]): PanelLayoutData[] => {
|
|
323
|
+
let totalActiveSize = 0
|
|
324
|
+
for (const panel of panels) {
|
|
325
|
+
if (panel.collapsed) {
|
|
326
|
+
continue
|
|
327
|
+
}
|
|
328
|
+
totalActiveSize += Math.max(panel.size, 0)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// アクティブなパネルサイズの合計がゼロの場合はすべての割合をゼロにリセット
|
|
332
|
+
if (totalActiveSize <= 0) {
|
|
333
|
+
let mutated = false
|
|
334
|
+
const normalizedPanels = panels.map((panel) => {
|
|
335
|
+
if ((panel.percentageSize ?? 0) !== 0) {
|
|
336
|
+
mutated = true
|
|
337
|
+
// 折りたたみ時は割合をゼロに初期化して整合性を保つ
|
|
338
|
+
return {
|
|
339
|
+
...panel,
|
|
340
|
+
percentageSize: 0,
|
|
341
|
+
measuredPercentageSize: panel.measuredPercentageSize !== undefined ? 0 : panel.measuredPercentageSize,
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (!panel.collapsed && panel.sizeBeforeCollapse !== panel.size) {
|
|
345
|
+
mutated = true
|
|
346
|
+
// 折りたたみ前のサイズがずれている場合は現在サイズで上書きする
|
|
347
|
+
return {
|
|
348
|
+
...panel,
|
|
349
|
+
sizeBeforeCollapse: panel.size,
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return panel
|
|
353
|
+
})
|
|
354
|
+
return mutated ? normalizedPanels : panels
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// パネル配列に変更が発生したかを示すフラグ
|
|
358
|
+
let mutated = false
|
|
359
|
+
|
|
360
|
+
const normalizedPanels = panels.map((panel) => {
|
|
361
|
+
// 折りたたまれたパネルの割合はゼロに固定
|
|
362
|
+
if (panel.collapsed) {
|
|
363
|
+
if ((panel.percentageSize ?? 0) !== 0) {
|
|
364
|
+
mutated = true
|
|
365
|
+
return {
|
|
366
|
+
...panel,
|
|
367
|
+
percentageSize: 0,
|
|
368
|
+
measuredPercentageSize: panel.measuredPercentageSize !== undefined ? 0 : panel.measuredPercentageSize,
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return panel
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// size から計算した割合
|
|
375
|
+
const calculatedPercentage = toRoundedPercentage(panel.size, totalActiveSize)
|
|
376
|
+
|
|
377
|
+
// 測定済み割合があり、size から計算した割合と近い場合は測定値を優先
|
|
378
|
+
// 乖離が大きい場合は size から計算した値を使用(RESIZE_PANELS 直後など)
|
|
379
|
+
const measuredPercentage = panel.measuredPercentageSize
|
|
380
|
+
const percentageDivergence = measuredPercentage !== undefined ? Math.abs(measuredPercentage - calculatedPercentage) : Infinity
|
|
381
|
+
const useMeasuredPercentage = percentageDivergence < 0.0001 // 0.01% 未満の乖離なら測定値を優先
|
|
382
|
+
|
|
383
|
+
// 判定結果に基づき割合の基準値を確定する
|
|
384
|
+
const basePercentage = useMeasuredPercentage && measuredPercentage !== undefined ? measuredPercentage : calculatedPercentage
|
|
385
|
+
const nextPercentage = roundHalfToEven(basePercentage, PERCENTAGE_DIGITS)
|
|
386
|
+
const percentageDiff = Math.abs((panel.percentageSize ?? 0) - nextPercentage)
|
|
387
|
+
const shouldUpdatePercentage = percentageDiff > 0 || panel.percentageSize === undefined
|
|
388
|
+
const sizeBeforeCollapseDiffers = panel.sizeBeforeCollapse === undefined || panel.sizeBeforeCollapse !== panel.size
|
|
389
|
+
const panelIsCollapsible = panelCanCollapse(panel)
|
|
390
|
+
const shouldUpdateSizeBeforeCollapse = panelIsCollapsible && sizeBeforeCollapseDiffers
|
|
391
|
+
const measuredSnapshot = panel.measuredPixelSize ?? panel.size
|
|
392
|
+
const measuredBeforeDiffers = panelIsCollapsible && (panel.measuredPixelSizeBeforeCollapse === undefined || panel.measuredPixelSizeBeforeCollapse !== measuredSnapshot)
|
|
393
|
+
|
|
394
|
+
if (!(shouldUpdatePercentage || shouldUpdateSizeBeforeCollapse || measuredBeforeDiffers)) {
|
|
395
|
+
return panel
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
mutated = true
|
|
399
|
+
const nextSizeBeforeCollapse = shouldUpdateSizeBeforeCollapse ? panel.size : panel.sizeBeforeCollapse
|
|
400
|
+
const nextMeasuredPixelBeforeCollapse = measuredBeforeDiffers ? measuredSnapshot : panel.measuredPixelSizeBeforeCollapse
|
|
401
|
+
return {
|
|
402
|
+
...panel,
|
|
403
|
+
percentageSize: nextPercentage,
|
|
404
|
+
sizeBeforeCollapse: nextSizeBeforeCollapse,
|
|
405
|
+
measuredPixelSizeBeforeCollapse: nextMeasuredPixelBeforeCollapse,
|
|
406
|
+
collapsedByDirection: panel.collapsed ? (panel.collapsedByDirection ?? null) : null,
|
|
407
|
+
}
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
return mutated ? normalizedPanels : panels
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
type ActivePanelCandidate = {
|
|
414
|
+
index: number
|
|
415
|
+
minSize: number
|
|
416
|
+
maxSize: number
|
|
417
|
+
isFlexible: boolean
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Reconcile active panel sizes so their total matches the container dimensions.
|
|
422
|
+
* コンテナ寸法に整合するようアクティブパネルのサイズ合計を補正する処理。
|
|
423
|
+
*/
|
|
424
|
+
const reconcilePanelSizesWithContainer = (panels: PanelLayoutData[], containerSize: number): boolean => {
|
|
425
|
+
if (containerSize <= 0) {
|
|
426
|
+
return false
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const activeIndices: number[] = []
|
|
430
|
+
let activeTotal = 0
|
|
431
|
+
|
|
432
|
+
// アクティブなパネルのインデックスとサイズ合計を収集
|
|
433
|
+
for (let index = 0; index < panels.length; index += 1) {
|
|
434
|
+
const panel = panels[index]
|
|
435
|
+
// 折りたたまれたパネルは除外
|
|
436
|
+
if (!panel || panel.collapsed) {
|
|
437
|
+
continue
|
|
438
|
+
}
|
|
439
|
+
activeIndices.push(index)
|
|
440
|
+
activeTotal += panel.size
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// アクティブなパネルが存在しない場合は調整不要
|
|
444
|
+
if (activeIndices.length === 0) {
|
|
445
|
+
return false
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// コンテナサイズとアクティブパネルの合計サイズの差分を計算
|
|
449
|
+
let remaining = containerSize - activeTotal
|
|
450
|
+
// 差分がゼロの場合は調整不要
|
|
451
|
+
if (Math.abs(remaining) <= 0) {
|
|
452
|
+
return false
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// パネルに変更が発生したかを追跡するフラグ
|
|
456
|
+
let mutated = false
|
|
457
|
+
|
|
458
|
+
// パネルインデックスから調整候補情報を構築 (キャッシュ付き)
|
|
459
|
+
const buildCandidate = (index: number): ActivePanelCandidate => {
|
|
460
|
+
// // キャッシュから取得を試みる
|
|
461
|
+
// const cached = candidateCache.get(index)
|
|
462
|
+
// if (cached) {
|
|
463
|
+
// return cached
|
|
464
|
+
// }
|
|
465
|
+
const panel = panels[index]
|
|
466
|
+
// パネルの制約をピクセル値に変換
|
|
467
|
+
const minFromConstraint = getConstraintInPixels(panel.minSize, 0, containerSize)
|
|
468
|
+
const autoMinSize = getConstraintInPixels(panel.autoMinSize, 0, containerSize)
|
|
469
|
+
const minSize = Math.max(minFromConstraint, autoMinSize)
|
|
470
|
+
const maxSize = getConstraintInPixels(panel.maxSize, containerSize, containerSize)
|
|
471
|
+
// パーセンテージ単位かつピクセル固定値を持たないパネルを柔軟とみなす
|
|
472
|
+
const isFlexible = panel.sizeUnit === "percentage" && panel.originalPixelSize === undefined
|
|
473
|
+
const candidate = {
|
|
474
|
+
index,
|
|
475
|
+
minSize,
|
|
476
|
+
maxSize,
|
|
477
|
+
isFlexible,
|
|
478
|
+
}
|
|
479
|
+
// // 構築した候補情報をキャッシュに保存
|
|
480
|
+
// candidateCache.set(index, candidate)
|
|
481
|
+
return candidate
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// 指定パネルにサイズ差分を適用
|
|
485
|
+
const applyDelta = (index: number, delta: number) => {
|
|
486
|
+
// 差分が有限数でない、またはゼロ以下の場合は適用不要
|
|
487
|
+
if (!Number.isFinite(delta) || Math.abs(delta) <= 0) {
|
|
488
|
+
return
|
|
489
|
+
}
|
|
490
|
+
const target = panels[index]
|
|
491
|
+
// 対象パネルが存在しない場合はスキップ
|
|
492
|
+
if (!target) {
|
|
493
|
+
return
|
|
494
|
+
}
|
|
495
|
+
// 差分を適用した新しいサイズを計算 (負値にならないよう保護)
|
|
496
|
+
const nextSize = Math.max(0, target.size + delta)
|
|
497
|
+
// サイズ変化が実質ゼロの場合は更新不要
|
|
498
|
+
if (Math.abs(nextSize - target.size) <= 0) {
|
|
499
|
+
return
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// 新しいサイズに対応する割合を計算
|
|
503
|
+
const nextPercentage = toRoundedPercentage(nextSize, containerSize)
|
|
504
|
+
// 折りたたみ可能な場合は新サイズを復元用に保存、そうでない場合は既存値を維持
|
|
505
|
+
const targetIsCollapsible = panelCanCollapse(target)
|
|
506
|
+
const nextSizeBeforeCollapse = targetIsCollapsible ? nextSize : target.sizeBeforeCollapse
|
|
507
|
+
// 折りたたみ可能な場合は測定ピクセル値を更新、そうでない場合は既存値を維持
|
|
508
|
+
const nextMeasuredPixelBeforeCollapse = targetIsCollapsible ? (target.measuredPixelSizeBeforeCollapse ?? target.measuredPixelSize ?? nextSize) : target.measuredPixelSizeBeforeCollapse
|
|
509
|
+
|
|
510
|
+
// パネルデータを更新し、測定値はリセットして再測定を促す
|
|
511
|
+
panels[index] = {
|
|
512
|
+
...target,
|
|
513
|
+
size: nextSize,
|
|
514
|
+
percentageSize: nextPercentage,
|
|
515
|
+
sizeBeforeCollapse: nextSizeBeforeCollapse,
|
|
516
|
+
measuredPixelSizeBeforeCollapse: nextMeasuredPixelBeforeCollapse,
|
|
517
|
+
measuredPixelSize: undefined,
|
|
518
|
+
measuredPercentageSize: undefined,
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// 残余スペースから適用した差分を減算
|
|
522
|
+
remaining -= delta
|
|
523
|
+
// 変更フラグを立てる
|
|
524
|
+
mutated = true
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Distribute remaining size among candidate panels to satisfy container constraints.
|
|
529
|
+
* コンテナ制約に合わせるため候補パネルへ残余サイズを再配分する処理。
|
|
530
|
+
*/
|
|
531
|
+
const adjustWithCandidates = (candidates: ActivePanelCandidate[]) => {
|
|
532
|
+
for (const candidate of candidates) {
|
|
533
|
+
// 残余スペースがゼロになったら調整完了
|
|
534
|
+
if (Math.abs(remaining) <= 0) {
|
|
535
|
+
break
|
|
536
|
+
}
|
|
537
|
+
const panel = panels[candidate.index]
|
|
538
|
+
// パネルが存在しないか折りたたまれている場合はスキップ
|
|
539
|
+
if (!panel || panel.collapsed) {
|
|
540
|
+
continue
|
|
541
|
+
}
|
|
542
|
+
const currentSize = panel.size
|
|
543
|
+
|
|
544
|
+
// 残余スペースが正の場合はパネルを拡張
|
|
545
|
+
if (remaining > 0) {
|
|
546
|
+
// 最大サイズまでの拡張可能量を計算
|
|
547
|
+
const capacity = Math.max(0, candidate.maxSize - currentSize)
|
|
548
|
+
// 拡張余地がない場合は次の候補へ
|
|
549
|
+
if (capacity <= 0) {
|
|
550
|
+
continue
|
|
551
|
+
}
|
|
552
|
+
// 拡張可能量と残余スペースの小さい方を適用量とする
|
|
553
|
+
const applied = Math.min(capacity, remaining)
|
|
554
|
+
// 適用量がゼロ以下の場合はスキップ
|
|
555
|
+
if (applied <= 0) {
|
|
556
|
+
continue
|
|
557
|
+
}
|
|
558
|
+
// 拡張差分を適用
|
|
559
|
+
applyDelta(candidate.index, applied)
|
|
560
|
+
} else {
|
|
561
|
+
// 残余スペースが負の場合はパネルを縮小
|
|
562
|
+
// 最小サイズまでの縮小可能量を計算
|
|
563
|
+
const capacity = Math.max(0, currentSize - candidate.minSize)
|
|
564
|
+
// 縮小余地がない場合は次の候補へ
|
|
565
|
+
if (capacity <= 0) {
|
|
566
|
+
continue
|
|
567
|
+
}
|
|
568
|
+
// 縮小可能量と残余スペースの絶対値の小さい方を適用量とする
|
|
569
|
+
const applied = Math.min(capacity, Math.abs(remaining))
|
|
570
|
+
// 適用量がゼロ以下の場合はスキップ
|
|
571
|
+
if (applied <= 0) {
|
|
572
|
+
continue
|
|
573
|
+
}
|
|
574
|
+
// 縮小差分を適用 (負の値)
|
|
575
|
+
applyDelta(candidate.index, -applied)
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// 柔軟パネル (パーセンテージ単位) の候補を抽出
|
|
581
|
+
const flexibleCandidates = activeIndices.map((index) => buildCandidate(index)).filter((candidate) => candidate.isFlexible)
|
|
582
|
+
// 柔軟パネルが存在する場合は優先的に調整
|
|
583
|
+
if (flexibleCandidates.length > 0) {
|
|
584
|
+
adjustWithCandidates(flexibleCandidates)
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// まだ残余スペースが残っている場合はピクセルパネルを優先度順に調整
|
|
588
|
+
if (Math.abs(remaining) > 0) {
|
|
589
|
+
const previousRemaining = remaining
|
|
590
|
+
// ピクセルパネルを優先度に基づいて調整
|
|
591
|
+
remaining = adjustPixelPanelsByPriority(panels, remaining, containerSize, { enforceAutoMinSize: true })
|
|
592
|
+
// 残余スペースが変化した場合は変更フラグを立てる
|
|
593
|
+
if (Math.abs(remaining - previousRemaining) > 0) {
|
|
594
|
+
mutated = true
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// // 残余が負でかつ最小サイズ合計がコンテナを超える場合は比率縮小を実施
|
|
599
|
+
// if (Math.abs(remaining) > 0 && remaining < 0 && totalMinSize - containerSize > 0) {
|
|
600
|
+
// // 調整後のアクティブパネル合計サイズを再計算
|
|
601
|
+
// const activeTotalAfter = activeIndices.reduce((sum, index) => {
|
|
602
|
+
// const panel = panels[index]
|
|
603
|
+
// // 折りたたまれたパネルは除外
|
|
604
|
+
// if (!panel || panel.collapsed) {
|
|
605
|
+
// return sum
|
|
606
|
+
// }
|
|
607
|
+
// return sum + Math.max(panel.size, 0)
|
|
608
|
+
// }, 0)
|
|
609
|
+
|
|
610
|
+
// // アクティブ合計がゼロより大きい場合は比率縮小を実行
|
|
611
|
+
// if (activeTotalAfter > 0) {
|
|
612
|
+
// // コンテナサイズに対する縮小比率を計算
|
|
613
|
+
// const scale = containerSize > 0 ? containerSize / activeTotalAfter : 0
|
|
614
|
+
// // 割り当て済みサイズの累積値
|
|
615
|
+
// let allocated = 0
|
|
616
|
+
|
|
617
|
+
// // 各アクティブパネルに対して比率縮小を適用
|
|
618
|
+
// for (let i = 0; i < activeIndices.length; i += 1) {
|
|
619
|
+
// const panelIndex = activeIndices[i]
|
|
620
|
+
// const target = panels[panelIndex]
|
|
621
|
+
|
|
622
|
+
// // 折りたたまれたパネルはスキップ
|
|
623
|
+
// if (!target || target.collapsed) {
|
|
624
|
+
// continue
|
|
625
|
+
// }
|
|
626
|
+
|
|
627
|
+
// // 現在サイズに縮小比率を掛けて新サイズを計算
|
|
628
|
+
// let nextSize = containerSize <= 0 ? 0 : target.size * scale
|
|
629
|
+
|
|
630
|
+
// // コンテナサイズがゼロまたはパネルが1つだけの場合はコンテナサイズ全体を割り当て
|
|
631
|
+
// if (containerSize <= 0 || activeIndices.length === 1) {
|
|
632
|
+
// nextSize = Math.max(0, containerSize)
|
|
633
|
+
// allocated = nextSize
|
|
634
|
+
// } else if (i === activeIndices.length - 1) {
|
|
635
|
+
// // 最後のパネルには残りスペース全体を割り当てて丸め誤差を吸収
|
|
636
|
+
// nextSize = Math.max(0, containerSize - allocated)
|
|
637
|
+
// } else {
|
|
638
|
+
// // 中間パネルは計算値をそのまま使用
|
|
639
|
+
// nextSize = Math.max(0, nextSize)
|
|
640
|
+
// allocated += nextSize
|
|
641
|
+
// }
|
|
642
|
+
|
|
643
|
+
// // 新しいサイズに対応する割合を計算
|
|
644
|
+
// const nextPercentage = toRoundedPercentage(nextSize, containerSize)
|
|
645
|
+
// // 折りたたみ可能な場合は新サイズを復元用に保存
|
|
646
|
+
// const nextSizeBeforeCollapse = target.collapsible ? nextSize : target.sizeBeforeCollapse
|
|
647
|
+
// // 折りたたみ可能な場合は測定ピクセル値を更新
|
|
648
|
+
// const nextMeasuredPixelBeforeCollapse = target.collapsible ? (target.measuredPixelSizeBeforeCollapse ?? target.measuredPixelSize ?? nextSize) : target.measuredPixelSizeBeforeCollapse
|
|
649
|
+
|
|
650
|
+
// // パネルデータを更新し、測定値はリセットして再測定を促す
|
|
651
|
+
// panels[panelIndex] = {
|
|
652
|
+
// ...target,
|
|
653
|
+
// size: nextSize,
|
|
654
|
+
// percentageSize: nextPercentage,
|
|
655
|
+
// sizeBeforeCollapse: nextSizeBeforeCollapse,
|
|
656
|
+
// measuredPixelSizeBeforeCollapse: nextMeasuredPixelBeforeCollapse,
|
|
657
|
+
// measuredPixelSize: undefined,
|
|
658
|
+
// measuredPercentageSize: undefined,
|
|
659
|
+
// }
|
|
660
|
+
// }
|
|
661
|
+
|
|
662
|
+
// // 残余スペースをゼロにリセット
|
|
663
|
+
// remaining = 0
|
|
664
|
+
// // 変更フラグを立てる
|
|
665
|
+
// mutated = true
|
|
666
|
+
// }
|
|
667
|
+
// }
|
|
668
|
+
|
|
669
|
+
// // まだ残余が残っている場合は、すべてのアクティブパネルをフォールバック候補として調整
|
|
670
|
+
// if (Math.abs(remaining) > 0) {
|
|
671
|
+
// const fallbackCandidates = activeIndices.map((index) => buildCandidate(index))
|
|
672
|
+
// adjustWithCandidates(fallbackCandidates)
|
|
673
|
+
// }
|
|
674
|
+
|
|
675
|
+
return mutated
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
export type PanelAction =
|
|
679
|
+
| { type: "REGISTER_PANEL"; panel: PanelLayoutData }
|
|
680
|
+
| { type: "UNREGISTER_PANEL"; id: string }
|
|
681
|
+
| { type: "RESIZE_PANELS"; leftId: string; rightId: string; leftSize: number; rightSize: number; containerSize?: number }
|
|
682
|
+
| { type: "SET_PANELS"; panels: PanelLayoutData[] }
|
|
683
|
+
| { type: "CONTAINER_RESIZED"; containerSize: number; direction: "horizontal" | "vertical" }
|
|
684
|
+
| { type: "COLLAPSE_PANEL"; id: string; containerSize: number; from: PanelCollapseDirection }
|
|
685
|
+
| { type: "EXPAND_PANEL"; id: string; containerSize: number; from: PanelCollapseDirection; targetSize?: number }
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Assign priority for pixel adjustment if the panel is pixel-based and priority is undefined.
|
|
689
|
+
* ピクセルベースのパネルで優先度が未定義の場合に調整優先度を自動割り当てする関数。
|
|
690
|
+
*
|
|
691
|
+
* @param panel - Target panel data / 対象のパネルデータ
|
|
692
|
+
* @param panels - All current panels in the group / グループ内の全パネル
|
|
693
|
+
* @param excludeIndex - Index to exclude from priority calculation (for updates) / 優先度計算から除外するインデックス (更新時用)
|
|
694
|
+
* @returns Updated panel with assigned priority / 優先度が割り当てられたパネル
|
|
695
|
+
*/
|
|
696
|
+
const assignPixelAdjustPriority = (panel: PanelLayoutData, panels: PanelLayoutData[], excludeIndex?: number): PanelLayoutData => {
|
|
697
|
+
// ピクセル単位パネルかどうかを判定
|
|
698
|
+
const isPixelPanel = panel.sizeUnit === "pixels" || panel.originalPixelSize !== undefined
|
|
699
|
+
// ピクセルパネルでない、または既に優先度が設定済みの場合はそのまま返す
|
|
700
|
+
if (!isPixelPanel || panel.pixelAdjustPriority !== undefined) {
|
|
701
|
+
return panel
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// 既存のピクセルパネルから優先度を収集 (除外インデックスを考慮)
|
|
705
|
+
const existingPriorities = panels
|
|
706
|
+
.filter((candidate, index) => index !== excludeIndex && (candidate.sizeUnit === "pixels" || candidate.originalPixelSize !== undefined))
|
|
707
|
+
.map((candidate) => candidate.pixelAdjustPriority)
|
|
708
|
+
.filter((value): value is number => value !== undefined)
|
|
709
|
+
|
|
710
|
+
// 既存の最大優先度 + 1 を新しい優先度として割り当て (既存がない場合は 1)
|
|
711
|
+
const nextPriority = existingPriorities.length > 0 ? Math.max(...existingPriorities) + 1 : 1
|
|
712
|
+
return {
|
|
713
|
+
...panel,
|
|
714
|
+
pixelAdjustPriority: nextPriority,
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* Handle panel registration and size updates for the panel group.
|
|
720
|
+
* パネルグループ内の登録・サイズ更新を処理するリデューサー。
|
|
721
|
+
*/
|
|
722
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Reducer logic
|
|
723
|
+
export const panelReducer = (state: PanelLayoutData[], action: PanelAction): PanelLayoutData[] => {
|
|
724
|
+
switch (action.type) {
|
|
725
|
+
case "REGISTER_PANEL": {
|
|
726
|
+
logger.debug("REGISTER_PANEL", action.panel)
|
|
727
|
+
const existingIndex = state.findIndex((p) => p.id === action.panel.id)
|
|
728
|
+
|
|
729
|
+
if (existingIndex >= 0) {
|
|
730
|
+
// 既存パネルの場合はプロパティを更新
|
|
731
|
+
const newState = [...state]
|
|
732
|
+
const existingPanel = state[existingIndex]
|
|
733
|
+
const updatedPanelBase: PanelLayoutData = {
|
|
734
|
+
...existingPanel,
|
|
735
|
+
minSize: action.panel.minSize,
|
|
736
|
+
maxSize: action.panel.maxSize,
|
|
737
|
+
collapseFromStart: action.panel.collapseFromStart,
|
|
738
|
+
collapseFromEnd: action.panel.collapseFromEnd,
|
|
739
|
+
collapsedByDirection: action.panel.collapsedByDirection !== undefined ? action.panel.collapsedByDirection : existingPanel.collapsedByDirection,
|
|
740
|
+
sizeUnit: action.panel.sizeUnit,
|
|
741
|
+
originalPixelSize: action.panel.originalPixelSize,
|
|
742
|
+
preferredPercentageSize: action.panel.preferredPercentageSize !== undefined ? action.panel.preferredPercentageSize : existingPanel.preferredPercentageSize,
|
|
743
|
+
preferredPercentageSizeBeforeCollapse: action.panel.preferredPercentageSizeBeforeCollapse !== undefined ? action.panel.preferredPercentageSizeBeforeCollapse : existingPanel.preferredPercentageSizeBeforeCollapse,
|
|
744
|
+
percentageSizeBeforeCollapse: action.panel.percentageSizeBeforeCollapse !== undefined ? action.panel.percentageSizeBeforeCollapse : existingPanel.percentageSizeBeforeCollapse,
|
|
745
|
+
pixelAdjustPriority: action.panel.pixelAdjustPriority !== undefined ? action.panel.pixelAdjustPriority : existingPanel.pixelAdjustPriority,
|
|
746
|
+
flexAdjustPriority: action.panel.flexAdjustPriority !== undefined ? action.panel.flexAdjustPriority : existingPanel.flexAdjustPriority,
|
|
747
|
+
autoMinSize: action.panel.autoMinSize !== undefined ? action.panel.autoMinSize : existingPanel.autoMinSize,
|
|
748
|
+
excludeFromAutoGrowth: action.panel.excludeFromAutoGrowth !== undefined ? action.panel.excludeFromAutoGrowth : existingPanel.excludeFromAutoGrowth,
|
|
749
|
+
}
|
|
750
|
+
// ピクセル単位パネルで優先度が未設定の場合は自動割り当て
|
|
751
|
+
const updatedPanel = assignPixelAdjustPriority(updatedPanelBase, state, existingIndex)
|
|
752
|
+
logger.debug("register panel (update)", {
|
|
753
|
+
panelId: updatedPanel.id,
|
|
754
|
+
incoming: snapshotSinglePanel(action.panel),
|
|
755
|
+
previous: snapshotSinglePanel(existingPanel),
|
|
756
|
+
next: snapshotSinglePanel(updatedPanel),
|
|
757
|
+
})
|
|
758
|
+
|
|
759
|
+
newState[existingIndex] = updatedPanel
|
|
760
|
+
|
|
761
|
+
// minSize/maxSize の更新で現行サイズが制約違反になった場合はレイアウトへ即時再適用する
|
|
762
|
+
// (従来は保存されるだけで、次のドラッグやコンテナリサイズまで反映されなかった)
|
|
763
|
+
// 制約を満たしている間は現行レイアウトに触れず、ユーザー操作後の配置を維持する
|
|
764
|
+
if (!updatedPanel.collapsed) {
|
|
765
|
+
const estimatedContainerSize = newState.reduce((sum, panel) => (panel.collapsed ? sum : sum + panel.size), 0)
|
|
766
|
+
if (estimatedContainerSize > 0) {
|
|
767
|
+
const minPixels = getConstraintInPixels(updatedPanel.minSize, 0, estimatedContainerSize)
|
|
768
|
+
const maxPixels = Math.max(minPixels, getConstraintInPixels(updatedPanel.maxSize, estimatedContainerSize, estimatedContainerSize))
|
|
769
|
+
if (updatedPanel.size < minPixels - SIZE_EPSILON || updatedPanel.size > maxPixels + SIZE_EPSILON) {
|
|
770
|
+
return recalculateFlexiblePanels(newState, estimatedContainerSize)
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
return newState
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// 新規パネルの場合は末尾に追加し、必要に応じて優先度を割り当て
|
|
779
|
+
const newPanelBase: PanelLayoutData = { ...action.panel }
|
|
780
|
+
const newPanel = assignPixelAdjustPriority(newPanelBase, state)
|
|
781
|
+
logger.debug("register panel (insert)", {
|
|
782
|
+
panelId: newPanel.id,
|
|
783
|
+
incoming: snapshotSinglePanel(newPanel),
|
|
784
|
+
previousLength: state.length,
|
|
785
|
+
})
|
|
786
|
+
return [...state, newPanel]
|
|
787
|
+
}
|
|
788
|
+
case "UNREGISTER_PANEL": {
|
|
789
|
+
logger.debug("UNREGISTER_PANEL", action.id)
|
|
790
|
+
// 指定 ID のパネルを削除し、残ったパネルの割合を正規化
|
|
791
|
+
const filtered = state.filter((panel) => panel.id !== action.id)
|
|
792
|
+
const normalized = normalizePanelPercentages(filtered)
|
|
793
|
+
// スナップショット構築が高コストなため、debug 出力が有効な場合のみ組み立てる
|
|
794
|
+
if (logger.enabledFor(LogLevel.DEBUG)) {
|
|
795
|
+
logger.debug("unregister panel", {
|
|
796
|
+
panelId: action.id,
|
|
797
|
+
previous: snapshotPanelState(state),
|
|
798
|
+
filtered: snapshotPanelState(filtered),
|
|
799
|
+
next: snapshotPanelState(normalized),
|
|
800
|
+
})
|
|
801
|
+
}
|
|
802
|
+
return normalized
|
|
803
|
+
}
|
|
804
|
+
case "RESIZE_PANELS": {
|
|
805
|
+
// PanelResizeHandle の onPointerMove/onPointerUp ハンドラから呼び出されるドラッグ更新ロジック
|
|
806
|
+
// フック側は「制約を大きく外さない初期値」を作る役割、リデューサー側が「最終確定」の役割を担う
|
|
807
|
+
// フック側はドラッグ中の左右ペアに即座に制約を適用して「安全な候補サイズ」を算出するだけで、他パネルとの整合や割合正規化までは扱わない
|
|
808
|
+
// フック側で暫定計算した結果を受け取り、ここで唯一の正規ロジックとして全パネルに対する最終調整と割合正規化を確定させる
|
|
809
|
+
|
|
810
|
+
logger.debug("RESIZE_PANELS", action.leftId, action.rightId, action.leftSize, action.rightSize)
|
|
811
|
+
const leftIndex = state.findIndex((p) => p.id === action.leftId)
|
|
812
|
+
const rightIndex = state.findIndex((p) => p.id === action.rightId)
|
|
813
|
+
if (leftIndex === -1 || rightIndex === -1) return state
|
|
814
|
+
|
|
815
|
+
const newState = [...state]
|
|
816
|
+
const leftPanel = newState[leftIndex]
|
|
817
|
+
const rightPanel = newState[rightIndex]
|
|
818
|
+
|
|
819
|
+
// 折りたたまれたパネルがある場合はリサイズを中断
|
|
820
|
+
if (leftPanel.collapsed || rightPanel.collapsed) {
|
|
821
|
+
return state
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// サイズ変化が無い場合はスキップして不要な再レンダリングを防止
|
|
825
|
+
if (action.leftSize === leftPanel.size && action.rightSize === rightPanel.size) {
|
|
826
|
+
return state
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// コンテナサイズを推定 (指定がない場合は全パネルのサイズ合計を使用)
|
|
830
|
+
const estimatedContainerSize = action.containerSize ?? state.reduce((sum, p) => sum + p.size, 0)
|
|
831
|
+
// 左パネルの最小・最大サイズをピクセル値に変換
|
|
832
|
+
const leftMinSize = getConstraintInPixels(leftPanel.minSize, 0, estimatedContainerSize)
|
|
833
|
+
const leftMaxSize = getConstraintInPixels(leftPanel.maxSize, estimatedContainerSize, estimatedContainerSize)
|
|
834
|
+
// 右パネルの最小・最大サイズをピクセル値に変換
|
|
835
|
+
const rightMinSize = getConstraintInPixels(rightPanel.minSize, 0, estimatedContainerSize)
|
|
836
|
+
const rightMaxSize = getConstraintInPixels(rightPanel.maxSize, estimatedContainerSize, estimatedContainerSize)
|
|
837
|
+
|
|
838
|
+
// リサイズ対象ペアの元の合計サイズを記録
|
|
839
|
+
const originalPairTotal = leftPanel.size + rightPanel.size
|
|
840
|
+
// 全アクティブパネルの合計サイズを計算
|
|
841
|
+
const activeTotalBefore = state.reduce((sum, panel) => (panel.collapsed ? sum : sum + panel.size), 0)
|
|
842
|
+
// リサイズ対象ペア以外のアクティブパネルの合計サイズを算出
|
|
843
|
+
const otherActiveTotal = activeTotalBefore - originalPairTotal
|
|
844
|
+
|
|
845
|
+
// ペアの最小・最大合計サイズを算出
|
|
846
|
+
const minPairTotal = leftMinSize + rightMinSize
|
|
847
|
+
const maxPairTotal = leftMaxSize + rightMaxSize
|
|
848
|
+
|
|
849
|
+
// リクエストされたペア合計を制約内にクランプ
|
|
850
|
+
const requestedPairTotal = clamp(action.leftSize + action.rightSize, minPairTotal, maxPairTotal)
|
|
851
|
+
|
|
852
|
+
// コンテナサイズとペア以外の合計から必要なペア合計を計算
|
|
853
|
+
let requiredPairTotal = estimatedContainerSize > 0 ? estimatedContainerSize - otherActiveTotal : requestedPairTotal
|
|
854
|
+
// 計算結果が無効な場合はリクエスト値を使用
|
|
855
|
+
if (!Number.isFinite(requiredPairTotal)) {
|
|
856
|
+
requiredPairTotal = requestedPairTotal
|
|
857
|
+
}
|
|
858
|
+
// リクエスト値と必要値の大きい方を選び、制約内にクランプして目標合計を決定
|
|
859
|
+
const targetPairTotal = clamp(Math.max(requestedPairTotal, requiredPairTotal), minPairTotal, maxPairTotal)
|
|
860
|
+
|
|
861
|
+
// 左パネルの有効範囲を計算 (右パネルの制約を考慮して決定)
|
|
862
|
+
const computeLeftRange = (sum: number) => ({
|
|
863
|
+
min: Math.max(leftMinSize, sum - rightMaxSize),
|
|
864
|
+
max: Math.min(leftMaxSize, sum - rightMinSize),
|
|
865
|
+
})
|
|
866
|
+
// 右パネルの有効範囲を計算 (左パネルの制約を考慮して決定)
|
|
867
|
+
const computeRightRange = (sum: number) => ({
|
|
868
|
+
min: Math.max(rightMinSize, sum - leftMaxSize),
|
|
869
|
+
max: Math.min(rightMaxSize, sum - leftMinSize),
|
|
870
|
+
})
|
|
871
|
+
|
|
872
|
+
// 左パネルのサイズ変化量を計算
|
|
873
|
+
const leftInfluence = Math.abs(action.leftSize - leftPanel.size)
|
|
874
|
+
// 右パネルのサイズ変化量を計算
|
|
875
|
+
const rightInfluence = Math.abs(action.rightSize - rightPanel.size)
|
|
876
|
+
// 変化量が大きい方のパネルを優先して調整
|
|
877
|
+
const prioritizeLeft = leftInfluence >= rightInfluence
|
|
878
|
+
|
|
879
|
+
// 最終的に適用するサイズを初期化
|
|
880
|
+
let finalLeftSize = leftPanel.size
|
|
881
|
+
let finalRightSize = rightPanel.size
|
|
882
|
+
|
|
883
|
+
// 左パネルを優先する場合の調整
|
|
884
|
+
if (prioritizeLeft) {
|
|
885
|
+
// 左パネルの有効範囲を取得
|
|
886
|
+
const leftRange = computeLeftRange(targetPairTotal)
|
|
887
|
+
// 有効範囲が不正な場合は変更をキャンセル (桁落ちで境界が 1ulp 交差しただけなら単一点として扱う)
|
|
888
|
+
if (leftRange.min > leftRange.max + SIZE_EPSILON) {
|
|
889
|
+
return state
|
|
890
|
+
}
|
|
891
|
+
// 左パネルの希望サイズを有効範囲内にクランプ
|
|
892
|
+
const desiredLeft = clamp(action.leftSize, leftRange.min, leftRange.max)
|
|
893
|
+
// 右パネルのサイズを合計から逆算
|
|
894
|
+
const adjustedRight = targetPairTotal - desiredLeft
|
|
895
|
+
// 右パネルのサイズが制約を満たさない場合は変更をキャンセル (桁落ち分は境界へ丸めて受理する)
|
|
896
|
+
if (adjustedRight < rightMinSize - SIZE_EPSILON || adjustedRight > rightMaxSize + SIZE_EPSILON) {
|
|
897
|
+
return state
|
|
898
|
+
}
|
|
899
|
+
// 計算結果を最終サイズとして確定
|
|
900
|
+
finalLeftSize = desiredLeft
|
|
901
|
+
finalRightSize = clamp(adjustedRight, rightMinSize, rightMaxSize)
|
|
902
|
+
} else {
|
|
903
|
+
// 右パネルを優先する場合の調整
|
|
904
|
+
// 右パネルの有効範囲を取得
|
|
905
|
+
const rightRange = computeRightRange(targetPairTotal)
|
|
906
|
+
// 有効範囲が不正な場合は変更をキャンセル (桁落ちで境界が 1ulp 交差しただけなら単一点として扱う)
|
|
907
|
+
if (rightRange.min > rightRange.max + SIZE_EPSILON) {
|
|
908
|
+
return state
|
|
909
|
+
}
|
|
910
|
+
// 右パネルの希望サイズを有効範囲内にクランプ
|
|
911
|
+
const desiredRight = clamp(action.rightSize, rightRange.min, rightRange.max)
|
|
912
|
+
// 左パネルのサイズを合計から逆算
|
|
913
|
+
const adjustedLeft = targetPairTotal - desiredRight
|
|
914
|
+
// 左パネルのサイズが制約を満たさない場合は変更をキャンセル (桁落ち分は境界へ丸めて受理する)
|
|
915
|
+
if (adjustedLeft < leftMinSize - SIZE_EPSILON || adjustedLeft > leftMaxSize + SIZE_EPSILON) {
|
|
916
|
+
return state
|
|
917
|
+
}
|
|
918
|
+
// 計算結果を最終サイズとして確定
|
|
919
|
+
finalRightSize = desiredRight
|
|
920
|
+
finalLeftSize = clamp(adjustedLeft, leftMinSize, leftMaxSize)
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// 誤差範囲を超えて合計が変化する場合は状態を維持
|
|
924
|
+
const pairTotalAfter = finalLeftSize + finalRightSize
|
|
925
|
+
if (Math.abs(targetPairTotal - pairTotalAfter) > SIZE_EPSILON) {
|
|
926
|
+
return state
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// リサイズ後のアクティブパネル合計サイズを計算
|
|
930
|
+
const activeTotalAfter = activeTotalBefore - leftPanel.size - rightPanel.size + pairTotalAfter
|
|
931
|
+
|
|
932
|
+
// // リサイズ前後でアクティブ合計が変化している場合は変更をキャンセル
|
|
933
|
+
// if (activeTotalAfter !== activeTotalBefore) {
|
|
934
|
+
// return state
|
|
935
|
+
// }
|
|
936
|
+
|
|
937
|
+
// // コンテナサイズが指定されているのにアクティブ合計が不足する場合は変更をキャンセル
|
|
938
|
+
// if (estimatedContainerSize > 0 && activeTotalAfter < estimatedContainerSize) {
|
|
939
|
+
// return state
|
|
940
|
+
// }
|
|
941
|
+
|
|
942
|
+
// 割合計算に使用するコンテナサイズを決定 (推定値がある場合はそれを使用、なければ合計値)
|
|
943
|
+
const containerSizeForPercentage = estimatedContainerSize > 0 ? estimatedContainerSize : activeTotalAfter
|
|
944
|
+
// 左パネルの新しい割合を計算
|
|
945
|
+
const nextLeftPercentage = containerSizeForPercentage > 0 ? toRoundedPercentage(finalLeftSize, containerSizeForPercentage) : 0
|
|
946
|
+
// 右パネルの新しい割合を計算
|
|
947
|
+
const nextRightPercentage = containerSizeForPercentage > 0 ? toRoundedPercentage(finalRightSize, containerSizeForPercentage) : 0
|
|
948
|
+
const nextLeftPreferredPixel = leftPanel.sizeUnit === "pixels" || leftPanel.originalPixelSize !== undefined ? finalLeftSize : leftPanel.preferredPixelSize
|
|
949
|
+
const nextRightPreferredPixel = rightPanel.sizeUnit === "pixels" || rightPanel.originalPixelSize !== undefined ? finalRightSize : rightPanel.preferredPixelSize
|
|
950
|
+
|
|
951
|
+
const leftAutoMin = getConstraintInPixels(leftPanel.autoMinSize, 0, estimatedContainerSize)
|
|
952
|
+
const rightAutoMin = getConstraintInPixels(rightPanel.autoMinSize, 0, estimatedContainerSize)
|
|
953
|
+
const leftZeroThreshold = Math.max(1e-3, leftAutoMin)
|
|
954
|
+
const rightZeroThreshold = Math.max(1e-3, rightAutoMin)
|
|
955
|
+
const manageLeftGrowth = leftPanel.sizeUnit === "percentage" && leftPanel.originalPixelSize === undefined
|
|
956
|
+
const manageRightGrowth = rightPanel.sizeUnit === "percentage" && rightPanel.originalPixelSize === undefined
|
|
957
|
+
let nextLeftExclude = leftPanel.excludeFromAutoGrowth
|
|
958
|
+
let nextRightExclude = rightPanel.excludeFromAutoGrowth
|
|
959
|
+
if (manageLeftGrowth) {
|
|
960
|
+
if (finalLeftSize <= leftZeroThreshold) {
|
|
961
|
+
nextLeftExclude = true
|
|
962
|
+
} else if (finalLeftSize > leftZeroThreshold) {
|
|
963
|
+
nextLeftExclude = false
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
if (manageRightGrowth) {
|
|
967
|
+
if (finalRightSize <= rightZeroThreshold) {
|
|
968
|
+
nextRightExclude = true
|
|
969
|
+
} else if (finalRightSize > rightZeroThreshold) {
|
|
970
|
+
nextRightExclude = false
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
const leftIsCollapsible = panelCanCollapse(leftPanel)
|
|
975
|
+
const rightIsCollapsible = panelCanCollapse(rightPanel)
|
|
976
|
+
|
|
977
|
+
// 左パネルの状態を更新 (測定値はリセットして再測定を促す)
|
|
978
|
+
newState[leftIndex] = {
|
|
979
|
+
...leftPanel,
|
|
980
|
+
size: finalLeftSize,
|
|
981
|
+
percentageSize: nextLeftPercentage,
|
|
982
|
+
preferredPercentageSize: nextLeftPercentage > 0 ? nextLeftPercentage : leftPanel.preferredPercentageSize,
|
|
983
|
+
preferredPixelSize: nextLeftPreferredPixel,
|
|
984
|
+
sizeBeforeCollapse: leftIsCollapsible ? finalLeftSize : leftPanel.sizeBeforeCollapse,
|
|
985
|
+
measuredPixelSize: undefined,
|
|
986
|
+
measuredPercentageSize: undefined,
|
|
987
|
+
excludeFromAutoGrowth: nextLeftExclude,
|
|
988
|
+
}
|
|
989
|
+
// 右パネルの状態を更新 (測定値はリセットして再測定を促す)
|
|
990
|
+
newState[rightIndex] = {
|
|
991
|
+
...rightPanel,
|
|
992
|
+
size: finalRightSize,
|
|
993
|
+
percentageSize: nextRightPercentage,
|
|
994
|
+
preferredPercentageSize: nextRightPercentage > 0 ? nextRightPercentage : rightPanel.preferredPercentageSize,
|
|
995
|
+
preferredPixelSize: nextRightPreferredPixel,
|
|
996
|
+
sizeBeforeCollapse: rightIsCollapsible ? finalRightSize : rightPanel.sizeBeforeCollapse,
|
|
997
|
+
measuredPixelSize: undefined,
|
|
998
|
+
measuredPercentageSize: undefined,
|
|
999
|
+
excludeFromAutoGrowth: nextRightExclude,
|
|
1000
|
+
}
|
|
1001
|
+
// コンテナサイズが有効な場合は他のパネルとの整合性を確保
|
|
1002
|
+
if (estimatedContainerSize > 0 && reconcilePanelSizesWithContainer(newState, estimatedContainerSize)) {
|
|
1003
|
+
return normalizePanelPercentages(newState)
|
|
1004
|
+
}
|
|
1005
|
+
return newState
|
|
1006
|
+
}
|
|
1007
|
+
case "SET_PANELS": {
|
|
1008
|
+
logger.debug("SET_PANELS", action.panels)
|
|
1009
|
+
// スナップショット構築が高コストなため、debug 出力が有効な場合のみログ専用の組み立てを行う
|
|
1010
|
+
const debugEnabled = logger.enabledFor(LogLevel.DEBUG)
|
|
1011
|
+
// パネル配列全体を置き換えて割合を正規化
|
|
1012
|
+
const enrichedPanels = action.panels.map((panel) => {
|
|
1013
|
+
const isPixelPanel = panel.sizeUnit === "pixels" || panel.originalPixelSize !== undefined
|
|
1014
|
+
const nextPreferredPercentage = panel.preferredPercentageSize !== undefined ? panel.preferredPercentageSize : panel.percentageSize
|
|
1015
|
+
const nextPreferredPixelSize = panel.preferredPixelSize !== undefined ? panel.preferredPixelSize : isPixelPanel ? panel.size : panel.preferredPixelSize
|
|
1016
|
+
return {
|
|
1017
|
+
...panel,
|
|
1018
|
+
preferredPercentageSize: nextPreferredPercentage,
|
|
1019
|
+
preferredPixelSize: nextPreferredPixelSize,
|
|
1020
|
+
}
|
|
1021
|
+
})
|
|
1022
|
+
const previousSnapshot = snapshotPanelState(state)
|
|
1023
|
+
// incomingSnapshot はログ専用のため debug 有効時のみ構築する
|
|
1024
|
+
const incomingSnapshot = debugEnabled ? snapshotPanelState(enrichedPanels) : null
|
|
1025
|
+
const normalized = normalizePanelPercentages(enrichedPanels)
|
|
1026
|
+
const nextSnapshot = snapshotPanelState(normalized)
|
|
1027
|
+
let isNoop = previousSnapshot.length === nextSnapshot.length
|
|
1028
|
+
if (isNoop) {
|
|
1029
|
+
for (let index = 0; index < nextSnapshot.length; index += 1) {
|
|
1030
|
+
const panel = nextSnapshot[index]
|
|
1031
|
+
const previous = previousSnapshot[index]
|
|
1032
|
+
if (!previous) {
|
|
1033
|
+
isNoop = false
|
|
1034
|
+
break
|
|
1035
|
+
}
|
|
1036
|
+
// スナップショットの全フィールドを比較する (一部だけ比較すると実際の状態変化を no-op と誤判定して捨ててしまう)
|
|
1037
|
+
const isSame =
|
|
1038
|
+
previous.id === panel.id &&
|
|
1039
|
+
previous.size === panel.size &&
|
|
1040
|
+
previous.percentageSize === panel.percentageSize &&
|
|
1041
|
+
previous.preferredPercentageSize === panel.preferredPercentageSize &&
|
|
1042
|
+
previous.sizeBeforeCollapse === panel.sizeBeforeCollapse &&
|
|
1043
|
+
previous.percentageSizeBeforeCollapse === panel.percentageSizeBeforeCollapse &&
|
|
1044
|
+
previous.preferredPercentageSizeBeforeCollapse === panel.preferredPercentageSizeBeforeCollapse &&
|
|
1045
|
+
previous.measuredPercentageSize === panel.measuredPercentageSize &&
|
|
1046
|
+
previous.excludeFromAutoGrowth === panel.excludeFromAutoGrowth &&
|
|
1047
|
+
previous.collapsed === panel.collapsed &&
|
|
1048
|
+
previous.collapseFromStart === panel.collapseFromStart &&
|
|
1049
|
+
previous.collapseFromEnd === panel.collapseFromEnd &&
|
|
1050
|
+
previous.collapsedByDirection === panel.collapsedByDirection
|
|
1051
|
+
if (!isSame) {
|
|
1052
|
+
isNoop = false
|
|
1053
|
+
break
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
if (isNoop) {
|
|
1059
|
+
if (debugEnabled) {
|
|
1060
|
+
logger.debug("set panels skipped (no diff)", {
|
|
1061
|
+
previous: previousSnapshot,
|
|
1062
|
+
incoming: incomingSnapshot,
|
|
1063
|
+
next: nextSnapshot,
|
|
1064
|
+
previousLength: previousSnapshot.length,
|
|
1065
|
+
incomingLength: incomingSnapshot?.length ?? enrichedPanels.length,
|
|
1066
|
+
})
|
|
1067
|
+
}
|
|
1068
|
+
return state
|
|
1069
|
+
}
|
|
1070
|
+
if (debugEnabled) {
|
|
1071
|
+
logger.debug("set panels", {
|
|
1072
|
+
previous: previousSnapshot,
|
|
1073
|
+
incoming: incomingSnapshot,
|
|
1074
|
+
next: nextSnapshot,
|
|
1075
|
+
previousLength: previousSnapshot.length,
|
|
1076
|
+
incomingLength: incomingSnapshot?.length ?? enrichedPanels.length,
|
|
1077
|
+
})
|
|
1078
|
+
}
|
|
1079
|
+
return normalized
|
|
1080
|
+
}
|
|
1081
|
+
case "CONTAINER_RESIZED": {
|
|
1082
|
+
logger.debug("CONTAINER_RESIZED", action.containerSize)
|
|
1083
|
+
// リサイズ毎ティックに走るホットパスのため、抑制中はスナップショット構築ごと省略する
|
|
1084
|
+
// (logger.debug は引数を先に評価するので、ガードなしでは全パネルのスナップショットが毎回作られる)
|
|
1085
|
+
const debugEnabled = logger.enabledFor(LogLevel.DEBUG)
|
|
1086
|
+
if (action.containerSize <= 0) {
|
|
1087
|
+
if (debugEnabled) {
|
|
1088
|
+
logger.debug("container resized skipped (non-positive size)", {
|
|
1089
|
+
containerSize: action.containerSize,
|
|
1090
|
+
panels: snapshotPanelState(state),
|
|
1091
|
+
})
|
|
1092
|
+
}
|
|
1093
|
+
return state
|
|
1094
|
+
}
|
|
1095
|
+
if (debugEnabled) {
|
|
1096
|
+
logger.debug("container resized (before)", {
|
|
1097
|
+
containerSize: action.containerSize,
|
|
1098
|
+
panels: snapshotPanelState(state),
|
|
1099
|
+
})
|
|
1100
|
+
}
|
|
1101
|
+
// コンテナサイズ変更時に柔軟パネルを再計算
|
|
1102
|
+
const recalculated = recalculateFlexiblePanels(state, action.containerSize)
|
|
1103
|
+
if (debugEnabled) {
|
|
1104
|
+
logger.debug("container resized (after recalc)", {
|
|
1105
|
+
containerSize: action.containerSize,
|
|
1106
|
+
panels: snapshotPanelState(recalculated),
|
|
1107
|
+
})
|
|
1108
|
+
}
|
|
1109
|
+
if (action.containerSize > 0 && reconcilePanelSizesWithContainer(recalculated, action.containerSize)) {
|
|
1110
|
+
const normalized = normalizePanelPercentages(recalculated)
|
|
1111
|
+
if (debugEnabled) {
|
|
1112
|
+
logger.debug("container resized (after reconcile)", {
|
|
1113
|
+
containerSize: action.containerSize,
|
|
1114
|
+
panels: snapshotPanelState(normalized),
|
|
1115
|
+
})
|
|
1116
|
+
}
|
|
1117
|
+
return normalized
|
|
1118
|
+
}
|
|
1119
|
+
if (debugEnabled) {
|
|
1120
|
+
logger.debug("container resized (final)", {
|
|
1121
|
+
containerSize: action.containerSize,
|
|
1122
|
+
panels: snapshotPanelState(recalculated),
|
|
1123
|
+
})
|
|
1124
|
+
}
|
|
1125
|
+
return recalculated
|
|
1126
|
+
}
|
|
1127
|
+
case "COLLAPSE_PANEL": {
|
|
1128
|
+
logger.debug("COLLAPSE_PANEL", action.id)
|
|
1129
|
+
const panelIndex = state.findIndex((panel) => panel.id === action.id)
|
|
1130
|
+
if (panelIndex === -1) {
|
|
1131
|
+
return state
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
const targetPanel = state[panelIndex]
|
|
1135
|
+
const directionAllowed = action.from === "start" ? targetPanel.collapseFromStart : targetPanel.collapseFromEnd
|
|
1136
|
+
const panelIsCollapsible = panelCanCollapse(targetPanel)
|
|
1137
|
+
// 折りたたみ不可または既に折りたたまれている場合はスキップ
|
|
1138
|
+
if (!(panelIsCollapsible && directionAllowed) || targetPanel.collapsed) {
|
|
1139
|
+
return state
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// 折りたたみ前のサイズと測定値をピクセル基準で保存
|
|
1143
|
+
const sizeBefore = targetPanel.size
|
|
1144
|
+
const measuredPixelBefore = targetPanel.measuredPixelSize ?? sizeBefore
|
|
1145
|
+
const percentageBefore = targetPanel.percentageSize ?? targetPanel.preferredPercentageSize ?? 0
|
|
1146
|
+
|
|
1147
|
+
const updatedPanel: PanelLayoutData = {
|
|
1148
|
+
...targetPanel,
|
|
1149
|
+
collapsed: true,
|
|
1150
|
+
collapsedByDirection: action.from,
|
|
1151
|
+
sizeBeforeCollapse: sizeBefore,
|
|
1152
|
+
percentageSizeBeforeCollapse: percentageBefore,
|
|
1153
|
+
preferredPercentageSizeBeforeCollapse: targetPanel.preferredPercentageSize ?? percentageBefore,
|
|
1154
|
+
preferredPixelSizeBeforeCollapse: targetPanel.preferredPixelSize ?? sizeBefore,
|
|
1155
|
+
measuredPixelSizeBeforeCollapse: measuredPixelBefore,
|
|
1156
|
+
size: 0,
|
|
1157
|
+
percentageSize: 0,
|
|
1158
|
+
preferredPercentageSize: targetPanel.preferredPercentageSize,
|
|
1159
|
+
measuredPixelSize: targetPanel.measuredPixelSize !== undefined ? 0 : targetPanel.measuredPixelSize,
|
|
1160
|
+
measuredPercentageSize: targetPanel.measuredPercentageSize !== undefined ? 0 : targetPanel.measuredPercentageSize,
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
const newState = [...state]
|
|
1164
|
+
newState[panelIndex] = updatedPanel
|
|
1165
|
+
|
|
1166
|
+
const recalculated = recalculateFlexiblePanels(newState, action.containerSize)
|
|
1167
|
+
if (action.containerSize > 0 && reconcilePanelSizesWithContainer(recalculated, action.containerSize)) {
|
|
1168
|
+
return normalizePanelPercentages(recalculated)
|
|
1169
|
+
}
|
|
1170
|
+
return recalculated
|
|
1171
|
+
}
|
|
1172
|
+
case "EXPAND_PANEL": {
|
|
1173
|
+
logger.debug("EXPAND_PANEL", action.id, action.targetSize)
|
|
1174
|
+
const panelIndex = state.findIndex((panel) => panel.id === action.id)
|
|
1175
|
+
if (panelIndex === -1) {
|
|
1176
|
+
return state
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
const containerSize = action.containerSize
|
|
1180
|
+
|
|
1181
|
+
const targetPanel = state[panelIndex]
|
|
1182
|
+
// 折りたたみ不可または既に展開されている場合はスキップ
|
|
1183
|
+
const directionAllowed = action.from === "start" ? targetPanel.collapseFromStart : targetPanel.collapseFromEnd
|
|
1184
|
+
if (!(panelCanCollapse(targetPanel) && directionAllowed && targetPanel.collapsed)) {
|
|
1185
|
+
return state
|
|
1186
|
+
}
|
|
1187
|
+
if (targetPanel.collapsedByDirection && targetPanel.collapsedByDirection !== action.from) {
|
|
1188
|
+
return state
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// 復元サイズを決定 (優先順: targetSize → sizeBeforeCollapse → measuredPixelSizeBeforeCollapse → preferredPixelSizeBeforeCollapse → originalPixelSize)
|
|
1192
|
+
const candidateSizes = [action.targetSize, targetPanel.sizeBeforeCollapse, targetPanel.measuredPixelSizeBeforeCollapse, targetPanel.preferredPixelSizeBeforeCollapse, targetPanel.originalPixelSize]
|
|
1193
|
+
const proposedSize = candidateSizes.find((value) => value !== undefined) ?? 0
|
|
1194
|
+
const minSize = getConstraintInPixels(targetPanel.minSize, 0, containerSize)
|
|
1195
|
+
const maxSize = getConstraintInPixels(targetPanel.maxSize, containerSize, containerSize)
|
|
1196
|
+
const restoredSize = clamp(proposedSize, minSize, maxSize)
|
|
1197
|
+
const restoredPercentage = toRoundedPercentage(restoredSize, containerSize)
|
|
1198
|
+
const restoredMeasuredPixel = targetPanel.measuredPixelSizeBeforeCollapse ?? restoredSize
|
|
1199
|
+
const restoredMeasuredPercentage = toRoundedPercentage(restoredMeasuredPixel, containerSize)
|
|
1200
|
+
const restoredPreferredPercentage = targetPanel.preferredPercentageSizeBeforeCollapse ?? targetPanel.percentageSizeBeforeCollapse ?? restoredPercentage
|
|
1201
|
+
const restoredPreferredPixel = targetPanel.preferredPixelSizeBeforeCollapse ?? targetPanel.preferredPixelSize ?? restoredSize
|
|
1202
|
+
|
|
1203
|
+
// 復元サイズを反映したパネル情報を構築し測定値も再設定
|
|
1204
|
+
const updatedPanel: PanelLayoutData = {
|
|
1205
|
+
...targetPanel,
|
|
1206
|
+
collapsed: false,
|
|
1207
|
+
collapsedByDirection: null,
|
|
1208
|
+
size: restoredSize,
|
|
1209
|
+
percentageSize: restoredPercentage,
|
|
1210
|
+
preferredPercentageSize: restoredPreferredPercentage,
|
|
1211
|
+
preferredPixelSize: restoredPreferredPixel,
|
|
1212
|
+
sizeBeforeCollapse: restoredSize,
|
|
1213
|
+
percentageSizeBeforeCollapse: undefined,
|
|
1214
|
+
preferredPercentageSizeBeforeCollapse: undefined,
|
|
1215
|
+
preferredPixelSizeBeforeCollapse: undefined,
|
|
1216
|
+
measuredPixelSize: restoredSize,
|
|
1217
|
+
measuredPixelSizeBeforeCollapse: restoredMeasuredPixel,
|
|
1218
|
+
measuredPercentageSize: restoredMeasuredPercentage,
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// 新しい状態配列を用意して対象パネルを差し替え
|
|
1222
|
+
const newState = [...state]
|
|
1223
|
+
newState[panelIndex] = updatedPanel
|
|
1224
|
+
|
|
1225
|
+
const recalculated = recalculateFlexiblePanels(newState, containerSize)
|
|
1226
|
+
if (containerSize > 0 && reconcilePanelSizesWithContainer(recalculated, containerSize)) {
|
|
1227
|
+
return normalizePanelPercentages(recalculated)
|
|
1228
|
+
}
|
|
1229
|
+
return recalculated
|
|
1230
|
+
}
|
|
1231
|
+
default:
|
|
1232
|
+
return state
|
|
1233
|
+
}
|
|
1234
|
+
}
|