一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
214 行
8.3 KiB
TypeScript
214 行
8.3 KiB
TypeScript
import { useMemo } from 'react';
|
|
import { LayoutItem, Size } from './types';
|
|
|
|
const dragBarThickness = 5;
|
|
|
|
export const itemMinWidth = 40;
|
|
export const itemMinHeight = 40;
|
|
|
|
// Bigger than itemMinWidth: used as a fallback for width-less non-absorber
|
|
// siblings so newly-shown panels (moveMode) get a usable width instead of
|
|
// squeezing to the minimum.
|
|
const nonAbsorberFallbackWidth = 250;
|
|
const nonAbsorberFallbackHeight = 250;
|
|
|
|
export interface LayoutItemSizes {
|
|
[key: string]: Size;
|
|
}
|
|
|
|
function containsFlexibleDescendant(item: LayoutItem): boolean {
|
|
if (item.flexible) return true;
|
|
if (!item.children) return false;
|
|
for (const child of item.children) {
|
|
if (containsFlexibleDescendant(child)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Whether this item's trailing (right/bottom) edge is a divider against a
|
|
// visible-in-render sibling. The parent-side flags cover the case where the
|
|
// item's edge coincides with an ancestor's divider (e.g. a column-nested item
|
|
// inheriting its column container's right edge).
|
|
export interface EdgeFlags {
|
|
ownRight: boolean;
|
|
ownBottom: boolean;
|
|
parentRight: boolean;
|
|
parentBottom: boolean;
|
|
}
|
|
|
|
const noEdges: EdgeFlags = { ownRight: false, ownBottom: false, parentRight: false, parentBottom: false };
|
|
|
|
// Container always take the full space while the items within it need to
|
|
// accommodate for the resize handle.
|
|
export function itemSize(item: LayoutItem, _parent: LayoutItem | null, sizes: LayoutItemSizes, isContainer: boolean, edges: EdgeFlags = noEdges): Size {
|
|
const rightGap = !isContainer && (edges.ownRight || edges.parentRight) ? dragBarThickness : 0;
|
|
const bottomGap = !isContainer && (edges.ownBottom || edges.parentBottom) ? dragBarThickness : 0;
|
|
|
|
return {
|
|
width: sizes[item.key].width - rightGap,
|
|
height: sizes[item.key].height - bottomGap,
|
|
};
|
|
}
|
|
|
|
// This calculate the size of each item within the layout. However
|
|
// the final size, as rendered by the component is determined by
|
|
// `itemSize()`, as it takes into account the resizer handle
|
|
function calculateChildrenSizes(item: LayoutItem, parent: LayoutItem | null, sizes: LayoutItemSizes, makeAllVisible: boolean): LayoutItemSizes {
|
|
if (!item.children) return sizes;
|
|
|
|
const parentSize = itemSize(item, parent, sizes, true);
|
|
|
|
const remainingSize: Size = {
|
|
width: parentSize.width,
|
|
height: parentSize.height,
|
|
};
|
|
|
|
const noWidthChildren: { item: LayoutItem; parent: LayoutItem }[] = [];
|
|
const noHeightChildren: { item: LayoutItem; parent: LayoutItem }[] = [];
|
|
|
|
// The minimum space required for items with no defined size
|
|
let noWidthChildrenMinWidth = 0;
|
|
let noHeightChildrenMinHeight = 0;
|
|
|
|
for (const child of item.children) {
|
|
let w = 'width' in child ? child.width : null;
|
|
let h = 'height' in child ? child.height : null;
|
|
if (!makeAllVisible && child.visible === false) {
|
|
w = 0;
|
|
h = 0;
|
|
}
|
|
|
|
sizes[child.key] = { width: w, height: h };
|
|
|
|
if (w !== null) remainingSize.width -= w;
|
|
if (h !== null) remainingSize.height -= h;
|
|
if (w === null) {
|
|
noWidthChildren.push({ item: child, parent: item });
|
|
noWidthChildrenMinWidth += child.minWidth || itemMinWidth;
|
|
}
|
|
if (h === null) {
|
|
noHeightChildren.push({ item: child, parent: item });
|
|
noHeightChildrenMinHeight += child.minHeight || itemMinHeight;
|
|
}
|
|
}
|
|
|
|
while (remainingSize.width < noWidthChildrenMinWidth) {
|
|
// There is not enough space, the widest item will be made smaller
|
|
let widestChild = item.children[0].key;
|
|
for (const child of item.children) {
|
|
if (!child.visible) continue;
|
|
if (sizes[child.key].width > sizes[widestChild].width) widestChild = child.key;
|
|
}
|
|
|
|
const dw = Math.abs(remainingSize.width - noWidthChildrenMinWidth);
|
|
sizes[widestChild].width -= dw;
|
|
remainingSize.width += dw;
|
|
}
|
|
|
|
while (remainingSize.height < noHeightChildrenMinHeight) {
|
|
// There is not enough space, the tallest item will be made smaller
|
|
let tallestChild = item.children[0].key;
|
|
for (const child of item.children) {
|
|
if (!child.visible) continue;
|
|
if (sizes[child.key].height > sizes[tallestChild].height) tallestChild = child.key;
|
|
}
|
|
|
|
const dh = Math.abs(remainingSize.height - noHeightChildrenMinHeight);
|
|
sizes[tallestChild].height -= dh;
|
|
remainingSize.height += dh;
|
|
}
|
|
|
|
// When several children lack a size on the parent's axis (e.g. in
|
|
// moveMode, where normally-hidden panels are shown without one), route
|
|
// remaining space to the one that carries the flexible descendant and
|
|
// give the others a fallback size. This keeps the absorber's role
|
|
// intact and avoids equal-split behaviour that would eat editor space.
|
|
if (noWidthChildren.length && item.direction === 'row' && noWidthChildren.length > 1) {
|
|
const absorber = noWidthChildren.find(c => containsFlexibleDescendant(c.item));
|
|
if (absorber) {
|
|
const absorberMin = absorber.item.minWidth || itemMinWidth;
|
|
let budget = Math.max(0, remainingSize.width - absorberMin);
|
|
for (const child of noWidthChildren) {
|
|
if (child.item.key === absorber.item.key) continue;
|
|
const min = child.item.minWidth || itemMinWidth;
|
|
const desired = child.item.minWidth || nonAbsorberFallbackWidth;
|
|
const w = Math.max(min, Math.min(desired, budget));
|
|
sizes[child.item.key].width = w;
|
|
budget -= w;
|
|
}
|
|
sizes[absorber.item.key].width = Math.max(absorberMin, remainingSize.width - (noWidthChildren.reduce((sum, c) => c.item.key === absorber.item.key ? sum : sum + sizes[c.item.key].width, 0)));
|
|
} else {
|
|
const w = Math.floor(remainingSize.width / noWidthChildren.length);
|
|
for (const child of noWidthChildren) sizes[child.item.key].width = w;
|
|
}
|
|
} else if (noWidthChildren.length) {
|
|
const w = item.direction === 'row' ? Math.floor(remainingSize.width / noWidthChildren.length) : parentSize.width;
|
|
for (const child of noWidthChildren) sizes[child.item.key].width = w;
|
|
}
|
|
|
|
if (noHeightChildren.length && item.direction === 'column' && noHeightChildren.length > 1) {
|
|
const absorber = noHeightChildren.find(c => containsFlexibleDescendant(c.item));
|
|
if (absorber) {
|
|
const absorberMin = absorber.item.minHeight || itemMinHeight;
|
|
let budget = Math.max(0, remainingSize.height - absorberMin);
|
|
for (const child of noHeightChildren) {
|
|
if (child.item.key === absorber.item.key) continue;
|
|
const min = child.item.minHeight || itemMinHeight;
|
|
const desired = child.item.minHeight || nonAbsorberFallbackHeight;
|
|
const h = Math.max(min, Math.min(desired, budget));
|
|
sizes[child.item.key].height = h;
|
|
budget -= h;
|
|
}
|
|
sizes[absorber.item.key].height = Math.max(absorberMin, remainingSize.height - (noHeightChildren.reduce((sum, c) => c.item.key === absorber.item.key ? sum : sum + sizes[c.item.key].height, 0)));
|
|
} else {
|
|
const h = Math.floor(remainingSize.height / noHeightChildren.length);
|
|
for (const child of noHeightChildren) sizes[child.item.key].height = h;
|
|
}
|
|
} else if (noHeightChildren.length) {
|
|
const h = item.direction === 'column' ? Math.floor(remainingSize.height / noHeightChildren.length) : parentSize.height;
|
|
for (const child of noHeightChildren) sizes[child.item.key].height = h;
|
|
}
|
|
|
|
for (const child of item.children) {
|
|
const childrenSizes = calculateChildrenSizes(child, parent, sizes, makeAllVisible);
|
|
sizes = { ...sizes, ...childrenSizes };
|
|
}
|
|
|
|
return sizes;
|
|
}
|
|
|
|
// Gives the maximum available space for this item that it can take up during resizing
|
|
// availableSize = totalSize - ( [size of items with set size, except for the current item] + [minimum size of items with no set size] )
|
|
export function calculateMaxSizeAvailableForItem(item: LayoutItem, parent: LayoutItem, sizes: LayoutItemSizes): Size {
|
|
const availableSize: Size = { ...sizes[parent.key] };
|
|
|
|
for (const sibling of parent.children) {
|
|
if (!sibling.visible) continue;
|
|
|
|
availableSize.width -= 'width' in sibling ? sizes[sibling.key].width : (sibling.minWidth || itemMinWidth);
|
|
availableSize.height -= 'height' in sibling ? sizes[sibling.key].height : (sibling.minHeight || itemMinHeight);
|
|
}
|
|
|
|
availableSize.width += sizes[item.key].width;
|
|
availableSize.height += sizes[item.key].height;
|
|
|
|
return availableSize;
|
|
}
|
|
|
|
export default function useLayoutItemSizes(layout: LayoutItem, makeAllVisible = false) {
|
|
return useMemo(() => {
|
|
let sizes: LayoutItemSizes = {};
|
|
|
|
if (!('width' in layout) || !('height' in layout)) throw new Error('width and height are required on layout root');
|
|
|
|
sizes[layout.key] = {
|
|
width: layout.width,
|
|
height: layout.height,
|
|
};
|
|
|
|
sizes = calculateChildrenSizes(layout, null, sizes, makeAllVisible);
|
|
|
|
return sizes;
|
|
}, [layout, makeAllVisible]);
|
|
}
|