refactor(editor): move shape toolbar config and components to its package (#11082)

This commit is contained in:
Saul-Mirone
2025-03-21 16:47:39 +00:00
parent 909426c644
commit f762797772
13 changed files with 410 additions and 386 deletions

View File

@@ -1,383 +1,72 @@
import { shapeToolbarConfig } from '@blocksuite/affine-gfx-shape';
import {
EdgelessCRUDIdentifier,
normalizeShapeBound,
} from '@blocksuite/affine-block-surface';
import {
packColor,
type PickColorEvent,
} from '@blocksuite/affine-components/color-picker';
import type { LineDetailType } from '@blocksuite/affine-components/edgeless-line-styles-panel';
import {
mountShapeTextEditor,
ShapeComponentConfig,
type ShapeToolOption,
} from '@blocksuite/affine-gfx-shape';
import {
type Color,
DefaultTheme,
FontFamily,
getShapeName,
getShapeRadius,
getShapeType,
isTransparent,
LineWidth,
MindmapElementModel,
resolveColor,
ShapeElementModel,
type ShapeName,
ShapeStyle,
ShapeType,
StrokeStyle,
} from '@blocksuite/affine-model';
import type {
ToolbarGenericAction,
ToolbarModuleConfig,
} from '@blocksuite/affine-shared/services';
import { getMostCommonValue } from '@blocksuite/affine-shared/utils';
import {
createTextActions,
getRootBlock,
LINE_STYLE_LIST,
renderMenu,
} from '@blocksuite/affine-widget-edgeless-toolbar';
import { Bound } from '@blocksuite/global/gfx';
import { AddTextIcon, ShapeIcon } from '@blocksuite/icons/lit';
import { html } from 'lit';
import isEqual from 'lodash-es/isEqual';
import type { ToolbarActions } from '@blocksuite/affine-shared/services';
import {
createMindmapLayoutActionMenu,
createMindmapStyleActionMenu,
} from './mindmap';
const mindmapActions = [
{
id: 'a.mindmap-style',
when(ctx) {
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
return models.some(hasGrouped);
},
content(ctx) {
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
if (!models.length) return null;
let mindmaps = models
.map(model => model.group)
.filter(model => ctx.matchModel(model, MindmapElementModel));
if (!mindmaps.length) return null;
// Not displayed when there is both a normal shape and a mindmap shape.
if (models.length !== mindmaps.length) return null;
mindmaps = Array.from(new Set(mindmaps));
return createMindmapStyleActionMenu(ctx, mindmaps);
},
},
{
id: 'b.mindmap-layout',
when(ctx) {
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
return models.some(hasGrouped);
},
content(ctx) {
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
if (!models.length) return null;
let mindmaps = models
.map(model => model.group)
.filter(model => ctx.matchModel(model, MindmapElementModel));
if (!mindmaps.length) return null;
// Not displayed when there is both a normal shape and a mindmap shape.
if (models.length !== mindmaps.length) return null;
mindmaps = Array.from(new Set(mindmaps));
// It's a sub node.
if (models.length === 1 && mindmaps[0].tree.element !== models[0])
return null;
return createMindmapLayoutActionMenu(ctx, mindmaps);
},
},
] as const satisfies ToolbarActions;
export const builtinShapeToolbarConfig = {
actions: [
{
id: 'a.mindmap-style',
when(ctx) {
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
return models.some(hasGrouped);
},
content(ctx) {
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
if (!models.length) return null;
let mindmaps = models
.map(model => model.group)
.filter(model => ctx.matchModel(model, MindmapElementModel));
if (!mindmaps.length) return null;
// Not displayed when there is both a normal shape and a mindmap shape.
if (models.length !== mindmaps.length) return null;
mindmaps = Array.from(new Set(mindmaps));
return createMindmapStyleActionMenu(ctx, mindmaps);
},
},
{
id: 'b.mindmap-layout',
when(ctx) {
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
return models.some(hasGrouped);
},
content(ctx) {
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
if (!models.length) return null;
let mindmaps = models
.map(model => model.group)
.filter(model => ctx.matchModel(model, MindmapElementModel));
if (!mindmaps.length) return null;
// Not displayed when there is both a normal shape and a mindmap shape.
if (models.length !== mindmaps.length) return null;
mindmaps = Array.from(new Set(mindmaps));
// It's a sub node.
if (models.length === 1 && mindmaps[0].tree.element !== models[0])
return null;
return createMindmapLayoutActionMenu(ctx, mindmaps);
},
},
{
id: 'c.switch-type',
when(ctx) {
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
return models.length > 0 && models.every(model => !hasGrouped(model));
},
content(ctx) {
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
if (!models.length) return null;
const shapeName =
getMostCommonValue<ShapeToolOption, 'shapeName'>(
models.map(model => ({
shapeName: getShapeName(model.shapeType, model.radius),
})),
'shapeName'
) ?? ShapeType.Rect;
const onPick = (shapeName: ShapeName) => {
const shapeType = getShapeType(shapeName);
const radius = getShapeRadius(shapeName);
ctx.std.store.captureSync();
for (const model of models) {
ctx.std
.get(EdgelessCRUDIdentifier)
.updateElement(model.id, { shapeType, radius });
}
};
return renderMenu({
icon: ShapeIcon(),
label: 'Switch type',
items: ShapeComponentConfig.map(item => ({
key: item.tooltip,
value: item.name,
// TODO(@fundon): should add a feature flag to switch style
icon: item.generalIcon,
disabled: item.disabled,
})),
currentValue: shapeName,
onPick,
});
},
},
{
id: 'd.style',
// TODO(@fundon): should add a feature flag
when: false,
content(ctx) {
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
if (!models.length) return null;
const field = 'shapeStyle';
const shapeStyle =
getMostCommonValue(models, field) ?? ShapeStyle.General;
const onPick = (value: boolean) => {
const shapeStyle = value ? ShapeStyle.Scribbled : ShapeStyle.General;
const fontFamily = value ? FontFamily.Kalam : FontFamily.Inter;
for (const model of models) {
ctx.std
.get(EdgelessCRUDIdentifier)
.updateElement(model.id, { shapeStyle, fontFamily });
}
};
return renderMenu({
label: 'Style',
items: LINE_STYLE_LIST,
currentValue: shapeStyle === ShapeStyle.Scribbled,
onPick,
});
},
},
{
id: 'e.color',
when(ctx) {
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
return models.length > 0 && models.every(model => !hasGrouped(model));
},
content(ctx) {
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
if (!models.length) return null;
const enableCustomColor = ctx.features.getFlag('enable_color_picker');
const theme = ctx.theme.edgeless$.value;
const firstModel = models[0];
const originalFillColor = firstModel.fillColor;
const originalStrokeColor = firstModel.strokeColor;
const mapped = models.map(
({ filled, fillColor, strokeColor, strokeWidth, strokeStyle }) => ({
fillColor: filled
? resolveColor(fillColor, theme)
: DefaultTheme.transparent,
strokeColor: resolveColor(strokeColor, theme),
strokeWidth,
strokeStyle,
})
);
const fillColor =
getMostCommonValue(mapped, 'fillColor') ??
resolveColor(DefaultTheme.shapeFillColor, theme);
const strokeColor =
getMostCommonValue(mapped, 'strokeColor') ??
resolveColor(DefaultTheme.shapeStrokeColor, theme);
const strokeWidth =
getMostCommonValue(mapped, 'strokeWidth') ?? LineWidth.Four;
const strokeStyle =
getMostCommonValue(mapped, 'strokeStyle') ?? StrokeStyle.Solid;
const onPickFillColor = (e: CustomEvent<PickColorEvent>) => {
e.stopPropagation();
const d = e.detail;
const field = 'fillColor';
if (d.type === 'pick') {
const value = d.detail.value;
const filled = isTransparent(value);
for (const model of models) {
const props = packColor(field, value);
// If `filled` can be set separately, this logic can be removed
if (field && !model.filled) {
const color = getTextColor(value, filled);
Object.assign(props, { filled, color });
}
ctx.std
.get(EdgelessCRUDIdentifier)
.updateElement(model.id, props);
}
return;
}
for (const model of models) {
model[d.type === 'start' ? 'stash' : 'pop'](field);
}
};
const onPickStrokeColor = (e: CustomEvent<PickColorEvent>) => {
e.stopPropagation();
const d = e.detail;
const field = 'strokeColor';
if (d.type === 'pick') {
const value = d.detail.value;
for (const model of models) {
const props = packColor(field, value);
ctx.std
.get(EdgelessCRUDIdentifier)
.updateElement(model.id, props);
}
return;
}
for (const model of models) {
model[d.type === 'start' ? 'stash' : 'pop'](field);
}
};
const onPickStrokeStyle = (e: CustomEvent<LineDetailType>) => {
e.stopPropagation();
const { type, value } = e.detail;
if (type === 'size') {
const strokeWidth = value;
for (const model of models) {
ctx.std
.get(EdgelessCRUDIdentifier)
.updateElement(model.id, { strokeWidth });
}
return;
}
const strokeStyle = value;
for (const model of models) {
ctx.std
.get(EdgelessCRUDIdentifier)
.updateElement(model.id, { strokeStyle });
}
};
return html`
<edgeless-shape-color-picker
@pickFillColor=${onPickFillColor}
@pickStrokeColor=${onPickStrokeColor}
@pickStrokeStyle=${onPickStrokeStyle}
.payload=${{
fillColor,
strokeColor,
strokeWidth,
strokeStyle,
originalFillColor,
originalStrokeColor,
theme,
enableCustomColor,
}}
>
</edgeless-shape-color-picker>
`;
},
},
{
id: 'f.text',
tooltip: 'Add text',
icon: AddTextIcon(),
when(ctx) {
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
return models.length === 1 && !hasGrouped(models[0]) && !models[0].text;
},
run(ctx) {
const model = ctx.getCurrentModelByType(ShapeElementModel);
if (!model) return;
const rootBlock = getRootBlock(ctx);
if (!rootBlock) return;
mountShapeTextEditor(model, rootBlock);
},
},
// id: `g.text`
...createTextActions(ShapeElementModel, 'shape', (ctx, model, props) => {
// No need to adjust element bounds
if (props['textAlign']) {
ctx.std.get(EdgelessCRUDIdentifier).updateElement(model.id, props);
return;
}
const xywh = normalizeShapeBound(
model,
Bound.fromXYWH(model.deserializedXYWH)
).serialize();
ctx.std
.get(EdgelessCRUDIdentifier)
.updateElement(model.id, { ...props, xywh });
}).map<ToolbarGenericAction>(action => ({
...action,
id: `g.text-${action.id}`,
when(ctx) {
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
return (
models.length > 0 &&
models.every(model => !hasGrouped(model) && model.text)
);
},
})),
],
when: ctx => ctx.getSurfaceModelsByType(ShapeElementModel).length > 0,
} as const satisfies ToolbarModuleConfig;
// When the shape is filled with black color, the text color should be white.
// When the shape is transparent, the text color should be set according to the theme.
// Otherwise, the text color should be black.
function getTextColor(fillColor: Color, isNotTransparent = false) {
if (isNotTransparent) {
if (isEqual(fillColor, DefaultTheme.black)) {
return DefaultTheme.white;
} else if (isEqual(fillColor, DefaultTheme.white)) {
return DefaultTheme.black;
} else if (isEqual(fillColor, DefaultTheme.pureBlack)) {
return DefaultTheme.pureWhite;
} else if (isEqual(fillColor, DefaultTheme.pureWhite)) {
return DefaultTheme.pureBlack;
}
}
// aka `DefaultTheme.pureBlack`
return DefaultTheme.shapeTextColor;
}
...shapeToolbarConfig,
actions: [...mindmapActions, ...shapeToolbarConfig.actions],
};
function hasGrouped(model: ShapeElementModel) {
return model.group instanceof MindmapElementModel;

View File

@@ -51,8 +51,6 @@ import { EdgelessFrameOrderMenu } from './edgeless/components/toolbar/present/fr
import { EdgelessNavigatorSettingButton } from './edgeless/components/toolbar/present/navigator-setting-button.js';
import { EdgelessPresentButton } from './edgeless/components/toolbar/present/present-button.js';
import { PresentationToolbar } from './edgeless/components/toolbar/presentation-toolbar.js';
import { EdgelessToolbarShapeDraggable } from './edgeless/components/toolbar/shape/shape-draggable.js';
import { EdgelessShapeToolButton } from './edgeless/components/toolbar/shape/shape-tool-button.js';
import { OverlayScrollbar } from './edgeless/components/toolbar/template/overlay-scrollbar.js';
import { AffineTemplateLoading } from './edgeless/components/toolbar/template/template-loading.js';
import { EdgelessTemplatePanel } from './edgeless/components/toolbar/template/template-panel.js';
@@ -168,7 +166,6 @@ function registerEdgelessToolbarComponents() {
EdgelessMindmapToolButton
);
customElements.define('edgeless-note-tool-button', EdgelessNoteToolButton);
customElements.define('edgeless-shape-tool-button', EdgelessShapeToolButton);
customElements.define('edgeless-template-button', EdgelessTemplateButton);
// Menus
@@ -181,10 +178,6 @@ function registerEdgelessToolbarComponents() {
customElements.define('edgeless-slide-menu', EdgelessSlideMenu);
// Toolbar components
customElements.define(
'edgeless-toolbar-shape-draggable',
EdgelessToolbarShapeDraggable
);
customElements.define('toolbar-arrow-up-icon', ToolbarArrowUpIcon);
// Frame order components
@@ -322,8 +315,6 @@ declare global {
'edgeless-frame-order-menu': EdgelessFrameOrderMenu;
'edgeless-navigator-setting-button': EdgelessNavigatorSettingButton;
'edgeless-present-button': EdgelessPresentButton;
'edgeless-toolbar-shape-draggable': EdgelessToolbarShapeDraggable;
'edgeless-shape-tool-button': EdgelessShapeToolButton;
'overlay-scrollbar': OverlayScrollbar;
'affine-template-loading': AffineTemplateLoading;
'edgeless-templates-panel': EdgelessTemplatePanel;

View File

@@ -15,6 +15,7 @@
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-rich-text": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-edgeless-toolbar": "workspace:*",
"@blocksuite/block-std": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.6",

View File

@@ -1,2 +1,4 @@
export * from './shape-draggable';
export * from './shape-menu';
export * from './shape-tool-button';
export * from './shape-tool-element';

View File

@@ -2,12 +2,6 @@ import {
CanvasElementType,
EdgelessCRUDIdentifier,
} from '@blocksuite/affine-block-surface';
import {
ellipseSvg,
roundedSvg,
ShapeTool,
triangleSvg,
} from '@blocksuite/affine-gfx-shape';
import {
getShapeRadius,
getShapeType,
@@ -30,6 +24,8 @@ import { classMap } from 'lit/directives/class-map.js';
import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js';
import { ShapeTool } from '../shape-tool.js';
import { ellipseSvg, roundedSvg, triangleSvg } from '../toolbar/icons.js';
import type { DraggableShape } from './utils.js';
import { buildVariablesObject } from './utils.js';
@@ -210,6 +206,7 @@ export class EdgelessToolbarShapeDraggable extends EdgelessToolbarToolMixin(
this._setShapeOverlayLock(false);
this.readyToDrop = false;
// @ts-expect-error FIXME: resolve after gfx tool refactor
this.gfx.tool.setTool('default');
this.gfx.selection.set({
elements: [id],

View File

@@ -1,9 +1,9 @@
import { ShapeTool } from '@blocksuite/affine-gfx-shape';
import { type ShapeName, ShapeType } from '@blocksuite/affine-model';
import { EdgelessToolbarToolMixin } from '@blocksuite/affine-widget-edgeless-toolbar';
import { SignalWatcher } from '@blocksuite/global/lit';
import { css, html, LitElement } from 'lit';
import { ShapeTool } from '../shape-tool.js';
import type { DraggableShape } from './utils.js';
export class EdgelessShapeToolButton extends EdgelessToolbarToolMixin(

View File

@@ -1,4 +1,9 @@
import { EdgelessShapeMenu, EdgelessShapeToolElement } from './draggable';
import {
EdgelessShapeMenu,
EdgelessShapeToolButton,
EdgelessShapeToolElement,
EdgelessToolbarShapeDraggable,
} from './draggable';
import { EdgelessShapeTextEditor } from './text/edgeless-shape-text-editor';
export function effects() {
@@ -8,6 +13,11 @@ export function effects() {
'edgeless-shape-tool-element',
EdgelessShapeToolElement
);
customElements.define('edgeless-shape-tool-button', EdgelessShapeToolButton);
customElements.define(
'edgeless-toolbar-shape-draggable',
EdgelessToolbarShapeDraggable
);
}
declare global {
@@ -15,5 +25,7 @@ declare global {
'edgeless-shape-text-editor': EdgelessShapeTextEditor;
'edgeless-shape-menu': EdgelessShapeMenu;
'edgeless-shape-tool-element': EdgelessShapeToolElement;
'edgeless-toolbar-shape-draggable': EdgelessToolbarShapeDraggable;
'edgeless-shape-tool-button': EdgelessShapeToolButton;
}
}

View File

@@ -1,2 +1,3 @@
export * from './icons';
export * from './shape';
export * from './shape-menu-config';

View File

@@ -0,0 +1,328 @@
import {
EdgelessCRUDIdentifier,
normalizeShapeBound,
} from '@blocksuite/affine-block-surface';
import {
packColor,
type PickColorEvent,
} from '@blocksuite/affine-components/color-picker';
import type { LineDetailType } from '@blocksuite/affine-components/edgeless-line-styles-panel';
import {
type Color,
DefaultTheme,
FontFamily,
getShapeName,
getShapeRadius,
getShapeType,
isTransparent,
LineWidth,
MindmapElementModel,
resolveColor,
ShapeElementModel,
type ShapeName,
ShapeStyle,
ShapeType,
StrokeStyle,
} from '@blocksuite/affine-model';
import type {
ToolbarGenericAction,
ToolbarModuleConfig,
} from '@blocksuite/affine-shared/services';
import { getMostCommonValue } from '@blocksuite/affine-shared/utils';
import {
createTextActions,
getRootBlock,
LINE_STYLE_LIST,
renderMenu,
} from '@blocksuite/affine-widget-edgeless-toolbar';
import { Bound } from '@blocksuite/global/gfx';
import { AddTextIcon, ShapeIcon } from '@blocksuite/icons/lit';
import { html } from 'lit';
import isEqual from 'lodash-es/isEqual';
import type { ShapeToolOption } from '../shape-tool';
import { mountShapeTextEditor } from '../text';
import { ShapeComponentConfig } from './shape-menu-config';
export const shapeToolbarConfig = {
actions: [
{
id: 'c.switch-type',
when(ctx) {
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
return models.length > 0 && models.every(model => !hasGrouped(model));
},
content(ctx) {
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
if (!models.length) return null;
const shapeName =
getMostCommonValue<ShapeToolOption, 'shapeName'>(
models.map(model => ({
shapeName: getShapeName(model.shapeType, model.radius),
})),
'shapeName'
) ?? ShapeType.Rect;
const onPick = (shapeName: ShapeName) => {
const shapeType = getShapeType(shapeName);
const radius = getShapeRadius(shapeName);
ctx.std.store.captureSync();
for (const model of models) {
ctx.std
.get(EdgelessCRUDIdentifier)
.updateElement(model.id, { shapeType, radius });
}
};
return renderMenu({
icon: ShapeIcon(),
label: 'Switch type',
items: ShapeComponentConfig.map(item => ({
key: item.tooltip,
value: item.name,
// TODO(@fundon): should add a feature flag to switch style
icon: item.generalIcon,
disabled: item.disabled,
})),
currentValue: shapeName,
onPick,
});
},
},
{
id: 'd.style',
// TODO(@fundon): should add a feature flag
when: false,
content(ctx) {
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
if (!models.length) return null;
const field = 'shapeStyle';
const shapeStyle =
getMostCommonValue(models, field) ?? ShapeStyle.General;
const onPick = (value: boolean) => {
const shapeStyle = value ? ShapeStyle.Scribbled : ShapeStyle.General;
const fontFamily = value ? FontFamily.Kalam : FontFamily.Inter;
for (const model of models) {
ctx.std
.get(EdgelessCRUDIdentifier)
.updateElement(model.id, { shapeStyle, fontFamily });
}
};
return renderMenu({
label: 'Style',
items: LINE_STYLE_LIST,
currentValue: shapeStyle === ShapeStyle.Scribbled,
onPick,
});
},
},
{
id: 'e.color',
when(ctx) {
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
return models.length > 0 && models.every(model => !hasGrouped(model));
},
content(ctx) {
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
if (!models.length) return null;
const enableCustomColor = ctx.features.getFlag('enable_color_picker');
const theme = ctx.theme.edgeless$.value;
const firstModel = models[0];
const originalFillColor = firstModel.fillColor;
const originalStrokeColor = firstModel.strokeColor;
const mapped = models.map(
({ filled, fillColor, strokeColor, strokeWidth, strokeStyle }) => ({
fillColor: filled
? resolveColor(fillColor, theme)
: DefaultTheme.transparent,
strokeColor: resolveColor(strokeColor, theme),
strokeWidth,
strokeStyle,
})
);
const fillColor =
getMostCommonValue(mapped, 'fillColor') ??
resolveColor(DefaultTheme.shapeFillColor, theme);
const strokeColor =
getMostCommonValue(mapped, 'strokeColor') ??
resolveColor(DefaultTheme.shapeStrokeColor, theme);
const strokeWidth =
getMostCommonValue(mapped, 'strokeWidth') ?? LineWidth.Four;
const strokeStyle =
getMostCommonValue(mapped, 'strokeStyle') ?? StrokeStyle.Solid;
const onPickFillColor = (e: CustomEvent<PickColorEvent>) => {
e.stopPropagation();
const d = e.detail;
const field = 'fillColor';
if (d.type === 'pick') {
const value = d.detail.value;
const filled = isTransparent(value);
for (const model of models) {
const props = packColor(field, value);
// If `filled` can be set separately, this logic can be removed
if (field && !model.filled) {
const color = getTextColor(value, filled);
Object.assign(props, { filled, color });
}
ctx.std
.get(EdgelessCRUDIdentifier)
.updateElement(model.id, props);
}
return;
}
for (const model of models) {
model[d.type === 'start' ? 'stash' : 'pop'](field);
}
};
const onPickStrokeColor = (e: CustomEvent<PickColorEvent>) => {
e.stopPropagation();
const d = e.detail;
const field = 'strokeColor';
if (d.type === 'pick') {
const value = d.detail.value;
for (const model of models) {
const props = packColor(field, value);
ctx.std
.get(EdgelessCRUDIdentifier)
.updateElement(model.id, props);
}
return;
}
for (const model of models) {
model[d.type === 'start' ? 'stash' : 'pop'](field);
}
};
const onPickStrokeStyle = (e: CustomEvent<LineDetailType>) => {
e.stopPropagation();
const { type, value } = e.detail;
if (type === 'size') {
const strokeWidth = value;
for (const model of models) {
ctx.std
.get(EdgelessCRUDIdentifier)
.updateElement(model.id, { strokeWidth });
}
return;
}
const strokeStyle = value;
for (const model of models) {
ctx.std
.get(EdgelessCRUDIdentifier)
.updateElement(model.id, { strokeStyle });
}
};
return html`
<edgeless-shape-color-picker
@pickFillColor=${onPickFillColor}
@pickStrokeColor=${onPickStrokeColor}
@pickStrokeStyle=${onPickStrokeStyle}
.payload=${{
fillColor,
strokeColor,
strokeWidth,
strokeStyle,
originalFillColor,
originalStrokeColor,
theme,
enableCustomColor,
}}
>
</edgeless-shape-color-picker>
`;
},
},
{
id: 'f.text',
tooltip: 'Add text',
icon: AddTextIcon(),
when(ctx) {
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
return models.length === 1 && !hasGrouped(models[0]) && !models[0].text;
},
run(ctx) {
const model = ctx.getCurrentModelByType(ShapeElementModel);
if (!model) return;
const rootBlock = getRootBlock(ctx);
if (!rootBlock) return;
mountShapeTextEditor(model, rootBlock);
},
},
// id: `g.text`
...createTextActions(ShapeElementModel, 'shape', (ctx, model, props) => {
// No need to adjust element bounds
if (props['textAlign']) {
ctx.std.get(EdgelessCRUDIdentifier).updateElement(model.id, props);
return;
}
const xywh = normalizeShapeBound(
model,
Bound.fromXYWH(model.deserializedXYWH)
).serialize();
ctx.std
.get(EdgelessCRUDIdentifier)
.updateElement(model.id, { ...props, xywh });
}).map<ToolbarGenericAction>(action => ({
...action,
id: `g.text-${action.id}`,
when(ctx) {
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
return (
models.length > 0 &&
models.every(model => !hasGrouped(model) && model.text)
);
},
})),
],
when: ctx => ctx.getSurfaceModelsByType(ShapeElementModel).length > 0,
} as const satisfies ToolbarModuleConfig;
// When the shape is filled with black color, the text color should be white.
// When the shape is transparent, the text color should be set according to the theme.
// Otherwise, the text color should be black.
function getTextColor(fillColor: Color, isNotTransparent = false) {
if (isNotTransparent) {
if (isEqual(fillColor, DefaultTheme.black)) {
return DefaultTheme.white;
} else if (isEqual(fillColor, DefaultTheme.white)) {
return DefaultTheme.black;
} else if (isEqual(fillColor, DefaultTheme.pureBlack)) {
return DefaultTheme.pureWhite;
} else if (isEqual(fillColor, DefaultTheme.pureWhite)) {
return DefaultTheme.pureBlack;
}
}
// aka `DefaultTheme.pureBlack`
return DefaultTheme.shapeTextColor;
}
function hasGrouped(model: ShapeElementModel) {
return model.group instanceof MindmapElementModel;
}

View File

@@ -12,6 +12,7 @@
{ "path": "../../model" },
{ "path": "../../rich-text" },
{ "path": "../../shared" },
{ "path": "../../widgets/widget-edgeless-toolbar" },
{ "path": "../../../framework/block-std" },
{ "path": "../../../framework/global" },
{ "path": "../../../framework/store" }