@vemjs/renderer-vecto 0.1.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/CHANGELOG.md +53 -0
- package/README.md +115 -0
- package/dist/CommandBar.d.ts +13 -0
- package/dist/CommandBar.d.ts.map +1 -0
- package/dist/CommandBar.js +60 -0
- package/dist/CommandBar.js.map +1 -0
- package/dist/FileSystemHandler.d.ts +9 -0
- package/dist/FileSystemHandler.d.ts.map +1 -0
- package/dist/FileSystemHandler.js +51 -0
- package/dist/FileSystemHandler.js.map +1 -0
- package/dist/VemEditorEntity.d.ts +16 -0
- package/dist/VemEditorEntity.d.ts.map +1 -0
- package/dist/VemEditorEntity.js +209 -0
- package/dist/VemEditorEntity.js.map +1 -0
- package/dist/Workspace.d.ts +13 -0
- package/dist/Workspace.d.ts.map +1 -0
- package/dist/Workspace.js +55 -0
- package/dist/Workspace.js.map +1 -0
- package/dist/WorkspaceExplorer.d.ts +18 -0
- package/dist/WorkspaceExplorer.d.ts.map +1 -0
- package/dist/WorkspaceExplorer.js +102 -0
- package/dist/WorkspaceExplorer.js.map +1 -0
- package/dist/WorkspaceLayout.d.ts +38 -0
- package/dist/WorkspaceLayout.d.ts.map +1 -0
- package/dist/WorkspaceLayout.js +165 -0
- package/dist/WorkspaceLayout.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/package.json +19 -0
- package/src/CommandBar.ts +69 -0
- package/src/FileSystemHandler.test.ts +51 -0
- package/src/FileSystemHandler.ts +60 -0
- package/src/VemEditorEntity.ts +235 -0
- package/src/Workspace.ts +65 -0
- package/src/WorkspaceExplorer.ts +124 -0
- package/src/WorkspaceLayout.ts +191 -0
- package/src/index.ts +48 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { UIComponent, Text, RichText } from '@vectojs/ui';
|
|
2
|
+
import type { IRenderer } from '@vectojs/core';
|
|
3
|
+
import type { VemEditorState } from '@vemjs/core';
|
|
4
|
+
import { CommandBar } from './CommandBar';
|
|
5
|
+
|
|
6
|
+
export class VemEditorEntity extends UIComponent {
|
|
7
|
+
private editorState: VemEditorState;
|
|
8
|
+
private gutterText: Text;
|
|
9
|
+
private bodyText: RichText;
|
|
10
|
+
private commandBar: CommandBar;
|
|
11
|
+
|
|
12
|
+
private charWidth = 8.4;
|
|
13
|
+
private lineHeight = 20;
|
|
14
|
+
private scrollY = 0; // scroll offset in lines
|
|
15
|
+
|
|
16
|
+
constructor(editorState: VemEditorState) {
|
|
17
|
+
super();
|
|
18
|
+
this.editorState = editorState;
|
|
19
|
+
this.width = 800;
|
|
20
|
+
this.height = 600;
|
|
21
|
+
this.clipChildren = true;
|
|
22
|
+
|
|
23
|
+
this.gutterText = new Text('', {
|
|
24
|
+
font: '14px monospace',
|
|
25
|
+
color: '#64748b', // slate-500
|
|
26
|
+
lineHeight: this.lineHeight,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Editor body text
|
|
30
|
+
this.bodyText = new RichText([], {
|
|
31
|
+
font: '14px monospace',
|
|
32
|
+
color: '#e2e8f0', // slate-200
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
this.commandBar = new CommandBar(editorState, this.width);
|
|
36
|
+
this.commandBar.setPosition(0, this.height - 30);
|
|
37
|
+
|
|
38
|
+
this.add(this.gutterText);
|
|
39
|
+
this.add(this.bodyText);
|
|
40
|
+
|
|
41
|
+
// Try to measure the exact monospace char width if running in browser
|
|
42
|
+
if (typeof document !== 'undefined') {
|
|
43
|
+
const canvas = document.createElement('canvas');
|
|
44
|
+
const ctx = canvas.getContext('2d');
|
|
45
|
+
if (ctx) {
|
|
46
|
+
ctx.font = '14px monospace';
|
|
47
|
+
this.charWidth = ctx.measureText('A').width;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this.updateFromState();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public updateFromState(): void {
|
|
55
|
+
const buffer = this.editorState.getBuffer();
|
|
56
|
+
const cursor = this.editorState.getCursor();
|
|
57
|
+
const lineCount = buffer.getLineCount();
|
|
58
|
+
|
|
59
|
+
// 1. Calculate gutter width dynamically
|
|
60
|
+
const maxLineDigits = Math.max(2, lineCount.toString().length);
|
|
61
|
+
const gutterWidth = maxLineDigits * this.charWidth + 15;
|
|
62
|
+
|
|
63
|
+
// 2. Set line numbers text
|
|
64
|
+
const lineNums: string[] = [];
|
|
65
|
+
for (let i = 1; i <= lineCount; i++) {
|
|
66
|
+
lineNums.push(i.toString().padStart(maxLineDigits, ' '));
|
|
67
|
+
}
|
|
68
|
+
this.gutterText.setText(lineNums.join('\n'));
|
|
69
|
+
this.gutterText.setPosition(5, 5);
|
|
70
|
+
|
|
71
|
+
// 3. Set editor body text
|
|
72
|
+
const spans = buffer.getLines().map((line, idx) => {
|
|
73
|
+
const suffix = idx === lineCount - 1 ? '' : '\n';
|
|
74
|
+
return { text: line + suffix };
|
|
75
|
+
});
|
|
76
|
+
this.bodyText.setSpans(spans);
|
|
77
|
+
this.bodyText.setPosition(gutterWidth + 5, 5);
|
|
78
|
+
|
|
79
|
+
// 4. Handle viewport scrolling to keep cursor visible
|
|
80
|
+
const visibleLines = Math.floor((this.height - 35) / this.lineHeight); // reserve 35px for status bar
|
|
81
|
+
if (cursor.line >= this.scrollY + visibleLines) {
|
|
82
|
+
this.scrollY = cursor.line - visibleLines + 1;
|
|
83
|
+
} else if (cursor.line < this.scrollY) {
|
|
84
|
+
this.scrollY = cursor.line;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Update children scroll positions
|
|
88
|
+
const scrollOffsetY = -this.scrollY * this.lineHeight;
|
|
89
|
+
this.gutterText.setPosition(5, 5 + scrollOffsetY);
|
|
90
|
+
this.bodyText.setPosition(gutterWidth + 5, 5 + scrollOffsetY);
|
|
91
|
+
|
|
92
|
+
// 5. Handle CommandBar visibility
|
|
93
|
+
if (this.editorState.getMode() === 'COMMAND') {
|
|
94
|
+
if (!this.children.includes(this.commandBar)) {
|
|
95
|
+
this.add(this.commandBar);
|
|
96
|
+
}
|
|
97
|
+
this.commandBar.clear();
|
|
98
|
+
} else {
|
|
99
|
+
if (this.children.includes(this.commandBar)) {
|
|
100
|
+
this.remove(this.commandBar);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
public render(r: IRenderer): void {
|
|
106
|
+
// 1. Draw editor background
|
|
107
|
+
r.beginPath();
|
|
108
|
+
r.moveTo(0, 0);
|
|
109
|
+
r.lineTo(this.width, 0);
|
|
110
|
+
r.lineTo(this.width, this.height);
|
|
111
|
+
r.lineTo(0, this.height);
|
|
112
|
+
r.closePath();
|
|
113
|
+
r.fill('#0f172a'); // slate-900
|
|
114
|
+
|
|
115
|
+
const lineCount = this.editorState.getBuffer().getLineCount();
|
|
116
|
+
const maxLineDigits = Math.max(2, lineCount.toString().length);
|
|
117
|
+
const gutterWidth = maxLineDigits * this.charWidth + 15;
|
|
118
|
+
|
|
119
|
+
// 2. Draw gutter background
|
|
120
|
+
r.beginPath();
|
|
121
|
+
r.moveTo(0, 0);
|
|
122
|
+
r.lineTo(gutterWidth, 0);
|
|
123
|
+
r.lineTo(gutterWidth, this.height);
|
|
124
|
+
r.lineTo(0, this.height);
|
|
125
|
+
r.closePath();
|
|
126
|
+
r.fill('#1e293b'); // slate-800
|
|
127
|
+
|
|
128
|
+
// Apply scrolling transformation for cursor and selections
|
|
129
|
+
r.save();
|
|
130
|
+
r.translate(0, -this.scrollY * this.lineHeight);
|
|
131
|
+
|
|
132
|
+
// 3. Draw Visual Mode selections
|
|
133
|
+
const selection = this.editorState.getVisualSelection();
|
|
134
|
+
if (selection) {
|
|
135
|
+
const type = selection.type;
|
|
136
|
+
let s = { ...selection.anchor };
|
|
137
|
+
let e = { ...selection.active };
|
|
138
|
+
|
|
139
|
+
if (s.line > e.line || (s.line === e.line && s.character > e.character)) {
|
|
140
|
+
const temp = s;
|
|
141
|
+
s = e;
|
|
142
|
+
e = temp;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const drawSelRect = (lineIdx: number, startChar: number, endChar: number) => {
|
|
146
|
+
const x = gutterWidth + 5 + startChar * this.charWidth;
|
|
147
|
+
const y = 5 + lineIdx * this.lineHeight;
|
|
148
|
+
const w = (endChar - startChar) * this.charWidth;
|
|
149
|
+
const h = this.lineHeight;
|
|
150
|
+
|
|
151
|
+
r.beginPath();
|
|
152
|
+
r.moveTo(x, y);
|
|
153
|
+
r.lineTo(x + w, y);
|
|
154
|
+
r.lineTo(x + w, y + h);
|
|
155
|
+
r.lineTo(x, y + h);
|
|
156
|
+
r.closePath();
|
|
157
|
+
r.fill('rgba(56, 189, 248, 0.3)'); // sky-400 opacity
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
if (type === 'line') {
|
|
161
|
+
for (let l = s.line; l <= e.line; l++) {
|
|
162
|
+
const lineText = this.editorState.getBuffer().getLine(l);
|
|
163
|
+
drawSelRect(l, 0, Math.max(1, lineText.length));
|
|
164
|
+
}
|
|
165
|
+
} else if (type === 'char') {
|
|
166
|
+
if (s.line === e.line) {
|
|
167
|
+
drawSelRect(s.line, s.character, e.character + 1);
|
|
168
|
+
} else {
|
|
169
|
+
const sLineText = this.editorState.getBuffer().getLine(s.line);
|
|
170
|
+
drawSelRect(s.line, s.character, Math.max(s.character + 1, sLineText.length + 1));
|
|
171
|
+
for (let l = s.line + 1; l < e.line; l++) {
|
|
172
|
+
const lText = this.editorState.getBuffer().getLine(l);
|
|
173
|
+
drawSelRect(l, 0, lText.length + 1);
|
|
174
|
+
}
|
|
175
|
+
drawSelRect(e.line, 0, e.character + 1);
|
|
176
|
+
}
|
|
177
|
+
} else if (type === 'block') {
|
|
178
|
+
const minCol = Math.min(selection.anchor.character, selection.active.character);
|
|
179
|
+
const maxCol = Math.max(selection.anchor.character, selection.active.character);
|
|
180
|
+
for (let l = s.line; l <= e.line; l++) {
|
|
181
|
+
drawSelRect(l, minCol, maxCol + 1);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 4. Draw Vim cursor
|
|
187
|
+
const cursor = this.editorState.getCursor();
|
|
188
|
+
const cursorX = gutterWidth + 5 + cursor.character * this.charWidth;
|
|
189
|
+
const cursorY = 5 + cursor.line * this.lineHeight;
|
|
190
|
+
const mode = this.editorState.getMode();
|
|
191
|
+
|
|
192
|
+
r.beginPath();
|
|
193
|
+
if (mode === 'INSERT') {
|
|
194
|
+
r.moveTo(cursorX, cursorY);
|
|
195
|
+
r.lineTo(cursorX + 2, cursorY);
|
|
196
|
+
r.lineTo(cursorX + 2, cursorY + this.lineHeight);
|
|
197
|
+
r.lineTo(cursorX, cursorY + this.lineHeight);
|
|
198
|
+
r.closePath();
|
|
199
|
+
r.fill('#f43f5e'); // rose-500
|
|
200
|
+
} else {
|
|
201
|
+
r.moveTo(cursorX, cursorY);
|
|
202
|
+
r.lineTo(cursorX + this.charWidth, cursorY);
|
|
203
|
+
r.lineTo(cursorX + this.charWidth, cursorY + this.lineHeight);
|
|
204
|
+
r.lineTo(cursorX, cursorY + this.lineHeight);
|
|
205
|
+
r.closePath();
|
|
206
|
+
r.fill('rgba(56, 189, 248, 0.7)'); // sky-400 opacity
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
r.restore(); // Restore scroll transform
|
|
210
|
+
|
|
211
|
+
// 5. Draw status bar at the bottom
|
|
212
|
+
const statusBarHeight = 30;
|
|
213
|
+
const statusY = this.height - statusBarHeight;
|
|
214
|
+
r.beginPath();
|
|
215
|
+
r.moveTo(0, statusY);
|
|
216
|
+
r.lineTo(this.width, statusY);
|
|
217
|
+
r.lineTo(this.width, this.height);
|
|
218
|
+
r.lineTo(0, this.height);
|
|
219
|
+
r.closePath();
|
|
220
|
+
r.fill('#1e293b'); // slate-800
|
|
221
|
+
|
|
222
|
+
if (mode !== 'COMMAND') {
|
|
223
|
+
const modeText = `-- ${mode} --`;
|
|
224
|
+
const posText = `${cursor.line + 1}:${cursor.character + 1}`;
|
|
225
|
+
const pendingKeys = this.editorState.getPendingKeys();
|
|
226
|
+
const pendingText = pendingKeys.length > 0 ? pendingKeys.join('') : '';
|
|
227
|
+
|
|
228
|
+
r.fillText(modeText, 10, statusY + 18, 'bold 12px monospace', '#38bdf8');
|
|
229
|
+
if (pendingText) {
|
|
230
|
+
r.fillText(pendingText, 120, statusY + 18, '12px monospace', '#e2e8f0');
|
|
231
|
+
}
|
|
232
|
+
r.fillText(posText, this.width - 60, statusY + 18, '12px monospace', '#94a3b8');
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
package/src/Workspace.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { UIComponent, Tabs } from '@vectojs/ui';
|
|
2
|
+
import type { IRenderer } from '@vectojs/core';
|
|
3
|
+
import { WorkspaceLayout } from './WorkspaceLayout';
|
|
4
|
+
|
|
5
|
+
export class VemWorkspace extends UIComponent {
|
|
6
|
+
private tabsComponent: Tabs | null = null;
|
|
7
|
+
private layouts: WorkspaceLayout[] = [];
|
|
8
|
+
|
|
9
|
+
constructor(width: number, height: number, initialText?: string) {
|
|
10
|
+
super();
|
|
11
|
+
this.width = width;
|
|
12
|
+
this.height = height;
|
|
13
|
+
|
|
14
|
+
const initialLayout = new WorkspaceLayout(width, height - 30, initialText || '');
|
|
15
|
+
this.layouts.push(initialLayout);
|
|
16
|
+
|
|
17
|
+
this.tabsComponent = new Tabs({
|
|
18
|
+
width: this.width,
|
|
19
|
+
height: this.height,
|
|
20
|
+
tabHeight: 30,
|
|
21
|
+
tabs: [{ id: 'tab-1', label: 'Tab 1', content: initialLayout }],
|
|
22
|
+
value: 'tab-1',
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
this.add(this.tabsComponent);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public addTab(initialText?: string): void {
|
|
29
|
+
const nextIndex = this.layouts.length + 1;
|
|
30
|
+
const newLayout = new WorkspaceLayout(this.width, this.height - 30, initialText || '');
|
|
31
|
+
this.layouts.push(newLayout);
|
|
32
|
+
|
|
33
|
+
const updatedTabs = this.layouts.map((layout, idx) => ({
|
|
34
|
+
id: `tab-${idx + 1}`,
|
|
35
|
+
label: `Tab ${idx + 1}`,
|
|
36
|
+
content: layout,
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
this.tabsComponent!.tabs = updatedTabs;
|
|
40
|
+
this.tabsComponent!.value = `tab-${nextIndex}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public update(dt: number, time: number): void {
|
|
44
|
+
super.update(dt, time);
|
|
45
|
+
if (this.tabsComponent) {
|
|
46
|
+
this.tabsComponent.width = this.width;
|
|
47
|
+
this.tabsComponent.height = this.height;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public getActiveLayout(): WorkspaceLayout | null {
|
|
52
|
+
if (!this.tabsComponent) return null;
|
|
53
|
+
const activeId = this.tabsComponent.value;
|
|
54
|
+
const match = activeId.match(/tab-(\d+)/);
|
|
55
|
+
if (match) {
|
|
56
|
+
const idx = parseInt(match[1], 10) - 1;
|
|
57
|
+
return this.layouts[idx] || null;
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public render(_r: IRenderer): void {
|
|
63
|
+
// Handled by tabsComponent
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { UIComponent, PanelGroup, Panel, TreeView, Button } from '@vectojs/ui';
|
|
2
|
+
import type { IRenderer } from '@vectojs/core';
|
|
3
|
+
import { VemWorkspace } from './Workspace';
|
|
4
|
+
import { FileSystemHandler } from './FileSystemHandler';
|
|
5
|
+
|
|
6
|
+
export class WorkspaceExplorer extends UIComponent {
|
|
7
|
+
private panelGroup: PanelGroup;
|
|
8
|
+
private leftPanel: Panel;
|
|
9
|
+
private rightPanel: Panel;
|
|
10
|
+
private workspace: VemWorkspace;
|
|
11
|
+
private treeView: TreeView | null = null;
|
|
12
|
+
private openBtn: Button;
|
|
13
|
+
private fsHandler: FileSystemHandler;
|
|
14
|
+
|
|
15
|
+
constructor(width: number, height: number, initialText?: string) {
|
|
16
|
+
super();
|
|
17
|
+
this.width = width;
|
|
18
|
+
this.height = height;
|
|
19
|
+
this.fsHandler = new FileSystemHandler();
|
|
20
|
+
|
|
21
|
+
this.panelGroup = new PanelGroup({
|
|
22
|
+
direction: 'horizontal',
|
|
23
|
+
width: width,
|
|
24
|
+
height: height,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
this.leftPanel = new Panel({ minSize: 150, defaultSize: 0.2 });
|
|
28
|
+
this.rightPanel = new Panel({ minSize: 300 });
|
|
29
|
+
|
|
30
|
+
this.workspace = new VemWorkspace(width * 0.8, height, initialText);
|
|
31
|
+
|
|
32
|
+
this.openBtn = new Button('Open Folder', {
|
|
33
|
+
onClick: () => this.handleOpenFolder(),
|
|
34
|
+
bg: '#1e293b', // slate-800
|
|
35
|
+
hoverBg: '#334155',
|
|
36
|
+
font: '14px monospace',
|
|
37
|
+
color: '#e2e8f0',
|
|
38
|
+
});
|
|
39
|
+
this.openBtn.width = 120;
|
|
40
|
+
this.openBtn.height = 35;
|
|
41
|
+
this.openBtn.setPosition(15, 15);
|
|
42
|
+
|
|
43
|
+
this.leftPanel.add(this.openBtn);
|
|
44
|
+
this.rightPanel.add(this.workspace);
|
|
45
|
+
|
|
46
|
+
this.panelGroup.addPanel(this.leftPanel);
|
|
47
|
+
this.panelGroup.addPanel(this.rightPanel);
|
|
48
|
+
|
|
49
|
+
this.add(this.panelGroup);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public getWorkspace(): VemWorkspace {
|
|
53
|
+
return this.workspace;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private async handleOpenFolder(): Promise<void> {
|
|
57
|
+
if (typeof window === 'undefined' || !(window as any).showDirectoryPicker) {
|
|
58
|
+
console.warn('File System Access API is not supported in this environment.');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const rootHandle = await (window as any).showDirectoryPicker();
|
|
64
|
+
const nodes = await this.fsHandler.readDirectory(rootHandle);
|
|
65
|
+
|
|
66
|
+
this.treeView = new TreeView({
|
|
67
|
+
nodes,
|
|
68
|
+
width: this.leftPanel.width,
|
|
69
|
+
height: this.height - 10,
|
|
70
|
+
font: '13px monospace',
|
|
71
|
+
color: '#cbd5e1',
|
|
72
|
+
selectedColor: 'rgba(56, 189, 248, 0.2)',
|
|
73
|
+
hoverColor: 'rgba(255, 255, 255, 0.05)',
|
|
74
|
+
onSelect: async (node) => {
|
|
75
|
+
const fileHandle = this.fsHandler.getFileHandle(node.id);
|
|
76
|
+
if (fileHandle) {
|
|
77
|
+
const content = await this.fsHandler.readFile(fileHandle);
|
|
78
|
+
this.workspace.addTab(content);
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
this.leftPanel.remove(this.openBtn);
|
|
84
|
+
this.leftPanel.add(this.treeView);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
console.error('Error selecting directory:', err);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
public update(dt: number, time: number): void {
|
|
91
|
+
super.update(dt, time);
|
|
92
|
+
|
|
93
|
+
if (this.panelGroup.width !== this.width || this.panelGroup.height !== this.height) {
|
|
94
|
+
this.panelGroup.width = this.width;
|
|
95
|
+
this.panelGroup.height = this.height;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (
|
|
99
|
+
this.treeView &&
|
|
100
|
+
(this.treeView.width !== this.leftPanel.width || this.treeView.height !== this.height)
|
|
101
|
+
) {
|
|
102
|
+
this.treeView.width = this.leftPanel.width;
|
|
103
|
+
this.treeView.height = this.height;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (
|
|
107
|
+
this.workspace.width !== this.rightPanel.width ||
|
|
108
|
+
this.workspace.height !== this.rightPanel.height
|
|
109
|
+
) {
|
|
110
|
+
this.workspace.width = this.rightPanel.width;
|
|
111
|
+
this.workspace.height = this.rightPanel.height;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
public render(_r: IRenderer): void {
|
|
116
|
+
_r.beginPath();
|
|
117
|
+
_r.moveTo(0, 0);
|
|
118
|
+
_r.lineTo(this.leftPanel.width, 0);
|
|
119
|
+
_r.lineTo(this.leftPanel.width, this.height);
|
|
120
|
+
_r.lineTo(0, this.height);
|
|
121
|
+
_r.closePath();
|
|
122
|
+
_r.fill('#090d16'); // deep slate sidebar background
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { UIComponent, PanelGroup, Panel } from '@vectojs/ui';
|
|
2
|
+
import type { Entity, IRenderer } from '@vectojs/core';
|
|
3
|
+
import { VemEditorState } from '@vemjs/core';
|
|
4
|
+
import { VemEditorEntity } from './VemEditorEntity';
|
|
5
|
+
|
|
6
|
+
export type PaneNode =
|
|
7
|
+
| { type: 'leaf'; id: string; state: VemEditorState }
|
|
8
|
+
| { type: 'split'; id: string; direction: 'horizontal' | 'vertical'; children: PaneNode[] };
|
|
9
|
+
|
|
10
|
+
export class EditorPane extends Panel {
|
|
11
|
+
public editorState: VemEditorState;
|
|
12
|
+
public editorEntity: VemEditorEntity;
|
|
13
|
+
|
|
14
|
+
constructor(state: VemEditorState) {
|
|
15
|
+
super();
|
|
16
|
+
this.editorState = state;
|
|
17
|
+
this.editorEntity = new VemEditorEntity(state);
|
|
18
|
+
this.add(this.editorEntity);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public update(dt: number, time: number): void {
|
|
22
|
+
super.update(dt, time);
|
|
23
|
+
if (this.editorEntity.width !== this.width || this.editorEntity.height !== this.height) {
|
|
24
|
+
this.editorEntity.width = this.width;
|
|
25
|
+
this.editorEntity.height = this.height;
|
|
26
|
+
this.editorEntity.updateFromState();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class WorkspaceLayout extends UIComponent {
|
|
32
|
+
private rootNode: PaneNode;
|
|
33
|
+
private activePaneId: string;
|
|
34
|
+
private layoutRoot: Entity | null = null;
|
|
35
|
+
private paneMap = new Map<string, PaneNode>();
|
|
36
|
+
|
|
37
|
+
constructor(width: number, height: number, initialText?: string) {
|
|
38
|
+
super();
|
|
39
|
+
this.width = width;
|
|
40
|
+
this.height = height;
|
|
41
|
+
|
|
42
|
+
const initialId = 'pane-1';
|
|
43
|
+
const initialState = new VemEditorState(initialText);
|
|
44
|
+
|
|
45
|
+
this.rootNode = {
|
|
46
|
+
type: 'leaf',
|
|
47
|
+
id: initialId,
|
|
48
|
+
state: initialState,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
this.activePaneId = initialId;
|
|
52
|
+
this.paneMap.set(initialId, this.rootNode);
|
|
53
|
+
|
|
54
|
+
this.bindStateEvents(initialId, initialState);
|
|
55
|
+
this.rebuildLayout();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private bindStateEvents(id: string, state: VemEditorState): void {
|
|
59
|
+
state.onSplit((dir) => {
|
|
60
|
+
this.splitPane(id, dir);
|
|
61
|
+
});
|
|
62
|
+
state.onQuit(() => {
|
|
63
|
+
this.closePane(id);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public getActiveState(): VemEditorState | null {
|
|
68
|
+
const pane = this.paneMap.get(this.activePaneId);
|
|
69
|
+
if (pane && pane.type === 'leaf') {
|
|
70
|
+
return pane.state;
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
public splitPane(paneId: string, direction: 'horizontal' | 'vertical'): void {
|
|
76
|
+
const targetNode = this.paneMap.get(paneId);
|
|
77
|
+
if (!targetNode || targetNode.type !== 'leaf') return;
|
|
78
|
+
|
|
79
|
+
const newId = `pane-${Date.now()}`;
|
|
80
|
+
const newState = new VemEditorState(targetNode.state.getBuffer().getText());
|
|
81
|
+
const newLeaf: PaneNode = {
|
|
82
|
+
type: 'leaf',
|
|
83
|
+
id: newId,
|
|
84
|
+
state: newState,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
this.paneMap.set(newId, newLeaf);
|
|
88
|
+
this.bindStateEvents(newId, newState);
|
|
89
|
+
|
|
90
|
+
const oldLeafCopy: PaneNode = { ...targetNode };
|
|
91
|
+
|
|
92
|
+
const splitNode = targetNode as any;
|
|
93
|
+
splitNode.type = 'split';
|
|
94
|
+
splitNode.direction = direction;
|
|
95
|
+
splitNode.children = [oldLeafCopy, newLeaf];
|
|
96
|
+
|
|
97
|
+
this.activePaneId = newId;
|
|
98
|
+
this.rebuildLayout();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
public closePane(paneId: string): void {
|
|
102
|
+
if (this.rootNode.type === 'leaf') return;
|
|
103
|
+
|
|
104
|
+
const parentNode = this.findParentNode(this.rootNode, paneId);
|
|
105
|
+
if (!parentNode || parentNode.type !== 'split') return;
|
|
106
|
+
|
|
107
|
+
const sibling = parentNode.children.find((c) => c.id !== paneId);
|
|
108
|
+
if (!sibling) return;
|
|
109
|
+
|
|
110
|
+
const grandparent = this.findParentNode(this.rootNode, parentNode.id);
|
|
111
|
+
if (grandparent && grandparent.type === 'split') {
|
|
112
|
+
const idx = grandparent.children.findIndex((c) => c.id === parentNode.id);
|
|
113
|
+
grandparent.children[idx] = sibling;
|
|
114
|
+
} else {
|
|
115
|
+
this.rootNode = sibling;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
this.paneMap.delete(paneId);
|
|
119
|
+
|
|
120
|
+
const remainingLeaf = this.findFirstLeaf(this.rootNode);
|
|
121
|
+
if (remainingLeaf) {
|
|
122
|
+
this.activePaneId = remainingLeaf.id;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
this.rebuildLayout();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private findParentNode(current: PaneNode, childId: string): PaneNode | null {
|
|
129
|
+
if (current.type === 'leaf') return null;
|
|
130
|
+
for (const child of current.children) {
|
|
131
|
+
if (child.id === childId) {
|
|
132
|
+
return current;
|
|
133
|
+
}
|
|
134
|
+
const parent = this.findParentNode(child, childId);
|
|
135
|
+
if (parent) return parent;
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private findFirstLeaf(current: PaneNode): PaneNode | null {
|
|
141
|
+
if (current.type === 'leaf') return current;
|
|
142
|
+
return this.findFirstLeaf(current.children[0]);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
public rebuildLayout(): void {
|
|
146
|
+
if (this.layoutRoot) {
|
|
147
|
+
this.remove(this.layoutRoot);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
this.layoutRoot = this.buildNode(this.rootNode);
|
|
151
|
+
this.add(this.layoutRoot);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private buildNode(node: PaneNode): Entity {
|
|
155
|
+
if (node.type === 'leaf') {
|
|
156
|
+
const pane = new EditorPane(node.state);
|
|
157
|
+
pane.id = node.id;
|
|
158
|
+
return pane;
|
|
159
|
+
} else {
|
|
160
|
+
const group = new PanelGroup({
|
|
161
|
+
direction: node.direction,
|
|
162
|
+
width: this.width,
|
|
163
|
+
height: this.height,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
for (const child of node.children) {
|
|
167
|
+
const childEntity = this.buildNode(child);
|
|
168
|
+
if (childEntity instanceof Panel) {
|
|
169
|
+
group.addPanel(childEntity);
|
|
170
|
+
} else {
|
|
171
|
+
const p = new Panel();
|
|
172
|
+
p.add(childEntity);
|
|
173
|
+
group.addPanel(p);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return group;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
public update(dt: number, time: number): void {
|
|
181
|
+
super.update(dt, time);
|
|
182
|
+
if (this.layoutRoot) {
|
|
183
|
+
this.layoutRoot.width = this.width;
|
|
184
|
+
this.layoutRoot.height = this.height;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
public render(_r: IRenderer): void {
|
|
189
|
+
// Handled by children
|
|
190
|
+
}
|
|
191
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Scene } from '@vectojs/core';
|
|
2
|
+
import type { VemEditorState } from '@vemjs/core';
|
|
3
|
+
import { VemEditorEntity } from './VemEditorEntity';
|
|
4
|
+
|
|
5
|
+
export { VemEditorEntity } from './VemEditorEntity';
|
|
6
|
+
export { CommandBar } from './CommandBar';
|
|
7
|
+
export { WorkspaceLayout, EditorPane, type PaneNode } from './WorkspaceLayout';
|
|
8
|
+
export { VemWorkspace } from './Workspace';
|
|
9
|
+
export { WorkspaceExplorer } from './WorkspaceExplorer';
|
|
10
|
+
|
|
11
|
+
export class VectoRenderer {
|
|
12
|
+
private editorState: VemEditorState;
|
|
13
|
+
private scene: Scene | null = null;
|
|
14
|
+
private editorEntity: VemEditorEntity | null = null;
|
|
15
|
+
|
|
16
|
+
constructor(editorState: VemEditorState) {
|
|
17
|
+
this.editorState = editorState;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public attach(canvas: HTMLCanvasElement): void {
|
|
21
|
+
this.scene = new Scene(canvas);
|
|
22
|
+
this.editorEntity = new VemEditorEntity(this.editorState);
|
|
23
|
+
this.scene.add(this.editorEntity);
|
|
24
|
+
this.scene.start();
|
|
25
|
+
|
|
26
|
+
// Listen to changes in the editor state to update rendering properties
|
|
27
|
+
this.editorState.onChange(() => {
|
|
28
|
+
if (this.editorEntity) {
|
|
29
|
+
this.editorEntity.updateFromState();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (this.editorState.getMode() === 'COMMAND') {
|
|
33
|
+
setTimeout(() => {
|
|
34
|
+
const inputEl = this.scene?.getA11yElement('vem-command-input');
|
|
35
|
+
if (inputEl) {
|
|
36
|
+
(inputEl as HTMLElement).focus();
|
|
37
|
+
}
|
|
38
|
+
}, 10);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public render(): void {
|
|
44
|
+
if (this.editorEntity) {
|
|
45
|
+
this.editorEntity.updateFromState();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"noEmit": false,
|
|
5
|
+
"declaration": true,
|
|
6
|
+
"declarationMap": true,
|
|
7
|
+
"sourceMap": true,
|
|
8
|
+
"rootDir": "./src",
|
|
9
|
+
"outDir": "./dist"
|
|
10
|
+
},
|
|
11
|
+
"include": ["src/**/*"],
|
|
12
|
+
"exclude": ["src/**/*.test.ts"]
|
|
13
|
+
}
|