@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.
@@ -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;
@@ -1,4 +1,4 @@
1
- import { BoxConstraints, Offset, Point, Rect, Size } from "../common/geometryPromitives.js";
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
- // Клип по границам ребёнка — инвариант отрисовки (Н2): нарисованное
205
- // не выходит за layoutSize, хит-зона совпадает с видимым.
206
- const child = item.element;
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
- "panel.background": packRgb(24, 24, 24),
58
- "panelTitle.inactiveForeground": packRgb(142, 142, 142),
59
- "panel.border": packRgb(43, 43, 43),
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 layout is already clean (e.g. a synchronous
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 layout is already clean (e.g. a synchronous
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?.isLayoutDirty) {
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?.isLayoutDirty) {
209
+ if (this.root?.needsRepaint === true) {
200
210
  this.renderFrame();
201
211
  }
202
212
  }
@@ -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}): скрытое не кликается,
@@ -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
- const rect = new Rect(new Point(parentOrigin.x + this.localPosition.dx, parentOrigin.y + this.localPosition.dy), this.allocatedSize);
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
- const offset = new Offset(child.localPosition.dx, child.localPosition.dy);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tuidom/core",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "type": "module",
5
5
  "description": "Experimental terminal UI DOM — core: element tree, flex layout, grid rendering, input parsing. API is unstable.",
6
6
  "license": "MIT",