restty 0.1.19 → 0.1.20

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/README.md CHANGED
@@ -101,7 +101,7 @@ restty.copySelectionToClipboard();
101
101
 
102
102
  ### Provide custom fonts
103
103
 
104
- By default, restty uses a CDN font preset. To fully control fonts, disable the preset and pass `fontSources`.
104
+ By default, restty uses a local-first font preset with CDN fallback. To fully control fonts, disable the preset and pass `fontSources`.
105
105
 
106
106
  ```ts
107
107
  const restty = new Restty({
@@ -1,5 +1,5 @@
1
1
  import type { ResttyFontPreset, ResttyFontSource } from "./types";
2
- /** Default font fallback chain with JetBrains Mono, Nerd Fonts symbols, Noto symbols, color emoji, black emoji, and CJK support. */
2
+ /** Local-first default font fallback chain with CDN fallback for JetBrains Mono, Nerd symbols, emoji, and CJK support. */
3
3
  export declare const DEFAULT_FONT_SOURCES: ResttyFontSource[];
4
4
  /** Validates user-provided font sources or returns defaults based on preset (none returns empty array, otherwise default CDN fonts). */
5
5
  export declare function normalizeFontSources(sources: ResttyFontSource[] | undefined, preset: ResttyFontPreset | undefined): ResttyFontSource[];
@@ -154,6 +154,12 @@ export type ResttyAppOptions = {
154
154
  renderer?: "auto" | "webgpu" | "webgl2";
155
155
  /** Font size in CSS pixels. */
156
156
  fontSize?: number;
157
+ /**
158
+ * Font sizing mode used by text-shaper scale resolution.
159
+ * - em: interpret fontSize as EM size
160
+ * - height: interpret fontSize as full font height (ascender-descender-lineGap)
161
+ */
162
+ fontSizeMode?: "em" | "height";
157
163
  /**
158
164
  * Alpha blending strategy.
159
165
  * - native: GPU-native premultiplied alpha
@@ -26598,7 +26598,7 @@ function buildFontAtlasIfNeeded(params) {
26598
26598
  if (union.size === 0) {
26599
26599
  return { rebuilt: false, atlas: null, rgba: null, preferNearest: false };
26600
26600
  }
26601
- const useHinting = fontIndex === 0 && !isSymbol;
26601
+ const useHinting = false;
26602
26602
  const atlasPadding = isSymbol ? Math.max(constants.atlasPadding, constants.symbolAtlasPadding) : constants.atlasPadding;
26603
26603
  const atlasMaxSize = isSymbol ? constants.symbolAtlasMaxSize : constants.defaultAtlasMaxSize;
26604
26604
  const glyphPixelMode = resolveGlyphPixelMode(entry);
@@ -26690,9 +26690,79 @@ function buildFontAtlasIfNeeded(params) {
26690
26690
 
26691
26691
  // src/app/font-sources.ts
26692
26692
  var DEFAULT_FONT_SOURCES = [
26693
+ {
26694
+ type: "local",
26695
+ matchers: [
26696
+ "jetbrainsmono nerd font",
26697
+ "jetbrains mono nerd font",
26698
+ "jetbrains mono nl nerd font mono",
26699
+ "jetbrains mono",
26700
+ "jetbrainsmono"
26701
+ ],
26702
+ label: "JetBrains Mono Nerd Font Regular (Local)"
26703
+ },
26704
+ {
26705
+ type: "local",
26706
+ matchers: [
26707
+ "jetbrainsmono nerd font bold",
26708
+ "jetbrains mono nerd font bold",
26709
+ "jetbrains mono nl nerd font mono bold",
26710
+ "jetbrains mono bold",
26711
+ "jetbrainsmono bold"
26712
+ ],
26713
+ label: "JetBrains Mono Nerd Font Bold (Local)"
26714
+ },
26715
+ {
26716
+ type: "local",
26717
+ matchers: [
26718
+ "jetbrainsmono nerd font italic",
26719
+ "jetbrains mono nerd font italic",
26720
+ "jetbrains mono nl nerd font mono italic",
26721
+ "jetbrains mono italic",
26722
+ "jetbrainsmono italic"
26723
+ ],
26724
+ label: "JetBrains Mono Nerd Font Italic (Local)"
26725
+ },
26726
+ {
26727
+ type: "local",
26728
+ matchers: [
26729
+ "jetbrainsmono nerd font bold italic",
26730
+ "jetbrains mono nerd font bold italic",
26731
+ "jetbrains mono nl nerd font mono bold italic",
26732
+ "jetbrains mono bold italic",
26733
+ "jetbrainsmono bold italic"
26734
+ ],
26735
+ label: "JetBrains Mono Nerd Font Bold Italic (Local)"
26736
+ },
26693
26737
  {
26694
26738
  type: "url",
26695
- url: "https://cdn.jsdelivr.net/gh/ryanoasis/nerd-fonts@v3.4.0/patched-fonts/JetBrainsMono/NoLigatures/Regular/JetBrainsMonoNLNerdFontMono-Regular.ttf"
26739
+ url: "https://cdn.jsdelivr.net/gh/ryanoasis/nerd-fonts@v3.4.0/patched-fonts/JetBrainsMono/NoLigatures/Regular/JetBrainsMonoNLNerdFontMono-Regular.ttf",
26740
+ label: "JetBrains Mono Nerd Font Regular"
26741
+ },
26742
+ {
26743
+ type: "url",
26744
+ url: "https://cdn.jsdelivr.net/gh/ryanoasis/nerd-fonts@v3.4.0/patched-fonts/JetBrainsMono/NoLigatures/Bold/JetBrainsMonoNLNerdFontMono-Bold.ttf",
26745
+ label: "JetBrains Mono Nerd Font Bold"
26746
+ },
26747
+ {
26748
+ type: "url",
26749
+ url: "https://cdn.jsdelivr.net/gh/ryanoasis/nerd-fonts@v3.4.0/patched-fonts/JetBrainsMono/NoLigatures/Italic/JetBrainsMonoNLNerdFontMono-Italic.ttf",
26750
+ label: "JetBrains Mono Nerd Font Italic"
26751
+ },
26752
+ {
26753
+ type: "url",
26754
+ url: "https://cdn.jsdelivr.net/gh/ryanoasis/nerd-fonts@v3.4.0/patched-fonts/JetBrainsMono/NoLigatures/BoldItalic/JetBrainsMonoNLNerdFontMono-BoldItalic.ttf",
26755
+ label: "JetBrains Mono Nerd Font Bold Italic"
26756
+ },
26757
+ {
26758
+ type: "local",
26759
+ matchers: [
26760
+ "symbols nerd font mono",
26761
+ "symbols nerd font",
26762
+ "nerd fonts symbols",
26763
+ "nerdfontssymbolsonly"
26764
+ ],
26765
+ label: "Symbols Nerd Font (Local)"
26696
26766
  },
26697
26767
  {
26698
26768
  type: "url",
@@ -50195,6 +50265,12 @@ var DEFAULT_SYMBOL_CONSTRAINT = {
50195
50265
  align_vertical: "center",
50196
50266
  max_constraint_width: 1
50197
50267
  };
50268
+ var DEFAULT_APPLE_SYMBOLS_CONSTRAINT = {
50269
+ size: "cover",
50270
+ align_horizontal: "center",
50271
+ align_vertical: "center",
50272
+ max_constraint_width: 1
50273
+ };
50198
50274
  var DEFAULT_EMOJI_CONSTRAINT = {
50199
50275
  size: "cover",
50200
50276
  align_horizontal: "center",
@@ -50305,6 +50381,7 @@ function createResttyApp(options) {
50305
50381
  const OVERLAY_SCROLLBAR_MARGIN_CSS_PX = 4;
50306
50382
  const OVERLAY_SCROLLBAR_INSET_Y_CSS_PX = 2;
50307
50383
  const OVERLAY_SCROLLBAR_MIN_THUMB_CSS_PX = 28;
50384
+ const OVERLAY_SCROLLBAR_CAP_SUPERSAMPLE = 8;
50308
50385
  let paused = false;
50309
50386
  let backend = "none";
50310
50387
  let preferredRenderer = options.renderer ?? "auto";
@@ -50627,24 +50704,63 @@ function createResttyApp(options) {
50627
50704
  const y02 = Math.round(y);
50628
50705
  const width = Math.max(1, Math.round(w));
50629
50706
  const height = Math.max(1, Math.round(h));
50630
- const radius = Math.min(Math.floor(width * 0.5), Math.floor(height * 0.5));
50707
+ const radius = Math.min(width * 0.5, height * 0.5);
50631
50708
  if (radius <= 0) {
50632
50709
  pushRectBox(out, x02, y02, width, height, color);
50633
50710
  return;
50634
50711
  }
50635
- const middleH = height - radius * 2;
50712
+ const capRows = Math.min(height, Math.max(1, Math.ceil(radius)));
50713
+ const middleStart = capRows;
50714
+ const middleEnd = Math.max(middleStart, height - capRows);
50715
+ const middleH = middleEnd - middleStart;
50636
50716
  if (middleH > 0) {
50637
- pushRectBox(out, x02, y02 + radius, width, middleH, color);
50717
+ pushRectBox(out, x02, y02 + middleStart, width, middleH, color);
50638
50718
  }
50639
50719
  const radiusSq = radius * radius;
50640
- for (let row = 0;row < radius; row += 1) {
50641
- const dy = radius - row - 0.5;
50642
- const inset = Math.max(0, Math.floor(radius - Math.sqrt(Math.max(0, radiusSq - dy * dy))));
50643
- const rowW = width - inset * 2;
50644
- if (rowW <= 0)
50645
- continue;
50646
- pushRectBox(out, x02 + inset, y02 + row, rowW, 1, color);
50647
- pushRectBox(out, x02 + inset, y02 + height - 1 - row, rowW, 1, color);
50720
+ const centerX = width * 0.5;
50721
+ const topCenterY = radius;
50722
+ const bottomCenterY = height - radius;
50723
+ const samplesPerAxis = Math.max(1, OVERLAY_SCROLLBAR_CAP_SUPERSAMPLE | 0);
50724
+ const totalSamples = samplesPerAxis * samplesPerAxis;
50725
+ const invSamples = 1 / totalSamples;
50726
+ const alphaBase = color[3];
50727
+ const alphaEpsilon = 1 / 255;
50728
+ const sampleCapPixelCoverage = (localX, localY, centerY) => {
50729
+ let hits = 0;
50730
+ for (let sy = 0;sy < samplesPerAxis; sy += 1) {
50731
+ const sampleY = localY + (sy + 0.5) / samplesPerAxis;
50732
+ for (let sx = 0;sx < samplesPerAxis; sx += 1) {
50733
+ const sampleX = localX + (sx + 0.5) / samplesPerAxis;
50734
+ const dx = sampleX - centerX;
50735
+ const dy = sampleY - centerY;
50736
+ if (dx * dx + dy * dy <= radiusSq)
50737
+ hits += 1;
50738
+ }
50739
+ }
50740
+ return hits * invSamples;
50741
+ };
50742
+ for (let row = 0;row < capRows; row += 1) {
50743
+ const topY = y02 + row;
50744
+ const bottomY = y02 + height - 1 - row;
50745
+ for (let col = 0;col < width; col += 1) {
50746
+ const coverageTop = sampleCapPixelCoverage(col, row, topCenterY);
50747
+ if (coverageTop > 0) {
50748
+ const alpha = alphaBase * coverageTop;
50749
+ if (alpha > alphaEpsilon) {
50750
+ out.push(x02 + col, topY, 1, 1, color[0], color[1], color[2], alpha);
50751
+ }
50752
+ }
50753
+ if (bottomY !== topY) {
50754
+ const localBottomY = height - 1 - row;
50755
+ const coverageBottom = sampleCapPixelCoverage(col, localBottomY, bottomCenterY);
50756
+ if (coverageBottom > 0) {
50757
+ const alpha = alphaBase * coverageBottom;
50758
+ if (alpha > alphaEpsilon) {
50759
+ out.push(x02 + col, bottomY, 1, 1, color[0], color[1], color[2], alpha);
50760
+ }
50761
+ }
50762
+ }
50763
+ }
50648
50764
  }
50649
50765
  }
50650
50766
  function appendOverlayScrollbar(overlayData, total, offset, len) {
@@ -51833,7 +51949,7 @@ function createResttyApp(options) {
51833
51949
  font: null,
51834
51950
  fonts: [],
51835
51951
  fontSizePx: 0,
51836
- sizeMode: "height",
51952
+ sizeMode: options.fontSizeMode === "em" ? "em" : "height",
51837
51953
  fontPickCache: new Map
51838
51954
  };
51839
51955
  const fontConfig = {
@@ -53312,6 +53428,33 @@ function createResttyApp(options) {
53312
53428
  const normalizedMatchers = matchers.map((matcher) => matcher.toLowerCase()).filter(Boolean);
53313
53429
  if (!normalizedMatchers.length)
53314
53430
  return null;
53431
+ const detectStyleHint = (value) => {
53432
+ const text = value.toLowerCase();
53433
+ let weight = 400;
53434
+ if (/\b(thin|hairline)\b/.test(text))
53435
+ weight = 100;
53436
+ else if (/\b(extra[- ]?light|ultra[- ]?light)\b/.test(text))
53437
+ weight = 200;
53438
+ else if (/\blight\b/.test(text))
53439
+ weight = 300;
53440
+ else if (/\bmedium\b/.test(text))
53441
+ weight = 500;
53442
+ else if (/\b(semi[- ]?bold|demi[- ]?bold)\b/.test(text))
53443
+ weight = 600;
53444
+ else if (/\bbold\b/.test(text))
53445
+ weight = 700;
53446
+ else if (/\b(extra[- ]?bold|ultra[- ]?bold)\b/.test(text))
53447
+ weight = 800;
53448
+ else if (/\b(black|heavy)\b/.test(text))
53449
+ weight = 900;
53450
+ return {
53451
+ bold: /\b(bold|semi[- ]?bold|demi[- ]?bold|extra[- ]?bold|black|heavy)\b/.test(text),
53452
+ italic: /\b(italic|oblique)\b/.test(text),
53453
+ regular: /\b(regular|book|roman|normal)\b/.test(text),
53454
+ weight
53455
+ };
53456
+ };
53457
+ const sourceHint = detectStyleHint(`${label} ${normalizedMatchers.join(" ")}`);
53315
53458
  const queryPermission = nav.permissions?.query;
53316
53459
  if (queryPermission) {
53317
53460
  try {
@@ -53323,13 +53466,49 @@ function createResttyApp(options) {
53323
53466
  }
53324
53467
  try {
53325
53468
  const fonts = await queryLocalFonts();
53326
- const match = fonts.find((font) => {
53469
+ const matches = fonts.filter((font) => {
53327
53470
  const name = `${font.family ?? ""} ${font.fullName ?? ""} ${font.postscriptName ?? ""}`.toLowerCase();
53328
53471
  return normalizedMatchers.some((matcher) => name.includes(matcher));
53329
53472
  });
53330
- if (match) {
53473
+ if (matches.length) {
53474
+ const scoreMatch = (font) => {
53475
+ const name = `${font.family ?? ""} ${font.fullName ?? ""} ${font.postscriptName ?? ""}`.toLowerCase();
53476
+ const hint = detectStyleHint(name);
53477
+ let score = 0;
53478
+ for (let i3 = 0;i3 < normalizedMatchers.length; i3 += 1) {
53479
+ if (name.includes(normalizedMatchers[i3]))
53480
+ score += 8;
53481
+ }
53482
+ if (sourceHint.bold || sourceHint.italic) {
53483
+ score += sourceHint.bold === hint.bold ? 40 : -40;
53484
+ score += sourceHint.italic === hint.italic ? 40 : -40;
53485
+ } else {
53486
+ score += !hint.bold && !hint.italic ? 60 : -30;
53487
+ }
53488
+ const targetWeight = sourceHint.bold ? 700 : 400;
53489
+ score -= Math.abs((hint.weight ?? 400) - targetWeight) * 0.25;
53490
+ if (!sourceHint.bold && hint.weight === 400)
53491
+ score += 12;
53492
+ if (!sourceHint.bold && hint.weight < 350)
53493
+ score -= 12;
53494
+ if (!sourceHint.bold && hint.weight > 650)
53495
+ score -= 8;
53496
+ if (sourceHint.regular && !hint.bold && !hint.italic)
53497
+ score += 20;
53498
+ return score;
53499
+ };
53500
+ let match = matches[0];
53501
+ let bestScore = Number.NEGATIVE_INFINITY;
53502
+ for (let i3 = 0;i3 < matches.length; i3 += 1) {
53503
+ const candidate = matches[i3];
53504
+ const candidateScore = scoreMatch(candidate);
53505
+ if (candidateScore > bestScore) {
53506
+ bestScore = candidateScore;
53507
+ match = candidate;
53508
+ }
53509
+ }
53331
53510
  const matchedName = `${match.family ?? ""} ${match.fullName ?? ""} ${match.postscriptName ?? ""}`.trim();
53332
- console.log(`[font] local matched (${label}): ${matchedName || "unnamed"}`);
53511
+ console.log(`[font] local matched (${label}): ${matchedName || "unnamed"} score=${bestScore}`);
53333
53512
  const blob = await match.blob();
53334
53513
  return blob.arrayBuffer();
53335
53514
  }
@@ -53550,10 +53729,10 @@ function createResttyApp(options) {
53550
53729
  }
53551
53730
  return "auto";
53552
53731
  }
53553
- function pickFontIndexForText2(text, expectedSpan = 1) {
53732
+ function pickFontIndexForText2(text, expectedSpan = 1, stylePreference = "regular") {
53554
53733
  if (!fontState.fonts.length)
53555
53734
  return 0;
53556
- const cacheKey = `${expectedSpan}:${text}`;
53735
+ const cacheKey = `${expectedSpan}:${stylePreference}:${text}`;
53557
53736
  const cached = fontState.fontPickCache.get(cacheKey);
53558
53737
  if (cached !== undefined)
53559
53738
  return cached;
@@ -53561,6 +53740,17 @@ function createResttyApp(options) {
53561
53740
  const firstCp = text.codePointAt(0) ?? 0;
53562
53741
  const nerdSymbol = isNerdSymbolCodepoint(firstCp);
53563
53742
  const presentation = resolvePresentationPreference2(text, chars);
53743
+ const styleHintsEnabled = stylePreference !== "regular" && presentation !== "emoji" && !nerdSymbol;
53744
+ const hasBoldHint = (entry) => /\bbold\b/i.test(entry.label ?? "");
53745
+ const hasItalicHint = (entry) => /\b(italic|oblique)\b/i.test(entry.label ?? "");
53746
+ const stylePredicates = stylePreference === "bold_italic" ? [
53747
+ (entry) => hasBoldHint(entry) && hasItalicHint(entry),
53748
+ (entry) => hasBoldHint(entry),
53749
+ (entry) => hasItalicHint(entry)
53750
+ ] : stylePreference === "bold" ? [(entry) => hasBoldHint(entry) && !hasItalicHint(entry), (entry) => hasBoldHint(entry)] : stylePreference === "italic" ? [
53751
+ (entry) => hasItalicHint(entry) && !hasBoldHint(entry),
53752
+ (entry) => hasItalicHint(entry)
53753
+ ] : [];
53564
53754
  const pickFirstMatch = (predicate) => {
53565
53755
  for (let i3 = 0;i3 < fontState.fonts.length; i3 += 1) {
53566
53756
  const entry = fontState.fonts[i3];
@@ -53580,6 +53770,21 @@ function createResttyApp(options) {
53580
53770
  }
53581
53771
  return -1;
53582
53772
  };
53773
+ const pickWithStyle = (predicate) => {
53774
+ if (styleHintsEnabled) {
53775
+ for (let i3 = 0;i3 < stylePredicates.length; i3 += 1) {
53776
+ const stylePredicate = stylePredicates[i3];
53777
+ const styledIndex = pickFirstMatch((entry) => {
53778
+ if (!stylePredicate(entry))
53779
+ return false;
53780
+ return predicate ? !!predicate(entry) : true;
53781
+ });
53782
+ if (styledIndex >= 0)
53783
+ return styledIndex;
53784
+ }
53785
+ }
53786
+ return pickFirstMatch(predicate);
53787
+ };
53583
53788
  const tryIndex = (index) => {
53584
53789
  if (index < 0)
53585
53790
  return null;
@@ -53587,7 +53792,7 @@ function createResttyApp(options) {
53587
53792
  return index;
53588
53793
  };
53589
53794
  if (nerdSymbol) {
53590
- const symbolIndex = pickFirstMatch((entry) => isNerdSymbolFont(entry) || isSymbolFont(entry));
53795
+ const symbolIndex = pickWithStyle((entry) => isNerdSymbolFont(entry) || isSymbolFont(entry));
53591
53796
  const result = tryIndex(symbolIndex);
53592
53797
  if (result !== null)
53593
53798
  return result;
@@ -53603,7 +53808,7 @@ function createResttyApp(options) {
53603
53808
  if (result !== null)
53604
53809
  return result;
53605
53810
  }
53606
- const firstIndex = pickFirstMatch();
53811
+ const firstIndex = pickWithStyle();
53607
53812
  if (firstIndex >= 0) {
53608
53813
  setBoundedMap(fontState.fontPickCache, cacheKey, firstIndex, FONT_PICK_CACHE_LIMIT);
53609
53814
  return firstIndex;
@@ -53611,6 +53816,24 @@ function createResttyApp(options) {
53611
53816
  setBoundedMap(fontState.fontPickCache, cacheKey, 0, FONT_PICK_CACHE_LIMIT);
53612
53817
  return 0;
53613
53818
  }
53819
+ function stylePreferenceFromFlags(bold, italic) {
53820
+ if (bold && italic)
53821
+ return "bold_italic";
53822
+ if (bold)
53823
+ return "bold";
53824
+ if (italic)
53825
+ return "italic";
53826
+ return "regular";
53827
+ }
53828
+ function isAppleSymbolsFont(entry) {
53829
+ return !!entry && /\bapple symbols\b/i.test(entry.label ?? "");
53830
+ }
53831
+ function fontEntryHasBoldStyle(entry) {
53832
+ return !!entry && /\bbold\b/i.test(entry.label ?? "");
53833
+ }
53834
+ function fontEntryHasItalicStyle(entry) {
53835
+ return !!entry && /\b(italic|oblique)\b/i.test(entry.label ?? "");
53836
+ }
53614
53837
  function computeCellMetrics2() {
53615
53838
  const primary = fontState.fonts[0];
53616
53839
  if (!primary)
@@ -54319,7 +54542,7 @@ function createResttyApp(options) {
54319
54542
  }
54320
54543
  if (extra > 0 && text.trim() === "")
54321
54544
  continue;
54322
- const fontIndex = pickFontIndexForText2(text, baseSpan);
54545
+ const fontIndex = pickFontIndexForText2(text, baseSpan, stylePreferenceFromFlags(bold, italic));
54323
54546
  const fontEntry = fontState.fonts[fontIndex] ?? fontState.fonts[0];
54324
54547
  const shaped = shapeClusterWithFont(fontEntry, text);
54325
54548
  if (!shaped.glyphs.length)
@@ -54588,7 +54811,8 @@ function createResttyApp(options) {
54588
54811
  let y = item.baseY + baselineAdjust - metrics.bearingY * bitmapScale - glyph.yOffset * itemScale;
54589
54812
  if (!glyphConstrained && symbolLike && item.cp) {
54590
54813
  const nerdConstraint = resolveSymbolConstraint(item.cp);
54591
- const constraint = nerdConstraint ?? (colorGlyph ? DEFAULT_EMOJI_CONSTRAINT : DEFAULT_SYMBOL_CONSTRAINT);
54814
+ const defaultConstraint = isAppleSymbolsFont(entry) ? DEFAULT_APPLE_SYMBOLS_CONSTRAINT : DEFAULT_SYMBOL_CONSTRAINT;
54815
+ const constraint = nerdConstraint ?? (colorGlyph ? DEFAULT_EMOJI_CONSTRAINT : defaultConstraint);
54592
54816
  const rowY = item.baseY - yPad - baselineOffset;
54593
54817
  const constraintWidth = Math.max(1, item.constraintWidth ?? Math.round(maxWidth / cellW));
54594
54818
  const adjusted = constrainGlyphBox({
@@ -54625,17 +54849,24 @@ function createResttyApp(options) {
54625
54849
  const glyphData = useNearest ? glyphDataNearest : glyphDataLinear;
54626
54850
  const italic = !!item.italic;
54627
54851
  const bold = !!item.bold;
54628
- const slant = italic && !colorGlyph ? gh * ITALIC_SLANT : 0;
54629
- const boldOffset = bold && !colorGlyph ? Math.max(1, Math.round(gw * BOLD_OFFSET)) : 0;
54852
+ const syntheticItalic = italic && !fontEntryHasItalicStyle(entry);
54853
+ const syntheticBold = bold && !fontEntryHasBoldStyle(entry);
54854
+ const slant = syntheticItalic && !colorGlyph ? gh * ITALIC_SLANT : 0;
54855
+ const boldOffset = syntheticBold && !colorGlyph ? Math.max(1, Math.round(gw * BOLD_OFFSET)) : 0;
54630
54856
  const renderMode = colorGlyph ? GLYPH_RENDER_MODE_COLOR : GLYPH_RENDER_MODE_MONO;
54631
54857
  const pushGlyph = (xPos) => {
54632
54858
  glyphData.push(xPos, py, gw, gh, u02, v02, u12, v12, item.fg[0], item.fg[1], item.fg[2], item.fg[3], bg[0], bg[1], bg[2], bg[3], slant, renderMode);
54633
54859
  };
54634
54860
  pushGlyph(px);
54635
54861
  if (boldOffset > 0) {
54636
- const maxX2 = item.x + maxWidth;
54637
- const bx = Math.min(px + boldOffset, Math.round(maxX2 - gw));
54638
- if (bx !== px)
54862
+ const minGlyphX = Math.round(item.x);
54863
+ const maxGlyphX = Math.round(item.x + maxWidth - gw);
54864
+ let bx = clamp(px + boldOffset, minGlyphX, maxGlyphX);
54865
+ if (bx === px)
54866
+ bx = clamp(px - boldOffset, minGlyphX, maxGlyphX);
54867
+ if (bx === px)
54868
+ pushGlyph(px);
54869
+ else
54639
54870
  pushGlyph(bx);
54640
54871
  }
54641
54872
  penX += glyph.xAdvance;
@@ -55177,7 +55408,7 @@ function createResttyApp(options) {
55177
55408
  }
55178
55409
  if (extra > 0 && text.trim() === "")
55179
55410
  continue;
55180
- const fontIndex = pickFontIndexForText2(text, baseSpan);
55411
+ const fontIndex = pickFontIndexForText2(text, baseSpan, stylePreferenceFromFlags(bold, italic));
55181
55412
  const fontEntry = fontState.fonts[fontIndex] ?? fontState.fonts[0];
55182
55413
  const shaped = shapeClusterWithFont(fontEntry, text);
55183
55414
  if (!shaped.glyphs.length)
@@ -55547,7 +55778,8 @@ function createResttyApp(options) {
55547
55778
  let y = item.baseY + baselineAdjust - metrics.bearingY * bitmapScale - glyph.yOffset * itemScale;
55548
55779
  if (!glyphConstrained && symbolLike && item.cp) {
55549
55780
  const nerdConstraint = resolveSymbolConstraint(item.cp);
55550
- const constraint = nerdConstraint ?? (colorGlyph ? DEFAULT_EMOJI_CONSTRAINT : DEFAULT_SYMBOL_CONSTRAINT);
55781
+ const defaultConstraint = isAppleSymbolsFont(entry) ? DEFAULT_APPLE_SYMBOLS_CONSTRAINT : DEFAULT_SYMBOL_CONSTRAINT;
55782
+ const constraint = nerdConstraint ?? (colorGlyph ? DEFAULT_EMOJI_CONSTRAINT : defaultConstraint);
55551
55783
  const rowY = item.baseY - yPad - baselineOffset;
55552
55784
  const constraintWidth = Math.max(1, item.constraintWidth ?? Math.round(maxWidth / cellW));
55553
55785
  const adjusted = constrainGlyphBox({
@@ -55576,17 +55808,24 @@ function createResttyApp(options) {
55576
55808
  const v12 = (metrics.atlasY + metrics.height - insetY) / atlasH;
55577
55809
  const italic = !!item.italic;
55578
55810
  const bold = !!item.bold;
55579
- const slant = italic && !colorGlyph ? gh * ITALIC_SLANT : 0;
55580
- const boldOffset = bold && !colorGlyph ? Math.max(1, Math.round(gw * BOLD_OFFSET)) : 0;
55811
+ const syntheticItalic = italic && !fontEntryHasItalicStyle(entry);
55812
+ const syntheticBold = bold && !fontEntryHasBoldStyle(entry);
55813
+ const slant = syntheticItalic && !colorGlyph ? gh * ITALIC_SLANT : 0;
55814
+ const boldOffset = syntheticBold && !colorGlyph ? Math.max(1, Math.round(gw * BOLD_OFFSET)) : 0;
55581
55815
  const renderMode = colorGlyph ? GLYPH_RENDER_MODE_COLOR : GLYPH_RENDER_MODE_MONO;
55582
55816
  const pushGlyph = (xPos) => {
55583
55817
  glyphData.push(xPos, py, gw, gh, u02, v02, u12, v12, item.fg[0], item.fg[1], item.fg[2], item.fg[3], bg[0], bg[1], bg[2], bg[3], slant, renderMode);
55584
55818
  };
55585
55819
  pushGlyph(px);
55586
55820
  if (boldOffset > 0) {
55587
- const maxX2 = item.x + maxWidth;
55588
- const bx = Math.min(px + boldOffset, Math.round(maxX2 - gw));
55589
- if (bx !== px)
55821
+ const minGlyphX = Math.round(item.x);
55822
+ const maxGlyphX = Math.round(item.x + maxWidth - gw);
55823
+ let bx = clamp(px + boldOffset, minGlyphX, maxGlyphX);
55824
+ if (bx === px)
55825
+ bx = clamp(px - boldOffset, minGlyphX, maxGlyphX);
55826
+ if (bx === px)
55827
+ pushGlyph(px);
55828
+ else
55590
55829
  pushGlyph(bx);
55591
55830
  }
55592
55831
  penX += glyph.xAdvance;
package/dist/internal.js CHANGED
@@ -89,7 +89,7 @@ import {
89
89
  updateComposition,
90
90
  updateGridState,
91
91
  updateImePosition
92
- } from "./chunk-53vdvhe3.js";
92
+ } from "./chunk-ef12eja6.js";
93
93
  // src/selection/selection.ts
94
94
  function createSelectionState() {
95
95
  return {
package/dist/restty.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  isBuiltinThemeName,
8
8
  listBuiltinThemeNames,
9
9
  parseGhosttyTheme
10
- } from "./chunk-53vdvhe3.js";
10
+ } from "./chunk-ef12eja6.js";
11
11
  export {
12
12
  parseGhosttyTheme,
13
13
  listBuiltinThemeNames,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "restty",
3
- "version": "0.1.19",
3
+ "version": "0.1.20",
4
4
  "description": "Browser terminal rendering library powered by WASM, WebGPU/WebGL2, and TypeScript text shaping.",
5
5
  "keywords": [
6
6
  "terminal",