Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

split platform-dock-layout #683

Merged
merged 8 commits into from
Jan 4, 2024
Merged
Show file tree
Hide file tree
Changes from 7 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
46 changes: 46 additions & 0 deletions src/renderer/components/docking/dock-layout-wrapper.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import 'rc-dock/dist/rc-dock.css';
import './dock-layout-wrapper.component.scss';

import { CSSProperties, ForwardedRef, PropsWithChildren, forwardRef } from 'react';
import DockLayout, { LayoutProps, LayoutData, LayoutBase } from 'rc-dock';

import { SavedTabInfo } from '@shared/models/docking-framework.model';

import { RCDockTabInfo } from './docking-framework-internal.model';
import { GROUPS } from './platform-dock-layout-positioning.util';

export type DockLayoutWrapperProps = PropsWithChildren<{
loadTab: (savedTabInfo: SavedTabInfo) => RCDockTabInfo;
saveTab: (dockTabInfo: RCDockTabInfo) => SavedTabInfo | undefined;
onLayoutChange: LayoutProps['onLayoutChange'];
defaultLayout?: LayoutBase;
style?: CSSProperties;
}>;

const DockLayoutWrapper = forwardRef(function DockLayoutWrapper(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tjcouch-sil: Is this correct? At least it renders :-)

{ loadTab, saveTab, onLayoutChange, defaultLayout, style }: DockLayoutWrapperProps,
ref: ForwardedRef<DockLayout> | undefined,
) {
return (
<DockLayout
ref={ref}
groups={GROUPS}
// DockLayout requires LayoutData, but it needs to be LayoutBase in case we use loadTab
/* eslint-disable no-type-assertion/no-type-assertion */
defaultLayout={
(defaultLayout as LayoutData) || { dockbox: { mode: 'horizontal', children: [] } }
}
style={style}
dropMode="edge"
loadTab={loadTab}
// Type assert `saveTab` as not returning `undefined` because rc-dock's types are wrong
// Here, if `saveTab` returns `undefined` the tab is not saved
// https://github.com/ticlo/rc-dock/blob/8b6481dca4b4dd07f89107d6f48b1831bbdf0470/src/Serializer.ts#L68
// eslint-disable-next-line no-type-assertion/no-type-assertion
saveTab={saveTab as (dockTabInfo: RCDockTabInfo) => SavedTabInfo}
onLayoutChange={onLayoutChange}
/>
);
});

export default DockLayoutWrapper;
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { TabData, PanelData, BoxData } from 'rc-dock';
import { TabInfo } from '@shared/models/docking-framework.model';

export type TabType = string;

export type RCDockTabInfo = TabData & TabInfo;

