@tuidom/core 0.4.0 → 0.5.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.
|
@@ -41,6 +41,13 @@ export interface OverlaySessionOptions {
|
|
|
41
41
|
* (владелец обработает Escape сам — например закроет только верхнее подменю).
|
|
42
42
|
*/
|
|
43
43
|
shouldCloseOnEscape?: () => boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Рисовать тень под оверлеем (`TUIElement.shadow`, цвет `widget.shadow`) —
|
|
46
|
+
* TUI-аналог box-shadow попапов VS Code. Выключено по умолчанию у всех
|
|
47
|
+
* сессий, включая меню tuidom: включает хост. Не задано — остаётся
|
|
48
|
+
* `element.shadow` как выставил владелец.
|
|
49
|
+
*/
|
|
50
|
+
shadow?: boolean;
|
|
44
51
|
}
|
|
45
52
|
export interface OverlaySessionHandle {
|
|
46
53
|
readonly element: TUIElement;
|
package/dist/dom/overlayLayer.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BoxConstraints,
|
|
1
|
+
import { BoxConstraints, Point, Size } from "../common/geometryPromitives.js";
|
|
2
2
|
import { RenderContext, TUIElement } from "./tuiElement.js";
|
|
3
3
|
export class OverlayLayer extends TUIElement {
|
|
4
4
|
items = [];
|
|
@@ -42,6 +42,8 @@ export class OverlayLayer extends TUIElement {
|
|
|
42
42
|
createSession(element, position, options) {
|
|
43
43
|
this.disposeSessionByElement(element);
|
|
44
44
|
const initialVisible = options.visible ?? false;
|
|
45
|
+
if (options.shadow !== undefined)
|
|
46
|
+
element.shadow = options.shadow;
|
|
45
47
|
this.addItem(element, position, false);
|
|
46
48
|
const session = {
|
|
47
49
|
element,
|
|
@@ -201,15 +203,9 @@ export class OverlayLayer extends TUIElement {
|
|
|
201
203
|
continue;
|
|
202
204
|
// Позиция — из localPosition (её выставил layoutChild), не из
|
|
203
205
|
// item.position: после layout авторитетна геометрия элемента (Н5).
|
|
204
|
-
// Клип по границам ребёнка —
|
|
205
|
-
// не выходит за layoutSize, хит-зона совпадает с видимым.
|
|
206
|
-
|
|
207
|
-
const childOffset = new Offset(child.localPosition.dx, child.localPosition.dy);
|
|
208
|
-
const clip = new Rect(child.globalPosition, child.layoutSize);
|
|
209
|
-
const childContext = context.withOffset(childOffset).withClip(clip);
|
|
210
|
-
if (childContext.clipRect.isEmpty)
|
|
211
|
-
continue;
|
|
212
|
-
child.render(childContext);
|
|
206
|
+
// Клип по границам ребёнка и тень за ним — общий renderChild (Н2):
|
|
207
|
+
// нарисованное не выходит за layoutSize, хит-зона совпадает с видимым.
|
|
208
|
+
this.renderChild(context, item.element);
|
|
213
209
|
}
|
|
214
210
|
}
|
|
215
211
|
openSession(session) {
|
|
@@ -72,6 +72,7 @@ export declare const STYLE_TOKEN_DEFAULTS: {
|
|
|
72
72
|
"editorSuggestWidget.selectedForeground": number;
|
|
73
73
|
"editorSuggestWidget.iconForeground": number;
|
|
74
74
|
"editorSuggestWidget.detailForeground": number;
|
|
75
|
+
"widget.shadow": number;
|
|
75
76
|
"editorWidget.foreground": number;
|
|
76
77
|
"editorWidget.background": number;
|
|
77
78
|
"editorWidget.border": number;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DEFAULT_COLOR, packRgb } from "../../common/colorUtils.js";
|
|
1
|
+
import { DEFAULT_COLOR, packRgb, packRgba } from "../../common/colorUtils.js";
|
|
2
2
|
/**
|
|
3
3
|
* Дефолтные значения цветовых токенов tuidom — единственное место, где у
|
|
4
4
|
* виджетов tuidom/ui есть RGB-литералы. Правило: токен, на который ссылается
|
|
@@ -85,6 +85,10 @@ export const STYLE_TOKEN_DEFAULTS = {
|
|
|
85
85
|
"editorSuggestWidget.selectedForeground": packRgb(255, 255, 255),
|
|
86
86
|
"editorSuggestWidget.iconForeground": packRgb(130, 170, 255),
|
|
87
87
|
"editorSuggestWidget.detailForeground": packRgb(120, 120, 130),
|
|
88
|
+
// ── Оверлеи ──
|
|
89
|
+
// Тень попапа (`TUIElement.shadow`): полупрозрачный чёрный, композитится с
|
|
90
|
+
// тем, что под ним. Значение — дефолт VS Code для тёмных тем (`#00000059`).
|
|
91
|
+
"widget.shadow": packRgba(0, 0, 0, 0x59),
|
|
88
92
|
// ── Виджеты редактора (диалоги, find) ──
|
|
89
93
|
"editorWidget.foreground": packRgb(204, 204, 204),
|
|
90
94
|
"editorWidget.background": packRgb(37, 37, 38),
|
package/dist/dom/tuiElement.d.ts
CHANGED
|
@@ -184,6 +184,25 @@ export declare class TUIElement {
|
|
|
184
184
|
get hidden(): boolean;
|
|
185
185
|
set hidden(value: boolean);
|
|
186
186
|
private hiddenValue;
|
|
187
|
+
/**
|
|
188
|
+
* Тень оверлея — TUI-аналог `box-shadow` VS Code: колонка справа и строка
|
|
189
|
+
* снизу от элемента (сдвиг 1×1) затемняются цветом `widget.shadow`
|
|
190
|
+
* (с альфой, композитится с тем, что уже нарисовано). Рисует не сам
|
|
191
|
+
* элемент, а его родитель после него ({@link renderChildren},
|
|
192
|
+
* `OverlayLayer.render`): клип ребёнка не выпускает его за собственный
|
|
193
|
+
* rect (Н2), а тень лежит снаружи. Damage-обход учитывает её через
|
|
194
|
+
* {@link paintOutset}. Отдаёт форму оверлею там, где тема рисует рамку
|
|
195
|
+
* цветом фона (Catppuccin: `menu.border` = фон меню).
|
|
196
|
+
*
|
|
197
|
+
* Рисуют её канонические циклы — {@link renderChildren} и
|
|
198
|
+
* `OverlayLayer.render` (оба через {@link renderChild}). Контейнеры со своим
|
|
199
|
+
* циклом (`SizedBoxElement`, `ScrollViewport`, строки списка) ребёнка
|
|
200
|
+
* растягивают на весь свой rect — тени там некуда лечь, она осталась бы за
|
|
201
|
+
* клипом самого контейнера.
|
|
202
|
+
*/
|
|
203
|
+
get shadow(): boolean;
|
|
204
|
+
set shadow(value: boolean);
|
|
205
|
+
private shadowValue;
|
|
187
206
|
/** Прикрепляет ребёнка в конец списка (снимая с прежнего родителя). */
|
|
188
207
|
protected appendChild(child: TUIElement): void;
|
|
189
208
|
/** Прикрепляет ребёнка на позицию index (снимая с прежнего родителя). */
|
|
@@ -363,6 +382,11 @@ export declare class TUIElement {
|
|
|
363
382
|
* запустило бы lazy-layout с мусорными constraints).
|
|
364
383
|
*/
|
|
365
384
|
collectDamage(sink: DamageList, parentOrigin: Point): void;
|
|
385
|
+
/**
|
|
386
|
+
* На сколько ячеек вправо и вниз родитель рисует за границей элемента
|
|
387
|
+
* (тень, см. {@link shadow}). Damage-rect элемента расширяется на столько же.
|
|
388
|
+
*/
|
|
389
|
+
protected get paintOutset(): number;
|
|
366
390
|
/** Обход детей damage-сбора — seam для контейнеров с нестандартной структурой. */
|
|
367
391
|
protected collectChildrenDamage(sink: DamageList, origin: Point): void;
|
|
368
392
|
/**
|
|
@@ -529,6 +553,32 @@ export declare class TUIElement {
|
|
|
529
553
|
* самый цикл, который раньше был скопирован в десяток контейнеров.
|
|
530
554
|
*/
|
|
531
555
|
protected renderChildren(context: RenderContext): void;
|
|
556
|
+
/**
|
|
557
|
+
* Отрисовка одного ребёнка в контексте родителя: сдвиг на localPosition,
|
|
558
|
+
* клип по границам ребёнка, затем его тень (см. {@link shadow}) — уже в
|
|
559
|
+
* контексте родителя, потому что она лежит за клипом ребёнка. Тень не
|
|
560
|
+
* зависит от того, попал ли сам ребёнок в клип: damage-rect может задеть
|
|
561
|
+
* только полосу тени (перерисовался виджет под ней), и тогда ребёнок
|
|
562
|
+
* пропускается, а тень обязана лечь заново.
|
|
563
|
+
*/
|
|
564
|
+
protected renderChild(context: RenderContext, child: TUIElement): void;
|
|
565
|
+
/**
|
|
566
|
+
* Тень ребёнка (см. {@link shadow}) в контексте РОДИТЕЛЯ: колонка справа
|
|
567
|
+
* (x = right, строки top+1…bottom) и строка снизу (y = bottom, колонки
|
|
568
|
+
* left+1…right) — классический сдвиг 1×1. Ячейка затемняется цветом
|
|
569
|
+
* `widget.shadow` из var-scope ребёнка: bg композитит грид, fg — здесь
|
|
570
|
+
* (грид кладёт fg с альфой на bg, а не на прежний fg); терминальный
|
|
571
|
+
* DEFAULT_COLOR у fg не трогаем — смешивать не с чем.
|
|
572
|
+
*
|
|
573
|
+
* Широкий глиф (CJK, эмодзи) наполовину не красится: голова тянет цвета в
|
|
574
|
+
* продолжение (`Grid.updateCell`), поэтому глиф под кромкой тени
|
|
575
|
+
* затемняется целиком — тень на этой строке шире на колонку. Продолжение
|
|
576
|
+
* в полосе пропускается: его голова либо сама в полосе (уже покрашена —
|
|
577
|
+
* иначе двойная тень), либо внутри ребёнка (его глиф у правой кромки).
|
|
578
|
+
* Единственное исключение — голова в пропущенном нижнем левом углу: такой
|
|
579
|
+
* глиф красится целиком через голову.
|
|
580
|
+
*/
|
|
581
|
+
protected paintChildShadow(context: RenderContext, child: TUIElement): void;
|
|
532
582
|
/**
|
|
533
583
|
* Финальный шаблон хит-теста — НЕ переопределять (кастомизация — через
|
|
534
584
|
* {@link hitTestChildren}/{@link hitTestSelf}): скрытое не кликается,
|
package/dist/dom/tuiElement.js
CHANGED
|
@@ -312,6 +312,32 @@ export class TUIElement {
|
|
|
312
312
|
this.markDirty();
|
|
313
313
|
}
|
|
314
314
|
hiddenValue = false;
|
|
315
|
+
/**
|
|
316
|
+
* Тень оверлея — TUI-аналог `box-shadow` VS Code: колонка справа и строка
|
|
317
|
+
* снизу от элемента (сдвиг 1×1) затемняются цветом `widget.shadow`
|
|
318
|
+
* (с альфой, композитится с тем, что уже нарисовано). Рисует не сам
|
|
319
|
+
* элемент, а его родитель после него ({@link renderChildren},
|
|
320
|
+
* `OverlayLayer.render`): клип ребёнка не выпускает его за собственный
|
|
321
|
+
* rect (Н2), а тень лежит снаружи. Damage-обход учитывает её через
|
|
322
|
+
* {@link paintOutset}. Отдаёт форму оверлею там, где тема рисует рамку
|
|
323
|
+
* цветом фона (Catppuccin: `menu.border` = фон меню).
|
|
324
|
+
*
|
|
325
|
+
* Рисуют её канонические циклы — {@link renderChildren} и
|
|
326
|
+
* `OverlayLayer.render` (оба через {@link renderChild}). Контейнеры со своим
|
|
327
|
+
* циклом (`SizedBoxElement`, `ScrollViewport`, строки списка) ребёнка
|
|
328
|
+
* растягивают на весь свой rect — тени там некуда лечь, она осталась бы за
|
|
329
|
+
* клипом самого контейнера.
|
|
330
|
+
*/
|
|
331
|
+
get shadow() {
|
|
332
|
+
return this.shadowValue;
|
|
333
|
+
}
|
|
334
|
+
set shadow(value) {
|
|
335
|
+
if (this.shadowValue === value)
|
|
336
|
+
return;
|
|
337
|
+
this.shadowValue = value;
|
|
338
|
+
this.markDirty();
|
|
339
|
+
}
|
|
340
|
+
shadowValue = false;
|
|
315
341
|
/** Прикрепляет ребёнка в конец списка (снимая с прежнего родителя). */
|
|
316
342
|
appendChild(child) {
|
|
317
343
|
this.insertChild(this.childrenList.length, child);
|
|
@@ -813,7 +839,11 @@ export class TUIElement {
|
|
|
813
839
|
this.hasPaintDirtyDescendant = false;
|
|
814
840
|
return;
|
|
815
841
|
}
|
|
816
|
-
|
|
842
|
+
// Rect для damage — с outset'ом: тень родитель рисует за границей
|
|
843
|
+
// allocatedSize, и её ячейки тоже должны перерисоваться при переезде,
|
|
844
|
+
// скрытии и paint-dirty.
|
|
845
|
+
const outset = this.paintOutset;
|
|
846
|
+
const rect = new Rect(new Point(parentOrigin.x + this.localPosition.dx, parentOrigin.y + this.localPosition.dy), new Size(this.allocatedSize.width + outset, this.allocatedSize.height + outset));
|
|
817
847
|
const old = this.lastPaintedRect;
|
|
818
848
|
// Ещё не рисовался — тоже moved. Optional chain здесь не подходит: после
|
|
819
849
|
// первого `old?.x` тип уже сужен, и остальные `old?.` стали бы лишними.
|
|
@@ -839,6 +869,13 @@ export class TUIElement {
|
|
|
839
869
|
if (descend)
|
|
840
870
|
this.collectChildrenDamage(sink, rect.origin);
|
|
841
871
|
}
|
|
872
|
+
/**
|
|
873
|
+
* На сколько ячеек вправо и вниз родитель рисует за границей элемента
|
|
874
|
+
* (тень, см. {@link shadow}). Damage-rect элемента расширяется на столько же.
|
|
875
|
+
*/
|
|
876
|
+
get paintOutset() {
|
|
877
|
+
return this.shadowValue ? 1 : 0;
|
|
878
|
+
}
|
|
842
879
|
/** Обход детей damage-сбора — seam для контейнеров с нестандартной структурой. */
|
|
843
880
|
collectChildrenDamage(sink, origin) {
|
|
844
881
|
for (const child of this.childrenList)
|
|
@@ -1147,16 +1184,75 @@ export class TUIElement {
|
|
|
1147
1184
|
for (const child of this.getChildren()) {
|
|
1148
1185
|
if (child.hidden)
|
|
1149
1186
|
continue;
|
|
1150
|
-
|
|
1151
|
-
const clip = new Rect(child.globalPosition, child.layoutSize);
|
|
1152
|
-
const childContext = context.withOffset(offset).withClip(clip);
|
|
1153
|
-
// Пустой клип — ребёнок целиком вне отрисовываемой области: пропуск
|
|
1154
|
-
// всего поддерева, включая side-эффекты его render.
|
|
1155
|
-
if (childContext.clipRect.isEmpty)
|
|
1156
|
-
continue;
|
|
1157
|
-
child.render(childContext);
|
|
1187
|
+
this.renderChild(context, child);
|
|
1158
1188
|
}
|
|
1159
1189
|
}
|
|
1190
|
+
/**
|
|
1191
|
+
* Отрисовка одного ребёнка в контексте родителя: сдвиг на localPosition,
|
|
1192
|
+
* клип по границам ребёнка, затем его тень (см. {@link shadow}) — уже в
|
|
1193
|
+
* контексте родителя, потому что она лежит за клипом ребёнка. Тень не
|
|
1194
|
+
* зависит от того, попал ли сам ребёнок в клип: damage-rect может задеть
|
|
1195
|
+
* только полосу тени (перерисовался виджет под ней), и тогда ребёнок
|
|
1196
|
+
* пропускается, а тень обязана лечь заново.
|
|
1197
|
+
*/
|
|
1198
|
+
renderChild(context, child) {
|
|
1199
|
+
const offset = new Offset(child.localPosition.dx, child.localPosition.dy);
|
|
1200
|
+
const clip = new Rect(child.globalPosition, child.layoutSize);
|
|
1201
|
+
const childContext = context.withOffset(offset).withClip(clip);
|
|
1202
|
+
// Пустой клип — ребёнок целиком вне отрисовываемой области: пропуск
|
|
1203
|
+
// всего поддерева, включая side-эффекты его render.
|
|
1204
|
+
if (!childContext.clipRect.isEmpty)
|
|
1205
|
+
child.render(childContext);
|
|
1206
|
+
if (child.shadow)
|
|
1207
|
+
this.paintChildShadow(context, child);
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* Тень ребёнка (см. {@link shadow}) в контексте РОДИТЕЛЯ: колонка справа
|
|
1211
|
+
* (x = right, строки top+1…bottom) и строка снизу (y = bottom, колонки
|
|
1212
|
+
* left+1…right) — классический сдвиг 1×1. Ячейка затемняется цветом
|
|
1213
|
+
* `widget.shadow` из var-scope ребёнка: bg композитит грид, fg — здесь
|
|
1214
|
+
* (грид кладёт fg с альфой на bg, а не на прежний fg); терминальный
|
|
1215
|
+
* DEFAULT_COLOR у fg не трогаем — смешивать не с чем.
|
|
1216
|
+
*
|
|
1217
|
+
* Широкий глиф (CJK, эмодзи) наполовину не красится: голова тянет цвета в
|
|
1218
|
+
* продолжение (`Grid.updateCell`), поэтому глиф под кромкой тени
|
|
1219
|
+
* затемняется целиком — тень на этой строке шире на колонку. Продолжение
|
|
1220
|
+
* в полосе пропускается: его голова либо сама в полосе (уже покрашена —
|
|
1221
|
+
* иначе двойная тень), либо внутри ребёнка (его глиф у правой кромки).
|
|
1222
|
+
* Единственное исключение — голова в пропущенном нижнем левом углу: такой
|
|
1223
|
+
* глиф красится целиком через голову.
|
|
1224
|
+
*/
|
|
1225
|
+
paintChildShadow(context, child) {
|
|
1226
|
+
const color = child.styleVar("widget.shadow");
|
|
1227
|
+
if (color === TRANSPARENT_COLOR)
|
|
1228
|
+
return;
|
|
1229
|
+
const left = child.localPosition.dx;
|
|
1230
|
+
const top = child.localPosition.dy;
|
|
1231
|
+
const right = left + child.layoutSize.width;
|
|
1232
|
+
const bottom = top + child.layoutSize.height;
|
|
1233
|
+
const shadeCell = (x, y, cell) => {
|
|
1234
|
+
context.setCell(x, y, { bg: color, fg: cell.fg < 0 ? cell.fg : compositeOver(color, cell.fg) });
|
|
1235
|
+
};
|
|
1236
|
+
const shade = (x, y, wholeGlyph) => {
|
|
1237
|
+
const cell = context.getCell(x, y);
|
|
1238
|
+
if (cell === null)
|
|
1239
|
+
return;
|
|
1240
|
+
if (cell.width !== 0) {
|
|
1241
|
+
shadeCell(x, y, cell);
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
if (!wholeGlyph)
|
|
1245
|
+
return;
|
|
1246
|
+
const head = context.getCell(x - 1, y);
|
|
1247
|
+
if (head === null)
|
|
1248
|
+
return;
|
|
1249
|
+
shadeCell(x - 1, y, head);
|
|
1250
|
+
};
|
|
1251
|
+
for (let y = top + 1; y <= bottom; y++)
|
|
1252
|
+
shade(right, y, false);
|
|
1253
|
+
for (let x = left + 1; x < right; x++)
|
|
1254
|
+
shade(x, bottom, x === left + 1);
|
|
1255
|
+
}
|
|
1160
1256
|
// ─── Hit-testing ───
|
|
1161
1257
|
//
|
|
1162
1258
|
// Правило системы (Н6): рендер и Tab обходят детей ВПЕРЁД, хит-тест — тем
|
package/package.json
CHANGED