ntk 7.5.0 → 7.6.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.
@@ -1396,14 +1396,35 @@ class RenderingContext2d {
1396
1396
  }
1397
1397
 
1398
1398
  /**
1399
- * Composite shaped glyph runs onto this context, honouring the clip.
1399
+ * Composite glyph runs onto this context, honouring the clip. This is
1400
+ * the primitive `fillText` and `TextLayout.draw` are built on, and it is
1401
+ * public: the run shape below is a documented contract
1402
+ * (docs/text.md#glyph-runs), so renderers that position glyphs
1403
+ * themselves — a terminal grid, a tabular column — can hand-build runs
1404
+ * instead of shaping.
1405
+ *
1406
+ * @param {number} op Render.PictOp (`ctx.Render.PictOp.Over` for normal text)
1407
+ * @param {Picture} src source picture the glyphs paint with — a solid
1408
+ * (`ctx.createSolidPicture(r, g, b, a)`, premultiplied 0..1) or a gradient
1409
+ * @param {Array<{run, x, y, textRendering?}>} positioned runs in visual
1410
+ * order; `x`/`y` is the run's baseline origin in device space. `run` is
1411
+ * `{ font, size, glyphs }` — a `Font`, a pixel size, and glyphs
1412
+ * `{ id, ax, dx, dy }` in drawing order: `id` a font glyph id
1413
+ * (`Font.shape()`'s `glyphs[].id`, or `Font.glyphIdFor(cp)`), `ax` the
1414
+ * pen advance in px, `dx`/`dy` the drawing offset from the pen position
1415
+ * (y-up: positive `dy` raises the glyph). The pen starts at `x`; each
1416
+ * glyph inks at `(pen + dx, y - dy)` and then advances it by `ax`.
1417
+ * `Font.shape()` returns runs of exactly this shape; extra fields
1418
+ * (`codePoints`, `width`, …) are ignored. `textRendering` optionally
1419
+ * overrides the bitmap/vector routing per run (docs/text.md).
1400
1420
  *
1401
1421
  * CompositeGlyphs writes straight to the destination picture, so it has
1402
1422
  * no way to consult our clip mask — text drawn through it used to spill
1403
1423
  * out of clipped boxes while every fill and stroke stayed inside. With a
1404
1424
  * clip active, render the glyph coverage into the scratch a8 mask
1405
1425
  * instead, intersect that with the clip, and paint the real source
1406
- * through the result — the same shape as _fillPolys.
1426
+ * through the result — the same shape as _fillPolys. A rectangular clip
1427
+ * takes the server-side fast path below instead of the mask.
1407
1428
  */
