From 8f32fd58cafe677c07d9ed9fff84f6e255156d37 Mon Sep 17 00:00:00 2001 From: darkuss Date: Sat, 22 Aug 2026 01:58:37 +0200 Subject: holy shit i'm finally done --- src/desktop/window.tsx | 251 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 204 insertions(+), 47 deletions(-) (limited to 'src/desktop/window.tsx') 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, (e: MouseEvent) => void] export function Window(props: Window.Props) { + const useDrag = useCallback((onStartDrag, onUpdatePosition) => { + const dragTimeout: Ref> = 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 = useRef(null); - const dragTimeout: Ref> = 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 startDrag = useCallback((e: MouseEvent) => { - if (e.button === 0) { + 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 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") + 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) + }, []) - if (dragTimeout.current !== null) { - clearTimeout(dragTimeout.current); - dragTimeout.current = null; + 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 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 + + useSignalEffect(() => { + if (title.value.value !== undefined && !titleReady) { + setTitleReady(true) + } + }) + + 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 } - }, []); - - 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 - } - }, []); + }, [titleReady, contentReady.value.value]) + + const handleMinimize = useCallback(() => { + minimized.value.value = !minimized.value.value + }, []) + + const handleMaximize = useCallback(() => { + maximized.value.value = !maximized.value.value + }, []) - return
-
-
{props.title}
-
e.stopPropagation()}> - - -
@@ -65,17 +215,24 @@ export function Window(props: Window.Props) {
{props.children}
+
+
+
+
+
+
+
+
} -namespace Window { +export namespace Window { export type Props = { - title: string; - width?: string | number; - height?: string | number; - x?: string | number; - y?: string | number; desktop: RefObject; - children: ComponentChildren + task: Task; + children: ComponentChildren; + + onClose: (task: Task) => void; + onFocus: (task: Task) => void; } } -- cgit v1.2.3