Skip to content

Commit f7c5ef8

Browse files
committed
window zindexes
1 parent 8f5a243 commit f7c5ef8

5 files changed

Lines changed: 142 additions & 46 deletions

File tree

src/components/BorderedApp/BorderedApp.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import React, { useRef } from "react";
2-
import useWindowManagerStore from "../../stores/windowManagerStore";
2+
import useWindowManagerStore, {
3+
BaseProps,
4+
} from "../../stores/windowManagerStore";
35
import { Dimensions } from "../../hooks/useDragToResize";
46
import BorderedAppMenu, {
57
BorderedAppMenuItemProps,
68
} from "./BorderedAppMenu/BorderedAppMenu";
79
import usePositionableElement from "../../hooks/usePositionableElement";
810

911
import "./BorderedApp.scss";
10-
interface BorderedAppProps {
12+
interface BorderedAppProps extends BaseProps {
1113
title: string;
1214
type: string;
1315
id: string;
@@ -25,6 +27,7 @@ function BorderedApp({
2527
initialDimensions,
2628
minDimensions = { height: 350, width: 350 },
2729
menus,
30+
zIndex,
2831
}: React.PropsWithChildren<BorderedAppProps>) {
2932
const winMan = useWindowManagerStore();
3033

@@ -56,9 +59,11 @@ function BorderedApp({
5659
<div
5760
className="bordered-app"
5861
ref={appRef}
62+
onMouseDown={() => winMan.focusWindow(type, id)}
5963
style={{
6064
width: initialDimensions.width,
6165
height: initialDimensions.height,
66+
zIndex,
6267
}}
6368
>
6469
<div className="bordered-app__corner-nw" ref={resizeHandleNW} />

src/components/BottomBar/Launcher/Launcher.tsx

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,24 +29,37 @@ function Launcher({
2929
const winMan = useWindowManagerStore();
3030
const ref = useRef<HTMLDivElement>(null);
3131

32-
function onLeftClick() {
32+
function addWindow() {
3333
const id = windowId ?? uuid();
34-
winMan.addWindow(
35-
windowType,
36-
id,
37-
<BorderedApp
38-
id={id}
39-
title={WindowTitle}
40-
type={windowType}
41-
initialDimensions={initialDimensions}
42-
menus={menus}
43-
key={id}
44-
>
45-
{appContent}
46-
</BorderedApp>
47-
);
34+
winMan.addWindow(windowType, id, {
35+
component: BorderedApp,
36+
props: {
37+
id,
38+
title: WindowTitle,
39+
type: windowType,
40+
initialDimensions,
41+
menus,
42+
},
43+
key: id,
44+
children: appContent,
45+
});
46+
}
47+
function onLeftClick() {
48+
// If there are one or more windows of this type open,
49+
// we want to focus them. This means revealing them if they
50+
// are minimized and bring them to the top of the window stack.
51+
if (winMan.windowsOfTypeExist(windowType)) {
52+
winMan.focusWindowsOfType(windowType);
53+
return;
54+
}
55+
56+
// If there are no windows of this type, we want to add one.
57+
addWindow();
4858
}
4959
function onRightClick() {
60+
// TODO: Display a context menu with various options that are
61+
// specified by the program-specific launcher and passed via props.
62+
// This will include things like "open new window", "close windows", etc.
5063
console.log("right clicked");
5164
}
5265

src/components/Content/Content.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,15 @@ interface ContentProps {}
66
// eslint-disable-next-line no-empty-pattern
77
function Content({}: ContentProps) {
88
const winMan = useWindowManagerStore();
9+
910
return (
1011
<div id="content__container">
1112
<div id="content" ref={winMan.contentRef}>
12-
{winMan.getWindows()}
13+
{winMan.getWindowDefinitions().map((definition) => (
14+
<definition.component {...definition.props} key={definition.key}>
15+
{definition.children}
16+
</definition.component>
17+
))}
1318
</div>
1419
</div>
1520
);

src/components/TopBar/ClockMenu/ClockMenu.tsx

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,16 @@ function ClockMenu({}: ClockMenuProps) {
1919

2020
function handleClick() {
2121
const id = uuid();
22-
winMan.addWindow(
23-
windowType,
24-
id,
25-
<BorderedApp
26-
title="Calendar"
27-
initialDimensions={{ height: 500, width: 500 }}
28-
type={windowType}
29-
id={id}
30-
children={<div></div>}
31-
key={id}
32-
/>
33-
);
22+
winMan.addWindow(windowType, id, {
23+
component: BorderedApp,
24+
props: {
25+
title: "Calendar",
26+
initialDimensions: { height: 500, width: 500 },
27+
type: windowType,
28+
id: id,
29+
},
30+
key: id,
31+
});
3432
}
3533
return (
3634
<div id="clock-menu" onClick={() => handleClick()}>

src/stores/windowManagerStore.ts

Lines changed: 91 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,122 @@
11
import React, { ReactNode, createRef } from "react";
22
import { create } from "zustand";
33

4+
export interface BaseProps {
5+
zIndex?: number;
6+
children?: ReactNode;
7+
}
8+
9+
interface ComponentDefinition<Props extends BaseProps = BaseProps> {
10+
component: React.FC<Props>;
11+
props: Props;
12+
children?: ReactNode;
13+
key: string | number;
14+
}
15+
416
interface WindowManagerStoreState {
517
contentRef: React.RefObject<HTMLDivElement>;
6-
/**
7-
* A map of all windows.
8-
* The key is the type of window, and the value is another map.
9-
* The key of the nested map is the ID of the window, and the value
10-
* is the window JSX element.
11-
*/
12-
windowsMap: Map<string, Map<string, ReactNode>>;
13-
getWindows: () => Array<ReactNode>;
14-
addWindow: (windowType: string, windowId: string, window: ReactNode) => void;
18+
windowsMap: Map<string, Map<string, ComponentDefinition>>;
19+
highestZIndex: number;
20+
getWindowDefinitions: () => Array<ComponentDefinition>;
21+
addWindow: <Props extends BaseProps = BaseProps>(
22+
windowType: string,
23+
windowId: string,
24+
definition: ComponentDefinition<Props>
25+
) => void;
1526
closeWindow: (windowType: string, windowId: string) => void;
27+
focusWindowsOfType: (windowType: string) => void;
28+
focusWindow: (windowType: string, windowId: string) => void;
29+
windowsOfTypeExist: (windowType: string) => boolean;
1630
}
31+
1732
const useWindowManagerStore = create<WindowManagerStoreState>()((set, get) => ({
1833
contentRef: createRef<HTMLDivElement>(),
1934
windowsMap: new Map(),
20-
getWindows() {
35+
highestZIndex: 0,
36+
getWindowDefinitions() {
2137
return Array.from(get().windowsMap.values()).flatMap((map) =>
2238
Array.from(map.values())
2339
);
2440
},
25-
addWindow(windowType, windowId, window) {
41+
addWindow<Props extends BaseProps = BaseProps>(
42+
windowType: string,
43+
windowId: string,
44+
definition: ComponentDefinition<Props>
45+
) {
2646
const windowsMap = get().windowsMap;
2747
const windowsOfType = windowsMap.get(windowType);
48+
const highestZIndex = get().highestZIndex + 1;
49+
definition.props.zIndex = highestZIndex;
2850

51+
// TODO: Need to find a better way of dealing with this rather than risky casting
2952
if (!windowsOfType) {
30-
windowsMap.set(windowType, new Map([[windowId, window]]));
53+
windowsMap.set(
54+
windowType,
55+
new Map([[windowId, definition as unknown as ComponentDefinition]])
56+
);
3157
} else {
32-
windowsOfType.set(windowId, window);
58+
windowsOfType.set(windowId, definition as unknown as ComponentDefinition);
3359
}
3460

35-
set({ windowsMap });
61+
set({ windowsMap, highestZIndex });
3662
},
3763
closeWindow(windowType, windowId) {
3864
const windowsMap = get().windowsMap;
3965
const windowsOfType = windowsMap.get(windowType);
40-
if (windowsOfType) {
41-
windowsOfType.delete(windowId);
66+
67+
// Delete the window, and if there are no windows left of this type,
68+
// remove the window type map from the master window map
69+
windowsOfType?.delete(windowId);
70+
71+
if (windowsOfType?.size === 0) {
72+
windowsMap.delete(windowType);
4273
}
74+
75+
// If there are no windows left open, reset the zIndex counter
76+
let highestZIndex = get().highestZIndex;
77+
if (!windowsMap.size) highestZIndex = 1;
78+
79+
set({ windowsMap, highestZIndex });
80+
},
81+
focusWindowsOfType(windowType) {
82+
const windowsMap = get().windowsMap;
83+
const windowsOfType = windowsMap.get(windowType);
84+
85+
// If no windows of this type exist, nothing to do
86+
if (!windowsOfType?.size) return;
87+
88+
let highestZIndex = get().highestZIndex;
89+
90+
// There may be multiple instances of this window type open,
91+
// so we want to preserve the zindex order they have relative to each other.
92+
// To do this, we sort them so we're processing the one with the lowest zindex first
93+
Array.from(windowsOfType.values())
94+
.sort((a, b) => (a.props.zIndex ?? 0) - (b.props.zIndex ?? 0))
95+
.forEach((window) => (window.props.zIndex = ++highestZIndex));
96+
4397
set({ windowsMap });
4498
},
99+
focusWindow(windowType, windowId) {
100+
let highestZIndex = get().highestZIndex;
101+
const windowsMap = get().windowsMap;
102+
const windowsOfType = windowsMap.get(windowType);
103+
const window = windowsOfType?.get(windowId);
104+
105+
// If the window doesnt exist, nothing to do
106+
if (!windowsOfType || !window) return;
107+
108+
// If the window is already top most, nothing to do
109+
if (window.props.zIndex === highestZIndex) return;
110+
111+
// Increase the zindex counter and set the window to have this zindex
112+
highestZIndex++;
113+
window.props.zIndex = highestZIndex;
114+
115+
set({ windowsMap, highestZIndex });
116+
},
117+
windowsOfTypeExist(windowType) {
118+
return (get().windowsMap.get(windowType)?.size ?? 0) > 0;
119+
},
45120
}));
46121

47122
export default useWindowManagerStore;

0 commit comments

Comments
 (0)