react-pebble 0.1.1 → 0.2.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/LICENSE +21 -0
- package/dist/lib/compiler.cjs +2 -2
- package/dist/lib/compiler.cjs.map +1 -1
- package/dist/lib/compiler.js +4 -1
- package/dist/lib/compiler.js.map +1 -1
- package/dist/lib/components.cjs +1 -1
- package/dist/lib/components.cjs.map +1 -1
- package/dist/lib/components.js +44 -5
- package/dist/lib/components.js.map +1 -1
- package/dist/lib/hooks.cjs +1 -1
- package/dist/lib/hooks.cjs.map +1 -1
- package/dist/lib/hooks.js +198 -3
- package/dist/lib/hooks.js.map +1 -1
- package/dist/lib/index.cjs +1 -1
- package/dist/lib/index.cjs.map +1 -1
- package/dist/lib/index.js +231 -108
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/plugin.cjs +25 -5
- package/dist/lib/plugin.cjs.map +1 -1
- package/dist/lib/plugin.js +62 -35
- package/dist/lib/plugin.js.map +1 -1
- package/dist/lib/src/compiler/index.d.ts +2 -0
- package/dist/lib/src/components/index.d.ts +28 -1
- package/dist/lib/src/hooks/index.d.ts +182 -0
- package/dist/lib/src/index.d.ts +4 -4
- package/dist/lib/src/pebble-output.d.ts +15 -0
- package/dist/lib/src/plugin/index.d.ts +6 -0
- package/package.json +10 -11
- package/scripts/compile-to-piu.ts +315 -26
- package/scripts/deploy.sh +0 -0
- package/scripts/test-emulator.sh +371 -0
- package/src/compiler/index.ts +8 -1
- package/src/components/index.tsx +75 -1
- package/src/hooks/index.ts +507 -19
- package/src/index.ts +26 -0
- package/src/pebble-output.ts +408 -48
- package/src/plugin/index.ts +101 -49
- package/src/types/moddable.d.ts +26 -4
package/src/pebble-output.ts
CHANGED
|
@@ -12,9 +12,10 @@
|
|
|
12
12
|
* named color / font exactly once.
|
|
13
13
|
* - **No native line, circle, or stroked rectangle.** Poco only has
|
|
14
14
|
* `fillRectangle`, `drawText`, and bitmap draws. Outlines (stroke) are
|
|
15
|
-
* emulated as four thin fillRectangles. Axis-aligned lines
|
|
16
|
-
* Circles
|
|
17
|
-
*
|
|
15
|
+
* emulated as four thin fillRectangles. Axis-aligned lines use
|
|
16
|
+
* fillRectangles. Circles use a midpoint circle algorithm and diagonal
|
|
17
|
+
* lines use Bresenham's algorithm — both decomposed into fillRectangle
|
|
18
|
+
* calls so they work on mock and real Poco without requiring extensions.
|
|
18
19
|
* - **Text alignment is manual.** `drawText(text, font, color, x, y)` only
|
|
19
20
|
* draws at a point. For center/right alignment we measure with
|
|
20
21
|
* `getTextWidth` and compute the origin ourselves.
|
|
@@ -53,10 +54,9 @@ export const COLOR_PALETTE: Readonly<Record<string, RGB>> = {
|
|
|
53
54
|
// ---------------------------------------------------------------------------
|
|
54
55
|
// Named font shortcuts — mapped to (family, size) pairs.
|
|
55
56
|
//
|
|
56
|
-
// The family names
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
// resources to the library manifest.
|
|
57
|
+
// The family names must match fonts available in the Moddable manifest.
|
|
58
|
+
// These correspond to the Pebble system font families available in Alloy.
|
|
59
|
+
// The piu compiler uses the same families via FONT_TO_PIU in compile-to-piu.ts.
|
|
60
60
|
// ---------------------------------------------------------------------------
|
|
61
61
|
|
|
62
62
|
export interface FontSpec {
|
|
@@ -65,19 +65,38 @@ export interface FontSpec {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
export const FONT_PALETTE: Readonly<Record<string, FontSpec>> = {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
68
|
+
// Gothic family — standard UI text
|
|
69
|
+
gothic14: { family: 'Gothic', size: 14 },
|
|
70
|
+
gothic14Bold: { family: 'Gothic-Bold', size: 14 },
|
|
71
|
+
gothic18: { family: 'Gothic', size: 18 },
|
|
72
|
+
gothic18Bold: { family: 'Gothic-Bold', size: 18 },
|
|
73
|
+
gothic24: { family: 'Gothic', size: 24 },
|
|
74
|
+
gothic24Bold: { family: 'Gothic-Bold', size: 24 },
|
|
75
|
+
gothic28: { family: 'Gothic', size: 28 },
|
|
76
|
+
gothic28Bold: { family: 'Gothic-Bold', size: 28 },
|
|
77
|
+
|
|
78
|
+
// Bitham family — large display fonts
|
|
79
|
+
bitham30Black: { family: 'Bitham-Black', size: 30 },
|
|
80
|
+
bitham42Bold: { family: 'Bitham-Bold', size: 42 },
|
|
81
|
+
bitham42Light: { family: 'Bitham-Light', size: 42 },
|
|
82
|
+
bitham34MediumNumbers: { family: 'Bitham', size: 34 },
|
|
83
|
+
bitham42MediumNumbers: { family: 'Bitham', size: 42 },
|
|
84
|
+
|
|
85
|
+
// Roboto family
|
|
86
|
+
robotoCondensed21: { family: 'Roboto-Condensed', size: 21 },
|
|
87
|
+
roboto21: { family: 'Roboto', size: 21 },
|
|
88
|
+
|
|
89
|
+
// Droid Serif
|
|
90
|
+
droid28: { family: 'Droid-Serif', size: 28 },
|
|
91
|
+
|
|
92
|
+
// LECO family — LED-style numeric fonts
|
|
93
|
+
leco20: { family: 'LECO', size: 20 },
|
|
94
|
+
leco26: { family: 'LECO', size: 26 },
|
|
95
|
+
leco28: { family: 'LECO', size: 28 },
|
|
96
|
+
leco32: { family: 'LECO', size: 32 },
|
|
97
|
+
leco36: { family: 'LECO', size: 36 },
|
|
98
|
+
leco38: { family: 'LECO', size: 38 },
|
|
99
|
+
leco42: { family: 'LECO', size: 42 },
|
|
81
100
|
};
|
|
82
101
|
|
|
83
102
|
const DEFAULT_FONT_KEY = 'gothic18';
|
|
@@ -195,18 +214,30 @@ export class PocoRenderer {
|
|
|
195
214
|
const h = num(p, 'h') || num(p, 'height');
|
|
196
215
|
const fill = str(p, 'fill');
|
|
197
216
|
const stroke = str(p, 'stroke');
|
|
217
|
+
const br = num(p, 'borderRadius');
|
|
198
218
|
|
|
199
|
-
if (
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
219
|
+
if (br > 0) {
|
|
220
|
+
// Rounded rectangle
|
|
221
|
+
if (fill) {
|
|
222
|
+
this.fillRoundRect(this.getColor(fill), x, y, w, h, br);
|
|
223
|
+
}
|
|
224
|
+
if (stroke) {
|
|
225
|
+
const sw = num(p, 'strokeWidth') || 1;
|
|
226
|
+
this.strokeRoundRect(this.getColor(stroke), x, y, w, h, br, sw);
|
|
227
|
+
}
|
|
228
|
+
} else {
|
|
229
|
+
// Sharp rectangle
|
|
230
|
+
if (fill) {
|
|
231
|
+
this.poco.fillRectangle(this.getColor(fill), x, y, w, h);
|
|
232
|
+
}
|
|
233
|
+
if (stroke) {
|
|
234
|
+
const sw = num(p, 'strokeWidth') || 1;
|
|
235
|
+
const c = this.getColor(stroke);
|
|
236
|
+
this.poco.fillRectangle(c, x, y, w, sw); // top
|
|
237
|
+
this.poco.fillRectangle(c, x, y + h - sw, w, sw); // bottom
|
|
238
|
+
this.poco.fillRectangle(c, x, y, sw, h); // left
|
|
239
|
+
this.poco.fillRectangle(c, x + w - sw, y, sw, h); // right
|
|
240
|
+
}
|
|
210
241
|
}
|
|
211
242
|
|
|
212
243
|
this.renderChildren(node, x, y);
|
|
@@ -218,21 +249,33 @@ export class PocoRenderer {
|
|
|
218
249
|
if (!text) break;
|
|
219
250
|
|
|
220
251
|
const boxW = num(p, 'w') || num(p, 'width') || this.poco.width - x;
|
|
252
|
+
const boxH = num(p, 'h') || num(p, 'height') || 0;
|
|
221
253
|
const font = this.getFont(str(p, 'font'));
|
|
222
254
|
const color = this.getColor(str(p, 'color') ?? 'white');
|
|
223
255
|
const align = str(p, 'align') ?? 'left';
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
256
|
+
const lineHeight = (font as unknown as { height: number }).height || 16;
|
|
257
|
+
|
|
258
|
+
// Word-wrap text into lines that fit within boxW
|
|
259
|
+
const lines = this.wrapText(text, font, boxW);
|
|
260
|
+
|
|
261
|
+
let ty = y;
|
|
262
|
+
for (const line of lines) {
|
|
263
|
+
// Stop if we'd exceed the box height (when specified)
|
|
264
|
+
if (boxH > 0 && ty - y + lineHeight > boxH) break;
|
|
265
|
+
|
|
266
|
+
let tx = x;
|
|
267
|
+
if (align === 'center' || align === 'right') {
|
|
268
|
+
const tw = this.poco.getTextWidth(line, font);
|
|
269
|
+
if (align === 'center') {
|
|
270
|
+
tx = x + Math.floor((boxW - tw) / 2);
|
|
271
|
+
} else {
|
|
272
|
+
tx = x + boxW - tw;
|
|
273
|
+
}
|
|
232
274
|
}
|
|
233
|
-
}
|
|
234
275
|
|
|
235
|
-
|
|
276
|
+
this.poco.drawText(line, font, color, tx, ty);
|
|
277
|
+
ty += lineHeight;
|
|
278
|
+
}
|
|
236
279
|
break;
|
|
237
280
|
}
|
|
238
281
|
|
|
@@ -255,20 +298,56 @@ export class PocoRenderer {
|
|
|
255
298
|
const w = Math.abs(x2 - x) || 1;
|
|
256
299
|
this.poco.fillRectangle(c, left, y, w, sw);
|
|
257
300
|
}
|
|
258
|
-
|
|
301
|
+
else {
|
|
302
|
+
// Diagonal line via Bresenham's algorithm
|
|
303
|
+
this.drawDiagonalLine(c, x, y, x2, y2, sw);
|
|
304
|
+
}
|
|
259
305
|
break;
|
|
260
306
|
}
|
|
261
307
|
|
|
262
308
|
case 'pbl-circle': {
|
|
263
|
-
|
|
264
|
-
|
|
309
|
+
const r = num(p, 'r') || num(p, 'radius');
|
|
310
|
+
if (r <= 0) break;
|
|
311
|
+
const fill = str(p, 'fill');
|
|
312
|
+
const stroke = str(p, 'stroke');
|
|
313
|
+
const sw = num(p, 'strokeWidth') || 1;
|
|
314
|
+
|
|
315
|
+
// Midpoint circle algorithm — works on any Poco (real or mock)
|
|
316
|
+
// without requiring the commodetto/outline extension.
|
|
317
|
+
if (fill) {
|
|
318
|
+
const fc = this.getColor(fill);
|
|
319
|
+
this.fillCircle(fc, x + r, y + r, r);
|
|
320
|
+
}
|
|
321
|
+
if (stroke) {
|
|
322
|
+
const sc = this.getColor(stroke);
|
|
323
|
+
this.strokeCircle(sc, x + r, y + r, r, sw);
|
|
324
|
+
}
|
|
265
325
|
break;
|
|
266
326
|
}
|
|
267
327
|
|
|
268
328
|
case 'pbl-image': {
|
|
269
329
|
const bitmap = p.bitmap;
|
|
270
330
|
if (bitmap) {
|
|
271
|
-
|
|
331
|
+
const rotation = num(p, 'rotation');
|
|
332
|
+
const scale = num(p, 'scale');
|
|
333
|
+
if (rotation || (scale && scale !== 1)) {
|
|
334
|
+
// Use Poco's extended drawBitmap with rotation/scale if available.
|
|
335
|
+
// The mock Poco records the intent; the real Poco handles transforms.
|
|
336
|
+
const bmp = bitmap as never;
|
|
337
|
+
const poco = this.poco as Poco & {
|
|
338
|
+
drawBitmapWithTransform?: (
|
|
339
|
+
bmp: never, x: number, y: number, rotation: number, scale: number,
|
|
340
|
+
) => void;
|
|
341
|
+
};
|
|
342
|
+
if (poco.drawBitmapWithTransform) {
|
|
343
|
+
poco.drawBitmapWithTransform(bmp, x, y, rotation, scale || 1);
|
|
344
|
+
} else {
|
|
345
|
+
// Fallback: draw without transform
|
|
346
|
+
this.poco.drawBitmap(bmp, x, y);
|
|
347
|
+
}
|
|
348
|
+
} else {
|
|
349
|
+
this.poco.drawBitmap(bitmap as never, x, y);
|
|
350
|
+
}
|
|
272
351
|
}
|
|
273
352
|
break;
|
|
274
353
|
}
|
|
@@ -278,10 +357,53 @@ export class PocoRenderer {
|
|
|
278
357
|
break;
|
|
279
358
|
}
|
|
280
359
|
|
|
281
|
-
case 'pbl-statusbar':
|
|
360
|
+
case 'pbl-statusbar': {
|
|
361
|
+
// Render a simple status bar: background + time text
|
|
362
|
+
const sbBg = str(p, 'backgroundColor') ?? 'black';
|
|
363
|
+
const sbColor = str(p, 'color') ?? 'white';
|
|
364
|
+
const sbH = 16;
|
|
365
|
+
const sbW = this.poco.width;
|
|
366
|
+
|
|
367
|
+
this.poco.fillRectangle(this.getColor(sbBg), 0, 0, sbW, sbH);
|
|
368
|
+
|
|
369
|
+
// Draw current time in the center
|
|
370
|
+
const sbFont = this.getFont('gothic14');
|
|
371
|
+
const now = new Date();
|
|
372
|
+
const timeStr = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
|
|
373
|
+
const tw = this.poco.getTextWidth(timeStr, sbFont);
|
|
374
|
+
this.poco.drawText(timeStr, sbFont, this.getColor(sbColor), Math.floor((sbW - tw) / 2), 1);
|
|
375
|
+
|
|
376
|
+
// Separator line
|
|
377
|
+
const sep = str(p, 'separator') ?? 'none';
|
|
378
|
+
if (sep === 'line') {
|
|
379
|
+
this.poco.fillRectangle(this.getColor(sbColor), 0, sbH - 1, sbW, 1);
|
|
380
|
+
} else if (sep === 'dotted') {
|
|
381
|
+
for (let dx = 0; dx < sbW; dx += 3) {
|
|
382
|
+
this.poco.fillRectangle(this.getColor(sbColor), dx, sbH - 1, 1, 1);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
|
|
282
388
|
case 'pbl-actionbar': {
|
|
283
|
-
//
|
|
284
|
-
|
|
389
|
+
// Render an action bar on the right edge: background column + icon placeholders
|
|
390
|
+
const abBg = str(p, 'backgroundColor') ?? 'darkGray';
|
|
391
|
+
const abW = 30;
|
|
392
|
+
const abX = this.poco.width - abW;
|
|
393
|
+
const abH = this.poco.height;
|
|
394
|
+
|
|
395
|
+
this.poco.fillRectangle(this.getColor(abBg), abX, 0, abW, abH);
|
|
396
|
+
|
|
397
|
+
// Draw dot placeholders for up/select/down icons
|
|
398
|
+
const dotColor = this.getColor('white');
|
|
399
|
+
const dotR = 3;
|
|
400
|
+
const centerX = abX + Math.floor(abW / 2);
|
|
401
|
+
// Up icon area (top third)
|
|
402
|
+
this.poco.fillRectangle(dotColor, centerX - dotR, Math.floor(abH / 6) - dotR, dotR * 2, dotR * 2);
|
|
403
|
+
// Select icon area (middle)
|
|
404
|
+
this.poco.fillRectangle(dotColor, centerX - dotR, Math.floor(abH / 2) - dotR, dotR * 2, dotR * 2);
|
|
405
|
+
// Down icon area (bottom third)
|
|
406
|
+
this.poco.fillRectangle(dotColor, centerX - dotR, Math.floor(abH * 5 / 6) - dotR, dotR * 2, dotR * 2);
|
|
285
407
|
break;
|
|
286
408
|
}
|
|
287
409
|
|
|
@@ -291,6 +413,244 @@ export class PocoRenderer {
|
|
|
291
413
|
}
|
|
292
414
|
}
|
|
293
415
|
}
|
|
416
|
+
|
|
417
|
+
// -------------------------------------------------------------------------
|
|
418
|
+
// Circle rendering — midpoint circle algorithm
|
|
419
|
+
// -------------------------------------------------------------------------
|
|
420
|
+
|
|
421
|
+
/** Fill a circle by drawing horizontal spans for each row. */
|
|
422
|
+
private fillCircle(color: PocoColor, cx: number, cy: number, r: number): void {
|
|
423
|
+
const { poco } = this;
|
|
424
|
+
let x0 = r;
|
|
425
|
+
let y0 = 0;
|
|
426
|
+
let err = 1 - r;
|
|
427
|
+
|
|
428
|
+
// Track the last drawn y to avoid duplicate spans
|
|
429
|
+
let lastY1 = -1;
|
|
430
|
+
let lastY2 = -1;
|
|
431
|
+
|
|
432
|
+
while (x0 >= y0) {
|
|
433
|
+
// Draw horizontal spans for each octant pair
|
|
434
|
+
if (cy + y0 !== lastY1) {
|
|
435
|
+
poco.fillRectangle(color, cx - x0, cy + y0, x0 * 2 + 1, 1);
|
|
436
|
+
lastY1 = cy + y0;
|
|
437
|
+
}
|
|
438
|
+
if (cy - y0 !== lastY2 && y0 !== 0) {
|
|
439
|
+
poco.fillRectangle(color, cx - x0, cy - y0, x0 * 2 + 1, 1);
|
|
440
|
+
lastY2 = cy - y0;
|
|
441
|
+
}
|
|
442
|
+
if (cy + x0 !== lastY1) {
|
|
443
|
+
poco.fillRectangle(color, cx - y0, cy + x0, y0 * 2 + 1, 1);
|
|
444
|
+
lastY1 = cy + x0;
|
|
445
|
+
}
|
|
446
|
+
if (cy - x0 !== lastY2) {
|
|
447
|
+
poco.fillRectangle(color, cx - y0, cy - x0, y0 * 2 + 1, 1);
|
|
448
|
+
lastY2 = cy - x0;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
y0++;
|
|
452
|
+
if (err < 0) {
|
|
453
|
+
err += 2 * y0 + 1;
|
|
454
|
+
} else {
|
|
455
|
+
x0--;
|
|
456
|
+
err += 2 * (y0 - x0) + 1;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** Stroke a circle outline by drawing small rects at each perimeter point. */
|
|
462
|
+
private strokeCircle(color: PocoColor, cx: number, cy: number, r: number, sw: number): void {
|
|
463
|
+
const { poco } = this;
|
|
464
|
+
let x0 = r;
|
|
465
|
+
let y0 = 0;
|
|
466
|
+
let err = 1 - r;
|
|
467
|
+
|
|
468
|
+
while (x0 >= y0) {
|
|
469
|
+
// 8 octant points
|
|
470
|
+
poco.fillRectangle(color, cx + x0, cy + y0, sw, sw);
|
|
471
|
+
poco.fillRectangle(color, cx - x0, cy + y0, sw, sw);
|
|
472
|
+
poco.fillRectangle(color, cx + x0, cy - y0, sw, sw);
|
|
473
|
+
poco.fillRectangle(color, cx - x0, cy - y0, sw, sw);
|
|
474
|
+
poco.fillRectangle(color, cx + y0, cy + x0, sw, sw);
|
|
475
|
+
poco.fillRectangle(color, cx - y0, cy + x0, sw, sw);
|
|
476
|
+
poco.fillRectangle(color, cx + y0, cy - x0, sw, sw);
|
|
477
|
+
poco.fillRectangle(color, cx - y0, cy - x0, sw, sw);
|
|
478
|
+
|
|
479
|
+
y0++;
|
|
480
|
+
if (err < 0) {
|
|
481
|
+
err += 2 * y0 + 1;
|
|
482
|
+
} else {
|
|
483
|
+
x0--;
|
|
484
|
+
err += 2 * (y0 - x0) + 1;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// -------------------------------------------------------------------------
|
|
490
|
+
// Diagonal line rendering — Bresenham's line algorithm
|
|
491
|
+
// -------------------------------------------------------------------------
|
|
492
|
+
|
|
493
|
+
private drawDiagonalLine(
|
|
494
|
+
color: PocoColor, x1: number, y1: number, x2: number, y2: number, sw: number,
|
|
495
|
+
): void {
|
|
496
|
+
const { poco } = this;
|
|
497
|
+
let dx = Math.abs(x2 - x1);
|
|
498
|
+
let dy = Math.abs(y2 - y1);
|
|
499
|
+
const sx = x1 < x2 ? 1 : -1;
|
|
500
|
+
const sy = y1 < y2 ? 1 : -1;
|
|
501
|
+
let err = dx - dy;
|
|
502
|
+
let cx = x1;
|
|
503
|
+
let cy = y1;
|
|
504
|
+
|
|
505
|
+
for (;;) {
|
|
506
|
+
poco.fillRectangle(color, cx, cy, sw, sw);
|
|
507
|
+
if (cx === x2 && cy === y2) break;
|
|
508
|
+
const e2 = 2 * err;
|
|
509
|
+
if (e2 > -dy) {
|
|
510
|
+
err -= dy;
|
|
511
|
+
cx += sx;
|
|
512
|
+
}
|
|
513
|
+
if (e2 < dx) {
|
|
514
|
+
err += dx;
|
|
515
|
+
cy += sy;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// -------------------------------------------------------------------------
|
|
521
|
+
// Rounded rectangle rendering
|
|
522
|
+
// -------------------------------------------------------------------------
|
|
523
|
+
|
|
524
|
+
/** Fill a rounded rectangle by combining rects and quarter-circle corners. */
|
|
525
|
+
private fillRoundRect(
|
|
526
|
+
color: PocoColor, x: number, y: number, w: number, h: number, r: number,
|
|
527
|
+
): void {
|
|
528
|
+
const { poco } = this;
|
|
529
|
+
const cr = Math.min(r, Math.floor(w / 2), Math.floor(h / 2));
|
|
530
|
+
|
|
531
|
+
// Center body
|
|
532
|
+
poco.fillRectangle(color, x, y + cr, w, h - cr * 2);
|
|
533
|
+
// Top strip (between corners)
|
|
534
|
+
poco.fillRectangle(color, x + cr, y, w - cr * 2, cr);
|
|
535
|
+
// Bottom strip (between corners)
|
|
536
|
+
poco.fillRectangle(color, x + cr, y + h - cr, w - cr * 2, cr);
|
|
537
|
+
|
|
538
|
+
// Four quarter-circle corners via midpoint algorithm
|
|
539
|
+
this.fillQuarterCircles(color, x + cr, y + cr, x + w - cr - 1, y + h - cr - 1, cr);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/** Stroke a rounded rectangle outline. */
|
|
543
|
+
private strokeRoundRect(
|
|
544
|
+
color: PocoColor, x: number, y: number, w: number, h: number, r: number, sw: number,
|
|
545
|
+
): void {
|
|
546
|
+
const { poco } = this;
|
|
547
|
+
const cr = Math.min(r, Math.floor(w / 2), Math.floor(h / 2));
|
|
548
|
+
|
|
549
|
+
// Straight edges
|
|
550
|
+
poco.fillRectangle(color, x + cr, y, w - cr * 2, sw); // top
|
|
551
|
+
poco.fillRectangle(color, x + cr, y + h - sw, w - cr * 2, sw); // bottom
|
|
552
|
+
poco.fillRectangle(color, x, y + cr, sw, h - cr * 2); // left
|
|
553
|
+
poco.fillRectangle(color, x + w - sw, y + cr, sw, h - cr * 2); // right
|
|
554
|
+
|
|
555
|
+
// Corner arcs
|
|
556
|
+
this.strokeQuarterCircles(color, x + cr, y + cr, x + w - cr - 1, y + h - cr - 1, cr, sw);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/** Fill four quarter-circles at the corners of a rounded rect. */
|
|
560
|
+
private fillQuarterCircles(
|
|
561
|
+
color: PocoColor, cx1: number, cy1: number, cx2: number, cy2: number, r: number,
|
|
562
|
+
): void {
|
|
563
|
+
const { poco } = this;
|
|
564
|
+
let x0 = r;
|
|
565
|
+
let y0 = 0;
|
|
566
|
+
let err = 1 - r;
|
|
567
|
+
|
|
568
|
+
while (x0 >= y0) {
|
|
569
|
+
// Top-left corner
|
|
570
|
+
poco.fillRectangle(color, cx1 - x0, cy1 - y0, x0, 1);
|
|
571
|
+
poco.fillRectangle(color, cx1 - y0, cy1 - x0, y0, 1);
|
|
572
|
+
// Top-right corner
|
|
573
|
+
poco.fillRectangle(color, cx2 + 1, cy1 - y0, x0, 1);
|
|
574
|
+
poco.fillRectangle(color, cx2 + 1, cy1 - x0, y0, 1);
|
|
575
|
+
// Bottom-left corner
|
|
576
|
+
poco.fillRectangle(color, cx1 - x0, cy2 + y0, x0, 1);
|
|
577
|
+
poco.fillRectangle(color, cx1 - y0, cy2 + x0, y0, 1);
|
|
578
|
+
// Bottom-right corner
|
|
579
|
+
poco.fillRectangle(color, cx2 + 1, cy2 + y0, x0, 1);
|
|
580
|
+
poco.fillRectangle(color, cx2 + 1, cy2 + x0, y0, 1);
|
|
581
|
+
|
|
582
|
+
y0++;
|
|
583
|
+
if (err < 0) {
|
|
584
|
+
err += 2 * y0 + 1;
|
|
585
|
+
} else {
|
|
586
|
+
x0--;
|
|
587
|
+
err += 2 * (y0 - x0) + 1;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/** Stroke four quarter-circle arcs at the corners of a rounded rect. */
|
|
593
|
+
private strokeQuarterCircles(
|
|
594
|
+
color: PocoColor, cx1: number, cy1: number, cx2: number, cy2: number, r: number, sw: number,
|
|
595
|
+
): void {
|
|
596
|
+
const { poco } = this;
|
|
597
|
+
let x0 = r;
|
|
598
|
+
let y0 = 0;
|
|
599
|
+
let err = 1 - r;
|
|
600
|
+
|
|
601
|
+
while (x0 >= y0) {
|
|
602
|
+
// Top-left
|
|
603
|
+
poco.fillRectangle(color, cx1 - x0, cy1 - y0, sw, sw);
|
|
604
|
+
poco.fillRectangle(color, cx1 - y0, cy1 - x0, sw, sw);
|
|
605
|
+
// Top-right
|
|
606
|
+
poco.fillRectangle(color, cx2 + x0, cy1 - y0, sw, sw);
|
|
607
|
+
poco.fillRectangle(color, cx2 + y0, cy1 - x0, sw, sw);
|
|
608
|
+
// Bottom-left
|
|
609
|
+
poco.fillRectangle(color, cx1 - x0, cy2 + y0, sw, sw);
|
|
610
|
+
poco.fillRectangle(color, cx1 - y0, cy2 + x0, sw, sw);
|
|
611
|
+
// Bottom-right
|
|
612
|
+
poco.fillRectangle(color, cx2 + x0, cy2 + y0, sw, sw);
|
|
613
|
+
poco.fillRectangle(color, cx2 + y0, cy2 + x0, sw, sw);
|
|
614
|
+
|
|
615
|
+
y0++;
|
|
616
|
+
if (err < 0) {
|
|
617
|
+
err += 2 * y0 + 1;
|
|
618
|
+
} else {
|
|
619
|
+
x0--;
|
|
620
|
+
err += 2 * (y0 - x0) + 1;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// -------------------------------------------------------------------------
|
|
626
|
+
// Text wrapping
|
|
627
|
+
// -------------------------------------------------------------------------
|
|
628
|
+
|
|
629
|
+
/** Break text into lines that fit within maxWidth. */
|
|
630
|
+
private wrapText(text: string, font: PocoFont, maxWidth: number): string[] {
|
|
631
|
+
// If the full text fits, skip wrapping
|
|
632
|
+
if (this.poco.getTextWidth(text, font) <= maxWidth) {
|
|
633
|
+
return [text];
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const words = text.split(' ');
|
|
637
|
+
const lines: string[] = [];
|
|
638
|
+
let currentLine = '';
|
|
639
|
+
|
|
640
|
+
for (const word of words) {
|
|
641
|
+
const testLine = currentLine ? `${currentLine} ${word}` : word;
|
|
642
|
+
if (this.poco.getTextWidth(testLine, font) <= maxWidth) {
|
|
643
|
+
currentLine = testLine;
|
|
644
|
+
} else {
|
|
645
|
+
if (currentLine) lines.push(currentLine);
|
|
646
|
+
// If a single word exceeds maxWidth, it goes on its own line (truncated visually)
|
|
647
|
+
currentLine = word;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
if (currentLine) lines.push(currentLine);
|
|
651
|
+
|
|
652
|
+
return lines;
|
|
653
|
+
}
|
|
294
654
|
}
|
|
295
655
|
|
|
296
656
|
// ---------------------------------------------------------------------------
|