/**
* Check if the input item is just a tab, i.e. not a panel, box, or float.
*
* @param tab To check.
* @returns `true` if its a tab or `false` otherwise.
*/
export function isTab(tab: PanelData | TabData | BoxData | undefined): tab is TabData {
// Assert the more specific type. Null to work with the external API.
// eslint-disable-next-line no-type-assertion/no-type-assertion, no-null/no-null
if (!tab || (tab as TabData).title == null) return false;
return true;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/* eslint-disable import/first */
jest.mock('../../../shared/services/logger.service');

import { FloatPosition } from 'rc-dock';
import { FloatLayout } from '@shared/models/docking-framework.model';
import { getFloatPosition } from './platform-dock-layout-positioning.util';
/* eslint-enable */

describe('Dock Layout Component', () => {
describe('getFloatPosition()', () => {
it('should cascade from top-left of layout', () => {
const layout: FloatLayout = { type: 'float', floatSize: { width: 20, height: 10 } };
const floatPosition: FloatPosition = { left: 0, top: 0, width: 0, height: 0 };

let nextPosition = getFloatPosition(layout, floatPosition, { width: 100, height: 100 });

expect(nextPosition).toEqual({
left: 28,
top: 28,
width: 20,
height: 10,
});

nextPosition = getFloatPosition(layout, nextPosition, { width: 100, height: 100 });

expect(nextPosition).toEqual({
left: 2 * 28,
top: 2 * 28,
width: 20,
height: 10,
});
});

it('should overflow right of layout', () => {
const layout: FloatLayout = { type: 'float', floatSize: { width: 20, height: 10 } };
const floatPosition: FloatPosition = { left: 2 * 28, top: 2 * 28, width: 0, height: 0 };
// right = 2*28 + 20 + 28 = 104
// bottom = 2*28 + 10 + 28 = 94

expect(getFloatPosition(layout, floatPosition, { width: 100, height: 100 })).toEqual({
left: 28,
top: 3 * 28,
width: 20,
height: 10,
});
});

it('should overflow bottom of layout', () => {
const layout: FloatLayout = { type: 'float', floatSize: { width: 20, height: 10 } };
const floatPosition: FloatPosition = { left: 2 * 28, top: 2 * 28, width: 0, height: 0 };
// right = 2*28 + 20 + 28 = 104
// bottom = 2*28 + 10 + 28 = 94

expect(getFloatPosition(layout, floatPosition, { width: 120, height: 90 })).toEqual({
left: 3 * 28,
top: 28,
width: 20,
height: 10,
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { FloatPosition, FloatSize, LayoutSize, TabGroup } from 'rc-dock';
import cloneDeep from 'lodash/cloneDeep';
import {
PanelDirection,
Layout,
SavedTabInfo,
FloatLayout,
} from '@shared/models/docking-framework.model';
import DIALOGS from '@renderer/components/dialogs';
import { TabType } from './docking-framework-internal.model';

/**
* The default initial size for floating tabs in CSS `px` units. Can be overridden by tabTypes'
* initial sizes
*/
const DEFAULT_FLOAT_SIZE: FloatSize = { width: 300, height: 150 };
/** Default direction a tab will be placed from an existing tab if created as a panel */
const DEFAULT_PANEL_DIRECTION: PanelDirection = 'right';

const DOCK_FLOAT_OFFSET = 28;
// NOTE: 'card' is a built-in style. We can likely remove it when we create a full theme for
// Platform.
export const TAB_GROUP = 'card platform-bible';

export const GROUPS: { [key: string]: TabGroup } = {
[TAB_GROUP]: {
maximizable: true, // Allow groups of tabs to be maximized
floatable: true, // Allow tabs to be floated
animated: false, // Don't animate tab transitions
// TODO: Currently allowing newWindow crashes since electron doesn't seem to have window.open defined?
// newWindow: true, // Allow floating windows to show in a native window
},
};

/** Initial sizes for each tab in CSS `px` units if created as floating tabs */
const tabInitialFloatingSize: Record<TabType, FloatSize> = Object.fromEntries(
Object.entries(DIALOGS).map(
([dialogTabType, dialogDefinition]) => [dialogTabType, dialogDefinition.initialSize] as const,
),
);

function offsetOrOverflowAxis(
axis: number,
size: number,
max: number,
offset = DOCK_FLOAT_OFFSET,
): number {
if (axis + size + offset >= max) return offset;
return axis + offset;
}

/**
* Get left & top so float windows cascade their position. Float window should not overflow the
* layout but start cascading again.
*
* @param layout Specified by the WebView. Must have all values - this function assumes this layout
* has had default values set already
* @param previousPosition Used with the previous float window.
* @param layoutSize Of the whole dock layout.
* @returns Cascaded position.
*/
export function getFloatPosition(
layout: FloatLayout,
previousPosition: FloatPosition,
layoutSize: LayoutSize,
): FloatPosition {
// Defaults are added in `layoutDefaults`.
// eslint-disable-next-line no-type-assertion/no-type-assertion
const { width, height } = layout.floatSize!;

let { left, top } = previousPosition;

switch (layout.position) {
case 'center':
left = layoutSize.width / 2 - width / 2;
top = layoutSize.height / 2 - height / 2;
break;
case 'cascade':
default:
left = offsetOrOverflowAxis(left, width, layoutSize.width);
top = offsetOrOverflowAxis(top, height, layoutSize.height);
break;
}
return { left, top, width, height };
}

/** Set up defaults for webview layout instructions */
export function layoutDefaults(layout: Layout, savedTabInfo: SavedTabInfo): Layout {
const layoutDefaulted = cloneDeep(layout);
switch (layoutDefaulted.type) {
case 'float': {
if (!layoutDefaulted.floatSize) {
layoutDefaulted.floatSize =
tabInitialFloatingSize[savedTabInfo.tabType] || DEFAULT_FLOAT_SIZE;
} else {
if (!layoutDefaulted.floatSize.width || layoutDefaulted.floatSize.width <= 0)
layoutDefaulted.floatSize.width =
tabInitialFloatingSize[savedTabInfo.tabType]?.width || DEFAULT_FLOAT_SIZE.width;

if (!layoutDefaulted.floatSize.height || layoutDefaulted.floatSize.height <= 0)
layoutDefaulted.floatSize.height =
tabInitialFloatingSize[savedTabInfo.tabType]?.height || DEFAULT_FLOAT_SIZE.height;
}

break;
}
case 'panel':
if (!layoutDefaulted.direction) layoutDefaulted.direction = DEFAULT_PANEL_DIRECTION;
break;
case 'tab':
default:
// do nothing
}
return layoutDefaulted;
}
Original file line number Diff line number Diff line change
@@ -1,78 +1,20 @@
/* eslint-disable import/first */
jest.mock('../../../shared/services/logger.service');

import DockLayout, { FloatPosition } from 'rc-dock';
import DockLayout from 'rc-dock';
import { anything, instance, mock, verify, when } from 'ts-mockito';
import {
FloatLayout,
Layout,
SavedTabInfo,
WebViewTabProps,
} from '@shared/models/docking-framework.model';
import {
addTabToDock,
addWebViewToDock,
getFloatPosition,
loadTab,
} from './platform-dock-layout.component';
import { addTabToDock, addWebViewToDock, loadTab } from './platform-dock-layout-storage.util';
/* eslint-enable */

describe('Dock Layout Component', () => {
const mockDockLayout = mock(DockLayout);

describe('getFloatPosition()', () => {
it('should cascade from top-left of layout', () => {
const layout: FloatLayout = { type: 'float', floatSize: { width: 20, height: 10 } };
const floatPosition: FloatPosition = { left: 0, top: 0, width: 0, height: 0 };

let nextPosition = getFloatPosition(layout, floatPosition, { width: 100, height: 100 });

expect(nextPosition).toEqual({
left: 28,
top: 28,
width: 20,
height: 10,
});

nextPosition = getFloatPosition(layout, nextPosition, { width: 100, height: 100 });

expect(nextPosition).toEqual({
left: 2 * 28,
top: 2 * 28,
width: 20,
height: 10,
});
});

it('should overflow right of layout', () => {
const layout: FloatLayout = { type: 'float', floatSize: { width: 20, height: 10 } };
const floatPosition: FloatPosition = { left: 2 * 28, top: 2 * 28, width: 0, height: 0 };
// right = 2*28 + 20 + 28 = 104
// bottom = 2*28 + 10 + 28 = 94

expect(getFloatPosition(layout, floatPosition, { width: 100, height: 100 })).toEqual({
left: 28,
top: 3 * 28,
width: 20,
height: 10,
});
});

it('should overflow bottom of layout', () => {
const layout: FloatLayout = { type: 'float', floatSize: { width: 20, height: 10 } };
const floatPosition: FloatPosition = { left: 2 * 28, top: 2 * 28, width: 0, height: 0 };
// right = 2*28 + 20 + 28 = 104
// bottom = 2*28 + 10 + 28 = 94

expect(getFloatPosition(layout, floatPosition, { width: 120, height: 90 })).toEqual({
left: 3 * 28,
top: 28,
width: 20,
height: 10,
});
});
});

describe('loadTab()', () => {
it('should throw when no id', () => {
// eslint-disable-next-line no-type-assertion/no-type-assertion
Expand Down
Loading