Skip to content

Commit e1abe4e

Browse files
committed
Grid-based selection component
1 parent 056faed commit e1abe4e

File tree

5 files changed

+517
-39
lines changed

5 files changed

+517
-39
lines changed

newIDE/app/src/BehaviorsEditor/Editors/AnchorBehaviorEditor.js

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import VerticalFillIcon from '../../UI/CustomSvgIcons/VerticalSize';
1717
import VerticalProportionalFillIcon from '../../UI/CustomSvgIcons/VerticalSizePercent';
1818
import useForceUpdate from '../../Utils/UseForceUpdate';
1919
import { ColumnStackLayout } from '../../UI/Layout';
20-
import { Line } from '../../UI/Grid';
2120
import Text from '../../UI/Text';
2221
import {
2322
getPropertyValue,
@@ -26,6 +25,8 @@ import {
2625
import CompactToggleButtons, {
2726
type CompactToggleButton,
2827
} from '../../UI/CompactToggleButtons';
28+
import AnchorGrid from './AnchorGrid';
29+
import { propertiesToGridSelection } from './AnchorGridMapping';
2930

3031
type BasicAnchor =
3132
| 'None'
@@ -324,29 +325,22 @@ const AnchorBehaviorEditor = ({
324325
[forceUpdate, onBehaviorUpdated]
325326
);
326327

327-
const horizontalBasicAnchor = getBasicHorizontalAnchor(_getPropertyValue);
328-
const verticalBasicAnchor = getBasicVerticalAnchor(_getPropertyValue);
328+
const gridSelection = propertiesToGridSelection(
329+
_getPropertyValue('leftEdgeAnchor'),
330+
_getPropertyValue('rightEdgeAnchor'),
331+
_getPropertyValue('topEdgeAnchor'),
332+
_getPropertyValue('bottomEdgeAnchor')
333+
);
329334

330335
return (
331336
<ColumnStackLayout expand>
332337
<Text size="sub-title">
333-
<Trans>Horizontal anchor</Trans>
334-
</Text>
335-
<Line noMargin>
336-
<HorizontalAnchorButtonGroup
337-
basicAnchor={horizontalBasicAnchor}
338-
onUpdateProperty={_updateProperty}
339-
/>
340-
</Line>
341-
<Text size="sub-title">
342-
<Trans>Vertical anchor</Trans>
338+
<Trans>Alignment</Trans>
343339
</Text>
344-
<Line noMargin>
345-
<VerticalAnchorButtonGroup
346-
basicAnchor={verticalBasicAnchor}
347-
onUpdateProperty={_updateProperty}
348-
/>
349-
</Line>
340+
<AnchorGrid
341+
getPropertyValue={_getPropertyValue}
342+
onUpdateProperty={_updateProperty}
343+
/>
350344
<BehaviorPropertiesEditor
351345
project={project}
352346
object={object}
@@ -355,8 +349,7 @@ const AnchorBehaviorEditor = ({
355349
resourceManagementProps={resourceManagementProps}
356350
projectScopedContainersAccessor={projectScopedContainersAccessor}
357351
isAdvancedSectionInitiallyUncollapsed={
358-
horizontalBasicAnchor === 'Advanced' ||
359-
verticalBasicAnchor === 'Advanced' ||
352+
gridSelection.isAdvanced ||
360353
_getPropertyValue('relativeToOriginalWindowSize') === 'false'
361354
}
362355
/>
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
// @flow
2+
import { Trans } from '@lingui/macro';
3+
import * as React from 'react';
4+
import Tooltip from '@material-ui/core/Tooltip';
5+
import LinkIcon from '../../UI/CustomSvgIcons/Link';
6+
import UnlinkIcon from '../../UI/CustomSvgIcons/Unlink';
7+
import {
8+
propertiesToGridSelection,
9+
getEndpointCells,
10+
handleCellClick,
11+
gridSelectionToProperties,
12+
} from './AnchorGridMapping';
13+
import styles from './AnchorGrid.module.css';
14+
15+
type Props = {|
16+
getPropertyValue: (propertyName: string) => string,
17+
onUpdateProperty: (propertyName: string, value: string) => void,
18+
|};
19+
20+
const gridPositions: Array<{| col: 0 | 1 | 2, row: 0 | 1 | 2 |}> = [
21+
{ col: 0, row: 0 },
22+
{ col: 1, row: 0 },
23+
{ col: 2, row: 0 },
24+
{ col: 0, row: 1 },
25+
{ col: 1, row: 1 },
26+
{ col: 2, row: 1 },
27+
{ col: 0, row: 2 },
28+
{ col: 1, row: 2 },
29+
{ col: 2, row: 2 },
30+
];
31+
32+
const AnchorGrid = ({
33+
getPropertyValue,
34+
onUpdateProperty,
35+
}: Props): React.Node => {
36+
// Read the current anchor properties and convert to grid state
37+
const leftEdgeAnchor = getPropertyValue('leftEdgeAnchor');
38+
const rightEdgeAnchor = getPropertyValue('rightEdgeAnchor');
39+
const topEdgeAnchor = getPropertyValue('topEdgeAnchor');
40+
const bottomEdgeAnchor = getPropertyValue('bottomEdgeAnchor');
41+
42+
const gridSelection = propertiesToGridSelection(
43+
leftEdgeAnchor,
44+
rightEdgeAnchor,
45+
topEdgeAnchor,
46+
bottomEdgeAnchor
47+
);
48+
49+
const endpointCells = getEndpointCells(gridSelection.rect);
50+
51+
// Store the last non-proportional rect so we can restore it when
52+
// the user toggles proportional mode off.
53+
const lastNonProportionalRect = React.useRef(gridSelection.rect);
54+
if (!gridSelection.proportional && gridSelection.rect) {
55+
lastNonProportionalRect.current = gridSelection.rect;
56+
}
57+
58+
const applyGridRect = React.useCallback(
59+
(rect, proportional) => {
60+
const properties = gridSelectionToProperties(rect, proportional);
61+
onUpdateProperty('leftEdgeAnchor', properties.leftEdgeAnchor);
62+
onUpdateProperty('rightEdgeAnchor', properties.rightEdgeAnchor);
63+
onUpdateProperty('topEdgeAnchor', properties.topEdgeAnchor);
64+
onUpdateProperty('bottomEdgeAnchor', properties.bottomEdgeAnchor);
65+
},
66+
[onUpdateProperty]
67+
);
68+
69+
const onCellClick = React.useCallback(
70+
(col: 0 | 1 | 2, row: 0 | 1 | 2) => {
71+
// If proportional is on, turn it off and select this cell
72+
if (gridSelection.proportional) {
73+
const newRect = { minCol: col, maxCol: col, minRow: row, maxRow: row };
74+
applyGridRect(newRect, false);
75+
return;
76+
}
77+
78+
const newRect = handleCellClick(gridSelection.rect, col, row);
79+
applyGridRect(newRect, false);
80+
},
81+
[gridSelection, applyGridRect]
82+
);
83+
84+
const onToggleProportional = React.useCallback(
85+
() => {
86+
if (gridSelection.proportional) {
87+
// Turn off proportional → restore last non-proportional rect
88+
applyGridRect(lastNonProportionalRect.current, false);
89+
} else {
90+
// Turn on proportional
91+
applyGridRect(gridSelection.rect, true);
92+
}
93+
},
94+
[gridSelection, applyGridRect]
95+
);
96+
97+
const rect = gridSelection.rect;
98+
const hasRange =
99+
rect && (rect.minCol !== rect.maxCol || rect.minRow !== rect.maxRow);
100+
101+
return (
102+
<div className={styles.wrapper}>
103+
<div className={styles.grid}>
104+
{rect && hasRange && (
105+
<div
106+
className={styles.selectionOverlay}
107+
style={{
108+
gridColumn: `${rect.minCol + 1} / ${rect.maxCol + 2}`,
109+
gridRow: `${rect.minRow + 1} / ${rect.maxRow + 2}`,
110+
}}
111+
/>
112+
)}
113+
{gridPositions.map(({ col, row }) => {
114+
const key = `${col},${row}`;
115+
const isEndpoint = endpointCells.has(key);
116+
const isInRange =
117+
hasRange &&
118+
rect &&
119+
col >= rect.minCol &&
120+
col <= rect.maxCol &&
121+
row >= rect.minRow &&
122+
row <= rect.maxRow;
123+
return (
124+
<button
125+
key={key}
126+
className={`${styles.cell}${
127+
isEndpoint && !hasRange ? ` ${styles.endpoint}` : ''
128+
}${isInRange ? ` ${styles.inRange}` : ''}`}
129+
style={{
130+
gridColumn: col + 1,
131+
gridRow: row + 1,
132+
}}
133+
onClick={() => onCellClick(col, row)}
134+
>
135+
{isEndpoint && <div className={styles.innerSquare} />}
136+
</button>
137+
);
138+
})}
139+
</div>
140+
<div className={styles.linkButton}>
141+
<Tooltip
142+
title={
143+
gridSelection.proportional ? (
144+
<Trans>Disable proportional resize</Trans>
145+
) : (
146+
<Trans>Enable proportional resize</Trans>
147+
)
148+
}
149+
>
150+
<button
151+
className={`${styles.linkToggle}${
152+
gridSelection.proportional ? ` ${styles.active}` : ''
153+
}`}
154+
onClick={onToggleProportional}
155+
>
156+
{gridSelection.proportional ? (
157+
<LinkIcon className={styles.linkIcon} />
158+
) : (
159+
<UnlinkIcon className={styles.linkIcon} />
160+
)}
161+
</button>
162+
</Tooltip>
163+
</div>
164+
</div>
165+
);
166+
};
167+
168+
export default AnchorGrid;
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
.wrapper {
2+
display: flex;
3+
align-items: center;
4+
gap: 8px;
5+
}
6+
7+
.grid {
8+
display: grid;
9+
grid-template-columns: repeat(3, 1fr);
10+
grid-template-rows: repeat(3, 1fr);
11+
gap: 4px;
12+
background-color: transparent;
13+
border-radius: 4px;
14+
padding: 0;
15+
flex: 1;
16+
}
17+
18+
.cell {
19+
position: relative;
20+
width: 100%;
21+
aspect-ratio: 1;
22+
min-width: 24px;
23+
min-height: 24px;
24+
border: 0;
25+
border-radius: 8px;
26+
background-color: var(--theme-text-field-default-background-color);
27+
cursor: pointer;
28+
display: flex;
29+
align-items: center;
30+
justify-content: center;
31+
transition: none;
32+
z-index: 1;
33+
}
34+
35+
.cell:hover {
36+
background-color: var(--theme-text-field-disabled-color);
37+
}
38+
39+
/* Single-cell endpoint (no range): purple background on the cell itself */
40+
.cell.endpoint {
41+
background-color: rgba(149, 128, 205, 0.4);
42+
}
43+
44+
/* Cells inside a range selection: transparent so the overlay behind shows */
45+
.cell.inRange {
46+
background-color: transparent;
47+
}
48+
49+
.cell.inRange:hover {
50+
background-color: rgba(149, 128, 205, 0.15);
51+
}
52+
53+
/* Continuous overlay that spans across cells and gaps for range selections.
54+
Uses grid-column/grid-row to cover the selected area as one solid block. */
55+
.selectionOverlay {
56+
background-color: rgba(149, 128, 205, 0.4);
57+
border-radius: 3px;
58+
z-index: 0;
59+
}
60+
61+
/* The inner square shown on endpoint cells — uses absolute positioning
62+
with equal inset on all sides so it stays a perfect square. */
63+
.innerSquare {
64+
position: absolute;
65+
inset: 16px;
66+
border-radius: 3px;
67+
background-color: var(--theme-icon-button-selected-background-color);
68+
transition: none;
69+
}
70+
71+
.linkButton {
72+
display: flex;
73+
flex-direction: column;
74+
align-items: center;
75+
gap: 4px;
76+
}
77+
78+
.linkToggle {
79+
width: 32px;
80+
height: 32px;
81+
border: 0;
82+
border-radius: 4px;
83+
background-color: var(--theme-text-field-default-background-color);
84+
color: var(--theme-text-default-color);
85+
cursor: pointer;
86+
display: flex;
87+
align-items: center;
88+
justify-content: center;
89+
transition: none;
90+
}
91+
92+
.linkToggle:hover {
93+
background-color: var(--theme-text-field-disabled-color);
94+
}
95+
96+
.linkToggle.active {
97+
background-color: var(--theme-icon-button-selected-background-color);
98+
color: var(--theme-icon-button-selected-color);
99+
}
100+
101+
svg.linkIcon {
102+
font-size: 20px;
103+
}

0 commit comments

Comments
 (0)