Skip to content

Commit 1073d2b

Browse files
committed
Improve wrapping behavior of containers
1 parent 2fe0c5e commit 1073d2b

File tree

11 files changed

+1054
-114
lines changed

11 files changed

+1054
-114
lines changed

examples/workflow-standalone/app/example1.wf

Lines changed: 303 additions & 2 deletions
Large diffs are not rendered by default.

packages/client/src/features/change-bounds/model.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,10 @@ import {
2323
Point,
2424
hoverFeedbackFeature,
2525
isBoundsAware,
26-
isMoveable,
2726
isSelectable
2827
} from '@eclipse-glsp/sprotty';
2928
import { CursorCSS } from '../../base/feedback/css-feedback';
30-
import { BoundsAwareModelElement, MoveableElement, ResizableModelElement } from '../../utils/gmodel-util';
29+
import type { ResizableModelElement } from '../../utils/gmodel-util';
3130

3231
export const resizeFeature = Symbol('resizeFeature');
3332

@@ -105,10 +104,6 @@ export namespace ResizeHandleLocation {
105104
}
106105
}
107106

108-
export function isBoundsAwareMoveable(element: GModelElement): element is BoundsAwareModelElement & MoveableElement {
109-
return isMoveable(element) && isBoundsAware(element);
110-
}
111-
112107
export class GResizeHandle extends GChildElement implements Hoverable {
113108
static readonly TYPE = 'resize-handle';
114109

packages/client/src/features/change-bounds/move-element-handler.ts

Lines changed: 80 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import {
1818
Action,
1919
ChangeBoundsOperation,
2020
ElementAndBounds,
21-
ElementMove,
2221
IActionDispatcher,
2322
IActionHandler,
2423
ICommand,
@@ -27,24 +26,40 @@ import {
2726
MoveViewportAction,
2827
Point,
2928
TYPES,
30-
isBoundsAware
29+
isBoundsAware,
30+
type Bounds,
31+
type ElementMove,
32+
type GModelElement
3133
} from '@eclipse-glsp/sprotty';
3234
import { inject, injectable, optional, postConstruct } from 'inversify';
3335
import { DebouncedFunc, debounce } from 'lodash';
3436
import { EditorContextService } from '../../base/editor-context-service';
3537
import { IFeedbackActionDispatcher } from '../../base/feedback/feedback-action-dispatcher';
3638
import { FeedbackEmitter } from '../../base/feedback/feedback-emitter';
37-
import { SelectableBoundsAware, getElements, isSelectableAndBoundsAware } from '../../utils/gmodel-util';
39+
import {
40+
SelectableBoundsAware,
41+
getElements,
42+
isNonRoutableSelectedMovableBoundsAware,
43+
isNotUndefined,
44+
type MoveableElement
45+
} from '../../utils/gmodel-util';
3846
import { isValidMove } from '../../utils/layout-utils';
3947
import { outsideOfViewport } from '../../utils/viewpoint-util';
4048
import { IMovementRestrictor } from '../change-bounds/movement-restrictor';
49+
import type { IChangeBoundsManager } from '../tools/change-bounds/change-bounds-manager';
50+
import { TrackedElementResize, type ChangeBoundsTracker } from '../tools/change-bounds/change-bounds-tracker';
51+
import { GResizeHandle } from './model';
4152
import { MoveElementRelativeAction } from './move-element-action';
4253

4354
/**
4455
* Action handler for moving elements.
4556
*/
4657
@injectable()
4758
export class MoveElementHandler implements IActionHandler {
59+
@inject(TYPES.IChangeBoundsManager)
60+
protected readonly changeBoundsManager: IChangeBoundsManager;
61+
protected tracker: ChangeBoundsTracker;
62+
4863
@inject(EditorContextService)
4964
protected editorContextService: EditorContextService;
5065

@@ -68,10 +83,12 @@ export class MoveElementHandler implements IActionHandler {
6883
@postConstruct()
6984
protected init(): void {
7085
this.moveFeedback = this.feedbackDispatcher.createEmitter();
86+
this.tracker = this.changeBoundsManager.createTracker();
7187
}
7288

7389
handle(action: Action): void | Action | ICommand {
74-
if (MoveElementRelativeAction.is(action)) {
90+
if (MoveElementRelativeAction.is(action) && action.elementIds.length > 0) {
91+
this.tracker.startTracking(this.editorContextService.modelRoot);
7592
this.handleMoveElement(action);
7693
}
7794
}
@@ -84,7 +101,7 @@ export class MoveElementHandler implements IActionHandler {
84101

85102
const viewportActions: Action[] = [];
86103
const elementMoves: ElementMove[] = [];
87-
const elements = getElements(viewport.index, action.elementIds, isSelectableAndBoundsAware);
104+
const elements = getElements(viewport.index, action.elementIds, this.isValidMoveable);
88105
for (const element of elements) {
89106
const newPosition = this.getTargetBounds(element, action);
90107
elementMoves.push({
@@ -103,12 +120,41 @@ export class MoveElementHandler implements IActionHandler {
103120
viewportActions.push(MoveViewportAction.create({ moveX: action.moveX, moveY: action.moveY }));
104121
}
105122
}
106-
107123
this.dispatcher.dispatchAll(viewportActions);
108-
const moveAction = MoveAction.create(elementMoves, { animate: false });
109-
this.moveFeedback.add(moveAction).submit();
124+
this.moveFeedback.add(this.createMoveAction(elementMoves));
125+
126+
const newBounds = elementMoves.map(this.toElementAndBounds.bind(this)).filter(isNotUndefined);
127+
const wraps = this.tracker.wrap(
128+
elements.map(element => {
129+
const bounds = newBounds.find(b => b.elementId === element.id)!;
130+
const toBounds: Bounds = {
131+
...element.bounds,
132+
...bounds.newSize,
133+
...bounds.newPosition
134+
};
135+
return {
136+
element: element,
137+
fromBounds: element.bounds,
138+
toBounds
139+
};
140+
}),
141+
{
142+
validate: true
143+
}
144+
);
145+
146+
this.moveFeedback.add(TrackedElementResize.createFeedbackActions(Object.values(wraps ?? {})));
147+
this.moveFeedback.submit();
148+
149+
if (Object.keys(wraps).length > 0) {
150+
newBounds.push(
151+
...Object.values(wraps)
152+
.filter(resize => !action.elementIds.includes(resize.element.id))
153+
.map(TrackedElementResize.toElementAndBounds)
154+
);
155+
}
110156

111-
this.scheduleChangeBounds(this.toElementAndBounds(elementMoves));
157+
this.scheduleChangeBounds(newBounds);
112158
}
113159

114160
protected getTargetBounds(element: SelectableBoundsAware, action: MoveElementRelativeAction): Point {
@@ -129,28 +175,35 @@ export class MoveElementHandler implements IActionHandler {
129175
this.moveFeedback.dispose();
130176
this.dispatcher.dispatchAll([ChangeBoundsOperation.create(elementAndBounds)]);
131177
this.debouncedChangeBounds = undefined;
178+
this.tracker.dispose();
132179
}, 300);
133180
this.debouncedChangeBounds();
134181
}
135182

136-
protected toElementAndBounds(elementMoves: ElementMove[]): ElementAndBounds[] {
137-
const elementBounds: ElementAndBounds[] = [];
138-
for (const elementMove of elementMoves) {
139-
const element = this.editorContextService.modelRoot.index.getById(elementMove.elementId);
140-
if (element && isBoundsAware(element)) {
141-
elementBounds.push({
142-
elementId: elementMove.elementId,
143-
newSize: {
144-
height: element.bounds.height,
145-
width: element.bounds.width
146-
},
147-
newPosition: {
148-
x: elementMove.toPosition.x,
149-
y: elementMove.toPosition.y
150-
}
151-
});
152-
}
183+
protected createMoveAction(moves: ElementMove[]): Action {
184+
return MoveAction.create(moves, { animate: false });
185+
}
186+
187+
protected isValidMoveable(element?: GModelElement): element is MoveableElement & SelectableBoundsAware {
188+
return !!element && isNonRoutableSelectedMovableBoundsAware(element) && !(element instanceof GResizeHandle);
189+
}
190+
191+
protected toElementAndBounds(elementMove: ElementMove): ElementAndBounds | undefined {
192+
const element = this.editorContextService.modelRoot.index.getById(elementMove.elementId);
193+
if (element && isBoundsAware(element)) {
194+
return {
195+
elementId: elementMove.elementId,
196+
newSize: {
197+
height: element.bounds.height,
198+
width: element.bounds.width
199+
},
200+
newPosition: {
201+
x: elementMove.toPosition.x,
202+
y: elementMove.toPosition.y
203+
}
204+
};
153205
}
154-
return elementBounds;
206+
207+
return undefined;
155208
}
156209
}

packages/client/src/features/element-template/mouse-tracking-element-position-listener.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,15 @@ import { getAbsolutePosition } from '../../utils/viewpoint-util';
3434
import { FeedbackAwareTool } from '../tools/base-tools';
3535
import { IChangeBoundsManager } from '../tools/change-bounds/change-bounds-manager';
3636
import { MoveFinishedEventAction } from '../tools/change-bounds/change-bounds-tool-feedback';
37-
import { ChangeBoundsTracker, TrackedMove } from '../tools/change-bounds/change-bounds-tracker';
37+
import { TrackedMove, type ChangeBoundsTracker } from '../tools/change-bounds/change-bounds-tracker';
3838

3939
export interface PositioningTool extends FeedbackAwareTool {
4040
readonly changeBoundsManager: IChangeBoundsManager;
4141
}
4242

4343
export class MouseTrackingElementPositionListener extends DragAwareMouseListener {
4444
protected moveGhostFeedback: FeedbackEmitter;
45-
protected tracker: ChangeBoundsTracker;
45+
protected changeBoundsTracker: ChangeBoundsTracker;
4646
protected toDispose = new DisposableCollection();
4747

4848
constructor(
@@ -52,7 +52,7 @@ export class MouseTrackingElementPositionListener extends DragAwareMouseListener
5252
protected editorContext?: EditorContextService
5353
) {
5454
super();
55-
this.tracker = this.tool.changeBoundsManager.createTracker();
55+
this.changeBoundsTracker = this.tool.changeBoundsManager.createTracker();
5656
this.moveGhostFeedback = this.tool.createFeedbackEmitter();
5757
this.toDispose.push(this.moveGhostFeedback);
5858
const modelRootChangedListener = editorContext?.onModelRootChanged(newRoot => this.modelRootChanged(newRoot));
@@ -72,11 +72,16 @@ export class MouseTrackingElementPositionListener extends DragAwareMouseListener
7272
if (!element) {
7373
return [];
7474
}
75-
const isInitializing = !this.tracker.isTracking();
75+
const isInitializing = !this.changeBoundsTracker.isTracking();
7676
if (isInitializing) {
7777
this.initialize(element, ctx, event);
7878
}
79-
const move = this.tracker.moveElements([element], { snap: event, restrict: event, skipStatic: !isInitializing });
79+
const move = this.changeBoundsTracker.moveElements([element], {
80+
snap: event,
81+
restrict: event,
82+
skipStatic: !isInitializing,
83+
wrap: false
84+
});
8085
const elementMove = move.elementMoves[0];
8186
if (!elementMove) {
8287
return [];
@@ -89,12 +94,12 @@ export class MouseTrackingElementPositionListener extends DragAwareMouseListener
8994
);
9095
this.addMoveFeedback(move, ctx, event);
9196
this.moveGhostFeedback.submit();
92-
this.tracker.updateTrackingPosition(elementMove.moveVector);
97+
this.changeBoundsTracker.updateTrackingPosition(elementMove.moveVector);
9398
return [];
9499
}
95100

96101
protected initialize(element: MoveableElement, target: GModelElement, event: MouseEvent): void {
97-
this.tracker.startTracking();
102+
this.changeBoundsTracker.startTracking(target.root);
98103
element.position = this.initializeElementPosition(element, target, event);
99104
}
100105

@@ -112,7 +117,7 @@ export class MouseTrackingElementPositionListener extends DragAwareMouseListener
112117

113118
protected modelRootChanged(root: Readonly<GModelRoot>): void {
114119
// stop the tracking once we receive a new model root and ensure proper alignment with next next mouse move
115-
this.tracker.stopTracking();
120+
this.changeBoundsTracker.stopTracking();
116121
}
117122

118123
override dispose(): void {

packages/client/src/features/layout/layout-elements-action.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@ import {
3434
} from '@eclipse-glsp/sprotty';
3535
import { inject, injectable, optional } from 'inversify';
3636
import { SelectionService } from '../../base/selection-service';
37-
import { BoundsAwareModelElement, getElements } from '../../utils/gmodel-util';
37+
import { BoundsAwareModelElement, getElements, isBoundsAwareMoveable } from '../../utils/gmodel-util';
3838
import { toValidElementAndBounds, toValidElementMove } from '../../utils/layout-utils';
39-
import { isBoundsAwareMoveable, isResizable } from '../change-bounds/model';
39+
import { isResizable } from '../change-bounds/model';
4040
import { IMovementRestrictor } from '../change-bounds/movement-restrictor';
4141

4242
/**

packages/client/src/features/tools/change-bounds/change-bounds-manager.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -211,24 +211,24 @@ export class ChangeBoundsManager implements IChangeBoundsManager {
211211
return !isLocateable(element) || isValidMove(element, position ?? element.position, this.movementRestrictor);
212212
}
213213

214-
hasValidSize(element: GModelElement, size?: Dimension): boolean {
214+
hasValidSize(element: GModelElement, size?: Dimension, options?: { useComputedDimensions?: boolean }): boolean {
215215
if (!isBoundsAware(element)) {
216216
return true;
217217
}
218218
const dimension: Dimension = size ?? element.bounds;
219-
const minimum = this.getMinimumSize(element);
219+
const minimum = this.getMinimumSize(element, options);
220220
if (dimension.width < minimum.width || dimension.height < minimum.height) {
221221
return false;
222222
}
223223
return true;
224224
}
225225

226-
getMinimumSize(element: GModelElement): Dimension {
226+
getMinimumSize(element: GModelElement, options: { useComputedDimensions?: boolean } = { useComputedDimensions: true }): Dimension {
227227
if (!isBoundsAware(element)) {
228228
return Dimension.EMPTY;
229229
}
230230
const definedMinimum = minDimensions(element);
231-
const computedMinimum = LayoutAware.getComputedDimensions(element);
231+
const computedMinimum = options.useComputedDimensions ? LayoutAware.getComputedDimensions(element) : undefined;
232232
return computedMinimum
233233
? {
234234
width: Math.max(definedMinimum.width, computedMinimum.width),
@@ -268,7 +268,14 @@ export class ChangeBoundsManager implements IChangeBoundsManager {
268268

269269
// restriction feedback on each element
270270
trackedMove.elementMoves.forEach(move => this.addMoveRestrictionFeedback(feedback, move, ctx, event));
271-
271+
// restriction feedback on each element
272+
Object.values(trackedMove.wrapResizes ?? {}).forEach(elementResize => {
273+
this.addMoveRestrictionFeedback(feedback, elementResize, ctx, event);
274+
feedback.add(
275+
toggleCssClasses(elementResize.element, !elementResize.valid.size, CSS_RESTRICTED_RESIZE),
276+
deleteCssClasses(elementResize.element, CSS_RESTRICTED_RESIZE)
277+
);
278+
});
272279
return feedback;
273280
}
274281

@@ -296,6 +303,7 @@ export class ChangeBoundsManager implements IChangeBoundsManager {
296303
deleteCssClasses(elementResize.element, CSS_RESTRICTED_RESIZE)
297304
);
298305
});
306+
299307
return feedback;
300308
}
301309

packages/client/src/features/tools/change-bounds/change-bounds-tool-move-feedback.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import {
3131
import { GResizeHandle } from '../../change-bounds/model';
3232
import { ChangeBoundsTool } from './change-bounds-tool';
3333
import { MoveFinishedEventAction, MoveInitializedEventAction } from './change-bounds-tool-feedback';
34-
import { ChangeBoundsTracker, TrackedMove } from './change-bounds-tracker';
34+
import { TrackedElementResize, TrackedMove, type ChangeBoundsTracker } from './change-bounds-tracker';
3535

3636
/**
3737
* This mouse listener provides visual feedback for moving by sending client-side
@@ -79,7 +79,7 @@ export class FeedbackMoveMouseListener extends DragAwareMouseListener implements
7979
}
8080
const moveable = findParentByFeature(target, this.isValidMoveable);
8181
if (moveable !== undefined) {
82-
this.tracker.startTracking();
82+
this.tracker.startTracking(target.root);
8383
this.scheduleMoveInitialized();
8484
} else {
8585
this.tracker.stopTracking();
@@ -91,7 +91,7 @@ export class FeedbackMoveMouseListener extends DragAwareMouseListener implements
9191
this.pendingMoveInitialized = debounce(() => {
9292
this.moveInitialized();
9393
this.pendingMoveInitialized = undefined;
94-
}, 750);
94+
}, this.moveInitializationTimeout());
9595
this.pendingMoveInitialized();
9696
}
9797

@@ -143,6 +143,7 @@ export class FeedbackMoveMouseListener extends DragAwareMouseListener implements
143143
// cancel any pending move
144144
this.pendingMoveInitialized?.cancel();
145145
this.moveFeedback.add(this.createMoveAction(move), () => this.resetElementPositions(target));
146+
this.moveFeedback.add(this.createWrapAction(move));
146147
this.addMoveFeedback(move, target, event);
147148
this.tracker.updateTrackingPosition(move);
148149
this.moveFeedback.submit();
@@ -157,6 +158,10 @@ export class FeedbackMoveMouseListener extends DragAwareMouseListener implements
157158
);
158159
}
159160

161+
protected createWrapAction(trackedMove: TrackedMove): Action {
162+
return TrackedElementResize.createFeedbackActions(Object.values(trackedMove.wrapResizes ?? {}), { validOnly: false });
163+
}
164+
160165
protected addMoveFeedback(trackedMove: TrackedMove, ctx: GModelElement, event: MouseEvent): void {
161166
this.tool.changeBoundsManager.addMoveFeedback(this.moveFeedback, trackedMove, ctx, event);
162167
}
@@ -220,7 +225,7 @@ export class FeedbackMoveMouseListener extends DragAwareMouseListener implements
220225
this.pendingMoveInitialized?.cancel();
221226
this.moveInitializedFeedback.dispose();
222227
this.moveFeedback.dispose();
223-
this.tracker.dispose();
228+
this.tracker.stopTracking();
224229
this.elementId2startPos.clear();
225230
super.dispose();
226231
}

0 commit comments

Comments
 (0)