1 module rpui.widgets.text_input.edit_component;
2 
3 import std.algorithm.comparison;
4 import std.algorithm.searching;
5 import std..string;
6 import std.math;
7 import std.conv;
8 
9 import rpui.input;
10 import rpui.primitives;
11 import rpui.math;
12 import rpui.widgets.text_input.widget;
13 import rpui.widgets.text_input.select_component;
14 import rpui.widgets.text_input.carriage;
15 import rpui.widgets.text_input.transforms_system;
16 import rpui.events;
17 import rpui.theme;
18 
19 struct EditComponent {
20     const commonSplitChars = " ,.;:?'!|/\\~*+-=(){}<>[]#%&^@$№`\""d;
21     const japanesePunctuation = "\u3000{}()[]【】、,…‥。〽「」『』〝〟〜:!?"d;
22     const splitChars = commonSplitChars ~ japanesePunctuation;
23 
24     utf32string text;
25     Carriage carriage;
26     float scrollDelta = 0.0f;
27     vec2 absoulteTextPosition;
28 
29     SelectRegion selectRegion;
30     TextInput textInput;
31     TextInputTransformsSystem transformsSystem;
32     private utf32string lastText;
33 
34     void attach(TextInput textInput, TextInputTransformsSystem transformsSystem) {
35         this.textInput = textInput;
36         this.transformsSystem = transformsSystem;
37         this.selectRegion.attach(&this);
38     }
39 
40     void reset() {
41         selectRegion.stopSelection();
42         carriage.reset();
43     }
44 
45     void onKeyPressed(in KeyPressedEvent event) {
46         carriage.timer = 0;
47         carriage.visible = true;
48 
49         if (isKeyPressed(KeyCode.Shift) && !selectRegion.startedSelection)
50             selectRegion.startSelection(carriage.pos);
51 
52         switch (event.key) {
53             case KeyCode.Left:
54                 if (isKeyPressed(KeyCode.Ctrl)) {
55                     carriage.setCarriagePos(carriage.navigateCarriage(-1));
56                 } else {
57                     carriage.moveCarriage(-1);
58                 }
59 
60                 break;
61 
62             case KeyCode.Right:
63                 if (isKeyPressed(KeyCode.Ctrl)) {
64                     carriage.setCarriagePos(carriage.navigateCarriage(1));
65                 } else {
66                     carriage.moveCarriage(1);
67                 }
68 
69                 break;
70 
71             case KeyCode.Home:
72                 carriage.setCarriagePos(0);
73                 break;
74 
75             case KeyCode.End:
76                 carriage.setCarriagePos(cast(int) text.length);
77                 break;
78 
79             case KeyCode.Delete:
80                 if (selectRegion.textIsSelected()) {
81                     removeSelectedRegion();
82                 } else {
83                     if (isKeyPressed(KeyCode.Ctrl)) {
84                         const end = carriage.navigateCarriage(1);
85                         removeRegion(carriage.pos, end);
86                     } else {
87                         removeRegion(carriage.pos, carriage.pos+1);
88                     }
89                 }
90 
91                 unselect();
92                 break;
93 
94             case KeyCode.BackSpace:
95                 if (selectRegion.textIsSelected()) {
96                     removeSelectedRegion();
97                 } else {
98                     if (isKeyPressed(KeyCode.Ctrl)) {
99                         const start = carriage.navigateCarriage(-1);
100                         removeRegion(start, carriage.pos);
101                     } else {
102                         removeRegion(carriage.pos-1, carriage.pos);
103                     }
104                 }
105 
106                 unselect();
107                 break;
108 
109             default:
110                 // Nothing
111         }
112     }
113 
114     void removeSelectedRegion() {
115         removeRegion(
116             selectRegion.start,
117             selectRegion.end
118         );
119     }
120 
121     void removeRegion(in int start, in int end) {
122         if (start < 0 || end > text.length)
123             return;
124 
125         const leftPart = text[0 .. start];
126         const rightPart = text[end .. text.length];
127 
128         text = leftPart ~ rightPart;
129         carriage.setCarriagePos(start);
130     }
131 
132     private bool isISOControlCharacter(in utf32char ch) {
133         // Control characters
134         // https://www.utf8-chartable.de/unicode-utf8-table.pl?utf8=0x
135         return (ch >= 0x00 && ch <= 0x1F) || (ch >= 0x7F && ch <= 0x9F);
136     }
137 
138     bool onTextEntered(in TextEnteredEvent event) {
139         return enterText(to!utf32string(event.key));
140     }
141 
142     bool enterText(in utf32string charToPut) {
143         carriage.timer = 0;
144         carriage.visible = true;
145 
146         foreach (ch; charToPut) {
147             if (isISOControlCharacter(ch))
148                 return false;
149         }
150 
151         // Splitting text to two parts by carriage position
152 
153         utf32string leftPart;
154         utf32string rightPart;
155         auto newCarriagePos = carriage.pos;
156 
157         if (!selectRegion.textIsSelected()) {
158             leftPart = text[0 .. carriage.pos];
159             rightPart = text[carriage.pos .. $];
160             newCarriagePos += charToPut.length;
161         }
162         else {
163             leftPart = text[0 .. selectRegion.start];
164             rightPart = text[selectRegion.end .. $];
165             newCarriagePos = selectRegion.start + cast(int) charToPut.length;
166         }
167 
168         const newText = leftPart ~ charToPut ~ rightPart;
169 
170         text = newText;
171         carriage.pos = newCarriagePos;
172 
173         selectRegion.stopSelection();
174         return true;
175     }
176 
177     void onMouseDown(in MouseDownEvent event) {
178         if (textInput.autoSelectOnFocus)
179             return;
180 
181         carriage.setCarriagePosFromMousePos(event.x, event.y);
182 
183         if (!isKeyPressed(KeyCode.Shift))
184             selectRegion.startSelection(carriage.pos);
185     }
186 
187     void onMouseMove(in MouseMoveEvent event) {
188         if (event.button != MouseButton.mouseLeft)
189             return;
190 
191         // if (!textInput.isNumberMode())
192         if (textInput.isFocused)
193             carriage.setCarriagePosFromMousePos(event.x, event.y);
194     }
195 
196     void onDblClick(in DblClickEvent event) {
197         const left = carriage.navigateCarriage(-1);
198         const right = carriage.navigateCarriage(1);
199 
200         selectRegion.start = left;
201         selectRegion.end = right;
202         carriage.pos = right;
203     }
204 
205     void onTripleClick(in TripleClickEvent event) {
206         selectAll();
207     }
208 
209     float getTextWidth() {
210         return textInput.measure.textWidth;
211     }
212 
213     float getTextRegionSize(in int start, in int end)
214         // in(start <= end)
215     {
216         if (start == end)
217             return 0.0f;
218 
219         return transformsSystem.getRegionTextWidth(start, end);
220     }
221 
222     vec2 getTextRegionOffset(in int charPos) {
223         const regionSize = getTextRegionSize(0, charPos);
224         const offset = vec2(
225             regionSize + scrollDelta,
226             textInput.measure.textTopMargin
227         );
228 
229         float alignOffset = 0;
230 
231         if (textInput.textAlign == Align.left) {
232             alignOffset = textInput.measure.textLeftMargin;
233         }
234         else if (textInput.textAlign == Align.right) {
235             alignOffset = -textInput.measure.textRightMargin;
236         }
237 
238         return vec2(alignOffset - textInput.measure.carriageBoundary, 0) +
239             textInput.measure.textRelativePosition + offset;
240     }
241 
242     void selectAll() {
243         selectRegion.start = 0;
244         selectRegion.end = cast(int) text.length;
245         selectRegion.startedSelection = true;
246         carriage.pos = selectRegion.end;
247     }
248 
249     void unselect() {
250         selectRegion.start = 0;
251         selectRegion.end = 0;
252         selectRegion.startedSelection = false;
253     }
254 
255     void onFocus() {
256         lastText = textInput.text;
257     }
258 
259     void onBlur() {
260         switch (textInput.inputType) {
261             case TextInput.InputType.integer:
262                 if (!isNumeric(textInput.text)) {
263                     textInput.text = lastText;
264                 } else {
265                     const value = textInput.text.to!float;
266                     textInput.text = round(value).to!utf32string;
267                 }
268 
269                 break;
270 
271             case TextInput.InputType.number:
272                 if (!isNumeric(textInput.text))
273                     textInput.text = lastText;
274 
275                 break;
276 
277             case TextInput.InputType.text:
278                 return;
279 
280             default:
281                 return;
282         }
283     }
284 }