diff options
| author | darkuss <darkussdelta@gmail.com> | 2026-08-22 01:58:37 +0200 |
|---|---|---|
| committer | darkuss <darkussdelta@gmail.com> | 2026-08-22 01:58:37 +0200 |
| commit | 8f32fd58cafe677c07d9ed9fff84f6e255156d37 (patch) | |
| tree | a5de06ffb2c0a8b878c8a563ec8cee692efc98ac /src/desktop/window.tsx | |
| parent | 5ac62cc1a4d9a5f50ea26d5bc1afb0fbf95da23c (diff) | |
holy shit i'm finally done
Diffstat (limited to 'src/desktop/window.tsx')
| -rw-r--r-- | src/desktop/window.tsx | 249 |
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; } } |
