@usman404/crowjs 1.0.2
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/CONTRIBUTING.md +1 -0
- package/Core/Component.js +426 -0
- package/Core/GUIEvent/GUIEvent.js +23 -0
- package/Core/GUIEvent/KeyboardEvent.js +14 -0
- package/Core/GUIEvent/MouseEvent.js +22 -0
- package/Core/Root.js +558 -0
- package/Frames/DummyFrame.js +185 -0
- package/Frames/Frame.js +531 -0
- package/Frames/FrameComponent.js +54 -0
- package/Frames/GridFrame.js +574 -0
- package/Frames/ScrollFrame.js +764 -0
- package/LICENSE +21 -0
- package/README.md +130 -0
- package/UIComponents/Input.js +78 -0
- package/UIComponents/Label.js +234 -0
- package/UIComponents/TextField.js +551 -0
- package/UIComponents/UIComponent.js +97 -0
- package/crowjs-01-01.png +0 -0
- package/index.html +15 -0
- package/package.json +23 -0
- package/sketch.js +65 -0
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
import { Input } from "./Input.js";
|
|
2
|
+
|
|
3
|
+
export class TextField extends Input{
|
|
4
|
+
/**
|
|
5
|
+
* Creates a text input field with advanced editing features
|
|
6
|
+
* @param {number} x - The x-coordinate
|
|
7
|
+
* @param {number} y - The y-coordinate
|
|
8
|
+
* @param {number} width - The width
|
|
9
|
+
* @param {number} height - The height
|
|
10
|
+
* @param {Object} options - Configuration options
|
|
11
|
+
* @param {string|null} options.id - Component ID
|
|
12
|
+
* @param {Component|null} options.parent - Parent component
|
|
13
|
+
* @param {string} options.backgroundColor - Background color
|
|
14
|
+
* @param {string} options.textColor - Text color
|
|
15
|
+
* @param {boolean} options.borderFlag - Show border
|
|
16
|
+
* @param {p5.Color} options.borderColor - Border color
|
|
17
|
+
* @param {number} options.borderWidth - Border width
|
|
18
|
+
* @param {number} options.cornerRadius - Corner radius
|
|
19
|
+
* @param {boolean} options.enableShadow - Enable shadow
|
|
20
|
+
* @param {string} options.shadowColor - Shadow color
|
|
21
|
+
* @param {number} options.shadowIntensity - Shadow opacity
|
|
22
|
+
* @param {number} options.shadowSpread - Shadow spread
|
|
23
|
+
* @param {number} options.shadowDetail - Shadow layers
|
|
24
|
+
* @param {string} options.placeholder - Placeholder text
|
|
25
|
+
* @param {string} options.text - Initial text
|
|
26
|
+
* @param {string} options.textAlign - Text alignment
|
|
27
|
+
* @param {number} options.padding - Internal padding
|
|
28
|
+
*/
|
|
29
|
+
constructor(x, y, width, height,
|
|
30
|
+
{
|
|
31
|
+
id=null,
|
|
32
|
+
parent=null,
|
|
33
|
+
backgroundColor='rgb(255, 255, 255)',
|
|
34
|
+
textColor='rgb(0, 0, 0)',
|
|
35
|
+
borderFlag = true,
|
|
36
|
+
borderColor = color(0),
|
|
37
|
+
borderWidth = 1,
|
|
38
|
+
cornerRadius = 0,
|
|
39
|
+
enableShadow=false,
|
|
40
|
+
shadowColor= 'rgb(0,0,0)',
|
|
41
|
+
shadowIntensity= 0.4,
|
|
42
|
+
shadowSpread= 3,
|
|
43
|
+
shadowDetail=5,
|
|
44
|
+
placeholder="",
|
|
45
|
+
text="",
|
|
46
|
+
textAlign = "left",
|
|
47
|
+
padding = 10,
|
|
48
|
+
}={}) {
|
|
49
|
+
super(x, y, width, height, backgroundColor, borderFlag, borderColor,
|
|
50
|
+
borderWidth, cornerRadius, enableShadow, shadowColor, shadowIntensity,
|
|
51
|
+
shadowSpread, shadowDetail, {parent: parent, type: "Input", id: id});
|
|
52
|
+
|
|
53
|
+
this.cursorPos = 0;
|
|
54
|
+
this.text = text;
|
|
55
|
+
this.textSize = 30;
|
|
56
|
+
this.displayXOffset = 0;
|
|
57
|
+
|
|
58
|
+
this.textAlign = textAlign;
|
|
59
|
+
this.padding = padding;
|
|
60
|
+
this.textColor = textColor;
|
|
61
|
+
this.placeholder = placeholder;
|
|
62
|
+
|
|
63
|
+
this.cursorVisible = true;
|
|
64
|
+
this.lastCursorToggle = millis();
|
|
65
|
+
this.cursorBlinkInterval = 500;
|
|
66
|
+
|
|
67
|
+
this.selectionStart = null;
|
|
68
|
+
this.selectionEnd = null;
|
|
69
|
+
this.isSelecting = false;
|
|
70
|
+
|
|
71
|
+
this.addEventListener("keyPress", (event)=>this.onKeyPress(event));
|
|
72
|
+
// this.addEventListener("click", (event)=>this.onMouseClick(event));
|
|
73
|
+
this.addEventListener("press", (event)=>this.onMousePress(event)); // NEW
|
|
74
|
+
this.addEventListener("drag", (event)=>this.onMouseDrag(event));
|
|
75
|
+
this.addEventListener("release", (event)=>this.onMouseRelease(event));
|
|
76
|
+
this.addEventListener("hover", (event)=>this.onMouseHover(event));
|
|
77
|
+
this.addEventListener("blur", (event)=>this.onBlur(event));
|
|
78
|
+
// this.addEventListener("focus", (event)=>this.onFocus(event));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Handles mouse hover events
|
|
83
|
+
* @param {MouseEvent} event - The hover event
|
|
84
|
+
*/
|
|
85
|
+
onMouseHover(event){
|
|
86
|
+
// event.stopPropagation();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Handles mouse release events
|
|
91
|
+
* @param {MouseEvent} event - The release event
|
|
92
|
+
*/
|
|
93
|
+
onMouseRelease(event) {
|
|
94
|
+
this.isSelecting = false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// onFocus(event){
|
|
98
|
+
// console.log("focus called!");
|
|
99
|
+
// }
|
|
100
|
+
|
|
101
|
+
onBlur(event){
|
|
102
|
+
this.isSelecting = false;
|
|
103
|
+
|
|
104
|
+
// Clear selection
|
|
105
|
+
this.selectionStart = null;
|
|
106
|
+
this.selectionEnd = null;
|
|
107
|
+
|
|
108
|
+
// Cursor usually hides after blur in textfields
|
|
109
|
+
this.cursorVisible = false;
|
|
110
|
+
|
|
111
|
+
console.log("blur called!");
|
|
112
|
+
|
|
113
|
+
event.stopPropagation();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
onMousePress(event) {
|
|
117
|
+
if (!this.isFocused) {
|
|
118
|
+
this.focus();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
this.isSelecting = true;
|
|
122
|
+
|
|
123
|
+
// Clamp X for initial cursor position
|
|
124
|
+
let x = event.x;
|
|
125
|
+
if (x <= this.x - 1000) x = this.x - 1000;
|
|
126
|
+
if (x >= this.x + this.width + 1000) x = this.x + this.width + 1000;
|
|
127
|
+
|
|
128
|
+
// Convert X → character index
|
|
129
|
+
let idx = this.getCursorIndexFromX(x);
|
|
130
|
+
|
|
131
|
+
// Set selection start and cursor immediately (start selecting instantly)
|
|
132
|
+
this.selectionStart = idx;
|
|
133
|
+
this.selectionEnd = idx;
|
|
134
|
+
this.cursorPos = idx;
|
|
135
|
+
|
|
136
|
+
// Ensure cursor visible & auto scroll
|
|
137
|
+
this.scrollCursorIntoView();
|
|
138
|
+
this.cursorVisible = true;
|
|
139
|
+
this.lastCursorToggle = millis();
|
|
140
|
+
|
|
141
|
+
event.stopPropagation();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
onMouseDrag(event) {
|
|
145
|
+
if (this.isSelecting && this.isFocused) {
|
|
146
|
+
|
|
147
|
+
let x = event.x;
|
|
148
|
+
if (x <= this.x - 1000) x = this.x - 1000;
|
|
149
|
+
if (x >= this.x + this.width + 1000) x = this.x + this.width + 1000;
|
|
150
|
+
|
|
151
|
+
let pos = this.getCursorIndexFromX(x);
|
|
152
|
+
|
|
153
|
+
this.selectionEnd = pos;
|
|
154
|
+
this.cursorPos = pos;
|
|
155
|
+
|
|
156
|
+
this.scrollCursorIntoView();
|
|
157
|
+
this.cursorVisible = true;
|
|
158
|
+
this.lastCursorToggle = millis();
|
|
159
|
+
|
|
160
|
+
event.stopPropagation();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
onKeyPress(event) {
|
|
165
|
+
// ADDED: if there's an active selection and user types or presses backspace/delete,
|
|
166
|
+
// we should remove the selection first (so typed char replaces selection)
|
|
167
|
+
const hasSelection = this.selectionStart !== null && this.selectionStart !== this.selectionEnd;
|
|
168
|
+
|
|
169
|
+
if (keyCode === LEFT_ARROW) {
|
|
170
|
+
if (keyIsDown(CONTROL)) {
|
|
171
|
+
this.jumpLeftByOneWord();
|
|
172
|
+
} else {
|
|
173
|
+
this.moveCursorLeft(1);
|
|
174
|
+
}
|
|
175
|
+
// collapse selection when using arrows (typical behavior)
|
|
176
|
+
this.selectionStart = this.selectionEnd = this.cursorPos;
|
|
177
|
+
} else if (keyCode === RIGHT_ARROW) {
|
|
178
|
+
if (keyIsDown(CONTROL)) {
|
|
179
|
+
this.jumpRightByOneWord();
|
|
180
|
+
} else {
|
|
181
|
+
this.moveCursorRight(1);
|
|
182
|
+
}
|
|
183
|
+
this.selectionStart = this.selectionEnd = this.cursorPos;
|
|
184
|
+
} else if (keyCode === BACKSPACE) {
|
|
185
|
+
if (hasSelection) {
|
|
186
|
+
// ADDED: delete selection (Backspace with selection deletes selection)
|
|
187
|
+
this.deleteSelectedText();
|
|
188
|
+
} else if (keyIsDown(CONTROL)) {
|
|
189
|
+
this.deleteOneWord();
|
|
190
|
+
} else {
|
|
191
|
+
this.deleteOneChar();
|
|
192
|
+
}
|
|
193
|
+
// collapse selection after deletion
|
|
194
|
+
this.selectionStart = this.selectionEnd = this.cursorPos;
|
|
195
|
+
} else if (key.length === 1) {
|
|
196
|
+
// ADDED: if a selection exists, remove it before insertion so typed char replaces selection
|
|
197
|
+
if (hasSelection) {
|
|
198
|
+
this.deleteSelectedText();
|
|
199
|
+
}
|
|
200
|
+
// Insert the character at cursor
|
|
201
|
+
this.text = this.text.slice(0, this.cursorPos) + key + this.text.slice(this.cursorPos);
|
|
202
|
+
this.moveCursorRight(1);
|
|
203
|
+
// collapse selection after insertion
|
|
204
|
+
this.selectionStart = this.selectionEnd = this.cursorPos;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
this.cursorVisible = true;
|
|
208
|
+
this.lastCursorToggle = millis();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Compute the X coordinate where the rendered text starts (accounts for alignment & displayXOffset)
|
|
214
|
+
* ADDED helper -- use this from click/drag/any cursor-from-x routines.
|
|
215
|
+
*/
|
|
216
|
+
computeTextStartX() {
|
|
217
|
+
// ADDED: measure full text width once
|
|
218
|
+
push();
|
|
219
|
+
textSize(this.textSize);
|
|
220
|
+
let fullWidth = textWidth(this.text || ""); // safe for empty string
|
|
221
|
+
pop();
|
|
222
|
+
|
|
223
|
+
if (this.textAlign === "left") {
|
|
224
|
+
// left aligned starts at left padding, minus the scroll offset
|
|
225
|
+
return this.x + this.padding - this.displayXOffset;
|
|
226
|
+
} else if (this.textAlign === "right") {
|
|
227
|
+
// right aligned: text ends at width - padding, so start is that minus fullWidth
|
|
228
|
+
return this.x + this.width - this.padding - fullWidth - this.displayXOffset;
|
|
229
|
+
} else { // center or anything else
|
|
230
|
+
return this.x + this.width / 2 - fullWidth / 2 - this.displayXOffset;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Convert absolute click X into cursor index (0..text.length).
|
|
236
|
+
* ADDED helper - replaces duplicated click/drag loop logic.
|
|
237
|
+
*/
|
|
238
|
+
getCursorIndexFromX(clickX) {
|
|
239
|
+
// Compute text start and relative click
|
|
240
|
+
let textStartX = this.computeTextStartX(); // ADDED use centralized function
|
|
241
|
+
let relativeX = clickX - textStartX;
|
|
242
|
+
|
|
243
|
+
// Early out: before start
|
|
244
|
+
if (relativeX <= 0) return 0;
|
|
245
|
+
|
|
246
|
+
// Measure and walk characters
|
|
247
|
+
push();
|
|
248
|
+
textSize(this.textSize);
|
|
249
|
+
let cumulativeWidth = 0;
|
|
250
|
+
let pos = 0;
|
|
251
|
+
const len = this.text.length;
|
|
252
|
+
for (let i = 0; i < len; i++) { // ADDED: loop < len (avoid charAt(len))
|
|
253
|
+
let ch = this.text.charAt(i);
|
|
254
|
+
let nextWidth = textWidth(ch);
|
|
255
|
+
// Midpoint rule: place cursor before the character if closer to left half
|
|
256
|
+
if (relativeX < cumulativeWidth + nextWidth / 2) {
|
|
257
|
+
pos = i;
|
|
258
|
+
pop();
|
|
259
|
+
return pos;
|
|
260
|
+
}
|
|
261
|
+
cumulativeWidth += nextWidth;
|
|
262
|
+
pos = i + 1; // cursor after this char
|
|
263
|
+
}
|
|
264
|
+
pop();
|
|
265
|
+
|
|
266
|
+
// If we got here, click is after all characters -> cursor at end
|
|
267
|
+
return len;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
deleteSelectedText(){
|
|
272
|
+
if (this.selectionStart !== null && this.selectionStart !== this.selectionEnd) {
|
|
273
|
+
let start = min(this.selectionStart, this.selectionEnd);
|
|
274
|
+
let end = max(this.selectionStart, this.selectionEnd);
|
|
275
|
+
|
|
276
|
+
this.text = this.text.slice(0, start) + this.text.slice(end);
|
|
277
|
+
this.cursorPos = start;
|
|
278
|
+
|
|
279
|
+
// ADDED: clear selection anchors and ensure they reflect cursor
|
|
280
|
+
this.selectionStart = this.selectionEnd = null;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Moves cursor left by one word (Ctrl+Left arrow)
|
|
286
|
+
*/
|
|
287
|
+
jumpLeftByOneWord(){
|
|
288
|
+
if (this.cursorPos > 0) {
|
|
289
|
+
let i = this.cursorPos - 1;
|
|
290
|
+
while (i > 0 && this.text[i] === ' ') i--;
|
|
291
|
+
while (i > 0 && this.text[i - 1] !== ' ') i--;
|
|
292
|
+
this.cursorPos = i;
|
|
293
|
+
|
|
294
|
+
this.scrollCursorIntoViewLeft();
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Moves cursor right by one word (Ctrl+Right arrow)
|
|
300
|
+
*/
|
|
301
|
+
jumpRightByOneWord(){
|
|
302
|
+
if (this.cursorPos < this.text.length) {
|
|
303
|
+
let i = this.cursorPos;
|
|
304
|
+
while (i < this.text.length && this.text[i] !== ' ') i++;
|
|
305
|
+
while (i < this.text.length && this.text[i] === ' ') i++;
|
|
306
|
+
this.cursorPos = i;
|
|
307
|
+
|
|
308
|
+
this.scrollCursorIntoViewRight();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Deletes one word to the left (Ctrl+Backspace)
|
|
314
|
+
*/
|
|
315
|
+
deleteOneWord(){
|
|
316
|
+
if (this.cursorPos > 0) {
|
|
317
|
+
let i = this.cursorPos - 1;
|
|
318
|
+
|
|
319
|
+
while (i > 0 && this.text[i] === ' ') {
|
|
320
|
+
i--;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
while (i > 0 && this.text[i - 1] !== ' ') {
|
|
324
|
+
i--;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
this.text = this.text.slice(0, i) + this.text.slice(this.cursorPos);
|
|
328
|
+
this.cursorPos = i;
|
|
329
|
+
|
|
330
|
+
this.scrollCursorIntoViewLeft();
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Deletes one character to the left (Backspace)
|
|
336
|
+
*/
|
|
337
|
+
deleteOneChar(){
|
|
338
|
+
if (this.cursorPos > 0) {
|
|
339
|
+
this.text = this.text.slice(0, this.cursorPos - 1) + this.text.slice(this.cursorPos);
|
|
340
|
+
this.moveCursorLeft(1);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Calculates the width of text between two positions
|
|
346
|
+
* @param {number} startPos - Starting character position
|
|
347
|
+
* @param {number} endPos - Ending character position
|
|
348
|
+
* @returns {number} The width in pixels
|
|
349
|
+
*/
|
|
350
|
+
findTextWidth(startPos, endPos){
|
|
351
|
+
startPos = constrain(startPos, 0, this.text.length);
|
|
352
|
+
endPos = constrain(endPos, 0, this.text.length);
|
|
353
|
+
|
|
354
|
+
push();
|
|
355
|
+
textSize(this.textSize);
|
|
356
|
+
let txt = this.text.slice(startPos, endPos);
|
|
357
|
+
let txtWidth = textWidth(txt);
|
|
358
|
+
pop();
|
|
359
|
+
return txtWidth;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Calculates the width of given text
|
|
364
|
+
* @param {string} text - The text to measure
|
|
365
|
+
* @returns {number} The width in pixels
|
|
366
|
+
*/
|
|
367
|
+
findTextWidthOfGivenText(text){
|
|
368
|
+
push();
|
|
369
|
+
textSize(this.textSize);
|
|
370
|
+
let txtWidth = textWidth(text);
|
|
371
|
+
pop();
|
|
372
|
+
return txtWidth;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Moves cursor right by specified number of characters
|
|
377
|
+
* @param {number} increment - Number of characters to move
|
|
378
|
+
*/
|
|
379
|
+
moveCursorRight(increment){
|
|
380
|
+
if (this.cursorPos < this.text.length) {
|
|
381
|
+
this.cursorPos += increment;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
this.scrollCursorIntoViewRight();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Moves cursor left by specified number of characters
|
|
389
|
+
* @param {number} decrement - Number of characters to move
|
|
390
|
+
*/
|
|
391
|
+
moveCursorLeft(decrement){
|
|
392
|
+
if(this.cursorPos > 0){
|
|
393
|
+
this.cursorPos -= decrement;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
this.scrollCursorIntoViewLeft();
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Ensures cursor remains visible when moving right
|
|
401
|
+
* @param {Object} options - Scroll options
|
|
402
|
+
* @param {number|null} options.cursorX - Optional cursor X position
|
|
403
|
+
*/
|
|
404
|
+
scrollCursorIntoViewRight({cursorX=null} = {}){
|
|
405
|
+
if(!cursorX){
|
|
406
|
+
cursorX = this.findTextWidth(0, this.cursorPos);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (cursorX - this.displayXOffset > this.width - this.padding) {
|
|
410
|
+
this.displayXOffset = cursorX - this.width + 2*this.padding;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if(!cursorX){
|
|
414
|
+
this.displayXOffset = max(0, this.displayXOffset);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Ensures cursor remains visible when moving left
|
|
420
|
+
* @param {Object} options - Scroll options
|
|
421
|
+
* @param {number|null} options.cursorX - Optional cursor X position
|
|
422
|
+
*/
|
|
423
|
+
scrollCursorIntoViewLeft({cursorX=null} = {}){
|
|
424
|
+
if(!cursorX){
|
|
425
|
+
cursorX = this.findTextWidth(0, this.cursorPos);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if(cursorX - this.displayXOffset < this.padding){
|
|
429
|
+
this.displayXOffset = cursorX - this.padding;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if(!cursorX){
|
|
433
|
+
this.displayXOffset = max(0, this.displayXOffset);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Ensures cursor remains within visible area
|
|
439
|
+
*/
|
|
440
|
+
scrollCursorIntoView(){
|
|
441
|
+
let cursorX = this.findTextWidth(0, this.cursorPos);
|
|
442
|
+
this.scrollCursorIntoViewRight(cursorX);
|
|
443
|
+
this.scrollCursorIntoViewLeft(cursorX);
|
|
444
|
+
this.displayXOffset = max(0, this.displayXOffset);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Converts text alignment string to P5 constant
|
|
449
|
+
* @returns {number} P5 alignment constant
|
|
450
|
+
*/
|
|
451
|
+
getTextAlignment(){
|
|
452
|
+
if(this.textAlign==="right"){
|
|
453
|
+
return RIGHT;
|
|
454
|
+
} else {
|
|
455
|
+
return LEFT;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Renders the text field with text, cursor, and selection
|
|
461
|
+
*/
|
|
462
|
+
show(){
|
|
463
|
+
if(this.enableShadow){
|
|
464
|
+
this.drawShadow();
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
push();
|
|
468
|
+
beginClip();
|
|
469
|
+
rect(this.x, this.y, this.width, this.height, this.cornerRadius);
|
|
470
|
+
endClip();
|
|
471
|
+
|
|
472
|
+
push();
|
|
473
|
+
fill(this.backgroundColor);
|
|
474
|
+
rect(this.x, this.y, this.width, this.height, this.cornerRadius);
|
|
475
|
+
pop();
|
|
476
|
+
|
|
477
|
+
fill(this.textColor);
|
|
478
|
+
textAlign(this.getTextAlignment(), CENTER);
|
|
479
|
+
textSize(this.textSize);
|
|
480
|
+
|
|
481
|
+
let x, y;
|
|
482
|
+
if(this.textAlign==="left"){
|
|
483
|
+
x = this.x - this.displayXOffset + this.padding;
|
|
484
|
+
} else if(this.textAlign==="right") {
|
|
485
|
+
x = this.x - this.displayXOffset + this.width - this.textSize;
|
|
486
|
+
}
|
|
487
|
+
y = this.y + this.height/2 + this.height*0.01;
|
|
488
|
+
|
|
489
|
+
if (this.selectionStart !== null && this.selectionEnd !== null && this.selectionStart !== this.selectionEnd) {
|
|
490
|
+
let start = min(this.selectionStart, this.selectionEnd);
|
|
491
|
+
let end = max(this.selectionStart, this.selectionEnd);
|
|
492
|
+
|
|
493
|
+
let highlightX = x + this.findTextWidth(0, start);
|
|
494
|
+
let highlightWidth = this.findTextWidth(start, end);
|
|
495
|
+
let highlightY = y - this.textSize * 0.7;
|
|
496
|
+
|
|
497
|
+
push();
|
|
498
|
+
fill('rgba(15, 111, 206, 0.7)');
|
|
499
|
+
noStroke();
|
|
500
|
+
rect(highlightX, highlightY, highlightWidth, this.textSize);
|
|
501
|
+
pop();
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
text(this.text, x, y);
|
|
505
|
+
|
|
506
|
+
if (millis() - this.lastCursorToggle > this.cursorBlinkInterval) {
|
|
507
|
+
this.cursorVisible = !this.cursorVisible;
|
|
508
|
+
this.lastCursorToggle = millis();
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if(this.isFocused){
|
|
512
|
+
let cursorX = x + this.findTextWidth(0, this.cursorPos);
|
|
513
|
+
if (this.cursorVisible) {
|
|
514
|
+
stroke(this.textColor);
|
|
515
|
+
strokeWeight(2);
|
|
516
|
+
line(cursorX, y - this.textSize * 0.5, cursorX, y + this.textSize * 0.45);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if(this.borderFlag) {
|
|
521
|
+
noFill();
|
|
522
|
+
stroke(this.borderColor);
|
|
523
|
+
strokeWeight(this.borderWidth);
|
|
524
|
+
rect(this.x, this.y, this.width, this.height, this.cornerRadius);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
pop();
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Updates text size based on current height
|
|
532
|
+
*/
|
|
533
|
+
updateTextSize(){
|
|
534
|
+
this.textSize = this.height * 0.9;
|
|
535
|
+
this.scrollCursorIntoView();
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Handles width changes
|
|
540
|
+
*/
|
|
541
|
+
updateWidth(){
|
|
542
|
+
//don't do anything here!
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Handles height changes and updates text size
|
|
547
|
+
*/
|
|
548
|
+
updateHeight(){
|
|
549
|
+
this.updateTextSize();
|
|
550
|
+
}
|
|
551
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Component } from "../Core/Component.js";
|
|
2
|
+
|
|
3
|
+
export class UIComponent extends Component{
|
|
4
|
+
/**
|
|
5
|
+
* Creates a new UIComponent with visual styling
|
|
6
|
+
* @param {number} x - The x-coordinate
|
|
7
|
+
* @param {number} y - The y-coordinate
|
|
8
|
+
* @param {number} width - The width
|
|
9
|
+
* @param {number} height - The height
|
|
10
|
+
* @param {p5.Color} backgroundColor - Background color
|
|
11
|
+
* @param {boolean} borderFlag - Whether to show border
|
|
12
|
+
* @param {p5.Color} borderColor - Border color
|
|
13
|
+
* @param {number} borderWidth - Border width
|
|
14
|
+
* @param {number} cornerRadius - Corner radius for rounded corners
|
|
15
|
+
* @param {boolean} enableShadow - Whether to render shadow
|
|
16
|
+
* @param {string} shadowColor - Shadow color in RGB format
|
|
17
|
+
* @param {number} shadowIntensity - Shadow opacity (0-1)
|
|
18
|
+
* @param {number} shadowSpread - Shadow spread amount
|
|
19
|
+
* @param {number} shadowDetail - Number of shadow layers
|
|
20
|
+
* @param {Object} options - Additional options
|
|
21
|
+
* @param {Component|null} options.parent - Parent component
|
|
22
|
+
* @param {string} options.type - Component type
|
|
23
|
+
* @param {string|null} options.id - Component ID
|
|
24
|
+
*/
|
|
25
|
+
constructor(x, y, width, height, backgroundColor, borderFlag, borderColor,
|
|
26
|
+
borderWidth, cornerRadius, enableShadow, shadowColor, shadowIntensity,
|
|
27
|
+
shadowSpread, shadowDetail, {parent=null, type="", id=null} = {}){
|
|
28
|
+
super(x, y, width, height, {parent: parent, type: type, id: id});
|
|
29
|
+
|
|
30
|
+
this.backgroundColor = backgroundColor;
|
|
31
|
+
|
|
32
|
+
this.borderFlag = borderFlag;
|
|
33
|
+
if(this.borderFlag){
|
|
34
|
+
this.borderColor = borderColor;
|
|
35
|
+
this.borderWidth = borderWidth;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
this.enableShadow = enableShadow;
|
|
39
|
+
|
|
40
|
+
if(this.enableShadow){
|
|
41
|
+
this.shadowColor = shadowColor;//rgb value
|
|
42
|
+
this.shadowIntensity = shadowIntensity;//opacity value between 0 and 1
|
|
43
|
+
this.shadowSpread = shadowSpread;//stroke width of each of those rectangles
|
|
44
|
+
this.shadowDetail = shadowDetail;//number of rectangles that will be drawn around the component
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
this.cornerRadius = cornerRadius;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Updates the component's width (abstract method)
|
|
51
|
+
* @abstract
|
|
52
|
+
*/
|
|
53
|
+
updateWidth(){};
|
|
54
|
+
/**
|
|
55
|
+
* Updates the component's height (abstract method)
|
|
56
|
+
* @abstract
|
|
57
|
+
*/
|
|
58
|
+
updateHeight(){};
|
|
59
|
+
/**
|
|
60
|
+
* Converts RGB color string to array of numbers
|
|
61
|
+
* @param {string} shadowColor - RGB color string like "rgb(255,255,255)"
|
|
62
|
+
* @returns {number[]|null} Array of [r, g, b] values or null if invalid
|
|
63
|
+
*/
|
|
64
|
+
rgbToArray(shadowColor) {
|
|
65
|
+
let match = shadowColor.match(/\d+/g);
|
|
66
|
+
return match ? match.map(Number) : null;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Renders a shadow effect around the component
|
|
70
|
+
* @param {Object} options - Shadow rendering options
|
|
71
|
+
*/
|
|
72
|
+
drawShadow({}={}){
|
|
73
|
+
let color = this.rgbToArray(this.shadowColor);
|
|
74
|
+
if(color==null){
|
|
75
|
+
console.log("shadow color value is not in the correct format: rgb(0,0,0)");
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if(this.shadowIntensity>1){
|
|
80
|
+
this.shadowIntensity=1;
|
|
81
|
+
console.log("shadow intensity should be between 0 and 1 inclusive.\nAny value given outside of the range will be clipped to the ends.");
|
|
82
|
+
} else if(this.shadowIntensity<0){
|
|
83
|
+
console.log("shadow intensity should be between 0 and 1 inclusive.\nAny value given outside of the range will be clipped to the ends.");
|
|
84
|
+
this.shadowIntensity=0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for(let i=1; i<=this.shadowDetail; i++){
|
|
88
|
+
push();
|
|
89
|
+
noFill();
|
|
90
|
+
let alpha = this.shadowIntensity * pow(1 - i / this.shadowDetail, 2);
|
|
91
|
+
stroke(`rgba(${color[0]}, ${color[1]}, ${color[2]}, ${alpha})`);
|
|
92
|
+
strokeWeight(this.shadowSpread);
|
|
93
|
+
rect(this.x-((i*this.shadowSpread)/2), this.y-((i*this.shadowSpread)/2), this.width+(i*this.shadowSpread), this.height+(i*this.shadowSpread), this.cornerRadius);
|
|
94
|
+
pop();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
package/crowjs-01-01.png
ADDED
|
Binary file
|
package/index.html
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.4/p5.js"></script>
|
|
5
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.4/addons/p5.sound.min.js"></script>
|
|
6
|
+
<link rel="stylesheet" type="text/css" href="style.css">
|
|
7
|
+
<meta charset="utf-8" />
|
|
8
|
+
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<main>
|
|
12
|
+
</main>
|
|
13
|
+
<script type="module" src="sketch.js"></script>
|
|
14
|
+
</body>
|
|
15
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@usman404/crowjs",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "lightweight and extensible GUI library built on top of p5.js",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"CROWJS"
|
|
7
|
+
],
|
|
8
|
+
"homepage": "https://github.com/UsmanAli404/P5GUI#readme",
|
|
9
|
+
"bugs": {
|
|
10
|
+
"url": "https://github.com/UsmanAli404/P5GUI/issues"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/UsmanAli404/P5GUI.git"
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"author": "Usman Ali",
|
|
18
|
+
"type": "commonjs",
|
|
19
|
+
"main": "sketch.js",
|
|
20
|
+
"scripts": {
|
|
21
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
22
|
+
}
|
|
23
|
+
}
|