@aiquants/resize-panels 1.9.0 → 2.0.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/README.md +129 -22
- package/dist/GlobalDebugOverlay-Cf3GOn2A.cjs +1 -0
- package/dist/{GlobalDebugOverlay-Do70T2l6.js → GlobalDebugOverlay-DQtUdNeF.js} +28 -28
- package/dist/debugOverlayStore-D7FT6zPE.js +135 -0
- package/dist/debugOverlayStore-DY09saU8.cjs +1 -0
- package/dist/index-DZ0itiBD.js +1299 -0
- package/dist/index-DqATP_BA.cjs +2 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +2 -29
- package/dist/index.js +25 -13
- package/dist/src/GlobalDebugOverlay.d.ts.map +1 -0
- package/dist/src/Panel.d.ts +46 -0
- package/dist/src/Panel.d.ts.map +1 -0
- package/dist/src/PanelDebugInfo.d.ts +34 -0
- package/dist/src/PanelDebugInfo.d.ts.map +1 -0
- package/dist/src/PanelGroup.d.ts.map +1 -0
- package/dist/src/PanelResizeHandle.d.ts.map +1 -0
- package/dist/src/allocateLayout.d.ts +60 -0
- package/dist/src/allocateLayout.d.ts.map +1 -0
- package/dist/src/context.d.ts.map +1 -0
- package/dist/{debugOverlayStore.d.ts → src/debugOverlayStore.d.ts} +10 -2
- package/dist/src/debugOverlayStore.d.ts.map +1 -0
- package/dist/src/hooks.d.ts +48 -0
- package/dist/src/hooks.d.ts.map +1 -0
- package/dist/src/index.d.ts +32 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/reducer.d.ts +49 -0
- package/dist/src/reducer.d.ts.map +1 -0
- package/dist/src/roundHalfToEven.d.ts.map +1 -0
- package/dist/{types.d.ts → src/types.d.ts} +164 -60
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/utils/simple-logger.d.ts.map +1 -0
- package/dist/src/utils.d.ts +164 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/dist/styles/resize-panels.standalone.css +1 -1
- package/dist/tests/support/dom-harness.d.ts +89 -0
- package/dist/tests/support/dom-harness.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/GlobalDebugOverlay.tsx +3 -3
- package/src/Panel.tsx +203 -541
- package/src/PanelDebugInfo.tsx +52 -106
- package/src/PanelGroup.tsx +27 -42
- package/src/PanelResizeHandle.tsx +470 -509
- package/src/allocateLayout.ts +412 -0
- package/src/debugOverlayStore.ts +19 -15
- package/src/hooks.ts +187 -779
- package/src/index.ts +12 -3
- package/src/reducer.ts +364 -1126
- package/src/types.ts +149 -63
- package/src/utils.ts +205 -164
- package/dist/GlobalDebugOverlay-sEQN6exo.cjs +0 -1
- package/dist/GlobalDebugOverlay.d.ts.map +0 -1
- package/dist/Panel.d.ts +0 -7
- package/dist/Panel.d.ts.map +0 -1
- package/dist/PanelDebugInfo.d.ts +0 -26
- package/dist/PanelDebugInfo.d.ts.map +0 -1
- package/dist/PanelGroup.d.ts.map +0 -1
- package/dist/PanelResizeHandle.d.ts.map +0 -1
- package/dist/context.d.ts.map +0 -1
- package/dist/debugOverlayStore-Cntyxboe.js +0 -141
- package/dist/debugOverlayStore-Dkl-fHoa.cjs +0 -1
- package/dist/debugOverlayStore.d.ts.map +0 -1
- package/dist/hooks.d.ts +0 -45
- package/dist/hooks.d.ts.map +0 -1
- package/dist/index-B6YGX2DH.js +0 -2009
- package/dist/index-DssZUxGw.cjs +0 -2
- package/dist/index.d.ts.map +0 -1
- package/dist/reducer.d.ts +0 -39
- package/dist/reducer.d.ts.map +0 -1
- package/dist/roundHalfToEven.d.ts.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/utils/simple-logger.d.ts.map +0 -1
- package/dist/utils.d.ts +0 -61
- package/dist/utils.d.ts.map +0 -1
- /package/dist/{GlobalDebugOverlay.d.ts → src/GlobalDebugOverlay.d.ts} +0 -0
- /package/dist/{PanelGroup.d.ts → src/PanelGroup.d.ts} +0 -0
- /package/dist/{PanelResizeHandle.d.ts → src/PanelResizeHandle.d.ts} +0 -0
- /package/dist/{context.d.ts → src/context.d.ts} +0 -0
- /package/dist/{roundHalfToEven.d.ts → src/roundHalfToEven.d.ts} +0 -0
- /package/dist/{utils → src/utils}/simple-logger.d.ts +0 -0
package/src/reducer.ts
CHANGED
|
@@ -1,1234 +1,472 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file
|
|
3
|
-
*
|
|
2
|
+
* @file Panel group reducer: the only writer of panel sizes.
|
|
3
|
+
* パネルグループのリデューサー: パネルサイズを書き込む唯一の主体。
|
|
4
|
+
*
|
|
5
|
+
* Every action edits configuration or preferences and then runs the same allocator, so the first paint, a
|
|
6
|
+
* container resize, a drag, a collapse and a restored layout all produce sizes from one function of
|
|
7
|
+
* (preferences, constraints, container length).
|
|
8
|
+
* すべてのアクションは構成か希望値を編集したうえで同一の配分器を実行する。したがって初回描画・コンテナリサイズ・
|
|
9
|
+
* ドラッグ・折りたたみ・レイアウト復元のいずれも、(希望値, 制約, コンテナ長) の同一関数からサイズを得る。
|
|
4
10
|
*/
|
|
5
|
-
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
11
|
+
|
|
12
|
+
import { allocatePanelSizes, isPixelPanel, resolvePanelBounds } from "./allocateLayout"
|
|
13
|
+
import { type LayoutConstraintViolation, PANEL_ALLOCATION_EPSILON, type PanelAction, type PanelCollapseDirection, type PanelGroupLayoutState, type PanelLayoutData, type PanelRegistration } from "./types"
|
|
14
|
+
import { calculateSnapThreshold, clamp } from "./utils"
|
|
8
15
|
import { Logger, LogLevel } from "./utils/simple-logger"
|
|
9
16
|
|
|
10
17
|
const logger = new Logger(LogLevel.INFO, "[resize-panels]")
|
|
11
18
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
panels
|
|
19
|
-
|
|
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]
|
|
19
|
+
/**
|
|
20
|
+
* Empty layout state used before the group has measured itself or registered any panel.
|
|
21
|
+
* グループが自身を計測する前・パネル登録前の空レイアウト状態。
|
|
22
|
+
*/
|
|
23
|
+
export const initialPanelGroupLayoutState: PanelGroupLayoutState = {
|
|
24
|
+
containerSize: 0,
|
|
25
|
+
panels: [],
|
|
26
|
+
violation: null,
|
|
39
27
|
}
|
|
40
28
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Report whether two violations describe the same situation.
|
|
31
|
+
* 2 つの違反が同じ状況を表すかどうかの判定。
|
|
32
|
+
*/
|
|
33
|
+
const isSameViolation = (left: LayoutConstraintViolation | null, right: LayoutConstraintViolation | null): boolean => {
|
|
34
|
+
if (left === null || right === null) {
|
|
35
|
+
return left === right
|
|
44
36
|
}
|
|
45
|
-
return
|
|
37
|
+
return (
|
|
38
|
+
left.reason === right.reason &&
|
|
39
|
+
Math.abs(left.totalMinimumSize - right.totalMinimumSize) <= PANEL_ALLOCATION_EPSILON &&
|
|
40
|
+
Math.abs(left.totalMaximumSize - right.totalMaximumSize) <= PANEL_ALLOCATION_EPSILON &&
|
|
41
|
+
Math.abs(left.availableContainerSize - right.availableContainerSize) <= PANEL_ALLOCATION_EPSILON
|
|
42
|
+
)
|
|
46
43
|
}
|
|
47
44
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const nextPanels = panels.map((panel) => ({ ...panel }))
|
|
66
|
-
const descriptors: Descriptor[] = []
|
|
45
|
+
/**
|
|
46
|
+
* Run the allocator over the given panels and fold the result into a layout state.
|
|
47
|
+
* 与えられたパネルへ配分器を実行し、結果をレイアウト状態へ畳み込む処理。
|
|
48
|
+
*
|
|
49
|
+
* Panel objects and the panel array keep their identity when the allocation did not move them, so a transition
|
|
50
|
+
* that changes nothing produces no new state and no render at all. (When something does change, every panel of
|
|
51
|
+
* the group re-renders: they all read the same context value.)
|
|
52
|
+
* 配分でサイズが動かなかったパネルとパネル配列は同一性を保つため、何も変えない遷移は新しい状態を作らず再描画も
|
|
53
|
+
* 起こらない (何かが変われば、同じコンテキスト値を読むグループ内の全パネルが再描画される)。
|
|
54
|
+
*
|
|
55
|
+
* @param panels - Panels in DOM order with up-to-date preferences / 希望値が最新の DOM 順パネル一覧
|
|
56
|
+
* @param containerSize - Container content-box length in pixels / コンテナ内容領域長 (px)
|
|
57
|
+
* @returns Layout state whose sizes satisfy the allocator / 配分結果を反映したレイアウト状態
|
|
58
|
+
*/
|
|
59
|
+
const allocate = (panels: PanelLayoutData[], containerSize: number): PanelGroupLayoutState => {
|
|
60
|
+
const { sizes, violation } = allocatePanelSizes(panels, containerSize)
|
|
67
61
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
size: 0,
|
|
74
|
-
percentageSize: 0,
|
|
75
|
-
}
|
|
76
|
-
continue
|
|
62
|
+
let changed = false
|
|
63
|
+
const nextPanels = panels.map((panel, index) => {
|
|
64
|
+
const size = panel.collapsed ? 0 : sizes[index]
|
|
65
|
+
if (Math.abs(panel.size - size) <= PANEL_ALLOCATION_EPSILON) {
|
|
66
|
+
return panel
|
|
77
67
|
}
|
|
68
|
+
changed = true
|
|
69
|
+
return { ...panel, size }
|
|
70
|
+
})
|
|
78
71
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
72
|
+
const next = { containerSize, panels: changed ? nextPanels : panels, violation }
|
|
73
|
+
reportInvariantBreach(next)
|
|
74
|
+
return next
|
|
75
|
+
}
|
|
88
76
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Find the layout invariant breach in a state, if there is one.
|
|
79
|
+
* レイアウト状態に不変条件の違反があれば、その内容を返す処理。
|
|
80
|
+
*
|
|
81
|
+
* The condition is the one the whole package rests on: with a measured container and no reported constraint
|
|
82
|
+
* conflict, the active panels fill it exactly. The misaligned handle this package once shipped was a silent
|
|
83
|
+
* violation of exactly this — the sizes stopped filling the container and nothing noticed.
|
|
84
|
+
* ここで見る条件はパッケージ全体の土台である: コンテナが計測済みで制約矛盾も報告されていないなら、
|
|
85
|
+
* アクティブなパネルはコンテナをちょうど満たす。かつて出荷された「ハンドルがずれる」欠陥は、まさにこの条件が
|
|
86
|
+
* 無言で破れた状態だった。
|
|
87
|
+
*
|
|
88
|
+
* @param state - Layout state to check / 検査するレイアウト状態
|
|
89
|
+
* @returns Allocated total and container length when they disagree, otherwise null / 食い違う場合の合計と
|
|
90
|
+
* コンテナ長、問題なければ null
|
|
91
|
+
*/
|
|
92
|
+
export const findLayoutInvariantBreach = (state: PanelGroupLayoutState): { allocatedTotal: number; containerSize: number } | null => {
|
|
93
|
+
if (state.containerSize <= 0 || state.violation !== null) {
|
|
94
|
+
return null
|
|
95
|
+
}
|
|
96
|
+
let total = 0
|
|
97
|
+
let activeCount = 0
|
|
98
|
+
for (const panel of state.panels) {
|
|
99
|
+
if (!panel.collapsed) {
|
|
100
|
+
total += panel.size
|
|
101
|
+
activeCount += 1
|
|
96
102
|
}
|
|
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
103
|
}
|
|
110
|
-
|
|
111
|
-
if (
|
|
112
|
-
return
|
|
104
|
+
// 全パネルが折りたたまれたグループは「満たすべきパネルが無い」正当な状態
|
|
105
|
+
if (activeCount === 0) {
|
|
106
|
+
return null
|
|
113
107
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
for (const descriptor of descriptors) {
|
|
118
|
-
assignments.set(descriptor.index, descriptor.minSize)
|
|
119
|
-
baseTotal += descriptor.minSize
|
|
108
|
+
const tolerance = PANEL_ALLOCATION_EPSILON * Math.max(1, state.containerSize)
|
|
109
|
+
if (!Number.isFinite(total) || Math.abs(total - state.containerSize) > tolerance) {
|
|
110
|
+
return { allocatedTotal: total, containerSize: state.containerSize }
|
|
120
111
|
}
|
|
112
|
+
return null
|
|
113
|
+
}
|
|
121
114
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
115
|
+
/**
|
|
116
|
+
* Report a layout invariant breach, which can only come from a defect in this package.
|
|
117
|
+
* レイアウト不変条件の違反を報告する処理 (違反はこのパッケージの欠陥でしか起こり得ない)。
|
|
118
|
+
*
|
|
119
|
+
* The check is O(panels) with no allocation, so it runs on every transition rather than behind a flag.
|
|
120
|
+
* 検査はパネル数に比例するだけで確保も伴わないため、フラグに隠さずすべての遷移で実行する。
|
|
121
|
+
*
|
|
122
|
+
* @param state - Layout state produced by the allocator / 配分器が生成したレイアウト状態
|
|
123
|
+
*/
|
|
124
|
+
const reportInvariantBreach = (state: PanelGroupLayoutState): void => {
|
|
125
|
+
const breach = findLayoutInvariantBreach(state)
|
|
126
|
+
if (breach) {
|
|
127
|
+
logger.error("Allocated sizes do not fill the container.", {
|
|
128
|
+
...breach,
|
|
129
|
+
panels: state.panels.map((panel) => ({ id: panel.id, size: panel.size, collapsed: panel.collapsed })),
|
|
130
|
+
})
|
|
136
131
|
}
|
|
132
|
+
}
|
|
137
133
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
}
|
|
134
|
+
/**
|
|
135
|
+
* Fold a freshly allocated state into the previous one, preserving identity where nothing moved.
|
|
136
|
+
* 新たに配分した状態を直前の状態へ畳み込み、変化が無い部分の同一性を保つ処理。
|
|
137
|
+
*/
|
|
138
|
+
const commit = (previous: PanelGroupLayoutState, next: PanelGroupLayoutState): PanelGroupLayoutState => {
|
|
139
|
+
if (previous.panels === next.panels && previous.containerSize === next.containerSize && isSameViolation(previous.violation, next.violation)) {
|
|
140
|
+
return previous
|
|
239
141
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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)
|
|
142
|
+
return {
|
|
143
|
+
containerSize: next.containerSize,
|
|
144
|
+
panels: next.panels,
|
|
145
|
+
violation: isSameViolation(previous.violation, next.violation) ? previous.violation : next.violation,
|
|
287
146
|
}
|
|
147
|
+
}
|
|
288
148
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
}
|
|
149
|
+
/**
|
|
150
|
+
* Create the layout data of a newly registered panel.
|
|
151
|
+
* 新規登録パネルのレイアウトデータを生成する処理。
|
|
152
|
+
*/
|
|
153
|
+
const createPanel = (registration: PanelRegistration, defaultCollapsedFrom: PanelCollapseDirection | null): PanelLayoutData => {
|
|
154
|
+
const preference = registration.sizeUnit === "pixels" ? registration.preferredPixelSize : registration.preferredPercentageSize
|
|
155
|
+
return {
|
|
156
|
+
...registration,
|
|
157
|
+
size: 0,
|
|
158
|
+
collapsed: registration.collapsed,
|
|
159
|
+
collapsedByDirection: registration.collapsed ? defaultCollapsedFrom : null,
|
|
160
|
+
preferenceBeforeCollapse: preference,
|
|
161
|
+
authoredPreference: preference,
|
|
313
162
|
}
|
|
314
|
-
|
|
315
|
-
return normalizePanelPercentages(nextPanels)
|
|
316
163
|
}
|
|
317
164
|
|
|
318
165
|
/**
|
|
319
|
-
*
|
|
320
|
-
*
|
|
166
|
+
* Resolve the direction a panel collapses to when it starts collapsed.
|
|
167
|
+
* 初期状態で折りたたまれているパネルの折りたたみ方向を解決する処理。
|
|
321
168
|
*/
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
if (panel.collapsed) {
|
|
326
|
-
continue
|
|
327
|
-
}
|
|
328
|
-
totalActiveSize += Math.max(panel.size, 0)
|
|
169
|
+
const initialCollapseDirection = (registration: Pick<PanelRegistration, "collapseFromStart" | "collapseFromEnd">): PanelCollapseDirection | null => {
|
|
170
|
+
if (registration.collapseFromStart) {
|
|
171
|
+
return "start"
|
|
329
172
|
}
|
|
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
|
|
173
|
+
if (registration.collapseFromEnd) {
|
|
174
|
+
return "end"
|
|
355
175
|
}
|
|
176
|
+
return null
|
|
177
|
+
}
|
|
356
178
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
179
|
+
/**
|
|
180
|
+
* Rewrite every active panel's preference so that it reproduces the sizes it currently has.
|
|
181
|
+
* アクティブな各パネルの希望値を、現在のサイズを再現する値へ書き換える処理。
|
|
182
|
+
*
|
|
183
|
+
* A drag is a statement about proportions, so the sizes it produced become the new preferences. Because a
|
|
184
|
+
* flexible panel's weight is then exactly `size / C × 100`, re-running the allocator returns the very same
|
|
185
|
+
* sizes: the layout the user released the pointer on is a fixed point.
|
|
186
|
+
* ドラッグは比率に対する利用者の意思表示なので、その結果のサイズを新たな希望値とする。柔軟パネルの重みは
|
|
187
|
+
* `size / C × 100` そのものになるため、配分器を再実行しても同じサイズが返る (ドラッグ結果は不動点)。
|
|
188
|
+
*/
|
|
189
|
+
const commitPreferences = (panels: PanelLayoutData[], containerSize: number): PanelLayoutData[] => {
|
|
190
|
+
if (containerSize <= 0) {
|
|
191
|
+
return panels
|
|
192
|
+
}
|
|
193
|
+
// 端まで引いて折りたたむ操作では、閉じる直前のフレームの寸法が復元用に残ると
|
|
194
|
+
// 展開しても潰れたままになる。復元用の希望値は「閉じたとみなす幅より広い間」だけ更新する
|
|
195
|
+
const snapThreshold = calculateSnapThreshold(containerSize)
|
|
196
|
+
return panels.map((panel) => {
|
|
362
197
|
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
198
|
return panel
|
|
372
199
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
const
|
|
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)) {
|
|
200
|
+
const preference = isPixelPanel(panel) ? panel.size : (panel.size / containerSize) * 100
|
|
201
|
+
const current = isPixelPanel(panel) ? panel.preferredPixelSize : panel.preferredPercentageSize
|
|
202
|
+
const preferenceBeforeCollapse = panel.size > snapThreshold ? preference : panel.preferenceBeforeCollapse
|
|
203
|
+
if (current === preference && preferenceBeforeCollapse === panel.preferenceBeforeCollapse) {
|
|
395
204
|
return panel
|
|
396
205
|
}
|
|
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
|
-
}
|
|
206
|
+
return isPixelPanel(panel) ? { ...panel, preferredPixelSize: preference, preferenceBeforeCollapse } : { ...panel, preferredPercentageSize: preference, preferenceBeforeCollapse }
|
|
408
207
|
})
|
|
409
|
-
|
|
410
|
-
return mutated ? normalizedPanels : panels
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
type ActivePanelCandidate = {
|
|
414
|
-
index: number
|
|
415
|
-
minSize: number
|
|
416
|
-
maxSize: number
|
|
417
|
-
isFlexible: boolean
|
|
418
208
|
}
|
|
419
209
|
|
|
420
210
|
/**
|
|
421
|
-
*
|
|
422
|
-
*
|
|
211
|
+
* Set a panel's preference from an absolute pixel length.
|
|
212
|
+
* 絶対ピクセル長からパネルの希望値を設定する処理。
|
|
213
|
+
*
|
|
214
|
+
* A flexible panel's weight is a share of the container, so an absolute length only lands where it was
|
|
215
|
+
* asked for when the active panels' weights are expressed on the same scale — which is why every caller
|
|
216
|
+
* that hands out absolute lengths rewrites the whole set.
|
|
217
|
+
* 柔軟パネルの重みはコンテナ比なので、絶対長がそのままの位置に収まるのはアクティブパネルの重みが同じ尺度で
|
|
218
|
+
* 表されているときだけである。絶対長を配る呼び出し側が常に集合全体を書き換えるのはこのため。
|
|
423
219
|
*/
|
|
424
|
-
const
|
|
425
|
-
if (
|
|
426
|
-
|
|
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
|
|
220
|
+
const withPreferenceFromPixels = (panel: PanelLayoutData, pixels: number, containerSize: number): PanelLayoutData => {
|
|
221
|
+
if (isPixelPanel(panel)) {
|
|
222
|
+
const preferredPixelSize = Math.max(0, pixels)
|
|
223
|
+
return { ...panel, preferredPixelSize, preferenceBeforeCollapse: preferredPixelSize }
|
|
525
224
|
}
|
|
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
|
-
}
|
|
225
|
+
if (containerSize <= 0) {
|
|
226
|
+
return panel
|
|
596
227
|
}
|
|
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
|
|
228
|
+
const preferredPercentageSize = Math.max(0, (pixels / containerSize) * 100)
|
|
229
|
+
return { ...panel, preferredPercentageSize, preferenceBeforeCollapse: preferredPercentageSize }
|
|
676
230
|
}
|
|
677
231
|
|
|
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
232
|
/**
|
|
688
|
-
*
|
|
689
|
-
*
|
|
233
|
+
* Express a panel's preference as a length in pixels for the given container.
|
|
234
|
+
* パネルの希望値を、指定コンテナに対するピクセル長として表す処理。
|
|
690
235
|
*
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
694
|
-
*
|
|
236
|
+
* This is the length the panel occupies when it is open, which is what a persisted layout has to carry: the
|
|
237
|
+
* allocated size of a collapsed panel is 0 and would restore as "closed and with nothing to open to".
|
|
238
|
+
* これは「開いているときに占める長さ」であり、保存レイアウトが持つべき値でもある。折りたたみ中のパネルの
|
|
239
|
+
* 配分結果は 0 であり、そのまま保存すると「閉じたまま、開く先も無い」状態として復元されてしまう。
|
|
240
|
+
*
|
|
241
|
+
* @param panel - Panel layout data / パネルレイアウトデータ
|
|
242
|
+
* @param containerSize - Container content-box length in pixels / コンテナ内容領域長 (px)
|
|
243
|
+
* @returns Preferred length in pixels / 希望する長さ (px)
|
|
695
244
|
*/
|
|
696
|
-
const
|
|
697
|
-
|
|
698
|
-
|
|
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)
|
|
245
|
+
export const getPanelPreferenceInPixels = (panel: PanelLayoutData, containerSize: number): number => {
|
|
246
|
+
const preference = panel.collapsed ? panel.preferenceBeforeCollapse : isPixelPanel(panel) ? panel.preferredPixelSize : panel.preferredPercentageSize
|
|
247
|
+
return isPixelPanel(panel) ? preference : (preference / 100) * containerSize
|
|
248
|
+
}
|
|
709
249
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
250
|
+
/**
|
|
251
|
+
* Give one panel an absolute length and let the other active panels share what is left, keeping their ratio.
|
|
252
|
+
* 1 枚のパネルへ絶対長を与え、残りのアクティブパネルが現在の比のまま残余を分け合うよう希望値を揃える処理。
|
|
253
|
+
*
|
|
254
|
+
* @param panels - Panels in DOM order / DOM 順のパネル一覧
|
|
255
|
+
* @param targetId - Panel that receives the absolute length / 絶対長を受け取るパネル
|
|
256
|
+
* @param pixels - Absolute length in pixels / 絶対長 (px)
|
|
257
|
+
* @param containerSize - Container content-box length in pixels / コンテナ内容領域長 (px)
|
|
258
|
+
* @returns Panels whose preferences reproduce the requested layout / 要求どおりの配置を再現する希望値を持つパネル一覧
|
|
259
|
+
*/
|
|
260
|
+
const withAbsoluteSize = (panels: PanelLayoutData[], targetId: string, pixels: number, containerSize: number): PanelLayoutData[] => {
|
|
261
|
+
const target = Math.max(0, Math.min(pixels, containerSize))
|
|
262
|
+
// ピクセルパネルの希望長は利用者が宣言した絶対値なので、他のパネルの都合で書き換えない。
|
|
263
|
+
// 残余を分け合うのは柔軟パネルだけ
|
|
264
|
+
const sharing = panels.filter((panel) => panel.id !== targetId && !panel.collapsed && !isPixelPanel(panel))
|
|
265
|
+
const reserved = panels.reduce((total, panel) => (panel.id !== targetId && !panel.collapsed && isPixelPanel(panel) ? total + panel.size : total), 0)
|
|
266
|
+
const sharingTotal = sharing.reduce((total, panel) => total + panel.size, 0)
|
|
267
|
+
const sharingBudget = Math.max(0, containerSize - target - reserved)
|
|
268
|
+
|
|
269
|
+
return panels.map((panel) => {
|
|
270
|
+
if (panel.id === targetId) {
|
|
271
|
+
return withPreferenceFromPixels(panel, target, containerSize)
|
|
272
|
+
}
|
|
273
|
+
if (panel.collapsed || isPixelPanel(panel) || sharing.length === 0) {
|
|
274
|
+
return panel
|
|
275
|
+
}
|
|
276
|
+
const share = sharingTotal > 0 ? (panel.size / sharingTotal) * sharingBudget : sharingBudget / sharing.length
|
|
277
|
+
return withPreferenceFromPixels(panel, share, containerSize)
|
|
278
|
+
})
|
|
716
279
|
}
|
|
717
280
|
|
|
718
281
|
/**
|
|
719
|
-
*
|
|
720
|
-
*
|
|
282
|
+
* Reduce the layout state of one panel group.
|
|
283
|
+
* パネルグループのレイアウト状態を遷移させるリデューサー。
|
|
284
|
+
*
|
|
285
|
+
* @param state - Current layout state / 現在のレイアウト状態
|
|
286
|
+
* @param action - Action describing the edit / 編集内容を表すアクション
|
|
287
|
+
* @returns Next layout state, identical by reference when nothing changed / 次のレイアウト状態 (無変化なら同一参照)
|
|
721
288
|
*/
|
|
722
|
-
|
|
723
|
-
export const panelReducer = (state: PanelLayoutData[], action: PanelAction): PanelLayoutData[] => {
|
|
289
|
+
export const panelReducer = (state: PanelGroupLayoutState, action: PanelAction): PanelGroupLayoutState => {
|
|
724
290
|
switch (action.type) {
|
|
725
|
-
case "
|
|
726
|
-
|
|
727
|
-
|
|
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
|
|
291
|
+
case "SET_CONTAINER_SIZE": {
|
|
292
|
+
if (!Number.isFinite(action.containerSize) || action.containerSize < 0 || action.containerSize === state.containerSize) {
|
|
293
|
+
return state
|
|
776
294
|
}
|
|
295
|
+
return commit(state, allocate(state.panels, action.containerSize))
|
|
296
|
+
}
|
|
777
297
|
|
|
778
|
-
|
|
779
|
-
const
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
298
|
+
case "REGISTER_PANEL": {
|
|
299
|
+
const index = state.panels.findIndex((panel) => panel.id === action.panel.id)
|
|
300
|
+
if (index === -1) {
|
|
301
|
+
const panels = [...state.panels, createPanel(action.panel, initialCollapseDirection(action.panel))]
|
|
302
|
+
return commit(state, allocate(panels, state.containerSize))
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const previous = state.panels[index]
|
|
306
|
+
const authored = action.panel.sizeUnit === "pixels" ? action.panel.preferredPixelSize : action.panel.preferredPercentageSize
|
|
307
|
+
// 利用者が指定した既定サイズが変わったときだけ希望値を採用する。
|
|
308
|
+
// 変わっていない再登録 (制約プロパティの変更など) でドラッグ結果を巻き戻さないための判定
|
|
309
|
+
const adoptsAuthoredPreference = authored !== previous.authoredPreference || action.panel.sizeUnit !== previous.sizeUnit
|
|
310
|
+
const collapseDirectionStillAllowed = previous.collapsedByDirection === "start" ? action.panel.collapseFromStart : previous.collapsedByDirection === "end" ? action.panel.collapseFromEnd : false
|
|
311
|
+
// 折りたたみ設定が変わったのに方向が残ると、どの方向でも展開できないパネルになる
|
|
312
|
+
const stillCollapsible = action.panel.collapseFromStart || action.panel.collapseFromEnd
|
|
313
|
+
const nextCollapsed = previous.collapsed && stillCollapsible
|
|
314
|
+
const updated: PanelLayoutData = {
|
|
315
|
+
...previous,
|
|
316
|
+
...action.panel,
|
|
317
|
+
collapsed: nextCollapsed,
|
|
318
|
+
collapsedByDirection: nextCollapsed ? (collapseDirectionStillAllowed ? previous.collapsedByDirection : initialCollapseDirection(action.panel)) : null,
|
|
319
|
+
preferredPercentageSize: adoptsAuthoredPreference ? action.panel.preferredPercentageSize : previous.preferredPercentageSize,
|
|
320
|
+
preferredPixelSize: adoptsAuthoredPreference ? action.panel.preferredPixelSize : previous.preferredPixelSize,
|
|
321
|
+
preferenceBeforeCollapse: adoptsAuthoredPreference ? authored : previous.preferenceBeforeCollapse,
|
|
322
|
+
authoredPreference: authored,
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const panels = [...state.panels]
|
|
326
|
+
panels[index] = updated
|
|
327
|
+
return commit(state, allocate(panels, state.containerSize))
|
|
787
328
|
}
|
|
329
|
+
|
|
788
330
|
case "UNREGISTER_PANEL": {
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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
|
-
})
|
|
331
|
+
const panels = state.panels.filter((panel) => panel.id !== action.id)
|
|
332
|
+
if (panels.length === state.panels.length) {
|
|
333
|
+
return state
|
|
801
334
|
}
|
|
802
|
-
return
|
|
335
|
+
return commit(state, allocate(panels, state.containerSize))
|
|
803
336
|
}
|
|
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
337
|
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
// 折りたたまれたパネルがある場合はリサイズを中断
|
|
820
|
-
if (leftPanel.collapsed || rightPanel.collapsed) {
|
|
338
|
+
case "REORDER_PANELS": {
|
|
339
|
+
if (action.ids.length !== state.panels.length) {
|
|
340
|
+
// DOM と登録状態が一時的にずれている間は並べ替えない (揃った時点で再度通知される)
|
|
821
341
|
return state
|
|
822
342
|
}
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
if (action.leftSize === leftPanel.size && action.rightSize === rightPanel.size) {
|
|
343
|
+
if (new Set(action.ids).size !== action.ids.length) {
|
|
344
|
+
// 同じ id が 2 度現れる並びは、片方のパネルを取り違えて落とすことになる
|
|
826
345
|
return state
|
|
827
346
|
}
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
const
|
|
831
|
-
|
|
832
|
-
|
|
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) {
|
|
347
|
+
const byId = new Map(state.panels.map((panel) => [panel.id, panel]))
|
|
348
|
+
const panels: PanelLayoutData[] = []
|
|
349
|
+
for (const id of action.ids) {
|
|
350
|
+
const panel = byId.get(id)
|
|
351
|
+
if (!panel) {
|
|
889
352
|
return state
|
|
890
353
|
}
|
|
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)
|
|
354
|
+
panels.push(panel)
|
|
921
355
|
}
|
|
922
|
-
|
|
923
|
-
// 誤差範囲を超えて合計が変化する場合は状態を維持
|
|
924
|
-
const pairTotalAfter = finalLeftSize + finalRightSize
|
|
925
|
-
if (Math.abs(targetPairTotal - pairTotalAfter) > SIZE_EPSILON) {
|
|
356
|
+
if (panels.every((panel, index) => panel === state.panels[index])) {
|
|
926
357
|
return state
|
|
927
358
|
}
|
|
359
|
+
return commit(state, allocate(panels, state.containerSize))
|
|
360
|
+
}
|
|
928
361
|
|
|
929
|
-
|
|
930
|
-
const
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
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
|
-
}
|
|
362
|
+
case "RESIZE_PANELS": {
|
|
363
|
+
const startIndex = state.panels.findIndex((panel) => panel.id === action.startId)
|
|
364
|
+
const endIndex = state.panels.findIndex((panel) => panel.id === action.endId)
|
|
365
|
+
if (startIndex === -1 || endIndex === -1) {
|
|
366
|
+
return state
|
|
972
367
|
}
|
|
973
368
|
|
|
974
|
-
const
|
|
975
|
-
const
|
|
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
|
-
}
|
|
369
|
+
const startPanel = state.panels[startIndex]
|
|
370
|
+
const endPanel = state.panels[endIndex]
|
|
371
|
+
if (startPanel.collapsed || endPanel.collapsed || state.containerSize <= 0 || !Number.isFinite(action.startSize)) {
|
|
372
|
+
return state
|
|
1056
373
|
}
|
|
1057
374
|
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
incomingLength: incomingSnapshot?.length ?? enrichedPanels.length,
|
|
1066
|
-
})
|
|
1067
|
-
}
|
|
375
|
+
// ドラッグはこの 2 枚の間で領域を移すだけの操作なので、合計は動かさない
|
|
376
|
+
const total = startPanel.size + endPanel.size
|
|
377
|
+
const startBounds = resolvePanelBounds(startPanel, state.containerSize)
|
|
378
|
+
const endBounds = resolvePanelBounds(endPanel, state.containerSize)
|
|
379
|
+
const lowerBound = Math.max(startBounds.min, total - endBounds.max)
|
|
380
|
+
const upperBound = Math.min(startBounds.max, total - endBounds.min)
|
|
381
|
+
if (lowerBound - upperBound > PANEL_ALLOCATION_EPSILON) {
|
|
1068
382
|
return state
|
|
1069
383
|
}
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
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
|
-
}
|
|
384
|
+
|
|
385
|
+
const startSize = clamp(action.startSize, lowerBound, Math.max(lowerBound, upperBound))
|
|
386
|
+
const endSize = total - startSize
|
|
387
|
+
if (Math.abs(startSize - startPanel.size) <= PANEL_ALLOCATION_EPSILON) {
|
|
1093
388
|
return state
|
|
1094
389
|
}
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
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
|
|
390
|
+
|
|
391
|
+
const resized = [...state.panels]
|
|
392
|
+
resized[startIndex] = { ...startPanel, size: startSize }
|
|
393
|
+
resized[endIndex] = { ...endPanel, size: endSize }
|
|
394
|
+
return commit(state, allocate(commitPreferences(resized, state.containerSize), state.containerSize))
|
|
1126
395
|
}
|
|
396
|
+
|
|
1127
397
|
case "COLLAPSE_PANEL": {
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
if (panelIndex === -1) {
|
|
398
|
+
const index = state.panels.findIndex((panel) => panel.id === action.id)
|
|
399
|
+
if (index === -1) {
|
|
1131
400
|
return state
|
|
1132
401
|
}
|
|
1133
|
-
|
|
1134
|
-
const
|
|
1135
|
-
|
|
1136
|
-
const panelIsCollapsible = panelCanCollapse(targetPanel)
|
|
1137
|
-
// 折りたたみ不可または既に折りたたまれている場合はスキップ
|
|
1138
|
-
if (!(panelIsCollapsible && directionAllowed) || targetPanel.collapsed) {
|
|
402
|
+
const panel = state.panels[index]
|
|
403
|
+
const allowed = action.from === "start" ? panel.collapseFromStart : panel.collapseFromEnd
|
|
404
|
+
if (!allowed || panel.collapsed) {
|
|
1139
405
|
return state
|
|
1140
406
|
}
|
|
1141
407
|
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
const percentageBefore = targetPanel.percentageSize ?? targetPanel.preferredPercentageSize ?? 0
|
|
1146
|
-
|
|
1147
|
-
const updatedPanel: PanelLayoutData = {
|
|
1148
|
-
...targetPanel,
|
|
408
|
+
const panels = [...state.panels]
|
|
409
|
+
panels[index] = {
|
|
410
|
+
...panel,
|
|
1149
411
|
collapsed: true,
|
|
1150
412
|
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
413
|
}
|
|
1170
|
-
return
|
|
414
|
+
return commit(state, allocate(panels, state.containerSize))
|
|
1171
415
|
}
|
|
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
416
|
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
if (!(panelCanCollapse(targetPanel) && directionAllowed && targetPanel.collapsed)) {
|
|
417
|
+
case "EXPAND_PANEL": {
|
|
418
|
+
const index = state.panels.findIndex((panel) => panel.id === action.id)
|
|
419
|
+
if (index === -1 || (action.targetSize !== undefined && state.containerSize <= 0)) {
|
|
1185
420
|
return state
|
|
1186
421
|
}
|
|
1187
|
-
|
|
422
|
+
const panel = state.panels[index]
|
|
423
|
+
const allowed = action.from === "start" ? panel.collapseFromStart : panel.collapseFromEnd
|
|
424
|
+
if (!(allowed && panel.collapsed) || (panel.collapsedByDirection !== null && panel.collapsedByDirection !== action.from)) {
|
|
1188
425
|
return state
|
|
1189
426
|
}
|
|
1190
427
|
|
|
1191
|
-
|
|
1192
|
-
|
|
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,
|
|
428
|
+
const restored: PanelLayoutData = {
|
|
429
|
+
...panel,
|
|
1206
430
|
collapsed: false,
|
|
1207
431
|
collapsedByDirection: null,
|
|
1208
|
-
|
|
1209
|
-
|
|
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,
|
|
432
|
+
preferredPixelSize: isPixelPanel(panel) ? panel.preferenceBeforeCollapse : panel.preferredPixelSize,
|
|
433
|
+
preferredPercentageSize: isPixelPanel(panel) ? panel.preferredPercentageSize : panel.preferenceBeforeCollapse,
|
|
1219
434
|
}
|
|
1220
435
|
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
436
|
+
const expanded = [...state.panels]
|
|
437
|
+
expanded[index] = restored
|
|
438
|
+
const panels = action.targetSize === undefined ? expanded : withAbsoluteSize(expanded, action.id, action.targetSize, state.containerSize)
|
|
439
|
+
return commit(state, allocate(panels, state.containerSize))
|
|
440
|
+
}
|
|
1224
441
|
|
|
1225
|
-
|
|
1226
|
-
if (
|
|
1227
|
-
return
|
|
442
|
+
case "RESTORE_LAYOUT": {
|
|
443
|
+
if (action.entries.length !== state.panels.length || state.containerSize <= 0) {
|
|
444
|
+
return state
|
|
445
|
+
}
|
|
446
|
+
const stored = new Map(action.entries.map((entry) => [entry.id, entry]))
|
|
447
|
+
if (state.panels.some((panel) => !stored.has(panel.id))) {
|
|
448
|
+
// 保存時と構成が違う。位置で当てはめると別のパネルへ他人の寸法を与えてしまうため適用しない
|
|
449
|
+
return state
|
|
1228
450
|
}
|
|
1229
|
-
|
|
451
|
+
const panels = state.panels.map((panel) => {
|
|
452
|
+
const entry = stored.get(panel.id)
|
|
453
|
+
if (!entry) {
|
|
454
|
+
return panel
|
|
455
|
+
}
|
|
456
|
+
// 保存されているのは「開いているときに占める長さ」なので、折りたたみ状態に関わらず希望値として適用する
|
|
457
|
+
const restored = withPreferenceFromPixels(panel, entry.size, state.containerSize)
|
|
458
|
+
const canCollapse = panel.collapseFromStart || panel.collapseFromEnd
|
|
459
|
+
if (entry.collapsed && canCollapse) {
|
|
460
|
+
return { ...restored, collapsed: true, collapsedByDirection: panel.collapsedByDirection ?? initialCollapseDirection(panel) }
|
|
461
|
+
}
|
|
462
|
+
return restored.collapsed ? { ...restored, collapsed: false, collapsedByDirection: null } : restored
|
|
463
|
+
})
|
|
464
|
+
logger.debug("restore layout", { containerSize: state.containerSize, entries: action.entries })
|
|
465
|
+
return commit(state, allocate(panels, state.containerSize))
|
|
1230
466
|
}
|
|
1231
|
-
|
|
467
|
+
|
|
468
|
+
default: {
|
|
1232
469
|
return state
|
|
470
|
+
}
|
|
1233
471
|
}
|
|
1234
472
|
}
|