Skip to content

Commit e5c7ee0

Browse files
committed
#40 desktop switching by border added
1 parent 9f4ac79 commit e5c7ee0

1 file changed

Lines changed: 173 additions & 61 deletions

File tree

src/webOS/coreservices/MissionControl/components/MissionControlUI/MissionControlUI.jsx

Lines changed: 173 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,22 @@ import { useStateManager } from '../../../../stores/StateManager/StateManager.js
1515
import './MissionControlUI.css';
1616
import MissionManager from '../MissionManager/MissionManager.jsx';
1717

18-
const FADE_DURATION = 300; // match CSS fade timing (ms)
19-
const SLIDE_DURATION = 300; // match wrapper transition (ms)
20-
const OPEN_DELAY = 200; // new delay before fade (ms)
18+
const FADE_DURATION = 300; // CSS fade timing (ms)
19+
const SLIDE_DURATION = 300; // overview slide timing (ms)
20+
const OPEN_DELAY = 200; // delay before fade (ms)
21+
22+
// Hint configuration
23+
const EDGE_THRESHOLD = 20; // px from either edge to trigger hint
24+
const HOVER_DELAY = 300; // ms to wait before showing hint (1 s)
25+
const HINT_OFFSET = 10; // px to slide as a hint
26+
const HINT_SLIDE_DURATION = 200; // ms for hint animation
2127

