summaryrefslogtreecommitdiff
path: root/src/desktop/window.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/desktop/window.tsx')
-rw-r--r--src/desktop/window.tsx249
1 files changed, 203 insertions, 46 deletions
diff --git a/src/desktop/window.tsx b/src/desktop/window.tsx
index b2deaec..ef5cdab 100644
--- a/src/desktop/window.tsx
+++ b/src/desktop/window.tsx
@@ -1,63 +1,213 @@
+import { Signal, useSignal, useSignalEffect } from "@preact/signals";
+import { useLiveSignal } from "@preact/signals/utils";
import { ComponentChildren, Ref, RefObject } from "preact";
-import { useCallback, useRef, useState } from "preact/hooks";
+import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
+import { Task } from ".";
+import { flushSync } from "preact/compat";
+
+
+type UseDragFunction = (onStartDrag: (e: MouseEvent) => void, onUpdatePosition: (e: MouseEvent) => void) => [Signal<Boolean>, (e: MouseEvent) => void]
export function Window(props: Window.Props) {
+ const useDrag = useCallback<UseDragFunction>((onStartDrag, onUpdatePosition) => {
+ const dragTimeout: Ref<ReturnType<typeof setTimeout>> = useRef(null);
+ const dragging = useSignal(false);
+
+ const stopDrag = useCallback((e: MouseEvent) => {
+ if (e.button === 0 && dragging.value && !dragTimeout.current) {
+ dragTimeout.current = setTimeout(() => {
+ dragging.value = false
+ dragTimeout.current = null;
+ props.desktop.current?.removeEventListener("mousemove", onUpdatePosition)
+ props.desktop.current?.removeEventListener("mouseup", stopDrag)
+ }, 50) // intrdouce a small delay where the user can still drag the window without actually pressing down lmb to prevent accidental drops
+ }
+ }, [onUpdatePosition]);
+
+ const startDrag = useCallback((e: MouseEvent) => {
+ if (e.button === 0 && !maximized.value.value) {
+ if (dragTimeout.current !== null) {
+ clearTimeout(dragTimeout.current);
+ dragTimeout.current = null;
+ } else {
+
+ onStartDrag(e);
+ dragging.value = true
+ props.desktop.current?.addEventListener("mousemove", onUpdatePosition)
+ props.desktop.current?.addEventListener("mouseleave", stopDrag, { once: true })
+ props.desktop.current?.addEventListener("mouseup", stopDrag, { once: true })
+ }
+ }
+ }, [onUpdatePosition, onStartDrag, stopDrag]);
+
+ return [dragging, startDrag]
+ }, [props])
+
const windowRef: Ref<HTMLDivElement> = useRef(null);
- const dragTimeout: Ref<ReturnType<typeof setTimeout>> = useRef(null);
-
- const [x, setX] = useState(props.x ?? 0);
- const [y, setY] = useState(props.y ?? 0);
+
+ const source = useLiveSignal(props.task.source);
+ const title = useLiveSignal(props.task.title);
+ const contentReady = useLiveSignal(props.task.contentReady);
+ const sizingFinished = useLiveSignal(props.task.sizingFinished);
+ const [titleReady, setTitleReady] = useState(false);
+ const [titleRendered, setTitleRendered] = useState(false);
+
+ const maximized = useLiveSignal(props.task.window.maximized);
+ const minimized = useLiveSignal(props.task.window.minimized);
+ const position = props.task.window.position;
+ const z = useLiveSignal(props.task.window.z);
+
+ const x = useSignal(position?.x ?? "50%");
+ const y = useSignal(position?.y ?? "50%");
const initialOffsetX = useRef(0);
const initialOffsetY = useRef(0);
- const updatePosition = useCallback((e: MouseEvent) => {
- setX(e.clientX - initialOffsetX.current)
- setY(e.clientY - initialOffsetY.current)
+ const onWindowUpdatePosition = useCallback((e: MouseEvent) => {
+ x.value = e.clientX - initialOffsetX.current
+ y.value = e.clientY - initialOffsetY.current
+ }, [])
+
+ const onStartWindowMove = useCallback((e: MouseEvent) => {
+ const BORDER_WIDTH = 1;
+ // Naturally, I have to add the border width here, because offset* uses padding-edge for the calculations
+ initialOffsetX.current = e.offsetX + BORDER_WIDTH + (e.target as HTMLDivElement).offsetLeft
+ initialOffsetY.current = e.offsetY + BORDER_WIDTH + (e.target as HTMLDivElement).offsetTop
+ document.getSelection()?.empty()
+ }, [])
+
+ const [dragging, startWindowMove] = useDrag(onStartWindowMove, onWindowUpdatePosition);
+
+ const height = useSignal(props.task.window.height);
+ const width = useSignal(props.task.window.width);
+
+ const onStartResize = useCallback((e: MouseEvent) => {
+ initialOffsetX.current = e.offsetX
+ initialOffsetY.current = e.offsetY
+ }, [])
+
+ const onNorthResize = useCallback((e: MouseEvent) => {
+ const oldY = y.value as number;
+ y.value = e.clientY - initialOffsetY.current;
+ height.value = (height.value as number) - (y.value - oldY)
+ }, [])
+
+ const [, startNorthResize] = useDrag(onStartResize, onNorthResize)
+
+
+ const onEastResize = useCallback((e: MouseEvent) => {
+ width.value = (e.clientX - (x.value as number) - initialOffsetX.current)
+ }, [])
+
+ const [, startEastResize] = useDrag(onStartResize, onEastResize)
+
+
+ const onSouthResize = useCallback((e: MouseEvent) => {
+ height.value = (e.clientY - (y.value as number) - initialOffsetY.current)
+ }, [])
+
+ const [, startSouthResize] = useDrag(onStartResize, onSouthResize)
+
+ const onWestResize = useCallback((e: MouseEvent) => {
+ const oldX = x.value as number;
+ x.value = e.clientX - initialOffsetX.current;
+ width.value = (width.value as number) - (x.value - oldX)
+ }, [])
+
+ const [, startWestResize] = useDrag(onStartResize, onWestResize)
+
+ const startNEResize = useCallback((e: MouseEvent) => {
+ startNorthResize(e)
+ startEastResize(e)
+ }, [])
+
+ const startNWResize = useCallback((e: MouseEvent) => {
+ startNorthResize(e)
+ startWestResize(e)
+ }, [])
+
+ const startSEResize = useCallback((e: MouseEvent) => {
+ startSouthResize(e)
+ startEastResize(e)
}, [])
- const startDrag = useCallback((e: MouseEvent) => {
- if (e.button === 0) {
+ const startSWResize = useCallback((e: MouseEvent) => {
+ startSouthResize(e)
+ startWestResize(e)
+ }, [])
+
+ // tldr: wait until the title signal is ready,
+ // rerender so that the new title is rendered, calculate the position,
+ // and then rerender *again*, this time to reposition and show the window
- const BORDER_WIDTH = 1;
- console.log(e);
- // Naturally, I have to add the border width here, because offset* uses padding-edge for the calculations
- initialOffsetX.current = e.offsetX + BORDER_WIDTH
- initialOffsetY.current = e.offsetY + BORDER_WIDTH
- windowRef.current?.classList.add("dragging")
+ useSignalEffect(() => {
+ if (title.value.value !== undefined && !titleReady) {
+ setTitleReady(true)
+ }
+ })
- if (dragTimeout.current !== null) {
- clearTimeout(dragTimeout.current);
- dragTimeout.current = null;
+ useLayoutEffect(() => {
+ if (titleReady && contentReady.value.value) {
+ setTitleRendered(true);
+ // normalize position and dimensions from css values to pixels
+ const { height: bounding_height, width: bounding_width } = windowRef.current!.getBoundingClientRect();
+ height.value = bounding_height
+ width.value = bounding_width
+
+ if (position === undefined) {
+ // center
+ x.value = windowRef.current!.offsetLeft - (bounding_width / 2);
+ y.value = windowRef.current!.offsetTop - (bounding_height / 2);
} else {
- props.desktop.current?.addEventListener("mousemove", updatePosition)
- props.desktop.current?.addEventListener("mouseleave", stopDrag)
+ x.value = windowRef.current!.offsetLeft;
+ y.value = windowRef.current!.offsetTop;
}
+ sizingFinished.value.value = true
}
- }, []);
+ }, [titleReady, contentReady.value.value])
- const stopDrag = useCallback((e: MouseEvent) => {
- if (e.button === 0) {
- dragTimeout.current = setTimeout(() => {
- dragTimeout.current = null;
- windowRef.current?.classList.remove("dragging")
- props.desktop.current?.removeEventListener("mousemove", updatePosition)
- props.desktop.current?.removeEventListener("mouseleave", stopDrag)
- }, 50) // intrdouce a small delay where the user can still drag the window without actually pressing down lmb to prevent accidental drops
- }
- }, []);
+ const handleMinimize = useCallback(() => {
+ minimized.value.value = !minimized.value.value
+ }, [])
+
+ const handleMaximize = useCallback(() => {
+ maximized.value.value = !maximized.value.value
+ }, [])
- return <div class="window" ref={windowRef} style={{ width: props.width, height: props.height, left: x, top: y}}>
- <div class="titlebar" onPointerDown={startDrag} onPointerUp={stopDrag}>
- <div class="title">{props.title}</div>
- <div class="buttons" onPointerDown={e => e.stopPropagation()}>
- <button>
+ const onClose = useCallback(() => {
+ props.onClose(props.task)
+ }, [props])
+
+ const onFocus = useCallback(() => {
+ props.onFocus(props.task);
+ }, [props])
+
+ const onPointerDownButton = useCallback((e: MouseEvent) => {
+ onFocus();
+ e.stopPropagation();
+ }, [])
+
+ return <div
+ class={`window ${(titleRendered && !props.task.window.minimized.value) ? "" : " hidden"}${dragging.value ? "dragging" : ""}${maximized.value.value ? " maximized" : ""}` }
+ ref={windowRef}
+ style={
+ maximized.value.value
+ ? { width: "100%", height: "100%", left: 0, top: 0, "z-index": z.value.value }
+ : { width: width.value, height: height.value, left: x.value, top: y.value, "z-index": z.value.value }
+ }
+ onPointerDown={onFocus}
+ >
+ <div class="titlebar" onPointerDown={startWindowMove}>
+ <img src={`/assets/${source.value.value.type}.png`} />
+ <div class="title">{title.value.value ?? "[404 Title Not Found]"}</div>
+ <div class="buttons" onPointerDown={onPointerDownButton}>
+ <button onClick={handleMinimize}>
<img src="/assets/minimize.png" />
</button>
- <button>
+ <button onClick={handleMaximize}>
<img src="/assets/maximize.png" />
</button>
- <button>
+ <button onClick={onClose}>
<img src="/assets/close.png" />
</button>
</div>
@@ -65,17 +215,24 @@ export function Window(props: Window.Props) {
<div class="content">
{props.children}
</div>
+ <div class="corner n w" onPointerDown={startNWResize}></div>
+ <div class="corner n e" onPointerDown={startNEResize}></div>
+ <div class="corner s e" onPointerDown={startSEResize}></div>
+ <div class="corner s w" onPointerDown={startSWResize}></div>
+ <div class="side_v n" onPointerDown={startNorthResize}></div>
+ <div class="side_h e" onPointerDown={startEastResize}></div>
+ <div class="side_v s" onPointerDown={startSouthResize}></div>
+ <div class="side_h w" onPointerDown={startWestResize}></div>
</div>
}
-namespace Window {
+export namespace Window {
export type Props = {
- title: string;
- width?: string | number;
- height?: string | number;
- x?: string | number;
- y?: string | number;
desktop: RefObject<HTMLDivElement>;
- children: ComponentChildren
+ task: Task;
+ children: ComponentChildren;
+
+ onClose: (task: Task) => void;
+ onFocus: (task: Task) => void;
}
}