@tuidom/core 0.4.0 → 0.5.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/dist/dom/overlayLayer.d.ts +7 -0
- package/dist/dom/overlayLayer.js +6 -10
- package/dist/dom/styles/styleTokens.d.ts +1 -3
- package/dist/dom/styles/styleTokens.js +9 -5
- package/dist/dom/tuiApplication.d.ts +11 -1
- package/dist/dom/tuiApplication.js +13 -3
- package/dist/dom/tuiElement.d.ts +67 -0
- package/dist/dom/tuiElement.js +127 -9
- package/package.json +1 -1
|
@@ -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) {
|
|
@@ -47,9 +47,6 @@ export declare const STYLE_TOKEN_DEFAULTS: {
|
|
|
47
47
|
"tab.inactiveForeground": number;
|
|
48
48
|
"tab.inactiveBackground": number;
|
|
49
49
|
"editorGroupHeader.tabsBackground": number;
|
|
50
|
-
"panel.background": number;
|
|
51
|
-
"panelTitle.inactiveForeground": number;
|
|
52
|
-
"panel.border": number;
|
|
53
50
|
"titledPanel.titleForeground": number;
|
|
54
51
|
"terminal.foreground": number;
|
|
55
52
|
"terminal.background": number;
|
|
@@ -72,6 +69,7 @@ export declare const STYLE_TOKEN_DEFAULTS: {
|
|
|
72
69
|
"editorSuggestWidget.selectedForeground": number;
|
|
73
70
|
"editorSuggestWidget.iconForeground": number;
|
|
74
71
|
"editorSuggestWidget.detailForeground": number;
|
|
72
|
+
"widget.shadow": number;
|
|
75
73
|
"editorWidget.foreground": number;
|
|
76
74
|
"editorWidget.background": number;
|
|
77
75
|
"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-литералы. Правило: токен, на который ссылается
|
|
@@ -53,10 +53,10 @@ export const STYLE_TOKEN_DEFAULTS = {
|
|
|
53
53
|
"tab.inactiveForeground": packRgb(150, 150, 150),
|
|
54
54
|
"tab.inactiveBackground": packRgb(45, 45, 45),
|
|
55
55
|
"editorGroupHeader.tabsBackground": packRgb(37, 37, 38),
|
|
56
|
-
// ── Панель ──
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
// ── Панель с заголовком ──
|
|
57
|
+
// Дефолтов `panel.*` здесь больше нет: виджет нижней панели уехал в vexx
|
|
58
|
+
// (прикладной — его API оперировал понятиями панели IDE), и токены вместе с
|
|
59
|
+
// ним, в его реестр цветов.
|
|
60
60
|
"titledPanel.titleForeground": packRgb(130, 130, 130),
|
|
61
61
|
// ── Терминал ──
|
|
62
62
|
"terminal.foreground": DEFAULT_COLOR,
|
|
@@ -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),
|
|
@@ -35,8 +35,13 @@ export declare class TuiApplication {
|
|
|
35
35
|
/**
|
|
36
36
|
* Schedules a deferred render via setImmediate.
|
|
37
37
|
* Batches multiple markDirty() calls into a single frame.
|
|
38
|
-
* Skips rendering if
|
|
38
|
+
* Skips rendering if there is nothing to repaint (e.g. a synchronous
|
|
39
39
|
* renderFrame from handleInput already ran).
|
|
40
|
+
*
|
|
41
|
+
* Гейт — {@link TUIElement.needsRepaint}, а не layout-грязь: layout снимает
|
|
42
|
+
* кто угодно (ленивый фоллбэк `layoutSize` у хит-теста мыши, инспектора,
|
|
43
|
+
* измеряющего виджета), и гейт по ней ронял уже запланированный кадр
|
|
44
|
+
* насовсем — экран ретейн-буфер, старые ячейки так и оставались на нём.
|
|
40
45
|
*/
|
|
41
46
|
scheduleRender(): void;
|
|
42
47
|
private handleInput;
|
|
@@ -50,6 +55,11 @@ export declare class TuiApplication {
|
|
|
50
55
|
* жгли CPU впустую. Стилевые изменения гейт проходят: markStyleDirty
|
|
51
56
|
* заканчивается markDirty. Синхронность оставлена намеренно: тесты и
|
|
52
57
|
* обработчики читают экран/геометрию сразу после события.
|
|
58
|
+
*
|
|
59
|
+
* Гейт — {@link TUIElement.needsRepaint} (см. {@link scheduleRender}): сам
|
|
60
|
+
* хит-тест мыши читает `layoutSize` и гасит layout-грязь, так что гейт по
|
|
61
|
+
* ней терял кадр, помеченный ДО этого события (например ответ субпроцесса,
|
|
62
|
+
* пришедший между press и release одного клика).
|
|
53
63
|
*/
|
|
54
64
|
private renderAfterInput;
|
|
55
65
|
private handleResize;
|
|
@@ -109,8 +109,13 @@ export class TuiApplication {
|
|
|
109
109
|
/**
|
|
110
110
|
* Schedules a deferred render via setImmediate.
|
|
111
111
|
* Batches multiple markDirty() calls into a single frame.
|
|
112
|
-
* Skips rendering if
|
|
112
|
+
* Skips rendering if there is nothing to repaint (e.g. a synchronous
|
|
113
113
|
* renderFrame from handleInput already ran).
|
|
114
|
+
*
|
|
115
|
+
* Гейт — {@link TUIElement.needsRepaint}, а не layout-грязь: layout снимает
|
|
116
|
+
* кто угодно (ленивый фоллбэк `layoutSize` у хит-теста мыши, инспектора,
|
|
117
|
+
* измеряющего виджета), и гейт по ней ронял уже запланированный кадр
|
|
118
|
+
* насовсем — экран ретейн-буфер, старые ячейки так и оставались на нём.
|
|
114
119
|
*/
|
|
115
120
|
scheduleRender() {
|
|
116
121
|
if (this.renderScheduled)
|
|
@@ -118,7 +123,7 @@ export class TuiApplication {
|
|
|
118
123
|
this.renderScheduled = true;
|
|
119
124
|
setImmediate(() => {
|
|
120
125
|
this.renderScheduled = false;
|
|
121
|
-
if (this.root?.
|
|
126
|
+
if (this.root?.needsRepaint === true) {
|
|
122
127
|
this.renderFrame();
|
|
123
128
|
}
|
|
124
129
|
});
|
|
@@ -194,9 +199,14 @@ export class TuiApplication {
|
|
|
194
199
|
* жгли CPU впустую. Стилевые изменения гейт проходят: markStyleDirty
|
|
195
200
|
* заканчивается markDirty. Синхронность оставлена намеренно: тесты и
|
|
196
201
|
* обработчики читают экран/геометрию сразу после события.
|
|
202
|
+
*
|
|
203
|
+
* Гейт — {@link TUIElement.needsRepaint} (см. {@link scheduleRender}): сам
|
|
204
|
+
* хит-тест мыши читает `layoutSize` и гасит layout-грязь, так что гейт по
|
|
205
|
+
* ней терял кадр, помеченный ДО этого события (например ответ субпроцесса,
|
|
206
|
+
* пришедший между press и release одного клика).
|
|
197
207
|
*/
|
|
198
208
|
renderAfterInput() {
|
|
199
|
-
if (this.root?.
|
|
209
|
+
if (this.root?.needsRepaint === true) {
|
|
200
210
|
this.renderFrame();
|
|
201
211
|
}
|
|
202
212
|
}
|
package/dist/dom/tuiElement.d.ts
CHANGED
|
@@ -133,6 +133,23 @@ export declare class TUIElement {
|
|
|
133
133
|
private hasPaintDirtyDescendant;
|
|
134
134
|
private lastPaintedRect;
|
|
135
135
|
private pendingDetachDamage;
|
|
136
|
+
/**
|
|
137
|
+
* Есть ли что отдать следующему кадру в этом поддереве: кто-то просил
|
|
138
|
+
* перерисовку ({@link markDirty}) или отцепил поддерево, и кадра с тех пор
|
|
139
|
+
* не было.
|
|
140
|
+
*
|
|
141
|
+
* Это — и только это — гейт отложенного кадра (`TuiApplication`
|
|
142
|
+
* `scheduleRender`) и кадра после ввода. В отличие от
|
|
143
|
+
* {@link isLayoutDirty}, снимается ТОЛЬКО damage-обходом настоящего кадра
|
|
144
|
+
* ({@link collectDamage}): layout снимает кто угодно — ленивый фоллбэк
|
|
145
|
+
* геттера {@link layoutSize} зовут хит-тест мыши
|
|
146
|
+
* ({@link elementFromPoint}), инспектор и любой виджет, измеряющий соседа.
|
|
147
|
+
* Пока гейтом была layout-грязь, такой читатель гасил флаг посреди
|
|
148
|
+
* ожидания, и запланированный кадр не состоялся вовсе — а экран
|
|
149
|
+
* ретейн-буфер, так что старые ячейки оставались на нём до следующей
|
|
150
|
+
* несвязанной перерисовки.
|
|
151
|
+
*/
|
|
152
|
+
get needsRepaint(): boolean;
|
|
136
153
|
protected _parent: TUIElement | null;
|
|
137
154
|
private isRootAnchor;
|
|
138
155
|
private requestRenderCallback;
|
|
@@ -184,6 +201,25 @@ export declare class TUIElement {
|
|
|
184
201
|
get hidden(): boolean;
|
|
185
202
|
set hidden(value: boolean);
|
|
186
203
|
private hiddenValue;
|
|
204
|
+
/**
|
|
205
|
+
* Тень оверлея — TUI-аналог `box-shadow` VS Code: колонка справа и строка
|
|
206
|
+
* снизу от элемента (сдвиг 1×1) затемняются цветом `widget.shadow`
|
|
207
|
+
* (с альфой, композитится с тем, что уже нарисовано). Рисует не сам
|
|
208
|
+
* элемент, а его родитель после него ({@link renderChildren},
|
|
209
|
+
* `OverlayLayer.render`): клип ребёнка не выпускает его за собственный
|
|
210
|
+
* rect (Н2), а тень лежит снаружи. Damage-обход учитывает её через
|
|
211
|
+
* {@link paintOutset}. Отдаёт форму оверлею там, где тема рисует рамку
|
|
212
|
+
* цветом фона (Catppuccin: `menu.border` = фон меню).
|
|
213
|
+
*
|
|
214
|
+
* Рисуют её канонические циклы — {@link renderChildren} и
|
|
215
|
+
* `OverlayLayer.render` (оба через {@link renderChild}). Контейнеры со своим
|
|
216
|
+
* циклом (`SizedBoxElement`, `ScrollViewport`, строки списка) ребёнка
|
|
217
|
+
* растягивают на весь свой rect — тени там некуда лечь, она осталась бы за
|
|
218
|
+
* клипом самого контейнера.
|
|
219
|
+
*/
|
|
220
|
+
get shadow(): boolean;
|
|
221
|
+
set shadow(value: boolean);
|
|
222
|
+
private shadowValue;
|
|
187
223
|
/** Прикрепляет ребёнка в конец списка (снимая с прежнего родителя). */
|
|
188
224
|
protected appendChild(child: TUIElement): void;
|
|
189
225
|
/** Прикрепляет ребёнка на позицию index (снимая с прежнего родителя). */
|
|
@@ -363,6 +399,11 @@ export declare class TUIElement {
|
|
|
363
399
|
* запустило бы lazy-layout с мусорными constraints).
|
|
364
400
|
*/
|
|
365
401
|
collectDamage(sink: DamageList, parentOrigin: Point): void;
|
|
402
|
+
/**
|
|
403
|
+
* На сколько ячеек вправо и вниз родитель рисует за границей элемента
|
|
404
|
+
* (тень, см. {@link shadow}). Damage-rect элемента расширяется на столько же.
|
|
405
|
+
*/
|
|
406
|
+
protected get paintOutset(): number;
|
|
366
407
|
/** Обход детей damage-сбора — seam для контейнеров с нестандартной структурой. */
|
|
367
408
|
protected collectChildrenDamage(sink: DamageList, origin: Point): void;
|
|
368
409
|
/**
|
|
@@ -529,6 +570,32 @@ export declare class TUIElement {
|
|
|
529
570
|
* самый цикл, который раньше был скопирован в десяток контейнеров.
|
|
530
571
|
*/
|
|
531
572
|
protected renderChildren(context: RenderContext): void;
|
|
573
|
+
/**
|
|
574
|
+
* Отрисовка одного ребёнка в контексте родителя: сдвиг на localPosition,
|
|
575
|
+
* клип по границам ребёнка, затем его тень (см. {@link shadow}) — уже в
|
|
576
|
+
* контексте родителя, потому что она лежит за клипом ребёнка. Тень не
|
|
577
|
+
* зависит от того, попал ли сам ребёнок в клип: damage-rect может задеть
|
|
578
|
+
* только полосу тени (перерисовался виджет под ней), и тогда ребёнок
|
|
579
|
+
* пропускается, а тень обязана лечь заново.
|
|
580
|
+
*/
|
|
581
|
+
protected renderChild(context: RenderContext, child: TUIElement): void;
|
|
582
|
+
/**
|
|
583
|
+
* Тень ребёнка (см. {@link shadow}) в контексте РОДИТЕЛЯ: колонка справа
|
|
584
|
+
* (x = right, строки top+1…bottom) и строка снизу (y = bottom, колонки
|
|
585
|
+
* left+1…right) — классический сдвиг 1×1. Ячейка затемняется цветом
|
|
586
|
+
* `widget.shadow` из var-scope ребёнка: bg композитит грид, fg — здесь
|
|
587
|
+
* (грид кладёт fg с альфой на bg, а не на прежний fg); терминальный
|
|
588
|
+
* DEFAULT_COLOR у fg не трогаем — смешивать не с чем.
|
|
589
|
+
*
|
|
590
|
+
* Широкий глиф (CJK, эмодзи) наполовину не красится: голова тянет цвета в
|
|
591
|
+
* продолжение (`Grid.updateCell`), поэтому глиф под кромкой тени
|
|
592
|
+
* затемняется целиком — тень на этой строке шире на колонку. Продолжение
|
|
593
|
+
* в полосе пропускается: его голова либо сама в полосе (уже покрашена —
|
|
594
|
+
* иначе двойная тень), либо внутри ребёнка (его глиф у правой кромки).
|
|
595
|
+
* Единственное исключение — голова в пропущенном нижнем левом углу: такой
|
|
596
|
+
* глиф красится целиком через голову.
|
|
597
|
+
*/
|
|
598
|
+
protected paintChildShadow(context: RenderContext, child: TUIElement): void;
|
|
532
599
|
/**
|
|
533
600
|
* Финальный шаблон хит-теста — НЕ переопределять (кастомизация — через
|
|
534
601
|
* {@link hitTestChildren}/{@link hitTestSelf}): скрытое не кликается,
|
package/dist/dom/tuiElement.js
CHANGED
|
@@ -200,6 +200,28 @@ export class TUIElement {
|
|
|
200
200
|
lastPaintedRect = null;
|
|
201
201
|
// Только на корне: rect'ы поддеревьев, отцеплённых с последнего кадра.
|
|
202
202
|
pendingDetachDamage = [];
|
|
203
|
+
/**
|
|
204
|
+
* Есть ли что отдать следующему кадру в этом поддереве: кто-то просил
|
|
205
|
+
* перерисовку ({@link markDirty}) или отцепил поддерево, и кадра с тех пор
|
|
206
|
+
* не было.
|
|
207
|
+
*
|
|
208
|
+
* Это — и только это — гейт отложенного кадра (`TuiApplication`
|
|
209
|
+
* `scheduleRender`) и кадра после ввода. В отличие от
|
|
210
|
+
* {@link isLayoutDirty}, снимается ТОЛЬКО damage-обходом настоящего кадра
|
|
211
|
+
* ({@link collectDamage}): layout снимает кто угодно — ленивый фоллбэк
|
|
212
|
+
* геттера {@link layoutSize} зовут хит-тест мыши
|
|
213
|
+
* ({@link elementFromPoint}), инспектор и любой виджет, измеряющий соседа.
|
|
214
|
+
* Пока гейтом была layout-грязь, такой читатель гасил флаг посреди
|
|
215
|
+
* ожидания, и запланированный кадр не состоялся вовсе — а экран
|
|
216
|
+
* ретейн-буфер, так что старые ячейки оставались на нём до следующей
|
|
217
|
+
* несвязанной перерисовки.
|
|
218
|
+
*/
|
|
219
|
+
get needsRepaint() {
|
|
220
|
+
return (this.isPaintDirty ||
|
|
221
|
+
this.hasPaintDirtyDescendant ||
|
|
222
|
+
this.isLayoutDirty ||
|
|
223
|
+
this.pendingDetachDamage.length > 0);
|
|
224
|
+
}
|
|
203
225
|
_parent = null;
|
|
204
226
|
// Якорь дерева: выставляется setAsRoot() (BodyElement, тестовые корни).
|
|
205
227
|
// Сам root НЕ кэшируется — getRoot() выводит его из цепочки родителей.
|
|
@@ -312,6 +334,32 @@ export class TUIElement {
|
|
|
312
334
|
this.markDirty();
|
|
313
335
|
}
|
|
314
336
|
hiddenValue = false;
|
|
337
|
+
/**
|
|
338
|
+
* Тень оверлея — TUI-аналог `box-shadow` VS Code: колонка справа и строка
|
|
339
|
+
* снизу от элемента (сдвиг 1×1) затемняются цветом `widget.shadow`
|
|
340
|
+
* (с альфой, композитится с тем, что уже нарисовано). Рисует не сам
|
|
341
|
+
* элемент, а его родитель после него ({@link renderChildren},
|
|
342
|
+
* `OverlayLayer.render`): клип ребёнка не выпускает его за собственный
|
|
343
|
+
* rect (Н2), а тень лежит снаружи. Damage-обход учитывает её через
|
|
344
|
+
* {@link paintOutset}. Отдаёт форму оверлею там, где тема рисует рамку
|
|
345
|
+
* цветом фона (Catppuccin: `menu.border` = фон меню).
|
|
346
|
+
*
|
|
347
|
+
* Рисуют её канонические циклы — {@link renderChildren} и
|
|
348
|
+
* `OverlayLayer.render` (оба через {@link renderChild}). Контейнеры со своим
|
|
349
|
+
* циклом (`SizedBoxElement`, `ScrollViewport`, строки списка) ребёнка
|
|
350
|
+
* растягивают на весь свой rect — тени там некуда лечь, она осталась бы за
|
|
351
|
+
* клипом самого контейнера.
|
|
352
|
+
*/
|
|
353
|
+
get shadow() {
|
|
354
|
+
return this.shadowValue;
|
|
355
|
+
}
|
|
356
|
+
set shadow(value) {
|
|
357
|
+
if (this.shadowValue === value)
|
|
358
|
+
return;
|
|
359
|
+
this.shadowValue = value;
|
|
360
|
+
this.markDirty();
|
|
361
|
+
}
|
|
362
|
+
shadowValue = false;
|
|
315
363
|
/** Прикрепляет ребёнка в конец списка (снимая с прежнего родителя). */
|
|
316
364
|
appendChild(child) {
|
|
317
365
|
this.insertChild(this.childrenList.length, child);
|
|
@@ -813,7 +861,11 @@ export class TUIElement {
|
|
|
813
861
|
this.hasPaintDirtyDescendant = false;
|
|
814
862
|
return;
|
|
815
863
|
}
|
|
816
|
-
|
|
864
|
+
// Rect для damage — с outset'ом: тень родитель рисует за границей
|
|
865
|
+
// allocatedSize, и её ячейки тоже должны перерисоваться при переезде,
|
|
866
|
+
// скрытии и paint-dirty.
|
|
867
|
+
const outset = this.paintOutset;
|
|
868
|
+
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
869
|
const old = this.lastPaintedRect;
|
|
818
870
|
// Ещё не рисовался — тоже moved. Optional chain здесь не подходит: после
|
|
819
871
|
// первого `old?.x` тип уже сужен, и остальные `old?.` стали бы лишними.
|
|
@@ -839,6 +891,13 @@ export class TUIElement {
|
|
|
839
891
|
if (descend)
|
|
840
892
|
this.collectChildrenDamage(sink, rect.origin);
|
|
841
893
|
}
|
|
894
|
+
/**
|
|
895
|
+
* На сколько ячеек вправо и вниз родитель рисует за границей элемента
|
|
896
|
+
* (тень, см. {@link shadow}). Damage-rect элемента расширяется на столько же.
|
|
897
|
+
*/
|
|
898
|
+
get paintOutset() {
|
|
899
|
+
return this.shadowValue ? 1 : 0;
|
|
900
|
+
}
|
|
842
901
|
/** Обход детей damage-сбора — seam для контейнеров с нестандартной структурой. */
|
|
843
902
|
collectChildrenDamage(sink, origin) {
|
|
844
903
|
for (const child of this.childrenList)
|
|
@@ -1147,16 +1206,75 @@ export class TUIElement {
|
|
|
1147
1206
|
for (const child of this.getChildren()) {
|
|
1148
1207
|
if (child.hidden)
|
|
1149
1208
|
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);
|
|
1209
|
+
this.renderChild(context, child);
|
|
1158
1210
|
}
|
|
1159
1211
|
}
|
|
1212
|
+
/**
|
|
1213
|
+
* Отрисовка одного ребёнка в контексте родителя: сдвиг на localPosition,
|
|
1214
|
+
* клип по границам ребёнка, затем его тень (см. {@link shadow}) — уже в
|
|
1215
|
+
* контексте родителя, потому что она лежит за клипом ребёнка. Тень не
|
|
1216
|
+
* зависит от того, попал ли сам ребёнок в клип: damage-rect может задеть
|
|
1217
|
+
* только полосу тени (перерисовался виджет под ней), и тогда ребёнок
|
|
1218
|
+
* пропускается, а тень обязана лечь заново.
|
|
1219
|
+
*/
|
|
1220
|
+
renderChild(context, child) {
|
|
1221
|
+
const offset = new Offset(child.localPosition.dx, child.localPosition.dy);
|
|
1222
|
+
const clip = new Rect(child.globalPosition, child.layoutSize);
|
|
1223
|
+
const childContext = context.withOffset(offset).withClip(clip);
|
|
1224
|
+
// Пустой клип — ребёнок целиком вне отрисовываемой области: пропуск
|
|
1225
|
+
// всего поддерева, включая side-эффекты его render.
|
|
1226
|
+
if (!childContext.clipRect.isEmpty)
|
|
1227
|
+
child.render(childContext);
|
|
1228
|
+
if (child.shadow)
|
|
1229
|
+
this.paintChildShadow(context, child);
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1232
|
+
* Тень ребёнка (см. {@link shadow}) в контексте РОДИТЕЛЯ: колонка справа
|
|
1233
|
+
* (x = right, строки top+1…bottom) и строка снизу (y = bottom, колонки
|
|
1234
|
+
* left+1…right) — классический сдвиг 1×1. Ячейка затемняется цветом
|
|
1235
|
+
* `widget.shadow` из var-scope ребёнка: bg композитит грид, fg — здесь
|
|
1236
|
+
* (грид кладёт fg с альфой на bg, а не на прежний fg); терминальный
|
|
1237
|
+
* DEFAULT_COLOR у fg не трогаем — смешивать не с чем.
|
|
1238
|
+
*
|
|
1239
|
+
* Широкий глиф (CJK, эмодзи) наполовину не красится: голова тянет цвета в
|
|
1240
|
+
* продолжение (`Grid.updateCell`), поэтому глиф под кромкой тени
|
|
1241
|
+
* затемняется целиком — тень на этой строке шире на колонку. Продолжение
|
|
1242
|
+
* в полосе пропускается: его голова либо сама в полосе (уже покрашена —
|
|
1243
|
+
* иначе двойная тень), либо внутри ребёнка (его глиф у правой кромки).
|
|
1244
|
+
* Единственное исключение — голова в пропущенном нижнем левом углу: такой
|
|
1245
|
+
* глиф красится целиком через голову.
|
|
1246
|
+
*/
|
|
1247
|
+
paintChildShadow(context, child) {
|
|
1248
|
+
const color = child.styleVar("widget.shadow");
|
|
1249
|
+
if (color === TRANSPARENT_COLOR)
|
|
1250
|
+
return;
|
|
1251
|
+
const left = child.localPosition.dx;
|
|
1252
|
+
const top = child.localPosition.dy;
|
|
1253
|
+
const right = left + child.layoutSize.width;
|
|
1254
|
+
const bottom = top + child.layoutSize.height;
|
|
1255
|
+
const shadeCell = (x, y, cell) => {
|
|
1256
|
+
context.setCell(x, y, { bg: color, fg: cell.fg < 0 ? cell.fg : compositeOver(color, cell.fg) });
|
|
1257
|
+
};
|
|
1258
|
+
const shade = (x, y, wholeGlyph) => {
|
|
1259
|
+
const cell = context.getCell(x, y);
|
|
1260
|
+
if (cell === null)
|
|
1261
|
+
return;
|
|
1262
|
+
if (cell.width !== 0) {
|
|
1263
|
+
shadeCell(x, y, cell);
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
if (!wholeGlyph)
|
|
1267
|
+
return;
|
|
1268
|
+
const head = context.getCell(x - 1, y);
|
|
1269
|
+
if (head === null)
|
|
1270
|
+
return;
|
|
1271
|
+
shadeCell(x - 1, y, head);
|
|
1272
|
+
};
|
|
1273
|
+
for (let y = top + 1; y <= bottom; y++)
|
|
1274
|
+
shade(right, y, false);
|
|
1275
|
+
for (let x = left + 1; x < right; x++)
|
|
1276
|
+
shade(x, bottom, x === left + 1);
|
|
1277
|
+
}
|
|
1160
1278
|
// ─── Hit-testing ───
|
|
1161
1279
|
//
|
|
1162
1280
|
// Правило системы (Н6): рендер и Tab обходят детей ВПЕРЁД, хит-тест — тем
|
package/package.json
CHANGED