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.lineedit;
7 
8 import core.time : MonoTime;
9 import std.format : formattedWrite;
10 import voxelman.container.gapbuffer;
11 import voxelman.log;
12 import voxelman.gui;
13 import voxelman.math;
14 import voxelman.graphics;
15 import voxelman.container.chunkedbuffer;
16 import voxelman.gui.textedit.cursor;
17 import voxelman.gui.textedit.textmodel;
18 import voxelman.gui.textedit.linebuffer;
19 import voxelman.gui.textedit.texteditor;
20 import voxelman.gui.textedit.texteditorview;
21 
22 @Component("gui.LineEditData", Replication.none)
23 struct LineEditData
24 {
25 	TextViewSettings settings;
26 	void delegate(string) enterHandler;
27 
28 	GapBuffer!char textData;
29 	Selection selection;
30 	MonoTime blinkStart;
31 }
32 
33 struct LineEditLogic
34 {
35 	LineEditData* data;
36 	GuiContext ctx;
37 
38 	alias data this;
39 
40 	void resetBlinkTimer()
41 	{
42 		blinkStart = MonoTime.currTime;
43 	}
44 
45 	void moveSelectionCursor(MoveCommand com, bool extendSelection)
46 	{
47 		selection.end = moveCursor(selection.end, com);
48 		if (!extendSelection) selection.start = selection.end;
49 	}
50 
51 	void copySelection()
52 	{
53 		auto sel = getSelectionForCopyCut();
54 		copyText(sel.start.byteOffset, sel.end.byteOffset);
55 	}
56 
57 	void copyText(size_t from, size_t to)
58 	{
59 		auto copiedText = textData[from..to].toChunkedRange.byItem;
60 		ctx.clipboard = copiedText;
61 	}
62 
63 	size_t length() { return textData.length; }
64 	alias opDollar = length;
65 
66 	ChunkedRange!char opSlice(size_t from, size_t to)
67 	{
68 		return textData[from..to].toChunkedRange;
69 	}
70 
71 	Selection getSelectionForCopyCut()
72 	{
73 		return selection.normalized;
74 	}
75 
76 	private void cutSelection()
77 	{
78 		auto sel = getSelectionForCopyCut();
79 		copyText(sel.start.byteOffset, sel.end.byteOffset);
80 		removeAndUpdateSelection(sel);
81 	}
82 
83 	void replaceSelection(const(char)[] str)
84 	{
85 		removeAndUpdateSelection(selection);
86 		selection = emptySelection(insertText(selection.end, str));
87 	}
88 
89 	/// Returns pos after inserted text
90 	Cursor insertText(Cursor cur, const(char)[] str)
91 	{
92 		if (str.length == 0) return cur;
93 
94 		//auto pieceUndo =
95 		textData.putAt(cur.byteOffset, str);
96 		//int insertedLines = lines.onPaste(cur, str);
97 		auto afterInsertedText = Cursor(cur.byteOffset + str.length, cur.line);
98 		//auto undoItem = UndoItem(pieceUndo, cur, afterInsertedText, UndoCommand.undoInsert, selection);
99 		//putUndo(undoItem);
100 		return afterInsertedText;
101 	}
102 
103 	void removeAndUpdateSelection(Selection sel)
104 	{
105 		Selection normSel = sel.normalized;
106 		removeText(normSel);
107 		// position cursor at the start of removed text
108 		selection = emptySelection(normSel.start);
109 	}
110 
111 	void removeText(Selection sel)
112 	{
113 		removeText(sel.start, sel.end);
114 	}
115 
116 	void removeText(Cursor from, Cursor to)
117 	{
118 		if (from == to) return;
119 		assert(from.byteOffset < to.byteOffset, "from.byteOffset < to.byteOffset");
120 		//auto pieceUndo =
121 		infof("remove %s %s", from.byteOffset, to.byteOffset-from.byteOffset);
122 		textData.remove(from.byteOffset, to.byteOffset-from.byteOffset);
123 		//auto undoItem = UndoItem(pieceUndo, from, to, UndoCommand.undoRemove, selection);
124 		//putUndo(undoItem);
125 		//lines.onRemove(from, to);
126 	}
127 
128 	void onCommand(EditorCommand com)
129 	{
130 		switch(com.type)
131 		{
132 			case EditorCommandType.insert_eol:
133 				if (enterHandler) enterHandler(cast(string)textData.getContinuousSlice(0, textData.length));
134 				break;
135 			case EditorCommandType.insert_tab:
136 				replaceSelection("\t");
137 				break;
138 			case EditorCommandType.delete_left_char:
139 				if (selection.empty)
140 				{
141 					auto leftCur = moveCursor(selection.end, MoveCommand.move_left_char);
142 					removeAndUpdateSelection(Selection(leftCur, selection.end));
143 				}
144 				else removeAndUpdateSelection(selection);
145 				break;
146 			case EditorCommandType.delete_right_char:
147 				if (selection.empty)
148 				{
149 					auto rightCur = moveCursor(selection.end, MoveCommand.move_right_char);
150 					removeAndUpdateSelection(Selection(selection.end, rightCur));
151 				}
152 				else removeAndUpdateSelection(selection);
153 				break;
154 			case EditorCommandType.delete_right_word: break;
155 			case EditorCommandType.delete_right_line: break;
156 			case EditorCommandType.cut: cutSelection(); break;
157 			case EditorCommandType.copy: copySelection(); break;
158 			case EditorCommandType.paste: replaceSelection(ctx.clipboard); break;
159 			case EditorCommandType.select_all:
160 				selection = Selection(Cursor(0,0), Cursor(length, 0));
161 				break;
162 			case EditorCommandType.undo:
163 				//undo();
164 				break;
165 			case EditorCommandType.redo:
166 				//redo();
167 				break;
168 			case EditorCommandType.cur_move_first: .. case EditorCommandType.cur_move_last:
169 				selection.end = moveCursor(selection.end, com.toMoveCommand);
170 				if (!com.extendSelection) selection.start = selection.end;
171 				break;
172 			default: break;
173 		}
174 	}
175 	import std.utf : stride, strideBack;
176 
177 	uint strideAt(size_t offset)
178 	{
179 		auto str = textData[offset..$];
180 		return stride(str);
181 	}
182 
183 	size_t nextOffset(size_t offset)
184 	{
185 		return offset + strideAt(offset);
186 	}
187 
188 	size_t prevOffset(size_t offset)
189 	{
190 		return offset - strideBack(textData[0u..offset]);
191 	}
192 
193 	Cursor moveCursor(Cursor cur, MoveCommand com)
194 	{
195 		switch(com) with(MoveCommand) {
196 			case move_right_char:
197 				if (cur.byteOffset < textData.length) {
198 					auto newOffset = nextOffset(cur.byteOffset);
199 					cur.byteOffset = newOffset;
200 				}
201 				break;
202 			case move_left_char:
203 				if (cur.byteOffset > 0) {
204 					auto newOffset = prevOffset(cur.byteOffset);
205 					cur.byteOffset = newOffset;
206 				}
207 				break;
208 			case move_to_bol: cur.byteOffset = 0; break;
209 			case move_to_eol: cur.byteOffset = textData.length; break;
210 			default: break;
211 		}
212 		return cur;
213 	}
214 
215 	void clear()
216 	{
217 		textData.clear();
218 		selection = Selection();
219 	}
220 
221 	Cursor calcCursorPos(ivec2 absPointerPos, ivec2 absPos)
222 	{
223 		ivec2 viewportPointerPos = absPointerPos - absPos;
224 		return calcCursorPos(viewportPointerPos);
225 	}
226 
227 	Cursor calcCursorPos(ivec2 canvasPointerPos)
228 	{
229 		//if (data.editor.textData.length == 0) return Cursor();
230 		ivec2 glyphSize = settings.scaledGlyphSize;
231 
232 		auto range = glyphWidthRange(&settings, textData[]);
233 		foreach(int x, int width; range)
234 		{
235 			int glyphCenter = x + width/2;
236 			if (canvasPointerPos.x < glyphCenter) break;
237 		}
238 		auto cursorBytes = range.byteOffset;
239 		return Cursor(cursorBytes);
240 	}
241 }
242 
243 struct LineEdit
244 {
245 	static:
246 
247 	enum vmargin = 2;
248 
249 	WidgetProxy create(WidgetProxy parent, void delegate(string) enterHandler = null)
250 	{
251 		TextViewSettings settings;
252 		settings.font = parent.ctx.style.font;
253 		settings.color = parent.ctx.style.color;
254 		return parent.createChild(
255 			LineEditData(settings, enterHandler),
256 			WidgetEvents(
257 				&enterWidget, &drawWidget,
258 				&pointerPressed, &pointerReleased, &pointerMoved,
259 				&keyPressed, &charTyped),
260 			WidgetIsFocusable()
261 		).minSize(0, settings.font.metrics.height + vmargin*2);
262 	}
263 
264 	void enterWidget(WidgetProxy widget, ref PointerEnterEvent event)
265 	{
266 		widget.ctx.cursorIcon = CursorIcon.ibeam;
267 	}
268 
269 	void drawWidget(WidgetProxy widget, ref DrawEvent event)
270 	{
271 		if (event.bubbling) return;
272 
273 		auto transform = widget.getOrCreate!WidgetTransform;
274 		auto data = widget.get!LineEditData;
275 		Selection sel = data.selection;
276 		ivec2 glyphSize = data.settings.scaledGlyphSize;
277 
278 		ivec2 textPos = transform.absPos + ivec2(0, vmargin);
279 
280 		auto mesherParams = event.renderQueue.defaultText();
281 		mesherParams.scissors = irect(transform.absPos, transform.size);
282 		mesherParams.color = data.settings.color;
283 		mesherParams.scale = data.settings.fontScale;
284 		mesherParams.font = cast(FontRef)data.settings.font;
285 		mesherParams.depth = event.depth+1;
286 		mesherParams.origin = textPos;
287 		mesherParams.monospaced = data.settings.monospaced;
288 
289 		mesherParams.meshText(data.textData[]);
290 
291 		// draw selection
292 		{
293 			Selection normSel = sel.normalized;
294 			enum selCol = Color4ub(180, 230, 255, 128);
295 			int selStartX = textWidth(&data.settings, data.textData[0..normSel.start.byteOffset]);
296 			int selEndX = textWidth(&data.settings, data.textData[0..normSel.end.byteOffset]);
297 			vec2 size = vec2(selEndX - selStartX, glyphSize.y);
298 			vec2 pos = vec2(textPos) + vec2(selStartX, 0);
299 			event.renderQueue.drawRectFill(pos, size, event.depth, selCol);
300 		}
301 
302 		// draw cursor
303 		if (widget.ctx.focusedWidget == widget)
304 		{
305 			auto sinceBlinkStart = MonoTime.currTime - data.blinkStart;
306 			if (sinceBlinkStart.total!"msecs" % 1000 < 500)
307 			{
308 				int cursorX = textWidth(&data.settings, data.textData[0..sel.end.byteOffset]);
309 				vec2 pos = vec2(textPos) + vec2(cursorX, 0);
310 				vec2 size = vec2(1, glyphSize.y);
311 				event.renderQueue.drawRectFill(pos, size, event.depth+1, data.settings.color);
312 			}
313 		}
314 
315 		event.depth += 2;
316 	}
317 
318 	void keyPressed(WidgetProxy widget, ref KeyPressEvent event)
319 	{
320 		auto data = widget.get!LineEditData;
321 		auto logic = LineEditLogic(data, widget.ctx);
322 
323 		logic.resetBlinkTimer();
324 
325 		auto command = keyPressToCommand(event);
326 		logic.onCommand(command);
327 	}
328 
329 	void charTyped(WidgetProxy widget, ref CharEnterEvent event)
330 	{
331 		import std.utf : encode;
332 		import std.typecons : Yes;
333 		auto data = widget.get!LineEditData;
334 		auto logic = LineEditLogic(data, widget.ctx);
335 		char[4] buf;
336 		auto numBytes = encode!(Yes.useReplacementDchar)(buf, event.character);
337 		logic.replaceSelection(buf[0..numBytes]);
338 		logic.resetBlinkTimer();
339 	}
340 
341 	void clear(WidgetProxy widget)
342 	{
343 		auto data = widget.get!LineEditData;
344 		auto logic = LineEditLogic(data, widget.ctx);
345 		logic.clear;
346 	}
347 
348 	void pointerPressed(WidgetProxy widget, ref PointerPressEvent event)
349 	{
350 		if (event.button == PointerButton.PB_LEFT)
351 		{
352 			auto transform = widget.getOrCreate!WidgetTransform;
353 			auto data = widget.get!LineEditData;
354 			auto logic = LineEditLogic(data, widget.ctx);
355 			Cursor cursorPos = logic.calcCursorPos(event.pointerPosition, transform.absPos);
356 
357 			data.selection.end = cursorPos;
358 
359 			bool extendSelection = event.shift;
360 			if (!extendSelection)
361 				data.selection.start = cursorPos;
362 		}
363 		event.handled = true;
364 	}
365 
366 	void pointerReleased(WidgetProxy widget, ref PointerReleaseEvent event)
367 	{
368 		event.handled = true;
369 	}
370 
371 	void setEnterHandler(WidgetProxy widget, void delegate(string) enterHandler)
372 	{
373 		auto data = widget.get!LineEditData;
374 		data.enterHandler = enterHandler;
375 	}
376 
377 	void pointerMoved(WidgetProxy widget, ref PointerMoveEvent event)
378 	{
379 		if (widget.ctx.state.pressedWidget == widget)
380 		{
381 			auto transform = widget.getOrCreate!WidgetTransform;
382 			auto data = widget.get!LineEditData;
383 			auto logic = LineEditLogic(data, widget.ctx);
384 			data.selection.end = logic.calcCursorPos(event.newPointerPos, transform.absPos);
385 		}
386 		event.handled = true;
387 	}
388 }