Files
uTab/src/renderer/components/Widget.tsx
T
2025-03-15 18:32:10 -04:00

164 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useRef, useState, useEffect } from 'react';
import { WidgetData } from '../types';
import { CogwheelIcon } from './CogwheelIcon';
import { useAtom, useAtomValue } from 'jotai';
import { draggableState } from '../state/draggable.state';
interface WidgetProps {
widget: WidgetData;
gridSize: { width: number; height: number };
children: React.ReactNode;
settingsContent?: React.ReactNode;
updateWidget: (id: string, data: Partial<WidgetData>) => void
}
export const Widget: React.FC<WidgetProps> = ({ widget, gridSize, children, settingsContent, updateWidget }) => {
const widgetRef = useRef<HTMLDivElement>(null);
const modalRef = useRef<HTMLDivElement>(null);
const [isDragging, setIsDragging] = useState(false);
const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const [widthInput, setWidthInput] = useState(widget.gridWidth);
const [heightInput, setHeightInput] = useState(widget.gridHeight);
const isDraggable = useAtomValue(draggableState)
const snapToGrid = (pos: number, grid: number) => Math.round(pos / grid);
const handleMouseDown = (e: React.MouseEvent) => {
if(!isDraggable || isSettingsOpen){
return
}
if (widgetRef.current && e.target === widgetRef.current) {
setIsDragging(true);
const rect = widgetRef.current.getBoundingClientRect();
setDragOffset({
x: e.clientX - rect.left,
y: e.clientY - rect.top
});
}
};
const handleMouseMove = (e: MouseEvent) => {
if (isDragging && widgetRef.current) {
e.preventDefault();
const newGridX = snapToGrid(e.clientX - dragOffset.x, gridSize.width);
const newGridY = snapToGrid(e.clientY - dragOffset.y, gridSize.height);
widgetRef.current.style.left = `${newGridX * gridSize.width}px`;
widgetRef.current.style.top = `${newGridY * gridSize.height}px`;
}
};
const handleMouseUp = () => {
if (isDragging && widgetRef.current) {
setIsDragging(false);
const rect = widgetRef.current.getBoundingClientRect();
const newGridX = snapToGrid(rect.left, gridSize.width);
const newGridY = snapToGrid(rect.top, gridSize.height);
updateWidget(widget.id, { gridX: newGridX, gridY: newGridY });
}
};
const handleClickOutside = (e: MouseEvent) => {
if (modalRef.current && !modalRef.current.contains(e.target as Node)) {
setIsSettingsOpen(false);
}
};
const stopPropagation = (e: React.MouseEvent) => {
e.stopPropagation();
};
const handleSizeChange = () => {
const newWidth = Math.max(1, widthInput);
const newHeight = Math.max(1, heightInput);
updateWidget(widget.id, { gridWidth: newWidth, gridHeight: newHeight });
setWidthInput(newWidth);
setHeightInput(newHeight);
setIsSettingsOpen(false);
};
useEffect(() => {
if (isDragging) {
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', handleMouseUp);
document.body.style.userSelect = 'none';
}
return () => {
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseUp);
document.body.style.userSelect = '';
};
}, [isDragging, dragOffset, gridSize]);
useEffect(() => {
if (isSettingsOpen) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isSettingsOpen]);
useEffect(() => {
if (widgetRef.current && !isDragging) {
widgetRef.current.style.left = `${widget.gridX * gridSize.width}px`;
widgetRef.current.style.top = `${widget.gridY * gridSize.height}px`;
widgetRef.current.style.width = `${(widget.gridWidth) * gridSize.width}px`;
widgetRef.current.style.height = `${(widget.gridHeight) * gridSize.height}px`;
}
}, [widget.gridX, widget.gridY, widget.gridWidth, widget.gridHeight, gridSize, isDragging]);
const genericSettingsContent = (
<>
<label>
Width (grid cells):
<input
type="number"
min="1"
value={widthInput}
onChange={(e) => setWidthInput(parseInt(e.target.value) || 1)}
/>
</label>
<label>
Height (grid cells):
<input
type="number"
min="1"
value={heightInput}
onChange={(e) => setHeightInput(parseInt(e.target.value) || 1)}
/>
</label>
<button onClick={handleSizeChange}>Apply</button>
</>
);
return (
<div
ref={widgetRef}
className="container"
onMouseDown={handleMouseDown}
style={{ cursor: isDraggable && !isSettingsOpen ? 'grab' : 'default' }}
>
<button className="close-button" onClick={() => undefined /*removeWidget(widget.id)*/} onMouseDown={stopPropagation}>×</button>
<button
className="settings-button"
onClick={() => setIsSettingsOpen(!isSettingsOpen)}
onMouseDown={stopPropagation}
>
<CogwheelIcon size={24} />
</button>
{children}
{isSettingsOpen && (
<div className="widget-settings-modal" onMouseDown={stopPropagation}>
<div ref={modalRef} className="widget-modal-content">
<button className="close-button" onClick={() => setIsSettingsOpen(false)} onMouseDown={stopPropagation}>×</button>
{settingsContent}
{genericSettingsContent}
</div>
</div>
)}
</div>
);
};