diff --git a/.gitignore b/.gitignore index 4d29575..d3e63b4 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ /.pnp .pnp.js +python-tests/ + # testing /coverage diff --git a/src/canvas/canvas.js b/src/canvas/canvas.js index ad3398c..b06c960 100644 --- a/src/canvas/canvas.js +++ b/src/canvas/canvas.js @@ -23,10 +23,11 @@ import { ReactComponent as DotsBackground } from "../assets/background/dots.svg" import DroppableWrapper from "../components/draggable/droppable" -import { PosType } from "./constants/layouts" +import { Layouts, PosType } from "./constants/layouts" import WidgetContainer from "./constants/containers" import { isSubClassOfWidget } from "../utils/widget" import { ButtonModal } from "../components/modals" +import ResizeWidgetContainer from "./resizeContainer" // const DotsBackground = require("../assets/background/dots.svg") @@ -65,6 +66,7 @@ class Canvas extends React.Component { this.widgetRefs = {} // stores the actual refs to the widgets inside the canvas {id: ref, id2, ref2...} this.state = { + isWidgetDragging: false, widgetResizing: "", // set this to "nw", "sw" etc based on the side when widgets resizing handles are selected widgets: [], // stores the mapping to widgetRefs, stores id and WidgetType, later used for rendering [{id: , widgetType: WidgetClass, children: [], parent: "", initialData: {}}] zoom: 1, @@ -181,8 +183,8 @@ class Canvas extends React.Component { let innerWidget = null for (let [key, ref] of Object.entries(this.widgetRefs)) { - - if (ref.current === target){ + + if (ref.current === target) { innerWidget = ref.current break } @@ -235,11 +237,13 @@ class Canvas extends React.Component { this.state.selectedWidget?.setZIndex(0) selectedWidget.setZIndex(1000) selectedWidget.select() + // console.log("selected widget", selectedWidget, this.state.selectedWidget) this.setState({ selectedWidget: selectedWidget, toolbarAttrs: selectedWidget.getToolbarAttrs() }) + // if (!this.state.selectedWidget || (selectedWidget.getId() !== this.state.selectedWidget?.getId())) { // this.state.selectedWidget?.deSelect() // deselect the previous widget before adding the new one // this.state.selectedWidget?.setZIndex(0) @@ -301,16 +305,16 @@ class Canvas extends React.Component { { key: "snap", label: (
{ - domtoimage.toPng(selectedWidget.getElement(), { - width: selectedWidget.getElement().offsetWidth * 2, // Multiply element's width by 2 - height: selectedWidget.getElement().offsetHeight * 2 // Multiply element's height by 2 - }).then((dataUrl) => { - saveAs(dataUrl, 'widget.png') - }).catch((error) => { - console.error('Error capturing widget as PNg:', error) - }) - }}> - Save as Image
), + domtoimage.toPng(selectedWidget.getElement(), { + width: selectedWidget.getElement().offsetWidth * 2, // Multiply element's width by 2 + height: selectedWidget.getElement().offsetHeight * 2 // Multiply element's height by 2 + }).then((dataUrl) => { + saveAs(dataUrl, 'widget.png') + }).catch((error) => { + console.error('Error capturing widget as PNg:', error) + }) + }}> + Save as Image), icons: , } ] @@ -638,6 +642,114 @@ class Canvas extends React.Component { }) } + /** + * Handles drop event to canvas from the sidebar and on canvas widget movement + * @param {DragEvent} e + */ + handleDropEvent = (e, draggedElement, widgetClass = null) => { + + e.preventDefault() + + this.setState({ isWidgetDragging: false }) + + if (!draggedElement || !draggedElement.getAttribute("data-drag-start-within")) { + // if the drag is starting from outside (eg: file drop) or if drag doesn't exist + return + } + + const container = draggedElement.getAttribute("data-container") + const canvasRect = this.canvasRef.current.getBoundingClientRect() + + const draggedElementRect = draggedElement.getBoundingClientRect() + const elementWidth = draggedElementRect.width + const elementHeight = draggedElementRect.height + + const { clientX, clientY } = e + + let finalPosition = { + x: (clientX - canvasRect.left) / this.state.zoom, + y: (clientY - canvasRect.top) / this.state.zoom, + } + + + + if (container === WidgetContainer.SIDEBAR) { + + if (!widgetClass) { + throw new Error("WidgetClass has to be passed for widgets dropped from sidebar") + } + + // if the widget is being dropped from the sidebar, use the info to create the widget first + this.createWidget(widgetClass, ({ id, widgetRef }) => { + widgetRef.current.setPos(finalPosition.x, finalPosition.y) + }) + + } else if ([WidgetContainer.CANVAS, WidgetContainer.WIDGET].includes(container)) { + + // snaps to center + finalPosition = { + x: (clientX - canvasRect.left) / this.state.zoom - (elementWidth / 2) / this.state.zoom, + y: (clientY - canvasRect.top) / this.state.zoom - (elementHeight / 2) / this.state.zoom, + } + + let widgetId = draggedElement.getAttribute("data-widget-id") + + const widgetObj = this.getWidgetById(widgetId) + // console.log("WidgetObj: ", widgetObj) + if (container === WidgetContainer.CANVAS) { + + widgetObj.current.setPos(finalPosition.x, finalPosition.y) + + } else if (container === WidgetContainer.WIDGET) { + + // if the widget was inside another widget move it outside + let childWidgetObj = this.findWidgetFromListById(widgetObj.current.getId()) + let parentWidgetObj = this.findWidgetFromListById(childWidgetObj.parent) + + const childData = widgetObj.current.serialize() // save the data and pass it the updated child object + + // remove child from current position + + const updatedChildWidget = { + ...childWidgetObj, + parent: "", + initialData: { + ...childData, + pos: { x: finalPosition.x, y: finalPosition.y }, + positionType: PosType.ABSOLUTE, // makes sure that after dropping the position is set to absolute value + zIndex: 0, + widgetContainer: WidgetContainer.CANVAS + } + } + + let updatedWidgets = this.removeWidgetFromCurrentList(widgetObj.current.getId()) + + + // Create a new copy of the parent widget with the child added + const updatedParentWidget = { + ...parentWidgetObj, + // children: parentWidgetObj.children.filter(child => child.id !== childWidgetObj.id) + } + + + updatedWidgets = updatedWidgets.map(widget => { + if (widget.id === parentWidgetObj.id) { + return updatedParentWidget // Update the parent widget with the child removed + } else { + return widget // Leave other widgets unchanged + } + }) + + + this.setState({ + widgets: [...updatedWidgets, updatedChildWidget] + }) + + } + } + + } + /** * Adds the child into the children attribute inside the this.widgets list of objects * // widgets data structure { id, widgetType: widgetComponentType, children: [], parent: "" } @@ -645,8 +757,9 @@ class Canvas extends React.Component { * @param {object} dragElement * @param {boolean} create - if create is set to true the widget will be created before adding to the child tree */ - handleAddWidgetChild = ({ parentWidgetId, dragElementID, swap = false }) => { - + handleAddWidgetChild = ({event, parentWidgetId, dragElementID, swap = false }) => { + + console.log("event: ", event) // widgets data structure { id, widgetType: widgetComponentType, children: [], parent: "" } const dropWidgetObj = this.findWidgetFromListById(parentWidgetId) // Find the dragged widget object @@ -658,6 +771,18 @@ class Canvas extends React.Component { const dragWidget = this.widgetRefs[dragWidgetObj.id] const dragData = dragWidget.current.serialize() + + const parentWidget = this.widgetRefs[parentWidgetId].current + const parentRect = parentWidget.getBoundingRect() + const canvasRect = this.canvasRef.current.getBoundingClientRect() + const { clientX, clientY } = event + + + let finalPosition = { + x: (clientX - parentRect.left) / this.state.zoom, + y: (clientY - parentRect.top) / this.state.zoom, + } + if (swap) { // If swapping, we need to find the common parent const grandParentWidgetObj = this.findWidgetFromListById(dropWidgetObj.parent) @@ -690,17 +815,25 @@ class Canvas extends React.Component { // Non-swap mode: Add the dragged widget as a child of the drop widget let updatedWidgets = this.removeWidgetFromCurrentList(dragElementID) + const parentLayout = parentWidget.getLayout()?.layout + + console.log("parent layout: ", parentLayout, parentWidget.getLayout(), parentWidget) + dragWidget.current.setPos(finalPosition.x, finalPosition.y) const updatedDragWidget = { ...dragWidgetObj, parent: dropWidgetObj.id, // Keep the parent reference initialData: { ...dragData, - positionType: PosType.NONE, + positionType: parentLayout === Layouts.PLACE ? PosType.ABSOLUTE : PosType.NONE, zIndex: 0, + pos: {x: finalPosition.x, y: finalPosition.y}, widgetContainer: WidgetContainer.WIDGET } } + console.log("updated widget: ", updatedDragWidget) + + const updatedDropWidget = { ...dropWidgetObj, children: [...dropWidgetObj.children, updatedDragWidget] @@ -725,7 +858,7 @@ class Canvas extends React.Component { */ createWidget(widgetComponentType, callback) { - if (!isSubClassOfWidget(widgetComponentType)){ + if (!isSubClassOfWidget(widgetComponentType)) { throw new Error("widgetComponentType must be a subclass of Widget class") } @@ -804,6 +937,28 @@ class Canvas extends React.Component { this._onWidgetListUpdated([]) } + getWidgetByIdFromWidgetList = (widgetId) => { + + function recursiveFind(objects) { + for (const obj of objects) { + // Check if the current object has the matching ID + if (obj.id === widgetId) { + return obj // Return the object if found + } + // Recursively check children if they exist + if (obj.children && obj.children.length > 0) { + const found = recursiveFind(obj.children) + if (found) { + return found // Return the found object from children + } + } + } + return null // Return null if not found + } + + return recursiveFind(this.state.widgets) + } + removeWidget(widgetId) { @@ -833,114 +988,23 @@ class Canvas extends React.Component { } + /** - * Handles drop event to canvas from the sidebar and on canvas widget movement - * @param {DragEvent} e + * informs the child about the parent layout */ - handleDropEvent = (e, draggedElement, widgetClass=null) => { + updateChildLayouts = ({parentId, parentLayout}) => { + + const parent = this.getWidgetByIdFromWidgetList(parentId) - e.preventDefault() + if (!parent) return - if (!draggedElement || !draggedElement.getAttribute("data-drag-start-within")){ - // if the drag is starting from outside (eg: file drop) or if drag doesn't exist - return - } - - const container = draggedElement.getAttribute("data-container") - const canvasRect = this.canvasRef.current.getBoundingClientRect() - - const draggedElementRect = draggedElement.getBoundingClientRect() - const elementWidth = draggedElementRect.width - const elementHeight = draggedElementRect.height - - const { clientX, clientY } = e - - let finalPosition = { - x: (clientX - canvasRect.left) / this.state.zoom, - y: (clientY - canvasRect.top) / this.state.zoom, - } - - - - if (container === WidgetContainer.SIDEBAR) { - - if (!widgetClass){ - throw new Error("WidgetClass has to be passed for widgets dropped from sidebar") - } - - // if the widget is being dropped from the sidebar, use the info to create the widget first - this.createWidget(widgetClass, ({ id, widgetRef }) => { - widgetRef.current.setPos(finalPosition.x, finalPosition.y) - }) - - } else if ([WidgetContainer.CANVAS, WidgetContainer.WIDGET].includes(container)) { - - // snaps to center - finalPosition = { - x: (clientX - canvasRect.left) / this.state.zoom - (elementWidth / 2) / this.state.zoom, - y: (clientY - canvasRect.top) / this.state.zoom - (elementHeight / 2) / this.state.zoom, - } - - let widgetId = draggedElement.getAttribute("data-widget-id") - - const widgetObj = this.getWidgetById(widgetId) - // console.log("WidgetObj: ", widgetObj) - if (container === WidgetContainer.CANVAS) { - - widgetObj.current.setPos(finalPosition.x, finalPosition.y) - - } else if (container === WidgetContainer.WIDGET) { - - // if the widget was inside another widget move it outside - let childWidgetObj = this.findWidgetFromListById(widgetObj.current.getId()) - let parentWidgetObj = this.findWidgetFromListById(childWidgetObj.parent) - - const childData = widgetObj.current.serialize() // save the data and pass it the updated child object - - // remove child from current position - - const updatedChildWidget = { - ...childWidgetObj, - parent: "", - initialData: { - ...childData, - pos: { x: finalPosition.x, y: finalPosition.y }, - positionType: PosType.ABSOLUTE, // makes sure that after dropping the position is set to absolute value - zIndex: 0, - widgetContainer: WidgetContainer.CANVAS - } - } - - let updatedWidgets = this.removeWidgetFromCurrentList(widgetObj.current.getId()) - - - // Create a new copy of the parent widget with the child added - const updatedParentWidget = { - ...parentWidgetObj, - // children: parentWidgetObj.children.filter(child => child.id !== childWidgetObj.id) - } - - - updatedWidgets = updatedWidgets.map(widget => { - if (widget.id === parentWidgetObj.id) { - return updatedParentWidget // Update the parent widget with the child removed - } else { - return widget // Leave other widgets unchanged - } - }) - - - this.setState({ - widgets: [...updatedWidgets, updatedChildWidget] - }) - - } + for (let child of parent.children){ + this.widgetRefs[child.id].current.setParentLayout(parentLayout) } } - renderWidget = (widget) => { const { id, widgetType: ComponentType, children = [], parent, initialData = {} } = widget @@ -959,6 +1023,7 @@ class Canvas extends React.Component { return ( + this.setState({ widgetResizing: resizeSide })} + // onWidgetDragStart={() => this.setState({isWidgetDragging: true})} + // onWidgetDragEnd={() => this.setState({isWidgetDragging: false})} + onLayoutUpdate={this.updateChildLayouts} > {/* Render children inside the parent with layout applied */} {renderChildren(children)} @@ -977,6 +1045,7 @@ class Canvas extends React.Component { } render() { + return (
@@ -986,13 +1055,13 @@ class Canvas extends React.Component {
diff --git a/src/canvas/resizeContainer.js b/src/canvas/resizeContainer.js new file mode 100644 index 0000000..1996ae2 --- /dev/null +++ b/src/canvas/resizeContainer.js @@ -0,0 +1,102 @@ +import Cursor from "./constants/cursor" +import Widget from "./widgets/base" +import { useEffect, useState } from "react" + +// FIXME: when using this if the widhet has invisible swappable area, this won't work +/** + * + * @param {Widget} - selectedWidget + * @returns + */ +const ResizeWidgetContainer = ({selectedWidget, onResize}) => { + + const [pos, setPos] = useState({x: 0, y: 0}) + const [size, setSize] = useState({width: 0, height: 0}) + + useEffect(() => { + + if (selectedWidget){ + setPos(selectedWidget.getPos()) + setSize(selectedWidget.getSize()) + } + + console.log("selected widget resizable: ", selectedWidget) + + }, [selectedWidget, selectedWidget?.getPos(), selectedWidget?.getSize()]) + + + + return ( +
+ +
+ {/* */} + +
{ + e.stopPropagation() + e.preventDefault() + onResize("nw") + // this.setState({ dragEnabled: false }) + }} + // onMouseUp={() => this.setState({ dragEnabled: true })} + /> +
{ + e.stopPropagation() + e.preventDefault() + onResize("nw") + // this.setState({ dragEnabled: false }) + }} + // onMouseUp={() => this.setState({ dragEnabled: true })} + /> +
{ + e.stopPropagation() + e.preventDefault() + onResize("nw") + // this.props.onWidgetResizing("sw") + // this.setState({ dragEnabled: false }) + }} + onMouseUp={() => this.setState({ dragEnabled: true })} + /> +
{ + e.stopPropagation() + e.preventDefault() + onResize("nw") + // this.props.onWidgetResizing("se") + // this.setState({ dragEnabled: false }) + }} + // onMouseUp={() => this.setState({ dragEnabled: true })} + /> + +
+ +
+ ) +} + + +export default ResizeWidgetContainer \ No newline at end of file diff --git a/src/canvas/widgets/base.js b/src/canvas/widgets/base.js index b188fcf..345546c 100644 --- a/src/canvas/widgets/base.js +++ b/src/canvas/widgets/base.js @@ -25,6 +25,9 @@ class Widget extends React.Component { static widgetType = "widget" + static requirements = [] // requirements for the widgets (libraries) eg: tkvideoplayer, tktimepicker + static requiredImports = [] // import statements + // static contextType = ActiveWidgetContext constructor(props) { @@ -71,6 +74,8 @@ class Widget extends React.Component { widgetName: widgetName || 'widget', // this will later be converted to variable name enableRename: false, // will open the widgets editable div for renaming + parentLayout: null, // depending on the parents layout the child will behave + isDragging: false, // tells if the widget is currently being dragged dragEnabled: true, @@ -128,6 +133,7 @@ class Widget extends React.Component { ], }, onChange: (value) => { + console.log("changed: ", value) // this.setAttrValue("layout", value) this.setLayout(value) } @@ -267,6 +273,18 @@ class Widget extends React.Component { return this.constructor.widgetType } + getRequirements = () => { + return this.constructor.requirements + } + + getImports = () => { + return this.constructor.requiredImports + } + + getCode = () => { + throw new NotImplementedError("Get Code must be implemented by the subclass") + } + getAttributes() { return this.state.attrs } @@ -344,19 +362,6 @@ class Widget extends React.Component { return this.elementRef.current } - getLayoutStyleForWidget = () => { - - switch (this.state.attrs.layout) { - case 'grid': - return { display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '10px' } - case 'flex': - return { display: 'flex', flexDirection: 'row', justifyContent: 'space-around' } - case 'absolute': - return { position: 'absolute', left: "0", top: "0" } // Custom positioning - default: - return {} - } - } /** * Given the key as a path, sets the value for the widget attribute @@ -454,24 +459,62 @@ class Widget extends React.Component { }) } + /** + * + * @param {Layouts} layout + */ + setParentLayout = (layout) => { + + let updates = { + parentLayout: layout, + } + + if (layout === Layouts.FLEX || layout === Layouts.GRID){ + + updates = { + ...updates, + positionType: PosType.NONE + } + + }else if (layout === Layouts.PLACE){ + updates = { + ...updates, + positionType: PosType.ABSOLUTE + } + } + + console.log("Parent layout updated: ", updates) + + this.setState(updates) + } + + getLayout = () => { + + return this.state?.attrs?.layout?.value || Layouts.FLEX + } + setLayout(value) { // FIXME: when the parent layout is place, the child widgets should have position absolute const { layout, direction, grid = { rows: 1, cols: 1 }, gap = 10 } = value + console.log("layout value: ", value) + const widgetStyle = { ...this.state.widgetStyling, - display: layout, + display: layout !== Layouts.PLACE ? layout : "block", flexDirection: direction, gap: `${gap}px`, flexWrap: "wrap" // TODO: add grid rows and cols } - this.setAttrValue("layout", value) this.updateState({ widgetStyling: widgetStyle }) + this.setAttrValue("layout", value) + this.props.onLayoutUpdate({parentId: this.__id, parentLayout: layout})// inform children about the layout update + } /** @@ -480,7 +523,7 @@ class Widget extends React.Component { * @param {string} value - Value of the style */ setWidgetStyling(key, value) { - + const widgetStyle = { ...this.state.widgetStyling, [key]: value @@ -555,6 +598,7 @@ class Widget extends React.Component { size: this.state.size, widgetContainer: this.state.widgetContainer, widgetStyling: this.state.widgetStyling, + parentLayout: this.state.parentLayout, positionType: this.state.positionType, attrs: this.serializeAttrsValues() // makes sure that functions are not serialized }) @@ -571,14 +615,40 @@ class Widget extends React.Component { data = {...data} // create a shallow copy - const {attrs, ...restData} = data + const {attrs, parentLayout, ...restData} = data // for (let [key, value] of Object.entries(attrs | {})) // this.setAttrValue(key, value) // delete data.attrs - this.setState(restData, () => { + let layoutUpdates = { + parentLayout: parentLayout + } + // FIXME: Need to load the data properly + if (parentLayout === Layouts.FLEX || parentLayout === Layouts.GRID){ + + layoutUpdates = { + ...layoutUpdates, + positionType: PosType.NONE + } + + }else if (parentLayout === Layouts.PLACE){ + layoutUpdates = { + ...layoutUpdates, + positionType: PosType.ABSOLUTE + } + } + + console.log("loaded layout: ", layoutUpdates) + + const newData = { + ...restData, + layoutUpdates + } + console.log("loaded layout2: ", newData) + + this.setState(newData, () => { // UPdates attrs let newAttrs = { ...this.state.attrs } @@ -611,6 +681,8 @@ class Widget extends React.Component { callback(this.elementRef?.current || null) + // this.props.onWidgetDragStart(this.elementRef?.current) + // Create custom drag image with full opacity, this will ensure the image isn't taken from part of the canvas const dragImage = this.elementRef?.current.cloneNode(true) dragImage.style.opacity = '1' // Ensure full opacity @@ -775,6 +847,7 @@ class Widget extends React.Component { // console.log("Dropped on meee: ", swapArea, this.swappableAreaRef.current.contains(e.target), thisContainer) this.props.onAddChildWidget({ + event: e, parentWidgetId: this.__id, dragElementID: draggedElement.getAttribute("data-widget-id"), swap: swapArea || false @@ -784,7 +857,11 @@ class Widget extends React.Component { // console.log("Dropped on Sidebar: ", this.__id) this.props.onCreateWidgetRequest(widgetClass, ({ id, widgetRef }) => { - this.props.onAddChildWidget({ parentWidgetId: this.__id, dragElementID: id }) // if dragged from the sidebar create the widget first + this.props.onAddChildWidget({ + event: e, + parentWidgetId: this.__id, + dragElementID: id + }) // if dragged from the sidebar create the widget first }) } @@ -811,6 +888,8 @@ class Widget extends React.Component { callback() this.setState({ isDragging: false }) this.enablePointerEvents() + + // this.props.onWidgetDragEnd(this.elementRef?.current) } disablePointerEvents = () => { @@ -854,6 +933,9 @@ class Widget extends React.Component { height: `${this.state.size.height}px`, opacity: this.state.isDragging ? 0.3 : 1, } + + // const boundingRect = this.getBoundingRect + // FIXME: if the parent container has tw-overflow-none, then the resizable indicator are also hidden return ( @@ -887,14 +969,14 @@ class Widget extends React.Component { >
{/* helps with swappable: if the mouse is in this area while hovering/dropping, then swap */}
@@ -916,13 +998,14 @@ class Widget extends React.Component { >
} - -
diff --git a/src/frameworks/tkinter/widgets/frame.js b/src/frameworks/tkinter/widgets/frame.js index 74ee13f..04b9ce7 100644 --- a/src/frameworks/tkinter/widgets/frame.js +++ b/src/frameworks/tkinter/widgets/frame.js @@ -26,7 +26,7 @@ class Frame extends Widget{ renderContent(){ return ( -
+
{this.props.children}
diff --git a/src/frameworks/tkinter/widgets/mainWindow.js b/src/frameworks/tkinter/widgets/mainWindow.js index fc0ccbc..f2e973f 100644 --- a/src/frameworks/tkinter/widgets/mainWindow.js +++ b/src/frameworks/tkinter/widgets/mainWindow.js @@ -65,7 +65,7 @@ class MainWindow extends Widget{
-
+
{this.props.children}