Head the groups of the inspector with bands that fold (#290)
The groups of the inspector had a small grey label as their title, in the same grid as a property row, so with many rows one group hardly stood apart from the next. Now every group has a band as its head, from edge to edge of the panel, and a click on it folds the group. - A band runs across the padding of its column in the header colour, with the title and a mark that says whether the group is open. The titles stay as they are, as answered in the issue. - A click on a band folds its group: the rows go, the band stays and the next band moves up against it. A control in the band (Open, + Variable, + Property) does its own job and folds nothing. - A placed object has a group SWITCHES under its name, asset and parent: Enabled, Keep on scene load and the switches of a behavior. An asset has SWITCHES and PROPERTIES, and a group without rows is left out. - The same bands head the tilemap panel, the Variables tab, the side of the Pixel art tab and the shape list of vector art. A list with a title (Layers, Frames, Shapes) folds with a click on its header, clear of its plus. - ui/sections.ts holds the rules as pure functions, SectionStack is the column and SectionHeader the band. Every title of the inspector goes through addTitle or sectionHeader in inspector/host.ts. Decided here: - A fold is kept for good, in localStorage of this browser. The issue asked whether for the session or for good and the answer left it open. - A fold is remembered by title, per panel. PROPERTIES folded once is folded on every object, asset and node. A number in a title does not count. - A behavior asset has all its rows under SWITCHES. A Variables asset and a state machine get a group SCOPE. - A greyed out control in a band takes the click too. Without that the click fell through to the band and folded the group. - The Curve tab has no column of this kind and is unchanged. Found on the way: - The inspector of a sprite is taller than the 800 pixel window of the checks now. The hooks that hand out a row scroll it into view first, and module 28 scrolls before it looks for a row by its text. Tests: - Gate: 202 files, 3008 tests (2982 + 26), exit 0 - e2e: no complete run for this state. Modules 28 to 61 passed alone (749 checks). A full run was cut off in the accounts module at 1492 passes without a failure, the modules after it and the smokes are missing. The sum of 1639 (1635 + 4) is reckoned, CI measures it. - 26 unit tests in tests/editor/sections.test.ts - Four e2e checks with a real mouse, three in module 02 and one in module 48 - 25 unit counter-probes all fail tests, run on stein2 - Screenshots of the bands looked at before the last build, none after it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c5ba21533d
commit
393caadc9d
22 files changed
+1072
-53
No files matched your search
@@ -1,5 +1,14 @@
|
||||
export { Widget, Box, type Size } from './Widget';
|
||||
export { HStack, VStack, Spacer, Stack, type StackOptions } from './layout/Stack';
|
||||
export { SectionStack } from './layout/SectionStack';
|
||||
export { SectionHeader, type SectionHeaderOptions } from './widgets/SectionHeader';
|
||||
export {
|
||||
hiddenBySections,
|
||||
SECTION_BAND_HEIGHT,
|
||||
sectionOffsets,
|
||||
type SectionRow,
|
||||
type SectionStart,
|
||||
} from './sections';
|
||||
export { Theme, textStyle, type TextStyleToken } from './Theme';
|
||||
export { loadEditorFonts } from './fonts';
|
||||
export { initUI, ui, type UIContext } from './UIContext';
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { hiddenBySections, sectionOffsets, type SectionRow, sectionStartOf } from '../sections';
|
||||
import { type Size, Widget } from '../Widget';
|
||||
import { type StackOptions, VStack } from './Stack';
|
||||
|
||||
/**
|
||||
* A column of rows in groups (#290): a `SectionHeader` runs across the padding as a band, the rows
|
||||
* after it stand inside it, and a folded header hides its rows down to the next one. Children are
|
||||
* never stretched along the column – it is meant for a scroll container.
|
||||
*/
|
||||
export class SectionStack extends VStack {
|
||||
/** After a group was folded or unfolded – the owner measures the column again. */
|
||||
onFold: () => void = () => {};
|
||||
/** The children a fold hid, so unfolding shows only those again. */
|
||||
private readonly hiddenByFold = new WeakSet<Widget>();
|
||||
|
||||
constructor(options: StackOptions = {}) {
|
||||
super(options);
|
||||
}
|
||||
|
||||
refold(): void {
|
||||
this.applyFolds();
|
||||
this.onFold();
|
||||
}
|
||||
|
||||
private applyFolds(): void {
|
||||
const widgets = this.children.filter((c): c is Widget => c instanceof Widget);
|
||||
const hidden = hiddenBySections(widgets.map((w) => sectionStartOf(w)?.sectionCollapsed ?? null));
|
||||
widgets.forEach((widget, i) => {
|
||||
if (hidden[i] && widget.visible) {
|
||||
widget.visible = false;
|
||||
this.hiddenByFold.add(widget);
|
||||
} else if (!hidden[i] && this.hiddenByFold.has(widget)) {
|
||||
widget.visible = true;
|
||||
this.hiddenByFold.delete(widget);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private rows(sizes: readonly Size[]): SectionRow[] {
|
||||
return this.items.map((item, i) => {
|
||||
const start = sectionStartOf(item);
|
||||
return {
|
||||
h: sizes[i]!.h,
|
||||
start: start !== null,
|
||||
band: start !== null && (start.sectionStart === 'band' || start.sectionCollapsed),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
override measure(): Size {
|
||||
this.applyFolds();
|
||||
const sizes = this.items.map((item) => item.measure());
|
||||
const w = Math.max(0, ...sizes.map((size) => size.w)) + this.padding * 2;
|
||||
const { height } = sectionOffsets(this.rows(sizes), this.gap, this.padding);
|
||||
return { w: Math.max(w, this.minWidth), h: Math.max(height, this.minHeight) };
|
||||
}
|
||||
|
||||
override layout(): void {
|
||||
this.applyFolds();
|
||||
const items = this.items;
|
||||
const sizes = items.map((item) => item.measure());
|
||||
const { ys } = sectionOffsets(this.rows(sizes), this.gap, this.padding);
|
||||
items.forEach((item, i) => {
|
||||
const band = sectionStartOf(item)?.sectionStart === 'band';
|
||||
item.position.set(band ? 0 : this.padding, Math.round(ys[i]!));
|
||||
item.resize(Math.max(0, band ? this.w : this.w - this.padding * 2), Math.round(sizes[i]!.h));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* Groups of rows in a column (#290): a header band opens a group, and everything down to the next
|
||||
* one belongs to it. A folded group keeps its band and hides its rows.
|
||||
*/
|
||||
import type { Graphics } from 'pixi.js';
|
||||
import { Theme } from './Theme';
|
||||
|
||||
/** Height of a band, here and at the head of a list that carries its own. */
|
||||
export const SECTION_BAND_HEIGHT = 26;
|
||||
/** Where a band's title starts, right of the mark that says whether the group is open. */
|
||||
export const SECTION_TITLE_INSET = 14;
|
||||
|
||||
/**
|
||||
* A child of a column that opens a group. `band` is laid across the column's padding, `inset`
|
||||
* stands inside it like a row and draws its band past its own edges (a list with a header).
|
||||
*/
|
||||
export interface SectionStart {
|
||||
readonly sectionStart: 'band' | 'inset' | null;
|
||||
readonly sectionCollapsed: boolean;
|
||||
}
|
||||
|
||||
export function sectionStartOf(widget: object): SectionStart | null {
|
||||
const start = widget as Partial<SectionStart>;
|
||||
return start.sectionStart ? (start as SectionStart) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Which children of a column a folded group hides. Per child: `null` for a row, else whether the
|
||||
* group it opens is folded. A start is never hidden itself.
|
||||
*/
|
||||
export function hiddenBySections(starts: readonly (boolean | null)[]): boolean[] {
|
||||
let folded = false;
|
||||
return starts.map((start) => {
|
||||
if (start === null) return folded;
|
||||
folded = start;
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
export interface SectionRow {
|
||||
h: number;
|
||||
/** Opens a group. */
|
||||
start: boolean;
|
||||
/** Ends in a band: a header, or a list folded down to its own. */
|
||||
band: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Where the shown children of a column go. Bands stand flush on each other and on the top edge,
|
||||
* a group keeps the column's padding to the band after it, and rows keep the gap between them.
|
||||
*/
|
||||
export function sectionOffsets(
|
||||
rows: readonly SectionRow[],
|
||||
gap: number,
|
||||
padding: number,
|
||||
): { ys: number[]; height: number } {
|
||||
const ys: number[] = [];
|
||||
let y = rows[0]?.start ? 0 : padding;
|
||||
rows.forEach((row, i) => {
|
||||
const prev = rows[i - 1];
|
||||
// One pixel shared, so two bands show one line between them, not two.
|
||||
if (prev) y += row.start ? (prev.band ? -1 : padding) : prev.band ? gap + 2 : gap;
|
||||
ys.push(y);
|
||||
y += row.h;
|
||||
});
|
||||
const last = rows.at(-1);
|
||||
return { ys, height: y + (last && !last.band ? padding : 0) };
|
||||
}
|
||||
|
||||
/** The band itself: header colour between two lines, from `x` on. `g` is cleared by the caller. */
|
||||
export function drawSectionBand(g: Graphics, x: number, y: number, w: number, h: number): void {
|
||||
g.rect(x, y, w, h).fill(Theme.colors.panelHeader);
|
||||
g.rect(x, y, w, 1).fill(Theme.colors.border);
|
||||
g.rect(x, y + h - 1, w, 1).fill(Theme.colors.border);
|
||||
}
|
||||
|
||||
/** The mark in front of a band's title, centred on `x`/`y`: pointing down while open, right while folded. */
|
||||
export function drawFoldMark(g: Graphics, x: number, y: number, collapsed: boolean, color: number): void {
|
||||
if (collapsed)
|
||||
g.moveTo(x - 2, y - 4)
|
||||
.lineTo(x + 3, y)
|
||||
.lineTo(x - 2, y + 4)
|
||||
.fill(color);
|
||||
else
|
||||
g.moveTo(x - 4, y - 2)
|
||||
.lineTo(x + 4, y - 2)
|
||||
.lineTo(x, y + 3)
|
||||
.fill(color);
|
||||
}
|
||||
|
||||
/** Lets the column a start stands in hide or show its group. */
|
||||
export function refoldAround(widget: { parent: object | null }): void {
|
||||
const column = widget.parent as { refold?: () => void } | null;
|
||||
column?.refold?.();
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { type FederatedPointerEvent, Graphics } from 'pixi.js';
|
||||
import { HStack } from '../layout/Stack';
|
||||
import {
|
||||
drawFoldMark,
|
||||
drawSectionBand,
|
||||
refoldAround,
|
||||
SECTION_BAND_HEIGHT,
|
||||
SECTION_TITLE_INSET,
|
||||
type SectionStart,
|
||||
} from '../sections';
|
||||
import { Theme } from '../Theme';
|
||||
import { type Size, Widget } from '../Widget';
|
||||
import { Label } from './Label';
|
||||
|
||||
export interface SectionHeaderOptions {
|
||||
collapsed?: boolean;
|
||||
/** After a click folded or unfolded the group – for an owner that remembers it. */
|
||||
onToggle?: (collapsed: boolean) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* The head of a group of rows (#290): a band across the column with the title in it, and whatever
|
||||
* `add()` puts at its right end. A click on the band folds the group; one on a control in it does not.
|
||||
*/
|
||||
export class SectionHeader extends Widget implements SectionStart {
|
||||
readonly sectionStart = 'band';
|
||||
private readonly bg = new Graphics();
|
||||
private readonly row = new HStack({ gap: Theme.gap, align: 'center' });
|
||||
private collapsed: boolean;
|
||||
|
||||
constructor(
|
||||
readonly title: string,
|
||||
private readonly options: SectionHeaderOptions = {},
|
||||
) {
|
||||
super();
|
||||
this.collapsed = options.collapsed ?? false;
|
||||
this.cursor = Theme.hoverCursor;
|
||||
this.minHeight = SECTION_BAND_HEIGHT;
|
||||
// The row takes no press itself, so one between its controls reaches the band.
|
||||
this.row.eventMode = 'passive';
|
||||
this.row.add(new Label(title, { style: 'title' }), 1);
|
||||
this.addChild(this.bg, this.row);
|
||||
this.trackHover();
|
||||
this.on('pointertap', (e: FederatedPointerEvent) => {
|
||||
if (e.button === 0 && e.target === this && !this.controlAt(e.global)) this.toggle();
|
||||
});
|
||||
}
|
||||
|
||||
/** Puts a control at the right end of the band. */
|
||||
add<T extends Widget>(widget: T): T {
|
||||
return this.row.add(widget);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a control of the band stands at this point. Asked on top of the event's target: a
|
||||
* disabled control takes no press, and the press it lets through must not fold the group.
|
||||
*/
|
||||
controlAt(global: { x: number; y: number }): boolean {
|
||||
const local = this.row.toLocal(global);
|
||||
return this.row.items
|
||||
.slice(1)
|
||||
.some((item) => local.x >= item.x && local.x < item.x + item.w && local.y >= 0 && local.y < this.h);
|
||||
}
|
||||
|
||||
get sectionCollapsed(): boolean {
|
||||
return this.collapsed;
|
||||
}
|
||||
|
||||
toggle(): void {
|
||||
this.collapsed = !this.collapsed;
|
||||
this.options.onToggle?.(this.collapsed);
|
||||
this.invalidate();
|
||||
refoldAround(this);
|
||||
}
|
||||
|
||||
private get rowLeft(): number {
|
||||
return Theme.padding + SECTION_TITLE_INSET;
|
||||
}
|
||||
|
||||
override measure(): Size {
|
||||
const row = this.row.measure();
|
||||
return { w: Math.max(this.minWidth, this.rowLeft + row.w + Theme.padding), h: SECTION_BAND_HEIGHT };
|
||||
}
|
||||
|
||||
override layout(): void {
|
||||
this.row.position.set(this.rowLeft, 0);
|
||||
this.row.resize(Math.max(0, this.w - this.rowLeft - Theme.padding), this.h);
|
||||
}
|
||||
|
||||
override redraw(): void {
|
||||
this.bg.clear();
|
||||
drawSectionBand(this.bg, 0, 0, this.w, this.h);
|
||||
const color = this.hovered ? Theme.colors.text : Theme.colors.textMuted;
|
||||
drawFoldMark(this.bg, Theme.padding + 4, this.h / 2, this.collapsed, color);
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,14 @@
|
||||
*/
|
||||
import { Container, type FederatedPointerEvent, Graphics, Text } from 'pixi.js';
|
||||
import { pressCount } from '../clicks';
|
||||
import {
|
||||
drawFoldMark,
|
||||
drawSectionBand,
|
||||
refoldAround,
|
||||
SECTION_BAND_HEIGHT,
|
||||
SECTION_TITLE_INSET,
|
||||
type SectionStart,
|
||||
} from '../sections';
|
||||
import { dropGap, dropMove, rowOrder } from '../sortable';
|
||||
import { Theme, textStyle } from '../Theme';
|
||||
import { ui } from '../UIContext';
|
||||
@@ -36,7 +44,7 @@ const DUPLICATE_SLOT = 22;
|
||||
const REMOVE_SLOT = 24;
|
||||
/** Width of the number field, where the rows have one. */
|
||||
const VALUE_WIDTH = 44;
|
||||
/** Height of the header, where there is one. */
|
||||
/** Height of a header that only holds the plus; one with a title is a band (see `sections.ts`). */
|
||||
const HEADER_HEIGHT = 24;
|
||||
/** Pixels the pointer must travel before a press on a row becomes a drag rather than a pick. */
|
||||
const DRAG_THRESHOLD = 4;
|
||||
@@ -97,8 +105,14 @@ export interface SortableListOptions {
|
||||
/** Setting this makes a double-click on a name edit it in the row; a blank or unchanged name is not reported. */
|
||||
onRename?: (id: string, name: string) => void;
|
||||
value?: SortableValue;
|
||||
/** Stands in the header, left. */
|
||||
/** Stands in the header, left. The header is then a band, and a click on it folds the rows away (#290). */
|
||||
title?: string;
|
||||
/** How far the band reaches past both edges – the padding of the column the list stands in. */
|
||||
bleed?: number;
|
||||
/** Whether the list starts folded. */
|
||||
collapsed?: boolean;
|
||||
/** After a click folded or unfolded the list – for an owner that remembers it. */
|
||||
onFold?: (collapsed: boolean) => void;
|
||||
/** Setting this puts a plus at the right end of the header. */
|
||||
onAdd?: () => void;
|
||||
multiSelect?: boolean;
|
||||
@@ -131,8 +145,8 @@ interface RowDrag {
|
||||
/** Which part of a row a point is on. */
|
||||
type RowPart = 'row' | 'eye' | 'duplicate' | 'cross' | 'name';
|
||||
|
||||
/** What the pointer is over: a part of a row, or the plus. */
|
||||
type Hover = { row: number; part: RowPart } | 'add' | null;
|
||||
/** What the pointer is over: a part of a row, the plus, or the band beside it. */
|
||||
type Hover = { row: number; part: RowPart } | 'add' | 'head' | null;
|
||||
|
||||
/** A name edited in its row: it goes away as soon as it loses the focus. */
|
||||
class RowNameField extends TextField {
|
||||
@@ -144,7 +158,8 @@ class RowNameField extends TextField {
|
||||
}
|
||||
}
|
||||
|
||||
export class SortableList extends Widget {
|
||||
export class SortableList extends Widget implements SectionStart {
|
||||
private collapsed: boolean;
|
||||
private readonly background = new Graphics();
|
||||
private readonly marker = new Graphics();
|
||||
private readonly heading = new Text({ text: '', style: textStyle('title') });
|
||||
@@ -160,6 +175,7 @@ export class SortableList extends Widget {
|
||||
|
||||
constructor(private readonly options: SortableListOptions) {
|
||||
super();
|
||||
this.collapsed = Boolean(options.title) && (options.collapsed ?? false);
|
||||
this.cursor = Theme.hoverCursor;
|
||||
this.addChild(this.background);
|
||||
this.heading.text = (options.title ?? '').toUpperCase();
|
||||
@@ -204,7 +220,31 @@ export class SortableList extends Widget {
|
||||
|
||||
override measure(): Size {
|
||||
const rows = Math.max(SORTABLE_ROW_HEIGHT, this.shown.length * SORTABLE_ROW_HEIGHT);
|
||||
return { w: this.minWidth, h: this.headerHeight + rows };
|
||||
return { w: this.minWidth, h: this.headerHeight + (this.collapsed ? 0 : rows) };
|
||||
}
|
||||
|
||||
/** A list with a title opens a group in its column: itself, and the rows after it. */
|
||||
get sectionStart(): 'inset' | null {
|
||||
return this.options.title ? 'inset' : null;
|
||||
}
|
||||
|
||||
get sectionCollapsed(): boolean {
|
||||
return this.collapsed;
|
||||
}
|
||||
|
||||
/** A point on the band, clear of the plus (test hook); null without a title. */
|
||||
headGlobal(): { x: number; y: number } | null {
|
||||
if (!this.options.title) return null;
|
||||
const origin = this.getGlobalPosition();
|
||||
return { x: origin.x + SECTION_TITLE_INSET + 20, y: origin.y + this.headerHeight / 2 };
|
||||
}
|
||||
|
||||
private toggleFold(): void {
|
||||
this.collapsed = !this.collapsed;
|
||||
this.drag = null;
|
||||
this.options.onFold?.(this.collapsed);
|
||||
this.invalidate();
|
||||
refoldAround(this);
|
||||
}
|
||||
|
||||
/** The ids as the list shows them, top first – for the check scripts. */
|
||||
@@ -276,11 +316,11 @@ export class SortableList extends Widget {
|
||||
addGlobal(): { x: number; y: number } | null {
|
||||
if (!this.options.onAdd) return null;
|
||||
const origin = this.getGlobalPosition();
|
||||
return { x: origin.x + this.w - REMOVE_SLOT / 2, y: origin.y + HEADER_HEIGHT / 2 };
|
||||
return { x: origin.x + this.w - REMOVE_SLOT / 2, y: origin.y + this.headerHeight / 2 };
|
||||
}
|
||||
|
||||
private get headerHeight(): number {
|
||||
return this.options.title || this.options.onAdd ? HEADER_HEIGHT : 0;
|
||||
return this.options.title ? SECTION_BAND_HEIGHT : this.options.onAdd ? HEADER_HEIGHT : 0;
|
||||
}
|
||||
|
||||
/** Whether the rows show a cross now – with `keepOne`, not on the last item. */
|
||||
@@ -354,12 +394,14 @@ export class SortableList extends Widget {
|
||||
|
||||
private rowAt(y: number): number {
|
||||
const index = Math.floor((y - this.headerHeight) / SORTABLE_ROW_HEIGHT);
|
||||
return index >= 0 && index < this.shown.length ? index : -1;
|
||||
return !this.collapsed && index >= 0 && index < this.shown.length ? index : -1;
|
||||
}
|
||||
|
||||
private hitAt(local: { x: number; y: number }): Hover {
|
||||
if (local.y < this.headerHeight)
|
||||
return this.options.onAdd && local.x >= this.w - REMOVE_SLOT ? 'add' : null;
|
||||
if (local.y < this.headerHeight) {
|
||||
if (this.options.onAdd && local.x >= this.w - REMOVE_SLOT) return 'add';
|
||||
return this.options.title ? 'head' : null;
|
||||
}
|
||||
const row = this.rowAt(local.y);
|
||||
if (row < 0) return null;
|
||||
return { row, part: this.partAt(local.x) };
|
||||
@@ -384,6 +426,10 @@ export class SortableList extends Widget {
|
||||
this.options.onAdd?.();
|
||||
return;
|
||||
}
|
||||
if (hit === 'head') {
|
||||
this.toggleFold();
|
||||
return;
|
||||
}
|
||||
const id = this.shown[hit.row]!.id;
|
||||
if (hit.part === 'cross') {
|
||||
this.options.onRemove?.(id);
|
||||
@@ -475,12 +521,15 @@ export class SortableList extends Widget {
|
||||
|
||||
override redraw(): void {
|
||||
const top = this.headerHeight;
|
||||
this.background
|
||||
.clear()
|
||||
.rect(0, top, this.w, this.h - top)
|
||||
.fill(Theme.colors.control);
|
||||
this.background.clear();
|
||||
if (!this.collapsed) this.background.rect(0, top, this.w, this.h - top).fill(Theme.colors.control);
|
||||
this.marker.clear();
|
||||
this.redrawHeader();
|
||||
for (const row of this.rows) {
|
||||
row.box.visible = !this.collapsed;
|
||||
if (row.field && this.collapsed) row.field.visible = false;
|
||||
}
|
||||
if (this.collapsed) return;
|
||||
const slots = this.slots;
|
||||
const nameRight = this.nameRight;
|
||||
const eyes = Boolean(this.options.onToggleHidden);
|
||||
@@ -489,7 +538,7 @@ export class SortableList extends Widget {
|
||||
const value = this.options.value;
|
||||
const thumbs = this.options.thumb;
|
||||
const textX = this.textX;
|
||||
const hovered = this.over !== 'add' ? this.over : null;
|
||||
const hovered = this.over !== 'add' && this.over !== 'head' ? this.over : null;
|
||||
for (const [index, item] of this.shown.entries()) {
|
||||
const row = this.rows[index]!;
|
||||
const y = top + index * SORTABLE_ROW_HEIGHT;
|
||||
@@ -565,9 +614,16 @@ export class SortableList extends Widget {
|
||||
|
||||
private redrawHeader(): void {
|
||||
const top = this.headerHeight;
|
||||
const band = Boolean(this.options.title);
|
||||
this.heading.visible = top > 0;
|
||||
this.heading.position.set(0, Math.round((top - this.heading.height) / 2));
|
||||
this.heading.position.set(band ? SECTION_TITLE_INSET : 0, Math.round((top - this.heading.height) / 2));
|
||||
this.plus.clear();
|
||||
if (band) {
|
||||
const bleed = this.options.bleed ?? 0;
|
||||
drawSectionBand(this.background, -bleed, 0, this.w + bleed * 2, top);
|
||||
const color = this.over === 'head' ? Theme.colors.text : Theme.colors.textMuted;
|
||||
drawFoldMark(this.background, 4, top / 2, this.collapsed, color);
|
||||
}
|
||||
if (!this.options.onAdd) return;
|
||||
const x = this.w - REMOVE_SLOT / 2;
|
||||
const y = top / 2;
|
||||
@@ -587,7 +643,7 @@ export class SortableList extends Widget {
|
||||
}
|
||||
|
||||
function sameHover(a: Hover, b: Hover): boolean {
|
||||
if (a === null || b === null || a === 'add' || b === 'add') return a === b;
|
||||
if (a === null || b === null || typeof a === 'string' || typeof b === 'string') return a === b;
|
||||
return a.row === b.row && a.part === b.part;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user