diff --git a/newIDE/app/src/BehaviorsEditor/Editors/AnchorBehaviorEditor.js b/newIDE/app/src/BehaviorsEditor/Editors/AnchorBehaviorEditor.js index 452540143778..e94b5b2d5170 100644 --- a/newIDE/app/src/BehaviorsEditor/Editors/AnchorBehaviorEditor.js +++ b/newIDE/app/src/BehaviorsEditor/Editors/AnchorBehaviorEditor.js @@ -4,30 +4,16 @@ import { Trans } from '@lingui/macro'; import * as React from 'react'; import { type BehaviorEditorProps } from './BehaviorEditorProps.flow'; import BehaviorPropertiesEditor from './BehaviorPropertiesEditor'; -import SmallCrossIcon from '../../UI/CustomSvgIcons/SmallCross'; -import LeftAlignmentIcon from '../../UI/CustomSvgIcons/LeftAlignment'; -import CenterAlignmentIcon from '../../UI/CustomSvgIcons/CenterAlignment'; -import RightAlignmentIcon from '../../UI/CustomSvgIcons/RightAlignment'; -import FillIcon from '../../UI/CustomSvgIcons/HorizontalSize'; -import ProportionalFillIcon from '../../UI/CustomSvgIcons/HorizontalSizePercent'; -import TopAlignmentIcon from '../../UI/CustomSvgIcons/TopAlignment'; -import CenterVerticalAlignmentIcon from '../../UI/CustomSvgIcons/CenterVerticalAlignment'; -import BottomAlignmentIcon from '../../UI/CustomSvgIcons/BottomAlignment'; -import VerticalFillIcon from '../../UI/CustomSvgIcons/VerticalSize'; -import VerticalProportionalFillIcon from '../../UI/CustomSvgIcons/VerticalSizePercent'; import useForceUpdate from '../../Utils/UseForceUpdate'; import { ColumnStackLayout } from '../../UI/Layout'; -import { Line } from '../../UI/Grid'; import Text from '../../UI/Text'; import { getPropertyValue, updateProperty, } from '../../ObjectEditor/CompactObjectPropertiesEditor/CompactBehaviorPropertiesEditor'; -import CompactToggleButtons, { - type CompactToggleButton, -} from '../../UI/CompactToggleButtons'; +import AnchorGrid from './AnchorGrid'; -type BasicAnchor = +export type BasicAnchor = | 'None' | 'ProportionalFill' | 'FixedFill' @@ -110,13 +96,13 @@ export const getBasicVerticalAnchor = ( getAnchorProperty(getPropertyValue, 'bottomEdgeAnchor') ); -type AnchorMapping = Array<{| +export type AnchorMapping = Array<{| basicAnchor: BasicAnchor, minEdge: string, maxEdge: string, |}>; -const horizontalAnchorMapping: AnchorMapping = [ +export const horizontalAnchorMapping: AnchorMapping = [ { basicAnchor: 'None', minEdge: 'None', maxEdge: 'None' }, { basicAnchor: 'MinEdge', minEdge: 'WindowLeft', maxEdge: 'WindowLeft' }, { basicAnchor: 'Center', minEdge: 'WindowCenter', maxEdge: 'WindowCenter' }, @@ -129,7 +115,7 @@ const horizontalAnchorMapping: AnchorMapping = [ }, ]; -const verticalAnchorMapping: AnchorMapping = [ +export const verticalAnchorMapping: AnchorMapping = [ { basicAnchor: 'None', minEdge: 'None', maxEdge: 'None' }, { basicAnchor: 'MinEdge', minEdge: 'WindowTop', maxEdge: 'WindowTop' }, { basicAnchor: 'Center', minEdge: 'WindowCenter', maxEdge: 'WindowCenter' }, @@ -142,157 +128,6 @@ const verticalAnchorMapping: AnchorMapping = [ }, ]; -const AnchorButtonGroup = ({ - id, - basicAnchor, - minEdgePropertyName, - maxEdgePropertyName, - anchorMapping, - renderIcon, - renderTooltip, - onUpdateProperty, - expand, -}: {| - id: string, - basicAnchor: BasicAnchor, - minEdgePropertyName: string, - maxEdgePropertyName: string, - anchorMapping: Array<{| - basicAnchor: BasicAnchor, - minEdge: string, - maxEdge: string, - |}>, - renderIcon: (basicAnchor: string, className?: string) => React.Node, - renderTooltip: (basicAnchor: string) => React.Node, - onUpdateProperty: (propertyName: string, value: string) => void, - expand?: boolean, -|}): React.Node => { - const buttons: Array = anchorMapping.map(item => ({ - id: item.basicAnchor, - renderIcon: (className?: string) => renderIcon(item.basicAnchor, className), - tooltip: renderTooltip(item.basicAnchor), - isActive: basicAnchor === item.basicAnchor, - onClick: () => { - onUpdateProperty(minEdgePropertyName, item.minEdge); - onUpdateProperty(maxEdgePropertyName, item.maxEdge); - }, - })); - - return ; -}; - -export const HorizontalAnchorButtonGroup = ({ - basicAnchor, - onUpdateProperty, - expand, -}: {| - basicAnchor: BasicAnchor, - onUpdateProperty: (propertyName: string, value: string) => void, - expand?: boolean, -|}): React.Node => { - return ( - { - switch (basicAnchor) { - case 'MinEdge': - return ; - case 'Center': - return ; - case 'MaxEdge': - return ; - case 'FixedFill': - return ; - case 'ProportionalFill': - return ; - case 'None': - default: - return ; - } - }} - renderTooltip={basicAnchor => { - switch (basicAnchor) { - case 'MinEdge': - return Left; - case 'Center': - return Center; - case 'MaxEdge': - return Right; - case 'FixedFill': - return Fill; - case 'ProportionalFill': - return Fill proportionally; - case 'None': - default: - return None; - } - }} - onUpdateProperty={onUpdateProperty} - expand={expand} - /> - ); -}; - -export const VerticalAnchorButtonGroup = ({ - basicAnchor, - onUpdateProperty, - expand, -}: {| - basicAnchor: BasicAnchor, - onUpdateProperty: (propertyName: string, value: string) => void, - expand?: boolean, -|}): React.Node => { - return ( - { - switch (basicAnchor) { - case 'MinEdge': - return ; - case 'Center': - return ; - case 'MaxEdge': - return ; - case 'FixedFill': - return ; - case 'ProportionalFill': - return ; - case 'None': - default: - return ; - } - }} - renderTooltip={basicAnchor => { - switch (basicAnchor) { - case 'MinEdge': - return Top; - case 'Center': - return Center; - case 'MaxEdge': - return Bottom; - case 'FixedFill': - return Fill; - case 'ProportionalFill': - return Fill proportionally; - case 'None': - default: - return None; - } - }} - onUpdateProperty={onUpdateProperty} - expand={expand} - /> - ); -}; - type Props = BehaviorEditorProps; const AnchorBehaviorEditor = ({ @@ -324,29 +159,20 @@ const AnchorBehaviorEditor = ({ [forceUpdate, onBehaviorUpdated] ); - const horizontalBasicAnchor = getBasicHorizontalAnchor(_getPropertyValue); - const verticalBasicAnchor = getBasicVerticalAnchor(_getPropertyValue); + const isAdvanced = + getBasicHorizontalAnchor(_getPropertyValue) === 'Advanced' || + getBasicVerticalAnchor(_getPropertyValue) === 'Advanced'; return ( - Horizontal anchor + Alignment - - - - - Vertical anchor - - - - + diff --git a/newIDE/app/src/BehaviorsEditor/Editors/AnchorGrid.js b/newIDE/app/src/BehaviorsEditor/Editors/AnchorGrid.js new file mode 100644 index 000000000000..c0ca9438c875 --- /dev/null +++ b/newIDE/app/src/BehaviorsEditor/Editors/AnchorGrid.js @@ -0,0 +1,330 @@ +// @flow +import { Trans } from '@lingui/macro'; +import * as React from 'react'; +import LeftAlignmentIcon from '../../UI/CustomSvgIcons/LeftAlignment'; +import CenterAlignmentIcon from '../../UI/CustomSvgIcons/CenterAlignment'; +import RightAlignmentIcon from '../../UI/CustomSvgIcons/RightAlignment'; +import FillIcon from '../../UI/CustomSvgIcons/HorizontalSize'; +import ProportionalFillIcon from '../../UI/CustomSvgIcons/HorizontalSizePercent'; +import TopAlignmentIcon from '../../UI/CustomSvgIcons/TopAlignment'; +import CenterVerticalAlignmentIcon from '../../UI/CustomSvgIcons/CenterVerticalAlignment'; +import BottomAlignmentIcon from '../../UI/CustomSvgIcons/BottomAlignment'; +import VerticalFillIcon from '../../UI/CustomSvgIcons/VerticalSize'; +import VerticalProportionalFillIcon from '../../UI/CustomSvgIcons/VerticalSizePercent'; +import LetterV from '../../UI/CustomSvgIcons/LetterV'; +import LetterH from '../../UI/CustomSvgIcons/LetterH'; +import CompactTextField from '../../UI/CompactTextField'; +import CompactToggleButtons, { + type CompactToggleButton, +} from '../../UI/CompactToggleButtons'; +import { + type BasicAnchor, + type AnchorMapping, + getBasicHorizontalAnchor, + getBasicVerticalAnchor, + horizontalAnchorMapping, + verticalAnchorMapping, +} from './AnchorBehaviorEditor'; +import styles from './AnchorGrid.module.css'; + +type Props = {| + getPropertyValue: (propertyName: string) => string, + onUpdateProperty: (propertyName: string, value: string) => void, + initialInstance: gdInitialInstance | null, +|}; + +const getAnchorDisplayText = ( + basicAnchor: BasicAnchor, + axis: 'horizontal' | 'vertical' +): string => { + switch (basicAnchor) { + case 'MinEdge': + return axis === 'horizontal' ? 'Left' : 'Bottom'; + case 'Center': + return 'Center'; + case 'MaxEdge': + return axis === 'horizontal' ? 'Right' : 'Top'; + case 'FixedFill': + return 'Fill'; + case 'ProportionalFill': + return 'Proportional'; + case 'Advanced': + return 'Advanced'; + default: + return ''; + } +}; + +const makeButtons = ( + currentBasicAnchor: BasicAnchor, + targetAnchors: Array, + mapping: AnchorMapping, + minEdgeProperty: string, + maxEdgeProperty: string, + onUpdateProperty: (propertyName: string, value: string) => void, + renderIcon: (basicAnchor: BasicAnchor, className?: string) => React.Node, + renderTooltip: (basicAnchor: BasicAnchor) => React.Node +): Array => { + return targetAnchors.map(target => { + const entry = mapping.find(m => m.basicAnchor === target); + const noneEntry = mapping.find(m => m.basicAnchor === 'None'); + return { + id: target, + renderIcon: (className?: string) => renderIcon(target, className), + tooltip: renderTooltip(target), + isActive: currentBasicAnchor === target, + onClick: () => { + if (currentBasicAnchor === target) { + if (noneEntry) { + onUpdateProperty(minEdgeProperty, noneEntry.minEdge); + onUpdateProperty(maxEdgeProperty, noneEntry.maxEdge); + } + } else if (entry) { + onUpdateProperty(minEdgeProperty, entry.minEdge); + onUpdateProperty(maxEdgeProperty, entry.maxEdge); + } + }, + }; + }); +}; + +const clearAnchor = ( + mapping: AnchorMapping, + minEdgeProperty: string, + maxEdgeProperty: string, + onUpdateProperty: (propertyName: string, value: string) => void +) => { + const noneEntry = mapping.find(m => m.basicAnchor === 'None'); + if (noneEntry) { + onUpdateProperty(minEdgeProperty, noneEntry.minEdge); + onUpdateProperty(maxEdgeProperty, noneEntry.maxEdge); + } +}; + +const AnchorGrid = ({ + getPropertyValue, + onUpdateProperty, + initialInstance, +}: Props): React.Node => { + const verticalAnchor = getBasicVerticalAnchor(getPropertyValue); + const horizontalAnchor = getBasicHorizontalAnchor(getPropertyValue); + + // Vertical axis + const vIsNone = verticalAnchor === 'None'; + const vDisplayValue = vIsNone + ? initialInstance + ? String(Math.round(initialInstance.getY())) + : '' + : getAnchorDisplayText(verticalAnchor, 'vertical'); + const vPlaceholder = vIsNone && !initialInstance ? 'None' : undefined; + + const vPositionButtons = makeButtons( + verticalAnchor, + ['MinEdge', 'Center', 'MaxEdge'], + verticalAnchorMapping, + 'topEdgeAnchor', + 'bottomEdgeAnchor', + onUpdateProperty, + (basicAnchor, className) => { + switch (basicAnchor) { + case 'MinEdge': + return ; + case 'Center': + return ; + case 'MaxEdge': + return ; + default: + return null; + } + }, + basicAnchor => { + switch (basicAnchor) { + case 'MinEdge': + return Bottom; + case 'Center': + return Center; + case 'MaxEdge': + return Top; + default: + return ''; + } + } + ); + + const vFillButtons = makeButtons( + verticalAnchor, + ['FixedFill', 'ProportionalFill'], + verticalAnchorMapping, + 'topEdgeAnchor', + 'bottomEdgeAnchor', + onUpdateProperty, + (basicAnchor, className) => { + switch (basicAnchor) { + case 'FixedFill': + return ; + case 'ProportionalFill': + return ; + default: + return null; + } + }, + basicAnchor => { + switch (basicAnchor) { + case 'FixedFill': + return Fill; + case 'ProportionalFill': + return Fill proportionally; + default: + return ''; + } + } + ); + + // Horizontal axis + const hIsNone = horizontalAnchor === 'None'; + const hDisplayValue = hIsNone + ? initialInstance + ? String(Math.round(initialInstance.getX())) + : '' + : getAnchorDisplayText(horizontalAnchor, 'horizontal'); + const hPlaceholder = hIsNone && !initialInstance ? 'None' : undefined; + + const hPositionButtons = makeButtons( + horizontalAnchor, + ['MinEdge', 'Center', 'MaxEdge'], + horizontalAnchorMapping, + 'leftEdgeAnchor', + 'rightEdgeAnchor', + onUpdateProperty, + (basicAnchor, className) => { + switch (basicAnchor) { + case 'MinEdge': + return ; + case 'Center': + return ; + case 'MaxEdge': + return ; + default: + return null; + } + }, + basicAnchor => { + switch (basicAnchor) { + case 'MinEdge': + return Left; + case 'Center': + return Center; + case 'MaxEdge': + return Right; + default: + return ''; + } + } + ); + + const hFillButtons = makeButtons( + horizontalAnchor, + ['FixedFill', 'ProportionalFill'], + horizontalAnchorMapping, + 'leftEdgeAnchor', + 'rightEdgeAnchor', + onUpdateProperty, + (basicAnchor, className) => { + switch (basicAnchor) { + case 'FixedFill': + return ; + case 'ProportionalFill': + return ; + default: + return null; + } + }, + basicAnchor => { + switch (basicAnchor) { + case 'FixedFill': + return Fill; + case 'ProportionalFill': + return Fill proportionally; + default: + return ''; + } + } + ); + + return ( +
+
+ {}} + disabled={!vIsNone} + placeholder={vPlaceholder} + renderLeftIcon={className => } + leftIconTooltip={Vertical} + onFocus={ + !vIsNone + ? () => + clearAnchor( + verticalAnchorMapping, + 'topEdgeAnchor', + 'bottomEdgeAnchor', + onUpdateProperty + ) + : undefined + } + /> +
+ + +
+
+
+ {}} + disabled={!hIsNone} + placeholder={hPlaceholder} + renderLeftIcon={className => } + leftIconTooltip={Horizontal} + onFocus={ + !hIsNone + ? () => + clearAnchor( + horizontalAnchorMapping, + 'leftEdgeAnchor', + 'rightEdgeAnchor', + onUpdateProperty + ) + : undefined + } + /> +
+ + +
+
+
+ ); +}; + +export default AnchorGrid; diff --git a/newIDE/app/src/BehaviorsEditor/Editors/AnchorGrid.module.css b/newIDE/app/src/BehaviorsEditor/Editors/AnchorGrid.module.css new file mode 100644 index 000000000000..cf43c52c8333 --- /dev/null +++ b/newIDE/app/src/BehaviorsEditor/Editors/AnchorGrid.module.css @@ -0,0 +1,21 @@ +.container { + display: flex; + flex-direction: column; + gap: 16px; +} + +.axisGroup { + display: flex; + flex-direction: column; + gap: 8px; +} + +.buttonGroups { + display: flex; + align-items: center; + gap: 4px; +} + +.buttonGroups > * { + flex: 1; +} diff --git a/newIDE/app/src/EditorFunctions/SimplifiedProject/SimplifiedProject.spec.js b/newIDE/app/src/EditorFunctions/SimplifiedProject/SimplifiedProject.spec.js index 92fcdfc89da9..26812d7e90a2 100644 --- a/newIDE/app/src/EditorFunctions/SimplifiedProject/SimplifiedProject.spec.js +++ b/newIDE/app/src/EditorFunctions/SimplifiedProject/SimplifiedProject.spec.js @@ -271,6 +271,10 @@ describe('SimplifiedProject', () => { }, Object { "behaviors": Array [ + Object { + "behaviorName": "Anchor", + "behaviorType": "AnchorBehavior::AnchorBehavior", + }, Object { "behaviorName": "Animation", "behaviorType": "AnimatableCapability::AnimatableBehavior", @@ -441,6 +445,10 @@ describe('SimplifiedProject', () => { }, Object { "behaviors": Array [ + Object { + "behaviorName": "Anchor", + "behaviorType": "AnchorBehavior::AnchorBehavior", + }, Object { "behaviorName": "Animation", "behaviorType": "AnimatableCapability::AnimatableBehavior", diff --git a/newIDE/app/src/ObjectEditor/CompactObjectPropertiesEditor/CompactAnchorBehaviorEditor.js b/newIDE/app/src/ObjectEditor/CompactObjectPropertiesEditor/CompactAnchorBehaviorEditor.js index e0fe82509fc4..d69815b61891 100644 --- a/newIDE/app/src/ObjectEditor/CompactObjectPropertiesEditor/CompactAnchorBehaviorEditor.js +++ b/newIDE/app/src/ObjectEditor/CompactObjectPropertiesEditor/CompactAnchorBehaviorEditor.js @@ -8,9 +8,8 @@ import { } from './CompactBehaviorPropertiesEditor'; import useForceUpdate from '../../Utils/UseForceUpdate'; import { ColumnStackLayout } from '../../UI/Layout'; +import AnchorGrid from '../../BehaviorsEditor/Editors/AnchorGrid'; import { - HorizontalAnchorButtonGroup, - VerticalAnchorButtonGroup, getBasicHorizontalAnchor, getBasicVerticalAnchor, } from '../../BehaviorsEditor/Editors/AnchorBehaviorEditor'; @@ -42,20 +41,16 @@ const CompactAnchorBehaviorEditor = ({ [forceUpdate, onBehaviorUpdated] ); - const horizontalBasicAnchor = getBasicHorizontalAnchor(_getPropertyValue); - const verticalBasicAnchor = getBasicVerticalAnchor(_getPropertyValue); + const isAdvanced = + getBasicHorizontalAnchor(_getPropertyValue) === 'Advanced' || + getBasicVerticalAnchor(_getPropertyValue) === 'Advanced'; return ( - - diff --git a/newIDE/app/src/UI/CompactToggleButtons/CompactToggleButtons.module.css b/newIDE/app/src/UI/CompactToggleButtons/CompactToggleButtons.module.css index 95bc6e181a53..406c2ba68d8e 100644 --- a/newIDE/app/src/UI/CompactToggleButtons/CompactToggleButtons.module.css +++ b/newIDE/app/src/UI/CompactToggleButtons/CompactToggleButtons.module.css @@ -20,7 +20,7 @@ justify-content: center; flex: 1; min-width: 0px; - margin: 3px; + padding: 3px; border: 0; } @@ -41,7 +41,7 @@ .separator { background-color: var(--theme-text-field-disabled-color); - height: 15px; + align-self: stretch; width: 1px; margin: 0 1px; } diff --git a/newIDE/app/src/UI/CustomSvgIcons/LetterV.js b/newIDE/app/src/UI/CustomSvgIcons/LetterV.js new file mode 100644 index 000000000000..6dfd216778bf --- /dev/null +++ b/newIDE/app/src/UI/CustomSvgIcons/LetterV.js @@ -0,0 +1,11 @@ +import React from 'react'; +import SvgIcon from '@material-ui/core/SvgIcon'; + +export default React.memo(props => ( + + + +)); diff --git a/newIDE/app/src/fixtures/TestProject.js b/newIDE/app/src/fixtures/TestProject.js index 4399f0fa8764..b6347c0817cb 100644 --- a/newIDE/app/src/fixtures/TestProject.js +++ b/newIDE/app/src/fixtures/TestProject.js @@ -351,6 +351,11 @@ export const makeTestProject = (gd /*: libGDevelop */) /*: TestProject */ => { 'DraggableBehavior::Draggable', 'Draggable' ); + spriteObjectWithBehaviors.addNewBehavior( + project, + 'AnchorBehavior::AnchorBehavior', + 'Anchor' + ); const group1 = new gd.ObjectGroup(); group1.setName('GroupOfSprites'); diff --git a/newIDE/app/src/stories/componentStories/ObjectEditor/AnchorBehaviorEditor.stories.js b/newIDE/app/src/stories/componentStories/ObjectEditor/AnchorBehaviorEditor.stories.js new file mode 100644 index 000000000000..cbed22a15adb --- /dev/null +++ b/newIDE/app/src/stories/componentStories/ObjectEditor/AnchorBehaviorEditor.stories.js @@ -0,0 +1,38 @@ +// @flow + +import * as React from 'react'; +import { action } from '@storybook/addon-actions'; + +// Keep first as it creates the `global.gd` object: +import { testProject } from '../../GDevelopJsInitializerDecorator'; + +import paperDecorator from '../../PaperDecorator'; +import AnchorBehaviorEditor from '../../../BehaviorsEditor/Editors/AnchorBehaviorEditor'; +import SerializedObjectDisplay from '../../SerializedObjectDisplay'; +import fakeResourceManagementProps from '../../FakeResourceManagement'; + +export default { + title: 'ObjectEditor/AnchorBehaviorEditor', + component: AnchorBehaviorEditor, + decorators: [paperDecorator], +}; + +export const Default = (): React.Node => { + const spriteObjectWithBehaviors = testProject.spriteObjectWithBehaviors; + const anchorBehavior = spriteObjectWithBehaviors.getBehavior('Anchor'); + + return ( + + + + ); +}; diff --git a/newIDE/app/src/stories/componentStories/ObjectEditor/CompactAnchorBehaviorEditor.stories.js b/newIDE/app/src/stories/componentStories/ObjectEditor/CompactAnchorBehaviorEditor.stories.js new file mode 100644 index 000000000000..6c7f42bd327d --- /dev/null +++ b/newIDE/app/src/stories/componentStories/ObjectEditor/CompactAnchorBehaviorEditor.stories.js @@ -0,0 +1,45 @@ +// @flow + +import * as React from 'react'; +import { action } from '@storybook/addon-actions'; + +// Keep first as it creates the `global.gd` object: +import { testProject } from '../../GDevelopJsInitializerDecorator'; + +import paperDecorator from '../../PaperDecorator'; +import CompactAnchorBehaviorEditor from '../../../ObjectEditor/CompactObjectPropertiesEditor/CompactAnchorBehaviorEditor'; +import SerializedObjectDisplay from '../../SerializedObjectDisplay'; +import fakeResourceManagementProps from '../../FakeResourceManagement'; + +const gd: libGDevelop = global.gd; + +export default { + title: 'ObjectEditor/CompactAnchorBehaviorEditor', + component: CompactAnchorBehaviorEditor, + decorators: [paperDecorator], +}; + +export const Default = (): React.Node => { + const spriteObjectWithBehaviors = testProject.spriteObjectWithBehaviors; + const anchorBehavior = spriteObjectWithBehaviors.getBehavior('Anchor'); + const behaviorMetadata = gd.MetadataProvider.getBehaviorMetadata( + gd.JsPlatform.get(), + 'AnchorBehavior::AnchorBehavior' + ); + + return ( + + + + ); +};