Skip to content

Commit 746685c

Browse files
committed
feat: Experiment: unconstrained movement using Dragger
Create an experimental implementation of unconstrained dragging using the existing Dragger class. Implement this on a new Mover subclass DragMover, to keep this experimental code (that will probably be thrown away) separate from the main Mover implementation. Known bugs: * Blocks lurch around somewhat wildly while being moved. * Moving top-level blocks works reasonably well, but attempting to move a non-top-level block will result in it being deleted unless the move is aborted.
1 parent 9b2b06e commit 746685c

File tree

2 files changed

+180
-1
lines changed

2 files changed

+180
-1
lines changed

src/actions/drag_mover.ts

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import * as Constants from '../constants';
8+
import {
9+
ASTNode,
10+
Connection,
11+
ShortcutRegistry,
12+
common,
13+
registry,
14+
utils,
15+
} from 'blockly';
16+
import type {Block, BlockSvg, IDragger, WorkspaceSvg} from 'blockly';
17+
import {Mover, MoveInfo} from './mover';
18+
import {Navigation} from '../navigation';
19+
20+
/**
21+
* An experimental implementation of Mover that uses a dragger to
22+
* perform unconstraind movment.
23+
*/
24+
export class DragMover extends Mover {
25+
/**
26+
* The distance to move an item, in workspace coordinates, when
27+
* making an unconstrained move.
28+
*/
29+
UNCONSTRAINED_MOVE_DISTANCE = 20;
30+
31+
/**
32+
* Map of moves in progress.
33+
*
34+
* An entry for a given workspace in this map means that the this
35+
* Mover is moving a block on that workspace, and will disable
36+
* normal cursor movement until the move is complete.
37+
*/
38+
protected declare moves: Map<WorkspaceSvg, DragMoveInfo>;
39+
40+
/**
41+
* Start moving the currently-focused item on workspace, if
42+
* possible.
43+
*
44+
* Should only be called if canMove has returned true.
45+
*
46+
* @param workspace The workspace we might be moving on.
47+
* @returns True iff a move has successfully begun.
48+
*/
49+
override startMove(workspace: WorkspaceSvg) {
50+
const cursor = workspace?.getCursor();
51+
const curNode = cursor?.getCurNode();
52+
const block = curNode?.getSourceBlock() as BlockSvg | null;
53+
if (!cursor || !block) throw new Error('precondition failure');
54+
55+
// Select and focus block.
56+
common.setSelected(block);
57+
cursor.setCurNode(ASTNode.createBlockNode(block)!);
58+
// Begin dragging block.
59+
const DraggerClass = registry.getClassFromOptions(
60+
registry.Type.BLOCK_DRAGGER,
61+
workspace.options,
62+
true,
63+
);
64+
if (!DraggerClass) throw new Error('no Dragger registered');
65+
const dragger = new DraggerClass(block, workspace);
66+
// Record that a move is in progress and start dragging.
67+
this.moves.set(workspace, new DragMoveInfo(block, dragger));
68+
dragger.onDragStart(new PointerEvent('pointerdown'));
69+
return true;
70+
}
71+
72+
/**
73+
* Finish moving the currently-focused item on workspace.
74+
*
75+
* Should only be called if isMoving has returned true.
76+
*
77+
* @param workspace The workspace on which we are moving.
78+
* @returns True iff move successfully finished.
79+
*/
80+
override finishMove(workspace: WorkspaceSvg) {
81+
if (!workspace) return false;
82+
const info = this.moves.get(workspace);
83+
if (!info) throw new Error('no move info for workspace');
84+
85+
info.dragger.onDragEnd(
86+
new PointerEvent('pointerup'),
87+
new utils.Coordinate(0, 0),
88+
);
89+
90+
this.moves.delete(workspace);
91+
return true;
92+
}
93+
94+
/**
95+
* Abort moving the currently-focused item on workspace.
96+
*
97+
* Should only be called if isMoving has returned true.
98+
*
99+
* @param workspace The workspace on which we are moving.
100+
* @returns True iff move successfully aborted.
101+
*/
102+
override abortMove(workspace: WorkspaceSvg) {
103+
if (!workspace) return false;
104+
const info = this.moves.get(workspace);
105+
if (!info) throw new Error('no move info for workspace');
106+
107+
// Monkey patch dragger to trigger call to draggable.revertDrag.
108+
(info.dragger as any).shouldReturnToStart = () => true;
109+
info.dragger.onDragEnd(
110+
new PointerEvent('pointerup'),
111+
new utils.Coordinate(0, 0),
112+
);
113+
114+
this.moves.delete(workspace);
115+
return true;
116+
}
117+
118+
/**
119+
* Action to move the item being moved in the given direction,
120+
* constrained to valid attachment points (if any).
121+
*
122+
* @param workspace The workspace to move on.
123+
* @returns True iff this action applies and has been performed.
124+
*/
125+
override moveConstrained(
126+
workspace: WorkspaceSvg,
127+
/* ... */
128+
) {
129+
// Not yet implemented. Absorb keystroke to avoid moving cursor.
130+
alert(`Constrained movement not implemented.
131+
132+
Use alt+arrow (option+arrow on macOS) for unconstrained move.
133+
Use enter to complete the move, or escape to abort.`);
134+
return true;
135+
}
136+
137+
/**
138+
* Action to move the item being moved in the given direction,
139+
* without constraint.
140+
*
141+
* @param workspace The workspace to move on.
142+
* @param xDirection -1 to move left. 1 to move right.
143+
* @param yDirection -1 to move up. 1 to move down.
144+
* @returns True iff this action applies and has been performed.
145+
*/
146+
override moveUnconstrained(
147+
workspace: WorkspaceSvg,
148+
xDirection: number,
149+
yDirection: number,
150+
): boolean {
151+
if (!workspace) return false;
152+
const info = this.moves.get(workspace);
153+
if (!info) throw new Error('no move info for workspace');
154+
155+
info.totalDelta.x +=
156+
xDirection * this.UNCONSTRAINED_MOVE_DISTANCE * workspace.scale;
157+
info.totalDelta.y +=
158+
yDirection * this.UNCONSTRAINED_MOVE_DISTANCE * workspace.scale;
159+
160+
info.dragger.onDrag(new PointerEvent('pointermove'), info.totalDelta);
161+
return true;
162+
}
163+
}
164+
165+
/**
166+
* Information about the currently in-progress move for a given
167+
* Workspace.
168+
*/
169+
class DragMoveInfo extends MoveInfo {
170+
/** Total distance moved, in screen pixels */
171+
totalDelta = new utils.Coordinate(0, 0);
172+
173+
constructor(
174+
public readonly block: Block,
175+
public readonly dragger: IDragger,
176+
) {
177+
super(block);
178+
}
179+
}

src/navigation_controller.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import {ExitAction} from './actions/exit';
3434
import {EnterAction} from './actions/enter';
3535
import {DisconnectAction} from './actions/disconnect';
3636
import {ActionMenu} from './actions/action_menu';
37-
import {Mover} from './actions/mover';
37+
import {DragMover as Mover} from './actions/drag_mover';
3838

3939
const KeyCodes = BlocklyUtils.KeyCodes;
4040
const createSerializedKey = ShortcutRegistry.registry.createSerializedKey.bind(

0 commit comments

Comments
 (0)