react-x11 2.25.0 → 2.26.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-x11",
3
- "version": "2.25.0",
3
+ "version": "2.26.1",
4
4
  "description": "react renderer with X11 as a target",
5
5
  "main": "./src/index.js",
6
6
  "files": [
@@ -150,6 +150,31 @@ function unpackLines(lineData = NO_GEOMETRY, runData = NO_GEOMETRY) {
150
150
  return lines;
151
151
  }
152
152
 
153
+ /**
154
+ * The one line of an empty paragraph, placed the way the native places a
155
+ * line with text in it: a `lineHeight` multiplies the face's natural line,
156
+ * and the leading is split either side of the glyphs (CSS half-leading,
157
+ * which ntk and `@windowkit/appkit` >= 0.15 both follow).
158
+ */
159
+ function blankLine(metrics, lineHeight, x) {
160
+ const { ascent, descent } = metrics;
161
+ const natural = metrics.lineHeight ?? ascent + descent;
162
+ const multiple = lineHeight >= 0 && lineHeight < Infinity ? lineHeight : 1;
163
+ const height = natural * multiple;
164
+ return {
165
+ x,
166
+ y: 0,
167
+ width: 0,
168
+ height,
169
+ baseline: (height - (ascent + descent)) / 2 + ascent,
170
+ ascent,
171
+ descent,
172
+ start: 0,
173
+ end: 0,
174
+ runs: [],
175
+ };
176
+ }
177
+
153
178
  /** Whether two spans read the same, field for field. */
154
179
  function sameSpan(a, b) {
155
180
  if (a === b) return true;
@@ -302,6 +327,19 @@ class CocoaTextLayout {
302
327
  ctx._drawLayout(this, x, y);
303
328
  }
304
329
 
330
+ /**
331
+ * An empty paragraph as ntk lays it out: one line with no runs, as tall as
332
+ * a line of the face it would have been set in. CoreText sets no line for
333
+ * no text, and a layout with no lines has no height and a caret of none —
334
+ * so an empty field's caret was its three-pixel mark padding either side
335
+ * of nothing, and grew to the line the moment the first letter arrived.
336
+ */
337
+ _setBlank(line) {
338
+ this.lines = [line];
339
+ this.height = line.height;
340
+ this._blank = line;
341
+ }
342
+
305
343
  /** Code point boundary nearest the point, in layout coordinates. */
306
344
  indexAt(x, y) {
307
345
  return this._cpOf(this._native.layoutIndexAt(this._handle, x, y));
@@ -309,6 +347,13 @@ class CocoaTextLayout {
309
347
 
310
348
  /** Caret rect for a code-point index: { x, y, height }. */
311
349
  caretPosition(cp) {
350
+ // the rect the native gives a caret on a line with text in it — the
351
+ // line's box — so typing the first letter moves the caret and nothing
352
+ // else
353
+ const blank = this._blank;
354
+ if (blank) {
355
+ return { x: blank.x, y: blank.y, height: blank.height, line: 0 };
356
+ }
312
357
  return this._native.layoutCaret(this._handle, this._cuOf(cp));
313
358
  }
314
359
 
@@ -1315,6 +1360,24 @@ export class CocoaFontManager {
1315
1360
  minContent ? laid.map((span) => span.text).join('') : text,
1316
1361
  );
1317
1362
  layout._contextInk = contextInk;
1363
+ if (!text) {
1364
+ const first = spans[0] ?? {};
1365
+ const size = first.size ?? base.size ?? 14;
1366
+ const face = first.font ?? base.font;
1367
+ const metrics = (
1368
+ typeof face?.metrics === 'function'
1369
+ ? face
1370
+ : this.match(first.family ?? base.family, {
1371
+ weight: first.weight ?? base.weight,
1372
+ style: first.style ?? base.style,
1373
+ })
1374
+ ).metrics(size);
1375
+ const container =
1376
+ Number.isFinite(maxWidth) && maxWidth > 0 ? maxWidth : 0;
1377
+ layout._setBlank(
1378
+ blankLine(metrics, lineHeight, flushFor(align, direction) * container),
1379
+ );
1380
+ }
1318
1381
  if (!minContent) {
1319
1382
  const runs = new Map();
1320
1383
  layout._hangRuns(sources, (i, rtl) => {
@@ -4,6 +4,7 @@
4
4
 
5
5
  import React, { useEffect, useRef, useState } from 'react';
6
6
  import { useAppOrNull } from '../appcontext.js';
7
+ import { flattenStyle } from '../styles.js';
7
8
  import { capBand, capTrim, rowRadius, useTheme } from './theme.js';
8
9
  import { Icon } from './Icon.js';
9
10
  import {
@@ -282,6 +283,14 @@ function Option({
282
283
  * the active option with wrapping; Home/End jump to the ends; Enter/Space
283
284
  * pick the active option (or open the menu when closed); Escape closes.
284
285
  * The active option is scrolled into view.
286
+ *
287
+ * `labelStyle` and `chevronStyle` are style slots over the caption and the
288
+ * chevron, each merged over the part's own default, as `<Slider>`'s are over
289
+ * its parts — so a select that has to wear someone else's look, a toolbar's
290
+ * or a web page's, is the widget restyled, with `style` on the trigger. The
291
+ * caption's `fontSize` is read back: the chevron is as tall as the capitals
292
+ * beside it. Any slot also chooses the drawn trigger over the platform's
293
+ * popup bezel, whose pixels no style reaches.
285
294
  */
286
295
  export function Select({
287
296
  value,
@@ -291,11 +300,18 @@ export function Select({
291
300
  placeholder = 'Select…',
292
301
  native,
293
302
  style,
303
+ labelStyle,
304
+ chevronStyle,
294
305
  ...boxProps
295
306
  }) {
296
307
  const theme = useTheme();
297
308
  const app = useAppOrNull();
298
- const nativeControls = useNativeControls(native);
309
+ const styled = Boolean(labelStyle || chevronStyle);
310
+ const nativeControls = useNativeControls(styled ? false : native);
311
+ // The size the caption is set in, where its slot names one: the chevron is
312
+ // measured against those capitals, and an `<Icon>`'s size does not inherit.
313
+ const ownSize = flattenStyle(labelStyle).fontSize;
314
+ const captionSize = typeof ownSize === 'number' ? ownSize : theme.fontSize;
299
315
  const [open, setOpen] = useState(false);
300
316
  const [anchor, setAnchor] = useState(null);
301
317
  const [focused, setFocused] = useState(false);
@@ -572,6 +588,7 @@ export function Select({
572
588
  ONE_LINE,
573
589
  nativeControls && nativeTitleStyle('regular'),
574
590
  { color: current ? theme.text : theme.textMuted },
591
+ labelStyle,
575
592
  ],
576
593
  },
577
594
  current ? current.label : placeholder,
@@ -593,8 +610,11 @@ export function Select({
593
610
  })
594
611
  : h(Icon, {
595
612
  name: 'chevronDown',
596
- size: capBand(theme.fontSize),
613
+ size: capBand(captionSize),
597
614
  color: theme.textMuted,
615
+ // `display: 'none'` leaves it out, for a trigger whose arrow is
616
+ // drawn by something else
617
+ style: chevronStyle,
598
618
  }),
599
619
  open &&
600
620
  anchor &&
@@ -511,6 +511,16 @@ export interface SelectProps<T = unknown>
511
511
  onChange?: (ev: WidgetChangeEvent<T>) => void;
512
512
  placeholder?: string;
513
513
  style?: StyleProp;
514
+ /**
515
+ * The caption's style — the value, or the placeholder — over the palette's
516
+ * `text` (`textMuted` for the placeholder). Its `fontSize`, as a number, is
517
+ * the one the chevron is sized against. Any slot chooses the drawn trigger
518
+ * over the platform's popup bezel.
519
+ */
520
+ labelStyle?: StyleProp;
521
+ /** The chevron's style, over `textMuted` at the caption's cap height.
522
+ * `display: 'none'` leaves it out. */
523
+ chevronStyle?: StyleProp;
514
524
  }
515
525
  export function Select<T = unknown>(props: SelectProps<T>): ReactNode;
516
526