Skip to content

Commit f005f79

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

10 files changed

Lines changed: 836 additions & 110 deletions

File tree

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: 82 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,42 @@ 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.
56+
*
57+
* Examples: nudging with arrow keys
4558
*/
4659
@injectable()
4760
export class MoveElementHandler implements IActionHandler {
61+
@inject(TYPES.IChangeBoundsManager)
62+
protected readonly changeBoundsManager: IChangeBoundsManager;
63+
protected tracker: ChangeBoundsTracker;
64+
4865
@inject(EditorContextService)
4966
protected editorContextService: EditorContextService;
5067

@@ -68,10 +85,12 @@ export class MoveElementHandler implements IActionHandler {
6885
@postConstruct()
6986
protected init(): void {
7087
this.moveFeedback = this.feedbackDispatcher.createEmitter();
88+
this.tracker = this.changeBoundsManager.createTracker();
7189
}
7290

7391
handle(action: Action): void | Action | ICommand {
74-
if (MoveElementRelativeAction.is(action)) {
92+
if (MoveElementRelativeAction.is(action) && action.elementIds.length > 0) {
93+
this.tracker.startTracking(this.editorContextService.modelRoot);
7594
this.handleMoveElement(action);
7695
}
7796
}
@@ -84,7 +103,7 @@ export class MoveElementHandler implements IActionHandler {
84103

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

111-
this.scheduleChangeBounds(this.toElementAndBounds(elementMoves));
159+
this.scheduleChangeBounds(newBounds);
112160
}
113161

114162
protected getTargetBounds(element: SelectableBoundsAware, action: MoveElementRelativeAction): Point {
@@ -129,28 +177,35 @@ export class MoveElementHandler implements IActionHandler {
129177
this.moveFeedback.dispose();
130178
this.dispatcher.dispatchAll([ChangeBoundsOperation.create(elementAndBounds)]);
131179
this.debouncedChangeBounds = undefined;
180+
this.tracker.dispose();
132181
}, 300);
133182
this.debouncedChangeBounds();
134183
}
135184

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-
}
185+
protected createMoveAction(moves: ElementMove[]): Action {
186+
return MoveAction.create(moves, { animate: false });
187+
}
188+
189+
protected isValidMoveable(element?: GModelElement): element is MoveableElement & SelectableBoundsAware {
190+
return !!element && isNonRoutableSelectedMovableBoundsAware(element) && !(element instanceof GResizeHandle);
191+
}
192+
193+
protected toElementAndBounds(elementMove: ElementMove): ElementAndBounds | undefined {
194+
const element = this.editorContextService.modelRoot.index.getById(elementMove.elementId);
195+
if (element && isBoundsAware(element)) {
196+
return {
197+
elementId: elementMove.elementId,
198+
newSize: {
199+
height: element.bounds.height,
200+
width: element.bounds.width
201+
},
202+
newPosition: {
203+
x: elementMove.toPosition.x,
204+
y: elementMove.toPosition.y
205+
}
206+
};
153207
}
154-
return elementBounds;
208+
209+
return undefined;
155210
}
156211
}

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ 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;
@@ -76,7 +76,13 @@ export class MouseTrackingElementPositionListener extends DragAwareMouseListener
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.tracker.moveElements([element], {
80+
snap: event,
81+
restrict: event,
82+
skipStatic: !isInitializing,
83+
// Ghost element is feedback-only, so no need to consider wraps
84+
wrap: false
85+
});
8086
const elementMove = move.elementMoves[0];
8187
if (!elementMove) {
8288
return [];
@@ -94,7 +100,7 @@ export class MouseTrackingElementPositionListener extends DragAwareMouseListener
94100
}
95101