1408
1429
  drawGlyphs(op, src, positioned) {
1409
1430
  const app = this.window.app;
@@ -1652,6 +1673,80 @@ class RenderingContext2d {
1652
1673
  this._fillPolys(flattenPath(tmp._cmds, this._m), "nonzero");
1653
1674
  }
1654
1675
 
1676
+ /**
1677
+ * Fill a batch of axis-aligned rectangles: `fillRect` once per rectangle
1678
+ * semantically — fillStyle, globalAlpha, the composite op, the clip and
1679
+ * damage reporting all apply — but priced for the "many small rectangles
1680
+ * per frame" caller (issue #253): terminal cell backgrounds, sparkline
1681
+ * bars, heat maps, row striping.
1682
+ *
1683
+ * `rects` is an array of `[x, y, w, h]` quadruples or one flat
1684
+ * `[x0, y0, w0, h0, x1, ...]` array; rectangles with non-positive width
1685
+ * or height are skipped.
1686
+ *
1687
+ * A solid-colour fillStyle under an identity transform and a rectangular
1688
+ * (or absent) clip compiles the whole list into a single
1689
+ * `Render.FillRectangles`, where N `fillRect` calls cost N composites.
1690
+ * Gradient/Picture styles, transforms and non-rectangular clips fall
1691
+ * back to that `fillRect` loop, so the answer is always right and only
1692
+ * the request count varies.
1693
+ */
1694
+ fillRects(rects) {
1695
+ if (!rects || !rects.length || this.globalAlpha <= 0) return;
1696
+ // normalize to one flat list, dropping empty rectangles up front — the
1697
+ // wire encodes width and height unsigned, so a negative would wrap
1698
+ const flat = [];
1699
+ if (Array.isArray(rects[0])) {
1700
+ for (const r of rects) {
1701
+ if (r[2] > 0 && r[3] > 0) flat.push(r[0], r[1], r[2], r[3]);
1702
+ }
1703
+ } else {
1704
+ for (let i = 0; i + 3 < rects.length; i += 4) {
1705
+ if (rects[i + 2] > 0 && rects[i + 3] > 0)
1706
+ flat.push(rects[i], rects[i + 1], rects[i + 2], rects[i + 3]);
1707
+ }
1708
+ }
1709
+ if (!flat.length) return;
1710
+
1711
+ const clip = this._clips.length ? this._clipRect() : null;
1712
+ if (
1713
+ !matIsIdentity(this._m) ||
1714
+ !isPlainColor(this._fillStyle) ||
1715
+ (this._clips.length && !clip)
1716
+ ) {
1717
+ for (let i = 0; i < flat.length; i += 4)
1718
+ this.fillRect(flat[i], flat[i + 1], flat[i + 2], flat[i + 3]);
1719
+ return;
1720
+ }
1721
+ if (clip && (clip.w === 0 || clip.h === 0)) return; // clipped away
1722
+ const R = this.Render;
1723
+ if (clip) {
1724
+ R.SetPictureClipRectangles(this.picture.id, 0, 0, [
1725
+ clip.x,
1726
+ clip.y,
1727
+ clip.w,
1728
+ clip.h,
1729
+ ]);
1730
+ }
1731
+ // globalAlpha folds into the premultiplied colour — scaling all four
1732
+ // components is exactly what compositing at that opacity means, for
1733
+ // any composite op (the mask slot would only scale the source the same
1734
+ // way)
1735
+ const color = this._foldedColor(this._fillStyle);
1736
+ // stay under the server's maximum request size
1737
+ const chunk = 10000 * 4;
1738
+ for (let i = 0; i < flat.length; i += chunk) {
1739
+ R.FillRectangles(
1740
+ this._op(),
1741
+ this.picture.id,
1742
+ color,
1743
+ i === 0 && flat.length <= chunk ? flat : flat.slice(i, i + chunk),
1744
+ );
1745
+ }
1746
+ if (clip) this._resetPictureClip();
1747
+ this._markDirty();
1748
+ }
1749
+
1655
1750
  strokeRect(x, y, w, h) {
1656
1751
  const m = this._m;
1657
1752
  if (m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 1) {
package/lib/surface.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import Picture from './picture.js';
2
2
  import Pixmap from './pixmap.js';
3
+ import { safeRelease } from './cleanup.js';
3
4
 
4
5
  /**
5
6
  * An offscreen drawing surface: a pixmap, its Picture, and enough of the
@@ -103,6 +104,67 @@ export class Surface {
103
104
  return this.pixmap.getContext(name, ...args);
104
105
  }
105
106
 
107
+ /**
108
+ * Scroll the pixels of `src` (surface coordinates, `{x, y, width, height}`)
109
+ * by (dx, dy) in place, server-side: one CopyArea of the band that
110
+ * survives the shift, in place of the caller re-drawing everything that
111
+ * merely moved. Returns true when the copy was issued; false means
112
+ * "nothing survives the shift here", and the caller repaints `src` exactly
113
+ * as it would have without this method — every refusal is the status quo.
114
+ *
115
+ * Refused when the delta is fractional (a sub-pixel shift changes every
116
+ * pixel, so there is nothing to copy), when it is zero, when nothing of
117
+ * `src` survives the shift after clamping to the surface, or on a
118
+ * destroyed surface.
119
+ *
120
+ * This is `Window#scrollRegion` for a retained offscreen surface, minus
121
+ * the damage bookkeeping a pixmap does not have: the caller draws the
122
+ * exposed strip and composites as usual. The overlapping self-copy is
123
+ * fully defined — pixmap contents cannot be occluded, and the server
124
+ * fetches the source region before storing. The copy goes out with a
125
+ * shared graphicsExposures: 0 GC — one per app and depth, created on
126
+ * first use and released with the connection, where a default GC would
127
+ * emit a NoExposure event per copy — and in-order with the caller's
128
+ * follow-up drawing of the exposed strip on the same connection.
129
+ */
130
+ copyWithin(src, dx, dy) {
131
+ if (this._destroyed) return false;
132
+ if (!Number.isInteger(dx) || !Number.isInteger(dy) || (dx === 0 && dy === 0)) return false;
133
+ const x0 = Math.max(0, Math.floor(src.x));
134
+ const y0 = Math.max(0, Math.floor(src.y));
135
+ const x1 = Math.min(this.width, Math.ceil(src.x + src.width));
136
+ const y1 = Math.min(this.height, Math.ceil(src.y + src.height));
137
+ // the band that survives: dest = clamped src ∩ (clamped src + delta)
138
+ const dstX0 = Math.max(x0, x0 + dx);
139
+ const dstY0 = Math.max(y0, y0 + dy);
140
+ const dstX1 = Math.min(x1, x1 + dx);
141
+ const dstY1 = Math.min(y1, y1 + dy);
142
+ if (dstX1 <= dstX0 || dstY1 <= dstY0) return false;
143
+ const X = this.app.X;
144
+ // a GC is valid for any drawable of its depth on the screen, so one per
145
+ // app and depth serves every surface — the upload-GC pattern in image.js
146
+ const gcs = (this.app._surfaceCopyGC ??= {});
147
+ safeRelease(X, () => {
148
+ let gc = gcs[this.depth];
149
+ if (!gc) {
150
+ gc = gcs[this.depth] = X.AllocID();
151
+ X.CreateGC(gc, this.pixmap.id, { graphicsExposures: 0 });
152
+ }
153
+ X.CopyArea(
154
+ this.pixmap.id,
155
+ this.pixmap.id,
156
+ gc,
157
+ dstX0 - dx,
158
+ dstY0 - dy,
159
+ dstX0,
160
+ dstY0,
161
+ dstX1 - dstX0,
162
+ dstY1 - dstY0
163
+ );
164
+ });
165
+ return true;
166
+ }
167
+
106
168
  destroy() {
107
169
  if (this._destroyed) return;
108
170
  this._destroyed = true;
package/lib/text/font.js CHANGED
@@ -240,6 +240,27 @@ export default class Font {
240
240
  return this.fk.hasGlyphForCodePoint(codepoint);
241
241
  }
242
242
 
243
+ /**
244
+ * Glyph id for a codepoint, or `null` when this face does not cover it —
245
+ * the lookup twin of `hasGlyph`, for callers that position glyphs
246
+ * themselves (a terminal grid, a tabular column) and hand runs straight
247
+ * to `ctx.drawGlyphs` without shaping. See docs/text.md#glyph-runs.
248
+ *
249
+ * `null` rather than 0: fontkit resolves an uncovered codepoint to the
250
+ * `.notdef` glyph, and a caller building runs wants "not covered" as a
251
+ * branch (pick a fallback face) rather than as a tofu box discovered on
252
+ * screen. This is a cmap lookup only — no shaping, so no ligatures,
253
+ * kerning or contextual forms; text that needs those goes through
254
+ * `shape()`.
255
+ *
256
+ * @param {number} codepoint Unicode codepoint (`str.codePointAt(i)`)
257
+ * @returns {number|null} font glyph id, as `shape()` would report in `glyphs[].id`
258
+ */
259
+ glyphIdFor(codepoint) {
260
+ if (!this.fk.hasGlyphForCodePoint(codepoint)) return null;
261
+ return this.fk.glyphForCodePoint(codepoint).id;
262
+ }
263
+
243
264
  /**
244
265
  * Shape a run of text: returns glyphs with pixel-space positioning.
245
266
  * RTL runs come back in visual (left-to-right drawing) order.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ntk",
3
- "version": "7.5.0",
3
+ "version": "7.6.0",
4
4
  "description": "Desktop UI toolkit for X11 with canvas-like 2d and OpenGL rendering",
5
5
  "author": "Andrey Sidorov <sidorares@yandex.ru>",
6
6
  "license": "MIT",