Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "regular-layout",
"version": "0.5.0",
"version": "0.6.0",
"description": "A regular CSS `grid` container",
"keywords": [],
"license": "Apache-2.0",
Expand Down
12 changes: 12 additions & 0 deletions src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,18 @@ export interface LayoutPath {
*/
export interface PresizeDetail {
calculatePresizePaths(): Record<string, LayoutPath>;

/**
* When the transition is a drag preview, the dragged panel's name and the
* {@link LayoutPath} of its preview box. The dragged panel is removed from
* the layout tree during a drag (so it is absent from
* {@link PresizeDetail.calculatePresizePaths}), but `path.view_window` here
* is exactly where the overlay renders it, letting consumers presize the
* dragged panel like any other. `null` when the pointer is outside every
* drop target (the dragged panel is hidden), `undefined` for non-drag
* transitions.
*/
overlay?: { slot: string; path: LayoutPath } | null;
}

/**
Expand Down
71 changes: 42 additions & 29 deletions src/model/overlay_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,36 +70,49 @@ export class OverlayController {
);
}

await host.presizeQueue.run(panel, () => {
if (mode === "grid" && drop_target) {
const path: [string, string] = [slot, drop_target?.slot];
const css = create_css_grid_layout(panel, path, host.physics);
host.stylesheet.replaceSync(css);
} else if (mode === "absolute") {
const grid_css = create_css_grid_layout(panel, undefined, host.physics);

while (drag_element?.tagName === "SLOT" && drag_element) {
drag_element = (
drag_element as HTMLSlotElement
).assignedElements()[0];
// The dragged panel is removed from `panel` above, so it is absent from
// the presize paths - `drop_target` carries its preview box instead
// (null when the pointer is outside every drop target and the dragged
// panel is hidden).
const overlay = drop_target ? { slot, path: drop_target } : null;
await host.presizeQueue.run(
panel,
() => {
if (mode === "grid" && drop_target) {
const path: [string, string] = [slot, drop_target?.slot];
const css = create_css_grid_layout(panel, path, host.physics);
host.stylesheet.replaceSync(css);
} else if (mode === "absolute") {
const grid_css = create_css_grid_layout(
panel,
undefined,
host.physics,
);

while (drag_element?.tagName === "SLOT" && drag_element) {
drag_element = (
drag_element as HTMLSlotElement
).assignedElements()[0];
}

const margin = drag_element
? getComputedStyle(drag_element)
: undefined;

const overlay_css = updateOverlaySheet(
slot,
box,
style,
drop_target,
host.physics,
margin,
);

host.stylesheet.replaceSync([grid_css, overlay_css].join("\n"));
}

const margin = drag_element
? getComputedStyle(drag_element)
: undefined;

const overlay_css = updateOverlaySheet(
slot,
box,
style,
drop_target,
host.physics,
margin,
);

host.stylesheet.replaceSync([grid_css, overlay_css].join("\n"));
}
});
},
overlay,
);

const event_name = `${host.physics.CUSTOM_EVENT_NAME_PREFIX}-before-update`;
const custom_event = new CustomEvent<Layout>(event_name, {
Expand Down
41 changes: 33 additions & 8 deletions src/model/presize_queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,15 @@
// ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

import type { Layout, PresizeDetail } from "../core/types.ts";
import type { Layout, LayoutPath, PresizeDetail } from "../core/types.ts";
import { calculate_presize_paths } from "../layout/calculate_presize_paths.ts";

/**
* The dragged panel's preview geometry during a drag transition - see
* {@link PresizeDetail.overlay}.
*/
export type PresizeOverlay = { slot: string; path: LayoutPath } | null;

/**
* Manages cancelable pre-resize gating for layout updates.
*
Expand All @@ -22,27 +28,40 @@ import { calculate_presize_paths } from "../layout/calculate_presize_paths.ts";
*/
export class PresizeQueue {
#resizing = false;
#queued: { layout: Layout; fn: () => void } | null = null;
#queued: {
layout: Layout;
fn: () => void;
overlay?: PresizeOverlay;
} | null = null;
#pending: (() => void) | null = null;

constructor(
private _target: EventTarget,
private _eventName: string,
private _onDispatch?: () => void,
) {}

async run(layout: Layout, fn: () => void): Promise<void> {
async run(
layout: Layout,
fn: () => void,
overlay?: PresizeOverlay,
): Promise<void> {
if (this.#resizing) {
this.#queued = { layout, fn };
this.#queued = { layout, fn, overlay };
return;
}

this.#resizing = true;
try {
await this.#dispatchAndMaybeWait(layout, fn);
await this.#dispatchAndMaybeWait(layout, fn, overlay);
while (this.#queued) {
const { layout: nextLayout, fn: nextFn } = this.#queued;
const {
layout: nextLayout,
fn: nextFn,
overlay: nextOverlay,
} = this.#queued;
this.#queued = null;
await this.#dispatchAndMaybeWait(nextLayout, nextFn);
await this.#dispatchAndMaybeWait(nextLayout, nextFn, nextOverlay);
}
} finally {
this.#resizing = false;
Expand All @@ -57,9 +76,15 @@ export class PresizeQueue {
}
}

async #dispatchAndMaybeWait(layout: Layout, fn: () => void): Promise<void> {
async #dispatchAndMaybeWait(
layout: Layout,
fn: () => void,
overlay?: PresizeOverlay,
): Promise<void> {
this._onDispatch?.();
const detail: PresizeDetail = {
calculatePresizePaths: () => calculate_presize_paths(layout),
overlay,
};

const event = new CustomEvent(this._eventName, {
Expand Down
2 changes: 1 addition & 1 deletion src/regular-layout-frame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const CSS = `
`;

const HTML_TEMPLATE = `
<div part="titlebar"><div class="tabs"><slot name="tab"><regular-layout-tab></regular-layout-tab></slot></div></div>
<div part="titlebar"><div class="tabs" part="titlebar-track"><slot name="tab"><regular-layout-tab></regular-layout-tab></slot></div></div>
<div part="container"><slot></slot></div>
`;

Expand Down
56 changes: 45 additions & 11 deletions src/regular-layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ export class RegularLayout extends HTMLElement {
private _presizeQueue: PresizeQueue;
private _overlayController: OverlayController;
private _maximized?: string;
private _resize_observer?: ResizeObserver;

constructor() {
super();
Expand All @@ -141,7 +142,13 @@ export class RegularLayout extends HTMLElement {
this._cursor_stylesheet = new CSSStyleSheet();
this._cursor_override = false;
const event_name = `${this._physics.CUSTOM_EVENT_NAME_PREFIX}-before-resize`;
this._presizeQueue = new PresizeQueue(this, event_name);
// Refresh the bounds cache once per layout transition, so consumers
// reading coordinates from a `before-resize` handler always see the
// element's current position - a `ResizeObserver` alone can't catch
// position-only moves of the host.
this._presizeQueue = new PresizeQueue(this, event_name, () => {
this._dimensions = undefined;
});
this._overlayController = new OverlayController(this.create_overlay_host());
this._shadowRoot.adoptedStyleSheets = [
this._stylesheet,
Expand All @@ -150,13 +157,19 @@ export class RegularLayout extends HTMLElement {
}

connectedCallback() {
this._resize_observer ??= new ResizeObserver(() => {
this._dimensions = undefined;
});

this._resize_observer.observe(this);
this.addEventListener("dblclick", this.onDblClick);
this.addEventListener("pointerdown", this.onPointerDown);
this.addEventListener("pointerup", this.onPointerUp);
this.addEventListener("pointermove", this.onPointerMove);
}

disconnectedCallback() {
this._resize_observer?.disconnect();
this.removeEventListener("dblclick", this.onDblClick);
this.removeEventListener("pointerdown", this.onPointerDown);
this.removeEventListener("pointerup", this.onPointerUp);
Expand Down Expand Up @@ -334,34 +347,54 @@ export class RegularLayout extends HTMLElement {
* {@link save}, and any subsequent {@link restore} resets the layout to the
* minimized (normal, multi-panel) view.
*
* Before applying, dispatches a cancelable `regular-layout-before-resize`
* event (as {@link restore} does) whose paths describe the post-maximize
* geometry - a single full-window entry for `name`. If the event is
* cancelled via `preventDefault()`, the update is suspended until
* {@link resumeResize} is called.
*
* Has no effect if `name` is not present in the current layout.
*
* @param name - The name of the panel to maximize.
*/
maximize = (name: string) => {
maximize = async (name: string) => {
if (!this.getPanel(name)) {
return;
}

this._maximized = name;
this._stylesheet.replaceSync(
create_css_maximize_layout(name, this._physics),
);
// The post-maximize geometry as a `Layout`: `name` alone, full-window.
// Panels hidden by the maximize are absent from the presize paths,
// consistent with `calculate_presize_paths`' visible-panels-only
// semantics.
const layout: Layout = { type: "tab-layout", tabs: [name], selected: 0 };
await this._presizeQueue.run(layout, () => {
this._maximized = name;
this._stylesheet.replaceSync(
create_css_maximize_layout(name, this._physics),
);
});
};

/**
* Restores the normal multi-panel view after a {@link maximize}, without
* altering the layout tree. No-op if no panel is currently maximized.
*
* Before applying, dispatches a cancelable `regular-layout-before-resize`
* event (as {@link restore} does) with paths for the restored multi-panel
* geometry. If the event is cancelled via `preventDefault()`, the update
* is suspended until {@link resumeResize} is called.
*/
minimize = () => {
minimize = async () => {
if (this._maximized === undefined) {
return;
}

this._maximized = undefined;
this._stylesheet.replaceSync(
create_css_grid_layout(this._panel, undefined, this._physics),
);
await this._presizeQueue.run(this._panel, () => {
this._maximized = undefined;
this._stylesheet.replaceSync(
create_css_grid_layout(this._panel, undefined, this._physics),
);
});
};

/**
Expand All @@ -371,6 +404,7 @@ export class RegularLayout extends HTMLElement {
* @param layout - The layout tree to restore
*/
restoreSync = (layout: Layout, _is_flattened: boolean = false) => {
this._dimensions = undefined;
this._maximized = undefined;
this._panel = !_is_flattened ? flatten(layout) : layout;
const css = create_css_grid_layout(this._panel, undefined, this._physics);
Expand Down
Loading
Loading