react-x11 1.2.0 → 2.0.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/README.md +330 -81
- package/package.json +132 -14
- package/src/ClickToComponent.js +9 -2
- package/src/DevToolsIntegration.js +440 -32
- package/src/Reconciler.js +463 -165
- package/src/a11y.js +1299 -0
- package/src/acceleratorhooks.js +86 -0
- package/src/accelerators.js +128 -0
- package/src/activate.js +168 -0
- package/src/anchor.js +360 -0
- package/src/appcontext.js +161 -0
- package/src/appearance.js +748 -0
- package/src/appearancehooks.js +96 -0
- package/src/apphooks.js +73 -0
- package/src/application.js +748 -0
- package/src/atspi.js +1953 -0
- package/src/bus.js +545 -0
- package/src/bushooks.js +136 -0
- package/src/clientmessage.js +140 -0
- package/src/clipboard.js +121 -0
- package/src/components/Button.js +148 -33
- package/src/components/Checkbox.js +66 -32
- package/src/components/Dialog.js +96 -29
- package/src/components/FileDialog.js +491 -0
- package/src/components/Icon.js +329 -0
- package/src/components/Menu.js +797 -133
- package/src/components/PasswordInput.js +434 -0
- package/src/components/ProgressBar.js +91 -12
- package/src/components/Radio.js +78 -35
- package/src/components/Select.js +255 -65
- package/src/components/Slider.js +102 -41
- package/src/components/SplitPane.js +206 -0
- package/src/components/Switch.js +100 -22
- package/src/components/Table.js +529 -0
- package/src/components/Tabs.js +211 -0
- package/src/components/Tooltip.js +528 -54
- package/src/components/anchor.js +154 -107
- package/src/components/change.js +34 -0
- package/src/components/dnd.js +103 -0
- package/src/components/index.js +16 -3
- package/src/components/keys.js +17 -20
- package/src/components/scribble.js +247 -0
- package/src/components/theme.js +350 -42
- package/src/components/typeahead.js +12 -2
- package/src/compose.js +868 -0
- package/src/compositing.js +224 -0
- package/src/dbusmenu.js +384 -0
- package/src/debug.d.ts +63 -0
- package/src/debug.js +642 -0
- package/src/decorations.js +486 -0
- package/src/desktopsettings.js +209 -0
- package/src/desktopsettingshooks.js +62 -0
- package/src/dnd.js +1725 -0
- package/src/editmenu.js +272 -0
- package/src/errors.js +98 -0
- package/src/events.js +1406 -162
- package/src/extensions.js +45 -0
- package/src/filedialog.js +375 -0
- package/src/filedialoghooks.js +132 -0
- package/src/fonthooks.js +64 -0
- package/src/fonts.js +301 -0
- package/src/foreignnodes.js +519 -0
- package/src/frame/child.js +33 -0
- package/src/frame/childmain.js +274 -0
- package/src/frame/env.js +140 -0
- package/src/frame/index.js +444 -0
- package/src/frame/lifecycle.js +67 -0
- package/src/frame/protocol.js +179 -0
- package/src/frames.js +73 -0
- package/src/glbackend.js +114 -0
- package/src/glnodes.js +125 -67
- package/src/globalmenu.js +683 -0
- package/src/host.d.ts +88 -0
- package/src/host.js +42 -0
- package/src/idle.js +490 -0
- package/src/idlehooks.js +100 -0
- package/src/imagesource.js +349 -0
- package/src/index.d.ts +475 -0
- package/src/index.js +83 -9
- package/src/inputtime.js +206 -0
- package/src/jsx-dev-runtime.d.ts +21 -0
- package/src/jsx-dev-runtime.js +2 -0
- package/src/jsx-runtime.d.ts +39 -0
- package/src/jsx-runtime.js +6 -0
- package/src/keyboard.js +256 -0
- package/src/keyboardstate.js +278 -0
- package/src/keyboardstatehooks.js +58 -0
- package/src/keysyms.d.ts +149 -0
- package/src/keysyms.js +270 -0
- package/src/locale.js +170 -0
- package/src/localehooks.js +47 -0
- package/src/menuitem.js +223 -0
- package/src/node.d.ts +597 -0
- package/src/node.js +44 -0
- package/src/nodes.js +9546 -690
- package/src/ntk.d.ts +44 -0
- package/src/ntk.js +25 -0
- package/src/paintcache.js +366 -0
- package/src/palette.js +380 -0
- package/src/pastestate.js +66 -0
- package/src/portal.js +461 -0
- package/src/priority.js +26 -0
- package/src/refresh/index.d.ts +40 -0
- package/src/refresh/index.js +122 -0
- package/src/refresh/loader.d.ts +37 -0
- package/src/refresh/loader.js +401 -0
- package/src/refresh/register.d.ts +5 -0
- package/src/refresh/register.js +13 -0
- package/src/registry.js +232 -0
- package/src/scale.js +626 -0
- package/src/scalehooks.js +27 -0
- package/src/screencolor.js +640 -0
- package/src/screencolorhooks.js +101 -0
- package/src/screens.js +754 -0
- package/src/screenshooks.js +137 -0
- package/src/startup.js +302 -0
- package/src/style.d.ts +126 -0
- package/src/style.js +33 -0
- package/src/styles.js +1482 -6
- package/src/svgnodes.js +306 -0
- package/src/testing/a11y.js +484 -0
- package/src/testing/components.js +414 -0
- package/src/testing/events.js +407 -0
- package/src/testing/harness.js +455 -0
- package/src/testing/index.d.ts +558 -0
- package/src/testing/index.js +85 -0
- package/src/testing/mock-app.js +463 -0
- package/src/testing/pixels.js +152 -0
- package/src/testing/queries.js +224 -0
- package/src/textrange.js +83 -0
- package/src/textselection.js +439 -0
- package/src/trace-registry.js +63 -0
- package/src/transfer.js +93 -0
- package/src/types/appearance.d.ts +84 -0
- package/src/types/application.d.ts +173 -0
- package/src/types/components.d.ts +820 -0
- package/src/types/dbus.d.ts +177 -0
- package/src/types/elements.d.ts +897 -0
- package/src/types/events.d.ts +549 -0
- package/src/types/filedialog.d.ts +260 -0
- package/src/types/fonts.d.ts +124 -0
- package/src/types/frame.d.ts +146 -0
- package/src/types/globalmenu.d.ts +42 -0
- package/src/types/nodes.d.ts +199 -0
- package/src/types/screencolor.d.ts +84 -0
- package/src/types/style.d.ts +434 -0
- package/src/types/system.d.ts +287 -0
- package/src/windowid.js +151 -0
- package/src/windowstate.js +393 -0
- package/src/xsettings.js +336 -0
- package/src/yoga.d.ts +55 -0
- package/src/yoga.js +135 -0
- package/src/components/Canvas3D.js +0 -28
- package/src/geometry3d.js +0 -223
- package/src/pointer3d.js +0 -158
- package/src/raycast3d.js +0 -146
- package/src/richnodes.js +0 -436
- package/src/scene3d.js +0 -683
package/src/palette.js
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
// The two built-in palettes, and which one is in force.
|
|
2
|
+
//
|
|
3
|
+
// Separate from `components/theme.js` because the palette is not a React
|
|
4
|
+
// concern. There are two routes into the tree — `useTheme()` through React
|
|
5
|
+
// context, and a `$token` resolved against the nearest `theme` **prop** by
|
|
6
|
+
// walking the node tree — and the second one lives in `nodes.js`, below the
|
|
7
|
+
// widget layer. Both have to agree on what "no theme was given" means, so the
|
|
8
|
+
// answer belongs underneath both of them.
|
|
9
|
+
//
|
|
10
|
+
// **The default follows the desktop.** A react-x11 app that says nothing
|
|
11
|
+
// about colour is dark on a dark desktop and light on a light one, the way a
|
|
12
|
+
// GTK or Qt app is; `<ThemeProvider>` and `colorScheme` are how an app that
|
|
13
|
+
// wants otherwise says so. See docs/appearance.md.
|
|
14
|
+
|
|
15
|
+
import { appearanceSnapshot } from './appearance.js';
|
|
16
|
+
import { readableInk, stepBeyond } from './styles.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The language subtags written right-to-left — CLDR's set, by the language
|
|
20
|
+
* rather than by the script, because a locale name is what an environment
|
|
21
|
+
* actually carries. Arabic and its neighbours, Hebrew, Persian and Dari,
|
|
22
|
+
* Pashto, Urdu, Sindhi, Kashmiri, Uyghur, Yiddish, Sorani Kurdish, Dhivehi,
|
|
23
|
+
* Syriac, N'Ko, and Adlam-written Fulah.
|
|
24
|
+
*/
|
|
25
|
+
const RTL_LANGUAGES = new Set([
|
|
26
|
+
'ae',
|
|
27
|
+
'ar',
|
|
28
|
+
'arc',
|
|
29
|
+
'bcc',
|
|
30
|
+
'bqi',
|
|
31
|
+
'ckb',
|
|
32
|
+
'dv',
|
|
33
|
+
'fa',
|
|
34
|
+
'ff',
|
|
35
|
+
'glk',
|
|
36
|
+
'he',
|
|
37
|
+
'iw',
|
|
38
|
+
'khw',
|
|
39
|
+
'ks',
|
|
40
|
+
'ku',
|
|
41
|
+
'mzn',
|
|
42
|
+
'nqo',
|
|
43
|
+
'pnb',
|
|
44
|
+
'prs',
|
|
45
|
+
'ps',
|
|
46
|
+
'sd',
|
|
47
|
+
'syr',
|
|
48
|
+
'ug',
|
|
49
|
+
'ur',
|
|
50
|
+
'yi',
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Which way this desktop reads, from the environment's locale.
|
|
55
|
+
*
|
|
56
|
+
* **This is the default, and it is a default rather than a setting on
|
|
57
|
+
* purpose.** An app started under `LANG=ar_EG.UTF-8` is an Arabic app, and
|
|
58
|
+
* the person who started it should not have to have been given a language
|
|
59
|
+
* menu before the panels are on the right side. It is what GTK and Qt both
|
|
60
|
+
* do — GTK asks the translation of `"default:LTR"`, Qt asks the system
|
|
61
|
+
* locale — and it costs an app that never thinks about this exactly nothing,
|
|
62
|
+
* because every locale not in the set above answers `'ltr'`.
|
|
63
|
+
*
|
|
64
|
+
* The overrides, nearest first: a `direction` style property on any node
|
|
65
|
+
* mirrors that subtree, and `<ThemeProvider value={{ direction }}>` mirrors
|
|
66
|
+
* everything under it *including the widgets*, which is the one an app with a
|
|
67
|
+
* language menu wants — see docs/styling.md.
|
|
68
|
+
*
|
|
69
|
+
* There is no environment in the playground bundle, which runs this in a
|
|
70
|
+
* browser — hence the guard rather than a bare `process.env`. A page has no
|
|
71
|
+
* locale to read either way; what it has is whatever the app writes.
|
|
72
|
+
*
|
|
73
|
+
* Read once, at load: a locale does not change under a running process, and
|
|
74
|
+
* this is on the path that resolves the palette for every node.
|
|
75
|
+
*/
|
|
76
|
+
export function localeDirection(env = ENV) {
|
|
77
|
+
const locale = env.LC_ALL || env.LC_MESSAGES || env.LANG || '';
|
|
78
|
+
// `ar_EG.UTF-8`, `he-IL`, `fa`, or `C`/`POSIX` — the language is whatever
|
|
79
|
+
// comes before the territory, and the separator is either spelling
|
|
80
|
+
const language = locale.split(/[._@-]/)[0].toLowerCase();
|
|
81
|
+
return RTL_LANGUAGES.has(language) ? 'rtl' : 'ltr';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const ENV = typeof process === 'undefined' ? {} : (process.env ?? {});
|
|
85
|
+
|
|
86
|
+
const LOCALE_DIRECTION = localeDirection();
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The palette every widget reads, and the shape of the controls with it.
|
|
90
|
+
* A theme overrides what it cares about and inherits the rest, so the
|
|
91
|
+
* defaults here are the look the widgets have always had.
|
|
92
|
+
*
|
|
93
|
+
* The shape tokens are what let a theme be more than a recolour: corner
|
|
94
|
+
* radius, border weight, text size and the padding inside a control are
|
|
95
|
+
* most of what separates one platform's buttons from another's.
|
|
96
|
+
*/
|
|
97
|
+
export const DefaultTheme = {
|
|
98
|
+
// colour
|
|
99
|
+
border: '#b2bec3',
|
|
100
|
+
borderFocus: '#2980b9',
|
|
101
|
+
// The ground and the things raised off it. `background` is what the window
|
|
102
|
+
// *is* — the fill under everything, painted by the window itself — and
|
|
103
|
+
// `surface` is what sits on it: a control's fill, a card, the sheet a menu
|
|
104
|
+
// or a dialog is drawn on. They are the same colour here because a white
|
|
105
|
+
// app on a white ground is what the light palette has always been; a theme
|
|
106
|
+
// that gives the ground its own tint (GitHub's `#f6f8fa` under white cards,
|
|
107
|
+
// and every macOS window) is exactly the case that had nowhere to say so.
|
|
108
|
+
background: 'white',
|
|
109
|
+
surface: 'white',
|
|
110
|
+
text: '#2d3436',
|
|
111
|
+
// Secondary ink: a placeholder, a caption, the label of a disabled
|
|
112
|
+
// control. Named for the text it is rather than for how it looks, so that
|
|
113
|
+
// `textMuted` and `text` read as one family.
|
|
114
|
+
textMuted: '#7f8c8d',
|
|
115
|
+
hoverBackground: '#2980b9',
|
|
116
|
+
hoverText: 'white',
|
|
117
|
+
accent: '#2980b9',
|
|
118
|
+
accentHover: '#1f6693',
|
|
119
|
+
accentText: 'white',
|
|
120
|
+
surfaceHover: '#f1f2f6',
|
|
121
|
+
track: '#dfe6e9',
|
|
122
|
+
// What a screen has to be able to *say*: this failed, this worked, look at
|
|
123
|
+
// this, here is a note. Without them every alert, badge and validation
|
|
124
|
+
// message hard-codes a hex, which is the one thing `$token` exists to
|
|
125
|
+
// prevent — and those are the colours most likely to be wrong on a dark
|
|
126
|
+
// desktop, because a red that reads on white disappears on near-black.
|
|
127
|
+
//
|
|
128
|
+
// Each is picked to work as **ink as well as fill**: 4.5:1 against this
|
|
129
|
+
// palette's ground, so "Password too short" under a field is legible
|
|
130
|
+
// without a second token for the text form of the same idea.
|
|
131
|
+
//
|
|
132
|
+
// Only `danger` has the hover and pressed steps, because it is the only
|
|
133
|
+
// one of the four you ever press — a destructive button. A success or a
|
|
134
|
+
// warning is something the app says, not something the user clicks; when
|
|
135
|
+
// one of them ends up on a control, the control is a `<Button>` with a
|
|
136
|
+
// `danger`-shaped ramp of its own.
|
|
137
|
+
//
|
|
138
|
+
// `info` is a blue of its own rather than the accent, even here where the
|
|
139
|
+
// accent is also blue: a theme with a green accent still gets a blue note,
|
|
140
|
+
// because that is what a note looks like everywhere — and this one is a
|
|
141
|
+
// step darker than `accent`, which is a fill first and only clears 4.3:1
|
|
142
|
+
// as letters.
|
|
143
|
+
danger: '#c0392b',
|
|
144
|
+
dangerHover: '#a93226',
|
|
145
|
+
dangerText: 'white',
|
|
146
|
+
success: '#1e8449',
|
|
147
|
+
successText: 'white',
|
|
148
|
+
warning: '#9a6700',
|
|
149
|
+
warningText: 'white',
|
|
150
|
+
info: '#1c6ea4',
|
|
151
|
+
infoText: 'white',
|
|
152
|
+
// The pressed step of each fill family: rest → …Hover → …Active. Every
|
|
153
|
+
// family that has a hover needs one, because a press is the state a
|
|
154
|
+
// control has to show *before* it has done anything — the activation
|
|
155
|
+
// itself only happens on the release, and half a second can pass in
|
|
156
|
+
// between.
|
|
157
|
+
//
|
|
158
|
+
// Written out here, but a theme almost never sets them: `resolveTheme`
|
|
159
|
+
// takes the step the palette's own hover made and takes it again. So a
|
|
160
|
+
// palette that names `accentHover` and stops there — which is every theme
|
|
161
|
+
// in `examples/themes.js` and every recipe in docs/ecosystem/theming.md —
|
|
162
|
+
// gets a press that matches it rather than one inherited from these.
|
|
163
|
+
accentActive: '#154c6d',
|
|
164
|
+
surfaceActive: '#e3e5ed',
|
|
165
|
+
textMutedActive: '#4c5a57',
|
|
166
|
+
dangerActive: '#922b21',
|
|
167
|
+
// The keyboard focus ring. Read by the renderer, not by the widgets: any
|
|
168
|
+
// focusable node under this palette draws it on `:focus-visible`, so a
|
|
169
|
+
// plain `<box focusable>` an application writes is indicated too, and a
|
|
170
|
+
// theme restyles every ring in the app from here.
|
|
171
|
+
focusRing: '#2980b9',
|
|
172
|
+
focusRingWidth: 2,
|
|
173
|
+
focusRingOffset: 1,
|
|
174
|
+
// shape
|
|
175
|
+
radius: 4,
|
|
176
|
+
radiusSmall: 3,
|
|
177
|
+
// Floating surfaces round on their own scale, and it is a wider one: a
|
|
178
|
+
// menu is a sheet of paper laid over the window, where a button is a
|
|
179
|
+
// control cut into it. Half the text size is the number every desktop
|
|
180
|
+
// lands near — 7px at a 14px body — and tying it to the type rather than
|
|
181
|
+
// to `radius` is what keeps a theme that only sets `fontSize` from
|
|
182
|
+
// getting a 20px menu with a 4px corner.
|
|
183
|
+
//
|
|
184
|
+
// The two inside it step down from there, because a rounded thing inside
|
|
185
|
+
// a rounded thing wants a *smaller* curve or the two read as concentric
|
|
186
|
+
// rings: the highlight on a menu row, and the tooltip bubble, which is
|
|
187
|
+
// the smallest floating surface there is and the one closest to text.
|
|
188
|
+
radiusPopup: 7,
|
|
189
|
+
radiusPopupItem: 5,
|
|
190
|
+
radiusTooltip: 4,
|
|
191
|
+
borderWidth: 1,
|
|
192
|
+
fontSize: 14,
|
|
193
|
+
// The two faces an app has. `fontFamily` is what a `<text>` inherits where
|
|
194
|
+
// nothing named one, so "this app is Inter" is a sentence said once here
|
|
195
|
+
// rather than on every label; `monoFamily` is the one every code surface
|
|
196
|
+
// reaches for — a `<Code>`, a log pane, a hex dump — and the reason it is a
|
|
197
|
+
// token at all is that those are written by *different* components, which
|
|
198
|
+
// would otherwise each grow their own prop for it and have to be set one by
|
|
199
|
+
// one.
|
|
200
|
+
//
|
|
201
|
+
// CSS-style family lists, the same as the `fontFamily` style property:
|
|
202
|
+
// `'"JetBrains Mono", monospace'` names a preference and a fallback, and
|
|
203
|
+
// ntk's `fonts.match` splits the list itself.
|
|
204
|
+
fontFamily: 'sans-serif',
|
|
205
|
+
monoFamily: 'monospace',
|
|
206
|
+
// Which way this app reads — `'ltr'` or `'rtl'`, seeded from the locale.
|
|
207
|
+
//
|
|
208
|
+
// In the palette rather than in a context of its own because both consumers
|
|
209
|
+
// are already here: the widgets read it through `useTheme()` to decide which
|
|
210
|
+
// way a slider travels or which side a submenu opens on, and the node tree
|
|
211
|
+
// reads it as the floor under the `direction` style property. An app with a
|
|
212
|
+
// language menu therefore switches the whole UI, layout and widgets
|
|
213
|
+
// together, with the `<ThemeProvider>` swap it was already doing for
|
|
214
|
+
// colours — and a `<ThemeProvider>` that names it plants the matching style
|
|
215
|
+
// in the tree, so the two routes cannot disagree.
|
|
216
|
+
direction: LOCALE_DIRECTION,
|
|
217
|
+
paddingX: 16,
|
|
218
|
+
// Measured from the **letters**, not from the font's line box: widget
|
|
219
|
+
// labels are trimmed to the capitals down to the baseline, so this is the
|
|
220
|
+
// space you actually see above and below the text — see `capTrim` in
|
|
221
|
+
// components/theme.js for why a line box cannot give an even one. Larger
|
|
222
|
+
// than it looks next to a CSS padding for that reason: 12 here is about
|
|
223
|
+
// what 8 came to once a typical face's ascent had been added on.
|
|
224
|
+
paddingY: 12,
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
// Which pressed token is derived from which pair, when the palette does not
|
|
228
|
+
// name it: the resting colour of the family and the hover it steps to.
|
|
229
|
+
//
|
|
230
|
+
// `textMutedActive` is measured from `border` because that is the ramp it
|
|
231
|
+
// belongs to: a `<Switch>` that is off has a `border`-coloured track, and
|
|
232
|
+
// the muted ink is the step it takes on hover. The ink and the track share a
|
|
233
|
+
// colour rather than a job.
|
|
234
|
+
const PRESSED_FROM = {
|
|
235
|
+
accentActive: ['accent', 'accentHover'],
|
|
236
|
+
surfaceActive: ['surface', 'surfaceHover'],
|
|
237
|
+
textMutedActive: ['border', 'textMuted'],
|
|
238
|
+
dangerActive: ['danger', 'dangerHover'],
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
// And which ink goes on which fill. A palette that names a fill and stops
|
|
242
|
+
// there gets the more legible of its own two inks — see `readableInk`.
|
|
243
|
+
//
|
|
244
|
+
// This is what makes the status family cheap to theme: naming four colours
|
|
245
|
+
// is a design decision, and naming the letters that go on top of each of
|
|
246
|
+
// them is bookkeeping that a contrast ratio can do. It is the same for
|
|
247
|
+
// `accent`, where it fixes a real trap — a palette whose accent is a yellow
|
|
248
|
+
// or a lime inherits `accentText: 'white'` and paints an invisible label.
|
|
249
|
+
const TEXT_FROM = {
|
|
250
|
+
accentText: 'accent',
|
|
251
|
+
hoverText: 'hoverBackground',
|
|
252
|
+
dangerText: 'danger',
|
|
253
|
+
successText: 'success',
|
|
254
|
+
warningText: 'warning',
|
|
255
|
+
infoText: 'info',
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// And the same for the floating-surface radii, which are a function of the
|
|
259
|
+
// text they wrap: a palette that sets `fontSize` and nothing else still gets
|
|
260
|
+
// menus in proportion to it.
|
|
261
|
+
const RADIUS_FROM_FONT = {
|
|
262
|
+
radiusPopup: (size) => Math.round(size / 2),
|
|
263
|
+
radiusPopupItem: (size) => Math.max(0, Math.round(size / 2) - 2),
|
|
264
|
+
radiusTooltip: (size) => Math.max(0, Math.round(size / 2) - 3),
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Merge a partial palette, filling in the pressed step for any family whose
|
|
269
|
+
* colours moved without it — and the popup radii for a palette that moved
|
|
270
|
+
* the text size without them.
|
|
271
|
+
*
|
|
272
|
+
* The rule per token: an explicit value wins; otherwise, if this palette
|
|
273
|
+
* touched either colour the step is measured between, it is re-derived; and
|
|
274
|
+
* otherwise whatever was already in force stands. That last clause is what
|
|
275
|
+
* keeps an inner `<ThemeProvider value={{ fontSize: 18 }}>` from throwing
|
|
276
|
+
* away a pressed colour an outer one set by hand.
|
|
277
|
+
*
|
|
278
|
+
* The alternative was three more tokens every theme has to remember, and a
|
|
279
|
+
* theme that forgets one does not fail loudly — it just stops answering
|
|
280
|
+
* presses, in the one state a control has to show while nothing else can.
|
|
281
|
+
*
|
|
282
|
+
* **`surface` follows `background` unless it is named**, which is the same
|
|
283
|
+
* bargain from the other end: a palette that has one ground has one ground,
|
|
284
|
+
* and only a design that actually raises its cards off it has to say so. Any
|
|
285
|
+
* other rule would leave a theme that names a `#1f1f23` background with the
|
|
286
|
+
* built-in dark palette's surface on its controls — a colour from a palette
|
|
287
|
+
* it had replaced.
|
|
288
|
+
*/
|
|
289
|
+
export function resolveTheme(value, base = DefaultTheme) {
|
|
290
|
+
if (!value) return base;
|
|
291
|
+
// What this palette said, with that one implication written in, so
|
|
292
|
+
// everything measured from `surface` below is measured from the right
|
|
293
|
+
// colour and re-derived when it moved.
|
|
294
|
+
const named =
|
|
295
|
+
value.surface == null && value.background != null
|
|
296
|
+
? { ...value, surface: value.background }
|
|
297
|
+
: value;
|
|
298
|
+
const merged = { ...base, ...named };
|
|
299
|
+
for (const [token, [rest, hover]] of Object.entries(PRESSED_FROM)) {
|
|
300
|
+
if (named[token] != null) continue;
|
|
301
|
+
if (named[rest] == null && named[hover] == null) continue;
|
|
302
|
+
merged[token] = stepBeyond(merged[rest], merged[hover]);
|
|
303
|
+
}
|
|
304
|
+
// The ink follows the fill it goes on, and also the two inks it is chosen
|
|
305
|
+
// between: a palette that moves only `background` has moved what "the
|
|
306
|
+
// legible one" means.
|
|
307
|
+
for (const [token, fill] of Object.entries(TEXT_FROM)) {
|
|
308
|
+
if (named[token] != null) continue;
|
|
309
|
+
if (named[fill] == null && named.text == null && named.background == null)
|
|
310
|
+
continue;
|
|
311
|
+
merged[token] = readableInk(merged[fill], [merged.text, merged.background]);
|
|
312
|
+
}
|
|
313
|
+
if (named.fontSize != null) {
|
|
314
|
+
for (const [token, from] of Object.entries(RADIUS_FROM_FONT)) {
|
|
315
|
+
if (named[token] == null) merged[token] = from(merged.fontSize);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return merged;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* The dark counterpart, as the *same design* in a dark scheme rather than a
|
|
323
|
+
* second design: it is built by merging colour overrides over
|
|
324
|
+
* {@link DefaultTheme}, so every shape token — radius, border width, font
|
|
325
|
+
* size, the control padding — is shared by construction and cannot drift.
|
|
326
|
+
*
|
|
327
|
+
* `resolveTheme` derives the pressed steps from the hovers named here, and
|
|
328
|
+
* `stepBeyond` takes the direction from the colours themselves, so a press
|
|
329
|
+
* in dark *lightens* where the light palette's darkens. Nothing has to be
|
|
330
|
+
* told which scheme it is in. The status inks are derived too — this palette
|
|
331
|
+
* names four fills and none of the letters on them, which is what every
|
|
332
|
+
* theme after it gets to do.
|
|
333
|
+
*/
|
|
334
|
+
export const DarkTheme = resolveTheme({
|
|
335
|
+
// A near-black with a little blue in it rather than #000: pure black shows
|
|
336
|
+
// every seam between a window and the widgets on it, and no desktop's dark
|
|
337
|
+
// theme uses it.
|
|
338
|
+
background: '#1e2228',
|
|
339
|
+
// Here the ground and the surface part company, which is the whole point
|
|
340
|
+
// of their being two tokens: a card at the ground's own colour is a card
|
|
341
|
+
// you cannot see, and dark designs raise by lightening because there is no
|
|
342
|
+
// shadow to cast on near-black.
|
|
343
|
+
surface: '#252a31',
|
|
344
|
+
surfaceHover: '#2a3038',
|
|
345
|
+
text: '#e6e9ed',
|
|
346
|
+
textMuted: '#8b939c',
|
|
347
|
+
border: '#454d55',
|
|
348
|
+
track: '#3a4149',
|
|
349
|
+
// Lighter and less saturated than the light palette's: the same four
|
|
350
|
+
// meanings, re-picked to clear 4.5:1 against near-black rather than
|
|
351
|
+
// against white. `#c0392b` on this ground is a bruise.
|
|
352
|
+
danger: '#ec6a5e',
|
|
353
|
+
dangerHover: '#f28d80',
|
|
354
|
+
success: '#2ecc71',
|
|
355
|
+
warning: '#f0b429',
|
|
356
|
+
info: '#5aa4e6',
|
|
357
|
+
// The accent lifts off the darker ground instead of sinking into it, so
|
|
358
|
+
// `accentHover` goes *up* from `accent` here and down in the light palette.
|
|
359
|
+
accent: '#3d8bd4',
|
|
360
|
+
accentHover: '#5aa4e6',
|
|
361
|
+
accentText: 'white',
|
|
362
|
+
hoverBackground: '#3d8bd4',
|
|
363
|
+
hoverText: 'white',
|
|
364
|
+
borderFocus: '#5aa4e6',
|
|
365
|
+
focusRing: '#5aa4e6',
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* The palette in force where nothing has been said — which is to say, the
|
|
370
|
+
* desktop's.
|
|
371
|
+
*
|
|
372
|
+
* Read synchronously and cheaply: `appearanceSnapshot()` is a frozen object
|
|
373
|
+
* seeded from disk before the first render, so this is a property lookup and
|
|
374
|
+
* a comparison, and it is called from the paint path.
|
|
375
|
+
*
|
|
376
|
+
* `'no-preference'` means *use your own default*, which is the light one.
|
|
377
|
+
*/
|
|
378
|
+
export function baseTheme() {
|
|
379
|
+
return appearanceSnapshot().colorScheme === 'dark' ? DarkTheme : DefaultTheme;
|
|
380
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Is there anything to paste? — kept per app, so a menu can answer without
|
|
2
|
+
// a round trip.
|
|
3
|
+
//
|
|
4
|
+
// The honest answer needs the server, and asking for it costs a full
|
|
5
|
+
// conversion against whatever foreign client owns the selection — plus a
|
|
6
|
+
// two second wait when that client is wedged. A menu cannot pay that while
|
|
7
|
+
// it is opening, which is why the built-in edit menu shipped with Paste
|
|
8
|
+
// always enabled.
|
|
9
|
+
//
|
|
10
|
+
// XFixes turns it around: the server says when the selection changes hands,
|
|
11
|
+
// and the answer is a boolean already in hand by the time a menu opens. One
|
|
12
|
+
// registration per app, armed the first time anything asks.
|
|
13
|
+
//
|
|
14
|
+
// Deliberately only "is it owned", not "does it hold text". Refining it
|
|
15
|
+
// would mean a TARGETS round trip per change, which reintroduces the cost
|
|
16
|
+
// this exists to avoid; an owner offering nothing a text field can take is
|
|
17
|
+
// rare enough to leave the row enabled and let the paste do nothing.
|
|
18
|
+
|
|
19
|
+
const states = new WeakMap();
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Start tracking `app`'s CLIPBOARD, once. Safe to call on every menu open —
|
|
23
|
+
* the second call and the ten thousandth do nothing.
|
|
24
|
+
*/
|
|
25
|
+
export function armPasteState(app, clipboard) {
|
|
26
|
+
if (!app || !clipboard || states.has(app)) return;
|
|
27
|
+
// A clipboard without these is an older ntk, or a stand-in someone
|
|
28
|
+
// supplied. Tracking is the enhancement, not the feature: skip it and
|
|
29
|
+
// leave the row enabled rather than throwing on the way into a menu.
|
|
30
|
+
if (
|
|
31
|
+
typeof clipboard.watch !== 'function' ||
|
|
32
|
+
typeof clipboard.targets !== 'function'
|
|
33
|
+
) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
// Unknown means "yes": until the server has said otherwise, behave the way
|
|
37
|
+
// this did before there was any tracking at all.
|
|
38
|
+
const state = { owned: true, live: false };
|
|
39
|
+
states.set(app, state);
|
|
40
|
+
|
|
41
|
+
clipboard
|
|
42
|
+
.watch('CLIPBOARD', (ev) => {
|
|
43
|
+
state.live = true;
|
|
44
|
+
state.owned = ev.owner !== 0;
|
|
45
|
+
})
|
|
46
|
+
.catch(() => {
|
|
47
|
+
// No XFixes on this server. Nothing to fall back to that is worth the
|
|
48
|
+
// latency, so the row stays enabled — which is where it started.
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// `watch` reports *changes*, so it says nothing about the selection that
|
|
52
|
+
// was already there when we armed. One question, once per app, off the
|
|
53
|
+
// menu's path entirely.
|
|
54
|
+
clipboard
|
|
55
|
+
.targets({ selection: 'CLIPBOARD' })
|
|
56
|
+
.then((offered) => {
|
|
57
|
+
// a change may have overtaken this round trip; it is the fresher answer
|
|
58
|
+
if (!state.live) state.owned = offered.length > 0;
|
|
59
|
+
})
|
|
60
|
+
.catch(() => {});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Whether a Paste row should be live. True when nothing is known yet. */
|
|
64
|
+
export function canPaste(app) {
|
|
65
|
+
return states.get(app)?.owned ?? true;
|
|
66
|
+
}
|