diff --git a/frontend/realtime/src/KonvaApp/RenderingPrimitives/RealTimeDrawEngine.ts b/frontend/realtime/src/KonvaApp/RenderingPrimitives/RealTimeDrawEngine.ts index 0efc9e5a29..134e444fdb 100644 --- a/frontend/realtime/src/KonvaApp/RenderingPrimitives/RealTimeDrawEngine.ts +++ b/frontend/realtime/src/KonvaApp/RenderingPrimitives/RealTimeDrawEngine.ts @@ -4,7 +4,7 @@ import { Shape } from "konva/lib/Shape"; import { Group } from "konva/lib/Group"; import { invoke } from "@tauri-apps/api/core"; -import { listen } from '@tauri-apps/api/event'; +import { listen } from "@tauri-apps/api/event"; import { FileUtilities } from "../FileUtilities/FileUtilities"; import { ImageNode, VideoNode, TextNode, ShapeNode, ShapeType } from "../Nodes"; @@ -31,7 +31,6 @@ interface ServerSettings { lora_path?: string; } - interface GenerateImageParams { image: string; prompt: string; @@ -210,16 +209,18 @@ export class RealTimeDrawEngine { console.log(event); // Handle model download events const payload = event.payload as any; - + // Model download started if (payload.model_download_started) { const modelInfo = payload.model_download_started; - console.log(`Model download started: ${modelInfo.model_name} (${modelInfo.model_type})`); - + console.log( + `Model download started: ${modelInfo.model_name} (${modelInfo.model_type})`, + ); + // Set up loading indicator isLoadingVisible.value = true; loadingProgress.value = 0; - + // Create fake progress updates const downloadTimer = setInterval(() => { loadingProgress.value += 2; @@ -230,23 +231,23 @@ export class RealTimeDrawEngine { } }, 500); } - + // Model download completed if (payload.model_download_complete) { const modelInfo = payload.model_download_complete; - console.log(`Model download completed: ${modelInfo.model_name} (${modelInfo.model_type})`); - + console.log( + `Model download completed: ${modelInfo.model_name} (${modelInfo.model_type})`, + ); + // Complete the loading progress loadingProgress.value = 100; - + // Hide loading indicator after a short delay setTimeout(() => { isLoadingVisible.value = false; }, 1000); } - }); - } private isEnabled: boolean = false; @@ -320,9 +321,16 @@ export class RealTimeDrawEngine { const ctx = cursorCanvas.getContext("2d"); if (!ctx) return; - // Draw the circle + // Draw the outer circle with a light stroke ctx.beginPath(); ctx.arc(size, size, size / 2, 0, Math.PI * 2); + ctx.strokeStyle = "white"; + ctx.lineWidth = 3; + ctx.stroke(); + + // Draw the inner circle with a dark stroke + ctx.beginPath(); + ctx.arc(size, size, size / 2 - 1, 0, Math.PI * 2); ctx.strokeStyle = "black"; ctx.lineWidth = 1; ctx.stroke(); diff --git a/frontend/realtime/src/app/global.css b/frontend/realtime/src/app/global.css index 3c0abd0ee7..2a70999845 100644 --- a/frontend/realtime/src/app/global.css +++ b/frontend/realtime/src/app/global.css @@ -202,3 +202,30 @@ background-position: -200% 0; } } + +.react-colorful__saturation { + margin: 16px 0; + border-radius: 10px !important; + border-bottom: none !important; +} + +.react-colorful__saturation-pointer { + width: 24px !important; + height: 24px !important; +} + +.react-colorful__hue, +.react-colorful__alpha { + height: 20px !important; + border-radius: 50px !important; +} + +.react-colorful__hue { + margin-bottom: 12px; +} + +.react-colorful__hue-pointer, +.react-colorful__alpha-pointer { + width: 20px !important; + height: 20px !important; +} diff --git a/frontend/realtime/src/components/ui/HexEyedropPicker.tsx b/frontend/realtime/src/components/ui/HexEyedropPicker.tsx index d2e1791b0b..9c0af09a8e 100644 --- a/frontend/realtime/src/components/ui/HexEyedropPicker.tsx +++ b/frontend/realtime/src/components/ui/HexEyedropPicker.tsx @@ -1,12 +1,13 @@ -import { MouseEvent, MouseEventHandler, useState } from "react"; -import { HexColorPicker } from "react-colorful"; -import { EyeDropper } from "react-eyedrop"; import { Button } from "./Button"; import { faEyedropper } from "@fortawesome/pro-solid-svg-icons"; import { twMerge } from "tailwind-merge"; -type ColorPickerHTMLAttributes = Omit, "color" | "onChange" | "onChangeCapture">; -interface ColorPickerBaseProps extends ColorPickerHTMLAttributes { +type ColorPickerHTMLAttributes = Omit< + React.HTMLAttributes, + "color" | "onChange" | "onChangeCapture" +>; +interface ColorPickerBaseProps + extends ColorPickerHTMLAttributes { color: T; onChange: (newColor: T) => void; } @@ -17,7 +18,7 @@ export const HexEyedropPicker = ({ dropperClassName, pickerClassName, onPickerChange, - Picker + Picker, }: { color: string; onDropperChange?: (newColor: string) => void; @@ -26,9 +27,8 @@ export const HexEyedropPicker = ({ onPickerChange: (newColor: string) => void; Picker: (props: Partial>) => JSX.Element; }) => { - const handleDropperClick = () => { - if (!('EyeDropper' in window)) { + if (!("EyeDropper" in window)) { // NOTE: This shouldn't happen cuz we're using Chromium console.error("EyeDropper not found in window"); return; @@ -46,42 +46,40 @@ export const HexEyedropPicker = ({ // User cancelled the picker return null; } - } + }; startEyeDrop().then((newColor) => { if (newColor) { onDropperChange?.(newColor); } }); - } + }; - const [isDragging, setIsDragging] = useState(false); - const handleOnMouseDown = () => { - setIsDragging(true); - } - const handleOnMouseUp = () => { - setIsDragging(false); - } + // const [isDragging, setIsDragging] = useState(false); + // const handleOnMouseDown = () => { + // setIsDragging(true); + // }; + // const handleOnMouseUp = () => { + // setIsDragging(false); + // }; return (
-
- ) -} + ); +}; diff --git a/frontend/realtime/src/components/ui/TextEditor/ColorPicker.tsx b/frontend/realtime/src/components/ui/TextEditor/ColorPicker.tsx index fc55f775b5..97cdaef38d 100644 --- a/frontend/realtime/src/components/ui/TextEditor/ColorPicker.tsx +++ b/frontend/realtime/src/components/ui/TextEditor/ColorPicker.tsx @@ -91,7 +91,7 @@ export const ColorPicker = ({ if (streamChanges) { onChange(color); } - } + }; return ( @@ -124,7 +124,7 @@ export const ColorPicker = ({ anchor={anchor} className={twMerge( paperWrapperStyles, - "flex flex-col items-center gap-2 overflow-hidden", + "flex flex-col items-center gap-2 overflow-hidden p-4", )} style={ { @@ -134,13 +134,14 @@ export const ColorPicker = ({ static > void) | null>(null); + const closeTimeoutRef = useRef(null); useEffect(() => { setIsOpen(defaultOpen); }, [defaultOpen]); + // Global mouse up handler + useEffect(() => { + const handleGlobalMouseUp = () => { + if (isMouseDown) { + setIsMouseDown(false); + + // If mouse is outside when released, close the panel after a short delay + if (isMouseOutside && closeOnMouseLeave && closeRef.current) { + closeTimeoutRef.current = setTimeout(() => { + onChange(currColor); + setIsOpen(false); + closeRef.current?.(); + }, 500); + } + } + }; + + window.addEventListener("mouseup", handleGlobalMouseUp); + + return () => { + window.removeEventListener("mouseup", handleGlobalMouseUp); + }; + }, [isMouseDown, isMouseOutside, closeOnMouseLeave, currColor, onChange]); + const handleMouseLeave = (close: () => void) => { - if (closeOnMouseLeave) { - onChange(currColor); - setIsOpen(false); - close(); + closeRef.current = close; + setIsMouseOutside(true); + + // Only close if mouse is not down and closeOnMouseLeave is true + if (closeOnMouseLeave && !isMouseDown) { + closeTimeoutRef.current = setTimeout(() => { + onChange(currColor); + setIsOpen(false); + close(); + }, 200); + } + }; + + const handleMouseEnter = () => { + setIsMouseOutside(false); + if (closeTimeoutRef.current) { + clearTimeout(closeTimeoutRef.current); + closeTimeoutRef.current = null; } }; @@ -92,7 +139,7 @@ export const PaintModeMenu = ({ if (streamChanges) { onChange(color); } - } + }; // Rerender the component when these signals change: useSignals(); @@ -123,12 +170,16 @@ export const PaintModeMenu = ({ )} {(open || isOpen) && ( -
handleMouseLeave(close)}> +
handleMouseLeave(close)} + onMouseEnter={handleMouseEnter} + onMouseDown={() => setIsMouseDown(true)} + > -
-

Brush Size:

+
+

+ Brush Size: +