diff --git a/README.md b/README.md index 49f4b19..1cdef42 100644 --- a/README.md +++ b/README.md @@ -1 +1,15 @@ -# TkBuilder +# PyUIBuilder - The only Python GUI builder you'll ever need + + + + + + + +## Features +* Framework agnostic - Can outputs code in multiple frameworks. +* Easy to use. +* Plugins to extend 3rd party UI libraries +* Generate Code. + +## Roadmap diff --git a/notes.md b/notes.md new file mode 100644 index 0000000..e0c1c09 --- /dev/null +++ b/notes.md @@ -0,0 +1 @@ +### State management in react is a f*king mess \ No newline at end of file diff --git a/src/App.js b/src/App.js index d5389e1..aeeca5a 100644 --- a/src/App.js +++ b/src/App.js @@ -13,6 +13,7 @@ import WidgetsContainer from './sidebar/widgetsContainer' import Widget from './canvas/widgets/base' import { DraggableWidgetCard } from './components/cards' import { DragProvider } from './components/draggable/draggableContext' +import { ActiveWidgetProvider } from './canvas/activeWidgetContext' function App() { @@ -141,7 +142,10 @@ function App() {
+ + {/* */} + {/* */}
{/* dragOverlay (dnd-kit) helps move items from one container to another */} diff --git a/src/canvas/activeWidgetContext.js b/src/canvas/activeWidgetContext.js new file mode 100644 index 0000000..f82ed90 --- /dev/null +++ b/src/canvas/activeWidgetContext.js @@ -0,0 +1,61 @@ +import React, { createContext, Component, useContext, useState, createRef, useMemo } from 'react' + + + +// NOTE: using this context provider causes many re-rendering when the canvas is panned the toolbar updates unnecessarily + + +// Create the Context +export const ActiveWidgetContext = createContext() + +// Create the Provider component +export class ActiveWidgetProvider extends Component { + state = { + activeWidgetId: null, + activeWidgetAttrs: {} + } + + updateActiveWidget = (widget) => { + this.setState({ activeWidgetId: widget }) + } + + updateToolAttrs = (widgetAttrs) => { + this.setState({activeWidgetAttrs: widgetAttrs}) + } + + render() { + return ( + + {this.props.children} + + ); + } +} + +// Custom hook for function components +export const useActiveWidget = () => { + const context = useContext(ActiveWidgetContext) + if (context === undefined) { + throw new Error('useActiveWidget must be used within an ActiveWidgetProvider') + } + return useMemo(() => context, [context.activeWidgetId, context.activeWidgetAttrs, context.updateToolAttrs, context.updateActiveWidget]) +} + +// Higher-Order Component for class-based components +export const withActiveWidget = (WrappedComponent) => { + return class extends Component { + + render() { + return ( + + {context => } + + ) + } + } +} + + + diff --git a/src/canvas/canvas.js b/src/canvas/canvas.js index 453ec94..86ad45e 100644 --- a/src/canvas/canvas.js +++ b/src/canvas/canvas.js @@ -19,6 +19,7 @@ import { WidgetContext } from './context/widgetContext' import DotsBackground from "../assets/background/dots.svg" import DroppableWrapper from "../components/draggable/droppable" +import { ActiveWidgetContext, ActiveWidgetProvider, withActiveWidget } from "./activeWidgetContext" // const DotsBackground = require("../assets/background/dots.svg") @@ -31,6 +32,8 @@ const CanvasModes = { class Canvas extends React.Component { + // static contextType = ActiveWidgetContext + constructor(props) { super(props) @@ -76,6 +79,8 @@ class Canvas extends React.Component { this.mouseDownEvent = this.mouseDownEvent.bind(this) this.mouseMoveEvent = this.mouseMoveEvent.bind(this) this.mouseUpEvent = this.mouseUpEvent.bind(this) + this.keyDownEvent = this.keyDownEvent.bind(this) + this.wheelZoom = this.wheelZoom.bind(this) this.onActiveWidgetUpdate = this.onActiveWidgetUpdate.bind(this) @@ -107,15 +112,42 @@ class Canvas extends React.Component { componentWillUnmount() { + this.canvasContainerRef.current.removeEventListener("mousedown", this.mouseDownEvent) + this.canvasContainerRef.current.removeEventListener("mouseup", this.mouseUpEvent) + this.canvasContainerRef.current.removeEventListener("mousemove", this.mouseMoveEvent) + this.canvasContainerRef.current.removeEventListener("wheel", this.wheelZoom) + + this.canvasContainerRef.current.removeEventListener("keydown", this.keyDownEvent) + + // NOTE: this will clear the canvas this.clearCanvas() } - /** + initEvents(){ + + this.canvasContainerRef.current.addEventListener("mousedown", this.mouseDownEvent) + this.canvasContainerRef.current.addEventListener("mouseup", this.mouseUpEvent) + this.canvasContainerRef.current.addEventListener("mousemove", this.mouseMoveEvent) + this.canvasContainerRef.current.addEventListener("wheel", this.wheelZoom) + + + this.canvasContainerRef.current.addEventListener("keydown", this.keyDownEvent, true) + // window.addEventListener("keydown", this.keyDownEvent, true) + + + } + + applyTransform(){ + const { currentTranslate, zoom } = this.state + this.canvasRef.current.style.transform = `translate(${currentTranslate.x}px, ${currentTranslate.y}px) scale(${zoom})` + } + + /** * * @returns {import("./widgets/base").Widget[]} */ - getWidgets(){ + getWidgets(){ return this.state.widgets } @@ -130,22 +162,6 @@ class Canvas extends React.Component { }) } - initEvents(){ - - this.canvasContainerRef.current.addEventListener("mousedown", this.mouseDownEvent) - this.canvasContainerRef.current.addEventListener("mouseup", this.mouseUpEvent) - this.canvasContainerRef.current.addEventListener("mousemove", this.mouseMoveEvent) - - this.canvasContainerRef.current.addEventListener('wheel', (event) => { - this.wheelZoom(event) - }) - - } - - applyTransform(){ - const { currentTranslate, zoom } = this.state - this.canvasRef.current.style.transform = `translate(${currentTranslate.x}px, ${currentTranslate.y}px) scale(${zoom})` - } /** * returns the widget that contains the target @@ -155,6 +171,7 @@ class Canvas extends React.Component { getWidgetFromTarget(target){ for (let [key, ref] of Object.entries(this.widgetRefs)){ + // console.log("ref: ", ref) if (ref.current.getElement().contains(target)){ return ref.current } @@ -162,6 +179,21 @@ class Canvas extends React.Component { } + keyDownEvent(event){ + + if (event.key === "Delete"){ + this.deleteSelectedWidgets() + } + + if (event.key === "+"){ + this.setZoom(this.state.zoom + 0.1) + } + + if (event.key === "-"){ + this.setZoom(this.state.zoom - 0.1) + } + + } mouseDownEvent(event){ @@ -190,6 +222,10 @@ class Canvas extends React.Component { selectedWidgets: [selectedWidget], toolbarAttrs: selectedWidget.getToolbarAttrs() }) + + this.context.updateActiveWidget(selectedWidget.__id) + this.context.updateToolAttrs(selectedWidget.getToolbarAttrs()) + // this.props.updateActiveWidget(selectedWidget) } this.currentMode = CanvasModes.MOVE_WIDGET } @@ -302,18 +338,6 @@ class Canvas extends React.Component { return this.state.currentTranslate } - /** - * Given a position relative to canvas container, - * returns the position relative to the canvas - */ - getRelativePositionToCanvas(x, y){ - - const canvasRect = this.canvasRef.current.getBoundingClientRect() - let zoom = this.state.zoom - - return {x: (canvasRect.left - x ), y: (canvasRect.top - y)} - } - /** * fits the canvas size to fit the widgets bounding box */ @@ -341,32 +365,30 @@ class Canvas extends React.Component { this.canvasContainerRef.current.style.cursor = cursor } - setZoom(zoom, pos={x:0, y:0}){ + setZoom(zoom, pos){ - // if (zoom < 0.5 || zoom > 2){ - // return - // } - const { currentTranslate } = this.state + let newTranslate = currentTranslate + + if (pos){ + // Calculate the new translation to zoom into the mouse position + const offsetX = pos.x - (this.canvasContainerRef.current.clientWidth / 2 + currentTranslate.x) + const offsetY = pos.y - (this.canvasContainerRef.current.clientHeight / 2 + currentTranslate.y) - // Calculate the new translation to zoom into the mouse position - const offsetX = pos.x - (this.canvasContainerRef.current.clientWidth / 2 + currentTranslate.x) - const offsetY = pos.y - (this.canvasContainerRef.current.clientHeight / 2 + currentTranslate.y) - - const newTranslateX = currentTranslate.x - offsetX * (zoom - this.state.zoom) - const newTranslateY = currentTranslate.y - offsetY * (zoom - this.state.zoom) - - this.setState({ - zoom: Math.max(0.5, Math.min(zoom, 1.5)), // clamp between 0.5 and 1.5 - currentTranslate: { + const newTranslateX = currentTranslate.x - offsetX * (zoom - this.state.zoom) + const newTranslateY = currentTranslate.y - offsetY * (zoom - this.state.zoom) + newTranslate = { x: newTranslateX, y: newTranslateY } + } + + this.setState({ + zoom: Math.max(0.5, Math.min(zoom, 1.5)), // clamp between 0.5 and 1.5 + currentTranslate: newTranslate }, this.applyTransform) - // this.canvasRef.current.style.width = `${100/zoom}%` - // this.canvasRef.current.style.height = `${100/zoom}%` } @@ -390,6 +412,9 @@ class Canvas extends React.Component { widget.current?.deSelect() }) + this.context?.updateActiveWidget("") + this.context.updateToolAttrs({}) + this.setState({ selectedWidgets: [], toolbarAttrs: null, @@ -433,7 +458,6 @@ class Canvas extends React.Component { // Store the ref in the instance variable this.widgetRefs[id] = widgetRef - // console.log("widget ref: ", this.widgetRefs) const widgets = [...this.state.widgets, { id, widgetType: widgetComponentType }] // don't add the widget refs in the state // Update the state to include the new widget's type and ID @@ -452,6 +476,10 @@ class Canvas extends React.Component { return {id, widgetRef} } + /** + * delete's the selected widgets from the canvas + * @param {null|Widget} widgets - optional widgets that can be deleted along the selected widgets + */ deleteSelectedWidgets(widgets=[]){ @@ -519,7 +547,9 @@ class Canvas extends React.Component { if (this.state.selectedWidgets.length === 0 || widgetId !== this.state.selectedWidgets[0].__id) return - // console.log("updating...") + console.log("updating...", this.state.toolbarAttrs, this.state.selectedWidgets.at(0).getToolbarAttrs()) + + // console.log("attrs: ", this.state.selectedWidgets.at(0).getToolbarAttrs()) this.setState({ toolbarAttrs: this.state.selectedWidgets.at(0).getToolbarAttrs() @@ -575,36 +605,39 @@ class Canvas extends React.Component { + {/* */} - {/* */} -
- {/* Canvas */} -
-
- { - this.state.widgets.map(this.renderWidget) - } -
+ +
+ {/* Canvas */} +
+
+ { + this.state.widgets.map(this.renderWidget) + }
- {/* */} +
+
+ {/* */}
) } diff --git a/src/canvas/toolbar.js b/src/canvas/toolbar.js index b5a3d6d..af8fca2 100644 --- a/src/canvas/toolbar.js +++ b/src/canvas/toolbar.js @@ -4,6 +4,7 @@ import { ColorPicker, Input, InputNumber, Select } from "antd" import { capitalize } from "../utils/common" import Tools from "./constants/tools.js" +import { useActiveWidget } from "./activeWidgetContext.js" // FIXME: Maximum recursion error @@ -16,6 +17,9 @@ import Tools from "./constants/tools.js" */ const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => { + // const { activeWidgetAttrs } = useActiveWidget() + + // console.log("active widget context: ", activeWidgetAttrs) const [toolbarOpen, setToolbarOpen] = useState(isOpen) const [toolbarAttrs, setToolbarAttrs] = useState(attrs) @@ -24,11 +28,19 @@ const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => { }, [isOpen]) useEffect(() => { + console.log("active widget: ", attrs) setToolbarAttrs(attrs) }, [attrs]) + // useEffect(() => { + + // console.log("active widget: ", activeWidgetAttrs) + // setToolbarAttrs(activeWidgetAttrs || {}) + + // }, [activeWidgetAttrs]) const handleChange = (value, callback) => { + console.log("changed...") if (callback) { callback(value) } @@ -42,14 +54,14 @@ const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => { const isFirstLevel = parentKey === "" const outerLabelClass = isFirstLevel - ? "tw-text-lg tw-text-blue-700 tw-font-medium" - : "tw-text-lg" + ? "tw-text-base tw-text-blue-700 tw-font-medium" + : "tw-text-base" // Render tool widgets if (typeof val === "object" && val.tool) { return (
-
{val.label}
+
{val.label}
{val.tool === Tools.INPUT && ( this.setWidgetStyling("backgroundColor", value) + onChange: (value) => { + this.setWidgetStyling("backgroundColor", value) + this.setAttrValue("styling.backgroundColor", value) + } }, foregroundColor: { label: "Foreground Color", @@ -145,13 +154,33 @@ class Widget extends React.Component { this.canvas.addEventListener("mouseup", this.stopResizing) } + componentDidUpdate(prevProps, prevState) { + // if (prevState !== this.state) { + // // State has changed + // console.log('State has been updated') + // } else { + // // State has not changed + // console.log('State has not changed') + // } + } + updateState = (newState, callback) => { + + // FIXME: maximum recursion error when updating size this.setState(newState, () => { + const { onWidgetUpdate } = this.props if (onWidgetUpdate) { onWidgetUpdate(this.__id) } + + // const { activeWidgetId, updateToolAttrs } = this.context + + // if (activeWidgetId === this.__id) + // updateToolAttrs(this.getToolbarAttrs()) + if (callback) callback() + }) } @@ -174,7 +203,7 @@ class Widget extends React.Component { onChange: (value) => this.setWidgetName(value) }, size: { - label: "Sizing", + label: "Size", display: "horizontal", width: { label: "Width", @@ -228,16 +257,6 @@ class Widget extends React.Component { mousePress(event) { // event.preventDefault() if (!this._disableSelection) { - - // const widgetSelected = new CustomEvent("selection:created", { - // detail: { - // event, - // id: this.__id, - // element: this - // }, - // // bubbles: true // Allow the event to bubble up the DOM tree - // }) - // this.canvas.dispatchEvent(widgetSelected) } } @@ -264,13 +283,14 @@ class Widget extends React.Component { // don't change position when resizing the widget return } - // this.setState({ - // pos: { x, y } - // }) - this.updateState({ + this.setState({ pos: { x, y } }) + + // this.updateState({ + // pos: { x, y } + // }) } setParent(parentId) { @@ -303,38 +323,6 @@ class Widget extends React.Component { return this.state.size } - getScaleAwareDimensions() { - // Get the bounding rectangle - const rect = this.elementRef.current.getBoundingClientRect() - - // Get the computed style of the element - const style = window.getComputedStyle(this.elementRef.current) - - // Get the transform matrix - const transform = style.transform - - // Extract scale factors from the matrix - let scaleX = 1 - let scaleY = 1 - - if (transform && transform !== 'none') { - // For 2D transforms (a, c, b, d) - const matrix = transform.match(/^matrix\(([^,]+),[^,]+,([^,]+),[^,]+,[^,]+,[^,]+\)$/); - - if (matrix) { - scaleX = parseFloat(matrix[1]) - scaleY = parseFloat(matrix[2]) - } - } - - // Return scaled width and height - return { - width: rect.width / scaleX, - height: rect.height / scaleY - } - } - - getWidgetFunctions() { return this.functions } @@ -400,7 +388,7 @@ class Widget extends React.Component { * @param {string} value - Value of the style * @param {function():void} [callback] - optional callback, thats called after setting the internal state */ - setWidgetStyling(key, value, callback) { + setWidgetStyling(key, value) { const widgetStyle = { ...this.state.widgetStyling, @@ -409,9 +397,6 @@ class Widget extends React.Component { this.setState({ widgetStyling: widgetStyle - }, () => { - if (callback) - callback(widgetStyle) }) } @@ -420,28 +405,15 @@ class Widget extends React.Component { * * @param {number|null} width * @param {number|null} height - * @param {function():void} [callback] - optional callback, thats called after setting the internal state */ - setWidgetSize(width, height, callback) { + setWidgetSize(width, height) { const newSize = { width: Math.max(this.minSize.width, Math.min(width || this.state.size.width, this.maxSize.width)), height: Math.max(this.minSize.height, Math.min(height || this.state.size.height, this.maxSize.height)), } - - this.setState({ - size: newSize - }, () => { - if (callback) { - callback(newSize) - } - }) - this.updateState({ size: newSize - }, () => { - if (callback) - callback(newSize) }) } @@ -530,9 +502,6 @@ class Widget extends React.Component { */ render() { - const widgetStyle = this.state.widgetStyling - - let outerStyle = { cursor: this.cursor, zIndex: this.state.zIndex, @@ -552,51 +521,50 @@ class Widget extends React.Component { // console.log("selected: ", this.state.selected) return ( +
-
+ {this.renderContent()} +
- {this.renderContent()} -
+
+ + +
this.startResizing("nw", e)} + /> +
this.startResizing("ne", e)} + /> +
this.startResizing("sw", e)} + /> +
this.startResizing("se", e)} + /> + +
-
- -
this.startResizing("nw", e)} - /> -
this.startResizing("ne", e)} - /> -
this.startResizing("sw", e)} - /> -
this.startResizing("se", e)} - />
- - -
-
) } diff --git a/src/components/cards.js b/src/components/cards.js index 082933e..7bb22e8 100644 --- a/src/components/cards.js +++ b/src/components/cards.js @@ -32,11 +32,11 @@ export function DraggableWidgetCard({ name, img, url, innerRef}){ // -
-
+
{name}
{name}