96102
protected initialize(element: MoveableElement, target: GModelElement, event: MouseEvent): void {
97-
this.tracker.startTracking();
103+
this.tracker.startTracking(target.root);
98104
element.position = this.initializeElementPosition(element, target, event);
99105
}
100106

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: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -98,14 +98,14 @@ export interface IChangeBoundsManager {
9898
* @param size - The size to check.
9999
* @returns True if the element has a valid size, false otherwise.
100100
*/
101-
hasValidSize(element: GModelElement, size?: Dimension): boolean;
101+
hasValidSize(element: GModelElement, size?: Dimension, options?: ChangeBoundsManagerSizeOptions): boolean;
102102

103103
/**
104104
* Get the minimum size of an element for changing bounds.
105105
* @param element - The element to get the minimum size for.
106106
* @returns The minimum size of the element.
107107
*/
108-
getMinimumSize(element: GModelElement): Dimension;
108+
getMinimumSize(element: GModelElement, options?: ChangeBoundsManagerSizeOptions): Dimension;
109109

110110
/**
111111
* Determine whether to use movement restriction for changing bounds.
@@ -177,6 +177,10 @@ export interface IChangeBoundsManager {
177177
createTracker(): ChangeBoundsTracker;
178178
}
179179

180+
export interface ChangeBoundsManagerSizeOptions {
181+
useComputedDimensions?: boolean;
182+
}
183+
180184
/**
181185
* The default {@link IChangeBoundsManager} implementation. It is responsible for managing
182186
* the change of bounds for {@link GModelElement}s.
@@ -211,24 +215,24 @@ export class ChangeBoundsManager implements IChangeBoundsManager {
211215
return !isLocateable(element) || isValidMove(element, position ?? element.position, this.movementRestrictor);
212216
}
213217

214-
hasValidSize(element: GModelElement, size?: Dimension): boolean {
218+
hasValidSize(element: GModelElement, size?: Dimension, options?: ChangeBoundsManagerSizeOptions): boolean {
215219
if (!isBoundsAware(element)) {
216220
return true;
217221
}
218222
const dimension: Dimension = size ?? element.bounds;
219-
const minimum = this.getMinimumSize(element);
223+
const minimum = this.getMinimumSize(element, options);
220224
if (dimension.width < minimum.width || dimension.height < minimum.height) {
221225
return false;
222226
}
223227
return true;
224228
}
225229

226-
getMinimumSize(element: GModelElement): Dimension {
230+
getMinimumSize(element: GModelElement, options: ChangeBoundsManagerSizeOptions = { useComputedDimensions: true }): Dimension {
227231
if (!isBoundsAware(element)) {
228232
return Dimension.EMPTY;
229233
}
230234
const definedMinimum = minDimensions(element);
231-
const computedMinimum = LayoutAware.getComputedDimensions(element);
235+
const computedMinimum = options.useComputedDimensions ? LayoutAware.getComputedDimensions(element) : undefined;
232236
return computedMinimum
233237
? {
234238
width: Math.max(definedMinimum.width, computedMinimum.width),
@@ -269,6 +273,14 @@ export class ChangeBoundsManager implements IChangeBoundsManager {
269273
// restriction feedback on each element
270274
trackedMove.elementMoves.forEach(move => this.addMoveRestrictionFeedback(feedback, move, ctx, event));
271275

276+
// restriction feedback on each wrapped element (mostly ancestors)
277+
Object.values(trackedMove.wrapResizes ?? {}).forEach(elementResize => {
278+
this.addMoveRestrictionFeedback(feedback, elementResize, ctx, event);
279+
feedback.add(
280+
toggleCssClasses(elementResize.element, !elementResize.valid.size, CSS_RESTRICTED_RESIZE),
281+
deleteCssClasses(elementResize.element, CSS_RESTRICTED_RESIZE)
282+
);
283+
});
272284
return feedback;
273285
}
274286

@@ -296,6 +308,7 @@ export class ChangeBoundsManager implements IChangeBoundsManager {
296308
deleteCssClasses(elementResize.element, CSS_RESTRICTED_RESIZE)
297309
);
298310
});
311+
299312
return feedback;
300313
}
301314

0 commit comments

Comments
 (0)