1 /**
2 Copyright: Copyright (c) 2017-2018 Andrey Penechko.
3 License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0).
4 Authors: Andrey Penechko.
5 */
6 module voxelman.gui.textedit.texteditorview;
7 
8 import core.time : MonoTime;
9 import std.stdio : writefln, writeln;
10 import std.array;
11 
12 import datadriven.entityman : EntityManager;
13 import voxelman.container.buffer;
14 import voxelman.graphics;
15 import voxelman.log;
16 import voxelman.gui;
17 import voxelman.math;
18 import voxelman.platform;
19 import voxelman.text.linebuffer;
20 import voxelman.text.scale;
21 
22 import voxelman.gui.textedit.texteditor;
23 import voxelman.gui.textedit.cursor;
24 import voxelman.gui.textedit.linebuffer;
25 import voxelman.gui.textedit.textmodel;
26 
27 void registerComponents(ref EntityManager widgets)
28 {
29 	widgets.registerComponent!TextEditorViewportData;
30 	widgets.registerComponent!TextEditorLineNumbersData;
31 }
32 
33 @Component("gui.TextEditorViewportData", Replication.none)
34 struct TextEditorViewportData
35 {
36 	TextModel editor;
37 	TextViewSettingsRef settings;
38 	MonoTime blinkStart;
39 
40 	ivec2 textPos; // on text canvas, in pixels
41 
42 	int autoscrollY;
43 	int firstVisibleLine;
44 	int lastVisibleLine;
45 	bool autoscroll;
46 	// If true prevents scrolling up
47 	// If false, scrolling up will disable autoscroll
48 	bool hardAutoscroll;
49 
50 	void resetBlinkTimer()
51 	{
52 		blinkStart = MonoTime.currTime;
53 	}
54 
55 	void scroll(ivec2 delta)
56 	{
57 		if (autoscroll && hardAutoscroll) return;
58 
59 		textPos += ivec2(0, delta.y * settings.scrollSpeedLines * settings.scaledGlyphH);
60 		if (textPos.y < autoscrollY)
61 		{
62 			autoscroll = false;
63 		}
64 	}
65 
66 	void update(GuiContext ctx, ivec2 size)
67 	{
68 		ivec2 textSizeInGlyphs = ivec2(0, editor.numLines);
69 		ivec2 textSizeInPixels = textSizeInGlyphs * settings.scaledGlyphSize;
70 
71 		int maxVisibleLines = divCeil(size.y, settings.scaledGlyphH);
72 		autoscrollY = (editor.numLines - maxVisibleLines) * settings.scaledGlyphH;
73 		autoscrollY = clamp(autoscrollY, 0, textSizeInPixels.y);
74 		if (autoscroll)
75 		{
76 			textPos.y = autoscrollY;
77 		}
78 
79 		textPos = vector_clamp(textPos, ivec2(0, 0), textSizeInPixels);
80 
81 		//if (editor.textSizeInGlyphs.y == 0) return;
82 
83 		firstVisibleLine = clamp(textPos.y / settings.scaledGlyphH, 0, editor.lastLine);
84 
85 		int viewportEndPos = textPos.y + size.y;
86 		lastVisibleLine = clamp(viewportEndPos / settings.scaledGlyphH, 0, editor.lastLine);
87 
88 		ctx.debugText.putfln("textPos %s", textPos);
89 		ctx.debugText.putfln("size %s", size);
90 		ctx.debugText.putfln("viewportEndPos %s", viewportEndPos);
91 		ctx.debugText.putfln("firstVisibleLine %s", firstVisibleLine);
92 		ctx.debugText.putfln("lastVisibleLine %s", lastVisibleLine);
93 	}
94 }
95 
96 struct TextEditorViewportLogic
97 {
98 	static:
99 	WidgetProxy create(
100 		WidgetProxy parent,
101 		TextModel editor,
102 		TextViewSettingsRef settings)
103 	{
104 		return parent.createChild(
105 			TextEditorViewportData(editor, settings, MonoTime.currTime),
106 			WidgetEvents(
107 				&onScroll, &enterWidget, &drawWidget,
108 				&pointerPressed, &pointerReleased, &pointerMoved,
109 				&keyPressed, &charTyped
110 				),
111 			WidgetIsFocusable()
112 		);
113 	}
114 
115 	void onScroll(WidgetProxy widget, ref ScrollEvent event)
116 	{
117 		auto data = widget.get!TextEditorViewportData;
118 		data.scroll(ivec2(event.delta));
119 	}
120 
121 	void enterWidget(WidgetProxy widget, ref PointerEnterEvent event)
122 	{
123 		widget.ctx.cursorIcon = CursorIcon.ibeam;
124 	}
125 
126 	void drawWidget(WidgetProxy widget, ref DrawEvent event)
127 	{
128 		if (event.bubbling) return;
129 
130 		auto transform = widget.getOrCreate!WidgetTransform;
131 		auto data = widget.get!TextEditorViewportData;
132 		auto editor = data.editor;
133 		auto settings = data.settings;
134 		data.update(widget.ctx, transform.size);
135 		Selection sel = data.editor.selection;
136 
137 		auto from = editor.lineInfo(data.firstVisibleLine).startOffset;
138 		auto to = editor.lineInfo(data.lastVisibleLine).endOffset;
139 		auto renderedText = editor[from..to].byItem;
140 
141 		MonoTime startTime = MonoTime.currTime;
142 
143 		auto mesherParams = event.renderQueue.defaultText();
144 		mesherParams.scissors = irect(transform.absPos, transform.size);
145 		mesherParams.scale = settings.fontScale;
146 		mesherParams.color = settings.color;
147 		mesherParams.font = cast(FontRef)settings.font;
148 		mesherParams.depth = event.depth+1;
149 		mesherParams.origin = transform.absPos;
150 		mesherParams.monospaced = settings.monospaced;
151 
152 		mesherParams.meshText(renderedText);
153 
154 		ivec2 glyphSize = settings.scaledGlyphSize;
155 
156 		// draw selection
157 		{
158 			enum selCol = rgb(180, 230, 255);
159 
160 			Selection normSel = sel.normalized;
161 			int firstSelectedLine = normSel.start.line;
162 			int lastSelectedLine = normSel.end.line;
163 			int firstVisibleSelectedLine = max(firstSelectedLine, data.firstVisibleLine);
164 			int lastVisibleSelectedLine = min(lastSelectedLine, data.lastVisibleLine);
165 
166 			foreach(line; firstVisibleSelectedLine..lastVisibleSelectedLine+1)
167 			{
168 				size_t lineStart = 0;
169 				size_t lineEnd;
170 
171 				auto lineInfo = editor.lineInfo(line);
172 
173 				if (line == firstSelectedLine) lineStart = normSel.start.byteOffset;
174 				else lineStart = lineInfo.startOffset;
175 
176 				if (line == lastSelectedLine) lineEnd = normSel.end.byteOffset;
177 				else lineEnd = lineInfo.endOffset;
178 
179 				int viewportTopOffset = line - data.firstVisibleLine;
180 
181 
182 				int selStartX = textWidth(settings, editor[lineInfo.startOffset..lineStart].byItem);
183 				int selEndX = textWidth(settings, editor[lineInfo.startOffset..lineEnd].byItem);
184 
185 				if (line != lastSelectedLine) selEndX += glyphSize.x; // make newline visible
186 
187 				vec2 size = vec2(selEndX - selStartX, glyphSize.y);
188 				vec2 pos = vec2(transform.absPos) + vec2(selStartX, viewportTopOffset*glyphSize.y);
189 
190 				event.renderQueue.drawRectFill(pos, size, event.depth, selCol);
191 			}
192 		}
193 
194 		// draw cursor
195 		if (widget.ctx.focusedWidget == widget)
196 		{
197 			auto sinceBlinkStart = MonoTime.currTime - data.blinkStart;
198 			if (sinceBlinkStart.total!"msecs" % 1000 < 500)
199 			{
200 				int viewportTopOffset = sel.end.line - data.firstVisibleLine;
201 				auto lineInfo = editor.lineInfo(sel.end.line);
202 				int cursorX = textWidth(settings, editor[lineInfo.startOffset..sel.end.byteOffset].byItem);
203 				vec2 pos = vec2(transform.absPos) + vec2(cursorX, viewportTopOffset*glyphSize.y);
204 				vec2 size = vec2(1, glyphSize.y);
205 				event.renderQueue.drawRectFill(pos, size, event.depth+1, settings.color);
206 			}
207 		}
208 
209 		event.depth += 2;
210 
211 		widget.ctx.debugText.putfln("Append glyphs: %sus", (MonoTime.currTime - startTime).total!"usecs");
212 		widget.ctx.debugText.putfln("Lines: %s", editor.numLines);
213 		//widget.ctx.debugText.putfln("Size: %s", data.editor.textSizeInGlyphs);
214 		widget.ctx.debugText.putfln("sel start: %s", sel.start);
215 		widget.ctx.debugText.putfln("sel end: %s", sel.end);
216 	}
217 
218 	void keyPressed(WidgetProxy widget, ref KeyPressEvent event)
219 	{
220 		auto data = widget.get!TextEditorViewportData;
221 		data.resetBlinkTimer();
222 		auto command = keyPressToCommand(event);
223 		data.editor.onCommand(command);
224 
225 		if (event.keyCode == KeyCode.KEY_M && event.control)
226 			data.settings.monospaced = !data.settings.monospaced;
227 	}
228 
229 	void charTyped(WidgetProxy widget, ref CharEnterEvent event)
230 	{
231 		import std.utf : encode;
232 		import std.typecons : Yes;
233 		auto data = widget.get!TextEditorViewportData;
234 		char[4] buf;
235 		auto numBytes = encode!(Yes.useReplacementDchar)(buf, event.character);
236 		data.editor.onCommand(EditorCommand(EditorCommandType.input, 0, buf[0..numBytes]));
237 		//data.editor.replaceSelection(buf[0..numBytes]);
238 		data.resetBlinkTimer();
239 	}
240 
241 	void pointerPressed(WidgetProxy widget, ref PointerPressEvent event)
242 	{
243 		if (event.button == PointerButton.PB_LEFT)
244 		{
245 			auto transform = widget.getOrCreate!WidgetTransform;
246 			auto data = widget.get!TextEditorViewportData;
247 
248 			Cursor cursorPos = calcCursorPos(event.pointerPosition, transform.absPos, data);
249 
250 			data.editor.selection.end = cursorPos;
251 
252 			bool extendSelection = event.shift;
253 			if (!extendSelection)
254 				data.editor.selection.start = cursorPos;
255 		}
256 		event.handled = true;
257 	}
258 
259 	void pointerReleased(WidgetProxy widget, ref PointerReleaseEvent event)
260 	{
261 		event.handled = true;
262 	}
263 
264 	void pointerMoved(WidgetProxy widget, ref PointerMoveEvent event)
265 	{
266 		if (widget.ctx.state.pressedWidget == widget)
267 		{
268 			auto transform = widget.getOrCreate!WidgetTransform;
269 			auto data = widget.get!TextEditorViewportData;
270 			data.editor.selection.end = calcCursorPos(event.newPointerPos, transform.absPos, data);
271 		}
272 		event.handled = true;
273 	}
274 
275 	Cursor calcCursorPos(ivec2 absPointerPos, ivec2 absPos, TextEditorViewportData* data)
276 	{
277 		ivec2 viewportPointerPos = absPointerPos - absPos;
278 		ivec2 canvasPointerPos = viewportPointerPos + data.textPos;
279 		return calcCursorPos(canvasPointerPos, data);
280 	}
281 
282 	Cursor calcCursorPos(ivec2 canvasPointerPos, TextEditorViewportData* data)
283 	{
284 		//if (data.editor.textData.length == 0) return Cursor();
285 		ivec2 glyphSize = data.settings.scaledGlyphSize;
286 
287 		int cursorLine = clamp(canvasPointerPos.y / glyphSize.y, 0, data.editor.lastLine);
288 
289 		auto lineInfo = data.editor.lineInfo(cursorLine);
290 		auto from = lineInfo.startOffset;
291 		auto to = lineInfo.endOffset;
292 		auto text = data.editor[from..to].byItem; // text without newline
293 
294 		auto range = glyphWidthRange(data.settings, text);
295 		foreach(int x, int width; range)
296 		{
297 			int glyphCenter = x + width/2;
298 			if (canvasPointerPos.x < glyphCenter) break;
299 		}
300 		auto cursorBytes = lineInfo.startOffset + range.byteOffset;
301 		return Cursor(cursorBytes, cursorLine);
302 	}
303 }
304 
305 int textWidth(T)(TextViewSettingsRef settings, T text)
306 {
307 	auto range = glyphWidthRange(settings, text);
308 	foreach(b, c; range) {}
309 	return range.x;
310 }
311 
312 
313 auto glyphWidthRange(R)(TextViewSettingsRef settings, R text)
314 {
315 	return GlyphWidthRange!R(settings, text);
316 }
317 
318 struct GlyphWidthRange(R)
319 {
320 	import std.utf : decodeFront;
321 	import std.typecons : Yes;
322 	TextViewSettingsRef settings;
323 	R input;
324 	int x;
325 	size_t byteOffset;
326 
327 	int opApply(scope int delegate(int x, int width) del)
328 	{
329 		int glyphW = settings.scaledGlyphW;
330 		x = 0;
331 		byteOffset = 0;
332 
333 		auto initialBytes = input.length;
334 		if (settings.monospaced)
335 		{
336 			int column;
337 
338 			while(!input.empty)
339 			{
340 				dchar codePoint = decodeFront!(Yes.useReplacementDchar)(input);
341 				int width;
342 				if (codePoint == '\t')
343 				{
344 					int tabGlyphs = tabWidth(settings.tabSize, column);
345 					width = tabGlyphs * glyphW;
346 					column += tabGlyphs;
347 				}
348 				else
349 				{
350 					width = glyphW;
351 					++column;
352 				}
353 
354 				if (auto ret = del(x, width)) return ret;
355 
356 				byteOffset = initialBytes - input.length;
357 				x += width;
358 			}
359 		}
360 		else
361 		{
362 			while(!input.empty)
363 			{
364 				dchar codePoint = decodeFront!(Yes.useReplacementDchar)(input);
365 				int width;
366 				if (codePoint == '\t')
367 				{
368 					int tabPixels = tabWidth(settings.tabSize * glyphW, x);
369 					width = tabPixels;
370 				}
371 				else
372 				{
373 					const Glyph* glyph = settings.font.getGlyph(codePoint);
374 					width = glyph.metrics.advanceX;
375 				}
376 
377 				if (auto ret = del(x, width)) return ret;
378 
379 				byteOffset = initialBytes - input.length;
380 				x += width;
381 			}
382 		}
383 		byteOffset = initialBytes - input.length;
384 		return 0;
385 	}
386 }
387 
388 EditorCommand keyPressToCommand(ref KeyPressEvent event)
389 {
390 	alias ComType = EditorCommandType;
391 
392 	EditorCommand moveCommand(MoveCommand com, bool extendSelection)
393 	{
394 		return EditorCommand(cast(EditorCommandType)(com + EditorCommandType.cur_move_first), extendSelection);
395 	}
396 
397 	switch(event.keyCode) with(KeyCode)
398 	{
399 	case KEY_LEFT:  return moveCommand(MoveCommand.move_left_char, event.shift);
400 	case KEY_RIGHT: return moveCommand(MoveCommand.move_right_char, event.shift);
401 	case KEY_UP:    return moveCommand(MoveCommand.move_up_line, event.shift);
402 	case KEY_DOWN:  return moveCommand(MoveCommand.move_down_line, event.shift);
403 	case KEY_HOME:  return moveCommand(MoveCommand.move_to_bol, event.shift);
404 	case KEY_END:   return moveCommand(MoveCommand.move_to_eol, event.shift);
405 	case KEY_TAB:   return EditorCommand(ComType.insert_tab);
406 	case KEY_ENTER: case KEY_KP_ENTER: return EditorCommand(ComType.insert_eol);
407 	case KEY_BACKSPACE:
408 		if (event.control)
409 		{
410 			if (event.shift) return EditorCommand(ComType.delete_left_line);
411 			else return EditorCommand(ComType.delete_left_word);
412 		}
413 		else return EditorCommand(ComType.delete_left_char);
414 	case KEY_DELETE:
415 		if (event.control)
416 		{
417 			if (event.shift) return EditorCommand(ComType.delete_right_line);
418 			else return EditorCommand(ComType.delete_right_word);
419 		}
420 		else return EditorCommand(ComType.delete_right_char);
421 	case KEY_A: if (event.control) return EditorCommand(ComType.select_all); break;
422 	case KEY_C: if (event.control) return EditorCommand(ComType.copy); break;
423 	case KEY_X: if (event.control) return EditorCommand(ComType.cut); break;
424 	case KEY_V: if (event.control) return EditorCommand(ComType.paste); break;
425 	case KEY_Z:
426 		if (event.control)
427 		{
428 			if (event.shift) return EditorCommand(ComType.redo);
429 			else return EditorCommand(ComType.undo);
430 		}
431 		break;
432 	default: break;
433 	}
434 	return EditorCommand(ComType.none);
435 }
436 
437 @Component("gui.TextEditorLineNumbersData", Replication.none)
438 struct TextEditorLineNumbersData
439 {
440 	// component connections
441 	TextModel editor;
442 	TextViewSettingsRef settings;
443 	WidgetId viewport;
444 
445 	// vars
446 	enum leftSpacing = 1;
447 	enum rightSpacing = 2;
448 
449 	int widthInGlyphs()
450 	{
451 		return numDigitsInNumber(editor.numLines) + leftSpacing + rightSpacing;
452 	}
453 
454 	int widthInPixels(int _widthInGlyphs)
455 	{
456 		return cast(int)(_widthInGlyphs * settings.scaledGlyphW);
457 	}
458 }
459 
460 struct TextEditorLineNumbersLogic
461 {
462 	static:
463 	WidgetProxy create(
464 		WidgetProxy parent,
465 		TextModel editor,
466 		TextViewSettingsRef settings)
467 	{
468 		return parent.createChild(
469 			TextEditorLineNumbersData(editor, settings),
470 			WidgetEvents(&drawWidget, &measure));
471 	}
472 
473 	void setViewport(WidgetProxy widget, WidgetProxy viewport)
474 	{
475 		auto data = widget.get!TextEditorLineNumbersData;
476 		data.viewport = viewport;
477 	}
478 
479 	void drawWidget(WidgetProxy widget, ref DrawEvent event)
480 	{
481 		if (event.bubbling) return;
482 
483 		auto transform = widget.getOrCreate!WidgetTransform;
484 		auto data = widget.get!TextEditorLineNumbersData;
485 		auto viewportData = widget.ctx.get!TextEditorViewportData(data.viewport);
486 
487 		int widthInGlyphs = data.widthInGlyphs;
488 
489 		auto mesherParams = event.renderQueue.defaultText();
490 		mesherParams.scale = data.settings.fontScale;
491 		mesherParams.origin = transform.absPos;
492 		mesherParams.monospaced = true;
493 
494 		foreach (line; viewportData.firstVisibleLine..viewportData.lastVisibleLine+1)
495 		{
496 			size_t lineNumber = line+1;
497 			size_t digits = numDigitsInNumber(lineNumber);
498 			size_t spacing = widthInGlyphs - (digits + data.rightSpacing);
499 			foreach (_; 0..spacing)
500 				mesherParams.meshText(" ");
501 			mesherParams.meshTextf("%s\n", lineNumber);
502 		}
503 
504 		widget.ctx.debugText.putfln("Line num w: %s", widthInGlyphs);
505 	}
506 
507 	void measure(WidgetProxy widget, ref MeasureEvent event)
508 	{
509 		auto transform = widget.getOrCreate!WidgetTransform;
510 		auto data = widget.get!TextEditorLineNumbersData;
511 		int widthInGlyphs = data.widthInGlyphs;
512 		int widthInPixels = data.widthInPixels(widthInGlyphs);
513 		transform.measuredSize = ivec2(widthInPixels, 0);
514 	}
515 }
516 /*
517 struct TextEditorMinimap
518 {
519 	// component connections
520 	TextEditorCRef document;
521 	TextViewSettingsRef settings;
522 	TextEditorViewportConstRef viewport;
523 
524 	enum glyphSize = ivec2(1, 2);
525 
526 	// vars
527 	ivec2 position;
528 	ivec2 size;
529 
530 	int glyphsX;
531 
532 	// private
533 	private Bitmap image;
534 
535 	void update(ref LineBuffer debugText)
536 	{
537 
538 	}
539 
540 	void render(RenderQueue renderQueue, ref LineBuffer debugText)
541 	{
542 		//lvec2 textSizeInPixels = document.textSizeInGlyphs * settings.scaledGlyphSize;
543 		//long scrollPercent = viewport.textPos.y / textSizeInPixels.y;
544 
545 		//long minimapAreaWidth = viewport.size.x / settings.scaledGlyphW;
546 
547 		//vec2 minimapPos = position + vec2(viewport.size.x - minimapAreaWidth, 0);
548 		//vec2 size = vec2(minimapAreaWidth, viewport.size.y);
549 		renderQueue.drawRectFill(vec2(position), vec2(size), 0, Color4ub(200, 200, 200, 255));
550 	}
551 }
552 */