ntk 3.5.1 → 3.5.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.
@@ -754,6 +754,19 @@ class RenderingContext2d {
754
754
  this._markDirty();
755
755
  return;
756
756
  }
757
+ // Fast path: a rectangular clip is something the server can do itself.
758
+ // Two small requests around the ordinary glyph composite, instead of
759
+ // clearing and compositing a full-surface a8 mask three times — which
760
+ // costs the same on the wire but many times the pixel work.
761
+ const rect = this._clipRect();
762
+ if (rect) {
763
+ if (rect.w === 0 || rect.h === 0) return;
764
+ R.SetPictureClipRectangles(this.picture.id, 0, 0, [rect.x, rect.y, rect.w, rect.h]);
765
+ drawGlyphRuns(app, op, src.id, this.picture.id, positioned);
766
+ R.SetPictureClipRectangles(this.picture.id, 0, 0, [0, 0, this.width, this.height]);
767
+ this._markDirty();
768
+ return;
769
+ }
757
770
  this._ensureFillMask();
758
771
  this._glyphSource ??= this.createSolidPicture(1, 1, 1, 1);
759
772
  R.FillRectangles(R.PictOp.Src, this.fillMask.id, [0, 0, 0, 0], [0, 0, this.width, this.height]);
@@ -841,9 +854,66 @@ class RenderingContext2d {
841
854
  * Cleared by restore() to the state at the matching save() — like the
842
855
  * browser canvas, there is no other way to widen the clip again.
843
856
  */
857
+ /**
858
+ * The axis-aligned rectangle a flattened path describes, or null. Clips
859
+ * in practice are almost always rectangles — a content box, a scroll
860
+ * viewport, `overflow: hidden` — and a rectangular clip can be pushed
861
+ * to the server instead of rasterized into a mask.
862
+ */
863
+ static _rectOfPolys(polys) {
864
+ if (polys.length !== 1) return null;
865
+ const { pts } = polys[0];
866
+ if (pts.length !== 8) return null;
867
+ const [x0, y0, x1, y1, x2, y2, x3, y3] = pts;
868
+ const axis =
869
+ (y0 === y1 && x1 === x2 && y2 === y3 && x3 === x0) ||
870
+ (x0 === x1 && y1 === y2 && x2 === x3 && y3 === y0);
871
+ if (!axis) return null;
872
+ const xs = [x0, x1, x2, x3];
873
+ const ys = [y0, y1, y2, y3];
874
+ const x = Math.min(...xs);
875
+ const y = Math.min(...ys);
876
+ const w = Math.max(...xs) - x;
877
+ const h = Math.max(...ys) - y;
878
+ if (!(w > 0 && h > 0)) return null;
879
+ // Server clip rectangles are integers, so a fractional edge could only
880
+ // be honoured by rounding — which would differ from the mask path's
881
+ // antialiased edge by up to a pixel. Rare, so leave those to the mask
882
+ // rather than quietly changing what they look like.
883
+ const integral = (v) => Math.abs(v - Math.round(v)) < 1e-6;
884
+ if (![x, y, w, h].every(integral)) return null;
885
+ return { x: Math.round(x), y: Math.round(y), w: Math.round(w), h: Math.round(h) };
886
+ }
887
+
888
+ /** Intersection of the whole clip stack, or null if any part is not a
889
+ * rectangle (or the stack is empty). */
890
+ _clipRect() {
891
+ if (!this._clips.length) return null;
892
+ let out = null;
893
+ for (const entry of this._clips) {
894
+ const r = entry.rect;
895
+ if (!r) return null;
896
+ if (!out) {
897
+ out = { ...r };
898
+ continue;
899
+ }
900
+ const x = Math.max(out.x, r.x);
901
+ const y = Math.max(out.y, r.y);
902
+ const right = Math.min(out.x + out.w, r.x + r.w);
903
+ const bottom = Math.min(out.y + out.h, r.y + r.h);
904
+ out = { x, y, w: Math.max(0, right - x), h: Math.max(0, bottom - y) };
905
+ }
906
+ // clamp into the surface: the server rejects out-of-range rectangles
907
+ const x = Math.max(0, Math.min(out.x, this.width));
908
+ const y = Math.max(0, Math.min(out.y, this.height));
909
+ const w = Math.max(0, Math.min(out.x + out.w, this.width) - x);
910
+ const h = Math.max(0, Math.min(out.y + out.h, this.height) - y);
911
+ return { x, y, w, h };
912
+ }
913
+
844
914
  clip(...args) {
845
915
  const { polys, rule } = this._polysFor(args);
846
- const entry = { polys, rule };
916
+ const entry = { polys, rule, rect: RenderingContext2d._rectOfPolys(polys) };
847
917
  // copy-on-write: earlier save() snapshots keep their own clip list
848
918
  this._clips = this._clips.concat([entry]);
849
919
 
@@ -328,29 +328,33 @@ export class TextLayout {
328
328
  draw(ctx, x = 0, y = 0) {
329
329
  const app = ctx.window.app;
330
330
  const Render = app.display.Render;
331
+ // One batch for the whole layout, not one per line: every positioned
332
+ // run carries its own absolute baseline, so a uniformly coloured
333
+ // paragraph becomes a single glyph composite instead of one per line.
334
+ // Only a colour change forces a flush.
335
+ let batch = [];
336
+ let batchColor;
337
+ const flush = () => {
338
+ if (batch.length === 0) return;
339
+ const src = batchColor ? ctx._stylePicture(batchColor) : ctx._backgroundPicture;
340
+ // via the context so the clip is applied (drawGlyphRuns composites
341
+ // straight onto the picture and cannot see it)
342
+ if (typeof ctx.drawGlyphs === 'function') {
343
+ ctx.drawGlyphs(Render.PictOp.Over, src, batch);
344
+ } else {
345
+ drawGlyphRuns(app, Render.PictOp.Over, src.id, ctx.picture.id, batch);
346
+ }
347
+ batch = [];
348
+ };
331
349
  for (const line of this.lines) {
332
- let batch = [];
333
- let batchColor;
334
- const flush = () => {
335
- if (batch.length === 0) return;
336
- const src = batchColor ? ctx._stylePicture(batchColor) : ctx._backgroundPicture;
337
- // via the context so the clip mask is applied (drawGlyphRuns
338
- // composites straight onto the picture and cannot see it)
339
- if (typeof ctx.drawGlyphs === 'function') {
340
- ctx.drawGlyphs(Render.PictOp.Over, src, batch);
341
- } else {
342
- drawGlyphRuns(app, Render.PictOp.Over, src.id, ctx.picture.id, batch);
343
- }
344
- batch = [];
345
- };
346
350
  for (const r of line.runs) {
347
351
  const color = r.span.color;
348
352
  if (batch.length && color !== batchColor) flush();
349
353
  batchColor = color;
350
354
  batch.push({ run: r.run, x: x + line.x + r.x, y: y + line.baseline });
351
355
  }
352
- flush();
353
356
  }
357
+ flush();
354
358
  ctx._markDirty();
355
359
  return this;
356
360
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ntk",
3
- "version": "3.5.1",
3
+ "version": "3.5.2",
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",