2228
const MissionControlUI = () => {
2329
const {
24-
createDesktop, // original: adds & switches
25-
addDesktop, // new: adds only
30+
createDesktop, // original: adds & switches
31+
addDesktop, // new: adds only
2632
switchDesktop,
27-
deleteDesktop, // newly passed through
33+
deleteDesktop,
2834
reorderDesktops,
2935
activeIndex,
3036
desktops
@@ -34,69 +40,78 @@ const MissionControlUI = () => {
3440
const overlayVisible =
3541
state.groups.missionControl?.overlayVisible === 'true';
3642

37-
// New flag: render wallpaper at opacity 0 immediately when opening
43+
// Basic UI state
3844
const [showWallpaperPlaceholder, setShowWallpaperPlaceholder] = useState(false);
39-
// track when each portal can mount (initially all false)
4045
const [portalReady, setPortalReady] = useState(() =>
4146
desktops.map(() => false)
4247
);
4348

49+
// Hint state
50+
const [showRightHint, setShowRightHint] = useState(false);
51+
const [showLeftHint, setShowLeftHint] = useState(false);
52+
const hintTimerRef = useRef(null);
53+
const hoverSideRef = useRef(null); // 'left' | 'right' | null
54+
const isMouseDownRef = useRef(false); // track if a button is held
55+
56+
// Window size for edge detection
57+
const [viewport, setViewport] = useState({
58+
width: window.innerWidth,
59+
height: window.innerHeight
60+
});
61+
62+
// Mission‐Control state
63+
const [overviewOpen, setOverviewOpen] = useState(false);
64+
const [isFading, setIsFading] = useState(false);
65+
const [disableSlideTransition, setDisableSlideTransition] = useState(false);
66+
const [barExpanded, setBarExpanded] = useState(false);
67+
const [prevIndex, setPrevIndex] = useState(0);
68+
69+
// Refs for tracking portal readiness
4470
const prevDesktopsRef = useRef(desktops);
4571
const prevPortalReadyRef = useRef(portalReady);
72+
const scaleRefs = useRef([]);
4673

74+
// Sync portalReady when desktops array changes
4775
useEffect(() => {
48-
const prevDesktops = prevDesktopsRef.current;
76+
const prev = prevDesktopsRef.current;
4977
const prevReady = prevPortalReadyRef.current;
50-
const prevLen = prevDesktops.length;
51-
const currLen = desktops.length;
52-
let newReady;
53-
if (currLen > prevLen) {
54-
const diffCount = currLen - prevLen;
55-
newReady = [...prevReady, ...Array(diffCount).fill(false)];
78+
const delta = desktops.length - prev.length;
79+
let nextReady;
80+
if (delta > 0) {
81+
nextReady = [...prevReady, ...Array(delta).fill(false)];
5682
} else {
57-
const idToReady = {};
58-
prevDesktops.forEach((d, idx) => {
59-
idToReady[d.id] = prevReady[idx];
60-
});
61-
newReady = desktops.map(d => idToReady[d.id] || false);
83+
const byId = Object.fromEntries(prev.map((d,i) => [d.id, prevReady[i]]));
84+
nextReady = desktops.map(d => !!byId[d.id]);
6285
}
63-
setPortalReady(newReady);
86+
setPortalReady(nextReady);
6487
prevDesktopsRef.current = desktops;
65-
prevPortalReadyRef.current = newReady;
88+
prevPortalReadyRef.current = nextReady;
6689
}, [desktops]);
6790

68-
const scaleRefs = useRef([]);
91+
// Mark portals ready as soon as their DOM node mounts
6992
useLayoutEffect(() => {
7093
let changed = false;
71-
const latestReady = prevPortalReadyRef.current.slice();
94+
const arr = prevPortalReadyRef.current.slice();
7295
desktops.forEach((_, i) => {
73-
if (!latestReady[i] && scaleRefs.current[i]) {
74-
latestReady[i] = true;
96+
if (!arr[i] && scaleRefs.current[i]) {
97+
arr[i] = true;
7598
changed = true;
7699
}
77100
});
78101
if (changed) {
79-
setPortalReady(latestReady);
80-
prevPortalReadyRef.current = latestReady;
102+
setPortalReady(arr);
103+
prevPortalReadyRef.current = arr;
81104
}
82105
});
83106

84-
const [overviewOpen, setOverviewOpen] = useState(false);
85-
const [isFading, setIsFading] = useState(false);
86-
const [barExpanded, setBarExpanded] = useState(false);
87-
const [prevIndex, setPrevIndex] = useState(0);
88-
const [viewport, setViewport] = useState({
89-
width: window.innerWidth,
90-
height: window.innerHeight
91-
});
92-
const [disableSlideTransition, setDisableSlideTransition] = useState(false);
93-
107+
// Initialize 'opened' flag
94108
useEffect(() => {
95109
if (!state.groups.missionControl.hasOwnProperty('opened')) {
96110
addState('missionControl', 'opened', 'false');
97111
}
98112
}, [addState, state.groups.missionControl]);
99113

114+
// Clear lingering 'opened' on first mount
100115
const initialMount = useRef(true);
101116
useEffect(() => {
102117
if (!initialMount.current) return;
@@ -109,25 +124,113 @@ const MissionControlUI = () => {
109124
// eslint-disable-next-line react-hooks/exhaustive-deps
110125
}, []);
111126

127+
// Track viewport size
112128
useEffect(() => {
113129
const onResize = () =>
114130
setViewport({ width: window.innerWidth, height: window.innerHeight });
115131
window.addEventListener('resize', onResize);
116132
return () => window.removeEventListener('resize', onResize);
117133
}, []);
118134

135+
// —— Edge‐hover hint (both sides), ignores when mouse is down ——
136+
useEffect(() => {
137+
if (overviewOpen) return;
138+
139+
const canRight = desktops.length > activeIndex + 1;
140+
const canLeft = activeIndex > 0;
141+
142+
const onMouseDown = () => { isMouseDownRef.current = true; };
143+
const onMouseUp = () => { isMouseDownRef.current = false; };
144+
145+
const onMouseMove = e => {
146+
if (isMouseDownRef.current) return; // ignore while dragging/clicking
147+
148+
const x = e.clientX;
149+
const atRight = canRight && x >= viewport.width - EDGE_THRESHOLD;
150+
const atLeft = canLeft && x <= EDGE_THRESHOLD;
151+
152+
if (atRight && hoverSideRef.current !== 'right') {
153+
clearTimeout(hintTimerRef.current);
154+
setShowLeftHint(false);
155+
setShowRightHint(false);
156+
hoverSideRef.current = 'right';
157+
hintTimerRef.current = setTimeout(() => {
158+
setShowRightHint(true);
159+
}, HOVER_DELAY);
160+
} else if (atLeft && hoverSideRef.current !== 'left') {
161+
clearTimeout(hintTimerRef.current);
162+
setShowRightHint(false);
163+
setShowLeftHint(false);
164+
hoverSideRef.current = 'left';
165+
hintTimerRef.current = setTimeout(() => {
166+
setShowLeftHint(true);
167+
}, HOVER_DELAY);
168+
} else if (!atRight && !atLeft && hoverSideRef.current) {
169+
clearTimeout(hintTimerRef.current);
170+
hintTimerRef.current = null;
171+
hoverSideRef.current = null;
172+
setShowRightHint(false);
173+
setShowLeftHint(false);
174+
}
175+
};
176+
177+
window.addEventListener('mousedown', onMouseDown);
178+
window.addEventListener('mouseup', onMouseUp);
179+
window.addEventListener('mousemove', onMouseMove);
180+
return () => {
181+
window.removeEventListener('mousedown', onMouseDown);
182+
window.removeEventListener('mouseup', onMouseUp);
183+
window.removeEventListener('mousemove', onMouseMove);
184+
clearTimeout(hintTimerRef.current);
185+
hintTimerRef.current = null;
186+
hoverSideRef.current = null;
187+
isMouseDownRef.current = false;
188+
};
189+
}, [
190+
overviewOpen,
191+
viewport.width,
192+
activeIndex,
193+
desktops.length
194+
]);
195+
196+
// —— Click to complete switch when hint is visible ——
119197
useEffect(() => {
120-
document.body.style.overflow = (overviewOpen || isFading) ? 'hidden' : '';
198+
if (!showRightHint && !showLeftHint) return;
199+
200+
const onClick = e => {
201+
const x = e.clientX;
202+
if (showRightHint && x >= viewport.width - EDGE_THRESHOLD) {
203+
switchDesktop(activeIndex + 1);
204+
setShowRightHint(false);
205+
} else if (showLeftHint && x <= EDGE_THRESHOLD) {
206+
switchDesktop(activeIndex - 1);
207+
setShowLeftHint(false);
208+
}
209+
};
210+
211+
window.addEventListener('click', onClick);
212+
return () => window.removeEventListener('click', onClick);
213+
}, [
214+
showRightHint,
215+
showLeftHint,
216+
viewport.width,
217+
activeIndex,
218+
switchDesktop
219+
]);
220+
221+
// Prevent body scroll during overview/fade
222+
useEffect(() => {
223+
document.body.style.overflow = overviewOpen || isFading ? 'hidden' : '';
121224
}, [overviewOpen, isFading]);
122225

226+
// Fade → open overview
123227
useEffect(() => {
124-
if (isFading) {
125-
const timer = setTimeout(() => {
126-
setOverviewOpen(true);
127-
setIsFading(false);
128-
}, FADE_DURATION);
129-
return () => clearTimeout(timer);
130-
}
228+
if (!isFading) return;
229+
const t = setTimeout(() => {
230+
setOverviewOpen(true);
231+
setIsFading(false);
232+
}, FADE_DURATION);
233+
return () => clearTimeout(t);
131234
}, [isFading]);
132235

133236
const openOverview = useCallback(() => {
@@ -144,17 +247,16 @@ const MissionControlUI = () => {
144247
}, [activeIndex, editStateValue]);
145248

146249
const instantSwitchDesktop = useCallback(
147-
index => {
250+
i => {
148251
setDisableSlideTransition(true);
149-
switchDesktop(index);
252+
switchDesktop(i);
150253
},
151254
[switchDesktop]
152255
);
153256
useEffect(() => {
154-
if (disableSlideTransition) {
155-
const t = setTimeout(() => setDisableSlideTransition(false), 0);
156-
return () => clearTimeout(t);
157-
}
257+
if (!disableSlideTransition) return;
258+
const t = setTimeout(() => setDisableSlideTransition(false), 0);
259+
return () => clearTimeout(t);
158260
}, [disableSlideTransition]);
159261

160262
const exitOverview = useCallback(
@@ -194,20 +296,30 @@ const MissionControlUI = () => {
194296
[reorderDesktops]
195297
);
196298

197-
const THUMB_H = 90;
198-
const scale = THUMB_H / viewport.height;
199-
const THUMB_W = viewport.width * scale;
299+
// Compute transform & transition
300+
const baseX = `calc(-${activeIndex} * (100vw + 60px))`;
301+
const rightHintX = `calc(${baseX} - ${HINT_OFFSET}px)`;
302+
const leftHintX = `calc(${baseX} + ${HINT_OFFSET}px)`;
303+
const wrapperTransform = showRightHint
304+
? `translateX(${rightHintX})`
305+
: showLeftHint
306+
? `translateX(${leftHintX})`
307+
: `translateX(${baseX})`;
200308
const wrapperStyle = overviewOpen
201309
? {
202310
top: 30,
203-
height: THUMB_H,
204-
marginleft: 0,
311+
height: 90,
312+
marginLeft: 0,
205313
transform: 'none',
206314
transition: `top ${SLIDE_DURATION}ms ease, height ${SLIDE_DURATION}ms ease, transform ${SLIDE_DURATION}ms ease`
207315
}
208316
: {
209-
transform: `translateX(calc(-${activeIndex} * (100vw + 60px)))`,
210-
transition: disableSlideTransition ? 'none' : undefined
317+
transform: wrapperTransform,
318+
...(disableSlideTransition
319+
? { transition: 'none' }
320+
: (showRightHint || showLeftHint)
321+
? { transition: `transform ${HINT_SLIDE_DURATION}ms ease` }
322+
: {})
211323
};
212324

213325
return (
@@ -262,8 +374,8 @@ const MissionControlUI = () => {
262374
onDragOver={onDragOver}
263375
onDrop={onDrop}
264376
viewport={viewport}
265-
createDesktop={addDesktop} // +New from bar
266-
deleteDesktop={deleteDesktop} // allow deletion per-panel
377+
createDesktop={addDesktop}
378+
deleteDesktop={deleteDesktop}
267379
/>
268380
</div>
269381
);

0 commit comments

Comments
 (0)