diff --git a/src/canvas/canvas.js b/src/canvas/canvas.js index 1ebe6f9..d410e26 100644 --- a/src/canvas/canvas.js +++ b/src/canvas/canvas.js @@ -130,7 +130,7 @@ class Canvas extends React.Component { this.initEvents() // NOTE: adding the transform will make the inner fixed position to be relative - // NOTE: this is needed to keep the resize widget poition correct in base widget + // NOTE: this is needed to keep the resize widget position correct in base widget this.applyTransform() } diff --git a/src/canvas/widgets/base.js b/src/canvas/widgets/base.js index 6718567..cb88991 100644 --- a/src/canvas/widgets/base.js +++ b/src/canvas/widgets/base.js @@ -1,6 +1,6 @@ import { v4 as uuidv4 } from 'uuid'; -import React from "react" +import React, { useEffect, useLayoutEffect, useRef, useState } from "react" import { NotImplementedError } from "../../utils/errors" import Tools from "../constants/tools" @@ -13,7 +13,6 @@ import EditableDiv from "../../components/editableDiv" import WidgetContainer from "../constants/containers" import { DragContext } from "../../components/draggable/draggableContext" import { isNumeric, removeKeyFromObject } from "../../utils/common" -import { info } from "autoprefixer" import { Layout, message } from "antd" @@ -21,7 +20,7 @@ import { Layout, message } from "antd" // FIXME: the drag drop indicator is not going invisible if the drop happens on the child -// FIXME: once the width and height is set to fit-content, it can no longer be resized +// FIXME: once the width and height is set to fit-content, it can no longer be resized (while resizing it shouldn't be fit-width and height instead it should show the actual width and height) // FIXME: if the label, buttons are dropped directly on canvas, the background colors don't apply @@ -209,10 +208,13 @@ class Widget extends React.Component { this.updateState = this.updateState.bind(this) this.stateUpdateCallback = null // allowing other components such as toolbar to subscribe to changes in this widget + this.resizeObserver = null + } componentDidMount() { + this.setLayout({layout: Layouts.FLEX, gap: 10}) // if (this.state.attrs.layout){ @@ -224,6 +226,32 @@ class Widget extends React.Component { this.setWidgetInnerStyle('backgroundColor', this.state.attrs.styling?.backgroundColor.value || "#fff") this.load(this.props.initialData || {}) // load the initial data + + // The elementRect is received only after the elemet is added so, it may not be accurate so use resize handler + // this.resizeObserver = new MutationObserver(this.handleResizeEvents) + // if (this.elementRef.current) { + // // this.resizeObserver.observe(this.elementRef.current, { attributes: true}) + // } + + + + } + + handleResizeEvents = () => { + if (!this.elementRef.current) return; + + const elementRect = this.elementRef.current.getBoundingClientRect(); + + const parentRect = this.props.parentWidgetRef?.current?.getBoundingRect() + + + const left = ((elementRect.left || 0) - (parentRect?.left || this.props.canvasRectInner?.left)) / this.props.canvasZoom; + const top = ((elementRect.top || 0) - (parentRect?.top || this.props.canvasRectInner?.top)) / this.props.canvasZoom; + + // const left = (elementRect?.left || 0) + // const top = (elementRect?.top || 0) + + this.setState({pos: { x: left, y: top }}); } componentDidUpdate(prevProps, prevState) { @@ -233,6 +261,10 @@ class Widget extends React.Component { } + componentWillUnmount(){ + // this.resizeObserver.disconnect() + } + // componentWillUnmount(){ // // TODO: serialize and store the widget data in setWidgets under widget context especially initialData // console.log("unmounting widget: ", this.state.attrs, this.serialize()) @@ -344,7 +376,6 @@ class Widget extends React.Component { forceRerender = () => { // this.forceUpdate() // Don't use forceUpdate widgets will loose their states this.setState({forceRerenderId: `${uuidv4()}`}) - console.log("rerender") } // TODO: add context menu items such as delete, add etc @@ -471,10 +502,6 @@ class Widget extends React.Component { return this.functions } - getId() { - return this.__id - } - getElement() { return this.elementRef.current } @@ -818,11 +845,20 @@ class Widget extends React.Component { */ serialize(){ // NOTE: when serializing make sure, you are only passing serializable objects not functions or other + + const elementRect = this.getBoundingRect() + + const pos = { + x: elementRect.x, + y: elementRect.y + } + return ({ zIndex: this.state.zIndex, selected: this.state.selected, widgetName: this.state.widgetName, - pos: this.state.pos, + // pos: this.state.pos, + pos: pos, size: this.state.size, widgetContainer: this.state.widgetContainer, widgetInnerStyling: this.state.widgetInnerStyling, @@ -904,7 +940,6 @@ class Widget extends React.Component { if (selected){ this.select() - console.log("selected again") } }) @@ -1249,6 +1284,20 @@ class Widget extends React.Component { // this.setState({ pos: {x: left, y: top} }); // } + /** + * + * @param {"sw"|"ne"|"se"|"nw"|null} side + */ + handleWidgetResize = (side) => { + if (side){ + this.props.onWidgetResizing(side) + this.setState({ dragEnabled: false }) + }else{ + this.setState({ dragEnabled: true }) + this.props.onWidgetResizing("") + } + } + /** * This is an internal methods don't override * @returns {HTMLElement} @@ -1302,10 +1351,8 @@ class Widget extends React.Component { // const boundingRect = this.getBoundingRect - const {zoom: canvasZoom, pan: canvasPan} = this.canvasMetaData + - const elementRect = this.elementRef.current?.getBoundingClientRect() - return ( @@ -1315,138 +1362,72 @@ class Widget extends React.Component { // const canvasRect = this.canvas.getBoundingClientRect() const canvasRectInner = this.props.canvasInnerContainerRef?.current?.getBoundingClientRect() - const elementRect = this.getBoundingRect() - const {zoom, pan} = this.props.canvasMetaData + const {zoom} = this.props.canvasMetaData - const left = ((elementRect?.left || 0) - canvasRectInner?.left) / canvasZoom - 10 - const top = ((elementRect?.top || 0) - canvasRectInner?.top) / canvasZoom - 10 - return ( +
this.handleDragOver(e, draggedElement)} - onDrop={(e) => {this.handleDropEvent(e, draggedElement, widgetClass, posMetaData); onDragEnd()}} + onDragOver={(e) => this.handleDragOver(e, draggedElement)} + onDrop={(e) => {this.handleDropEvent(e, draggedElement, widgetClass, posMetaData); onDragEnd()}} - onDragEnter={(e) => this.handleDragEnter(e, draggedElement, setOverElement)} - onDragLeave={(e) => this.handleDragLeave(e, draggedElement, overElement)} + onDragEnter={(e) => this.handleDragEnter(e, draggedElement, setOverElement)} + onDragLeave={(e) => this.handleDragLeave(e, draggedElement, overElement)} - onDragStart={(e) => this.handleDragStart(e, onDragStart)} - onDragEnd={(e) => this.handleDragEnd(onDragEnd)} + onDragStart={(e) => this.handleDragStart(e, onDragStart)} + onDragEnd={(e) => this.handleDragEnd(onDragEnd)} - // onPointerDown={setInitialPos} - onPointerDown={(e) => handleSetInitialPosition(e, setPosMetaData)} - > -
handleSetInitialPosition(e, setPosMetaData)} + > +
- > - -
- {this.renderContent()} -
- { - // show drop style on drag hover - draggedElement && this.state.showDroppableStyle.show && -
+
+ {this.renderContent()}
- } - -
+ { + // show drop style on drag hover + draggedElement && this.state.showDroppableStyle.show && +
{/* ${this.state.isDragging ? "tw-pointer-events-none" : "tw-pointer-events-auto"} */} - + `} + style={{ + width: "calc(100% + 10px)", + height: "calc(100% + 10px)", + }} + > +
+ } -
{ - e.stopPropagation() - e.preventDefault() - this.props.onWidgetResizing("nw") - this.setState({ dragEnabled: false }) - }} - onMouseUp={() => this.setState({ dragEnabled: true })} - /> -
{ - e.stopPropagation() - e.preventDefault() - this.props.onWidgetResizing("ne") - this.setState({ dragEnabled: false }) - }} - onMouseUp={() => this.setState({ dragEnabled: true })} - /> -
{ - e.stopPropagation() - e.preventDefault() - this.props.onWidgetResizing("sw") - this.setState({ dragEnabled: false }) - }} - onMouseUp={() => this.setState({ dragEnabled: true })} - /> -
{ - e.stopPropagation() - e.preventDefault() - this.props.onWidgetResizing("se") - this.setState({ dragEnabled: false }) - }} - onMouseUp={() => this.setState({ dragEnabled: true })} - /> - -
+
- -
-
) } } @@ -1458,5 +1439,138 @@ class Widget extends React.Component { } +/** + * + * a component that displays resize handles, don't remove this function and try to add it directly + * to base widget as with base widget getBoundingClient rect is always stale without useEffect + * + * // TODO: make this smoother + * + */ +function ResizeHandle({elementRef, show, canvasRect, + canvasZoom, onWidgetResizing, + widgetName, setWidgetName, enableRename + }){ + const [rect, setRect] = useState({ + left: 0, + top: 0, + width: 0, + height: 0 + }) + + const timeoutRef = useRef(null) + const isResizingRef = useRef(false) + + useLayoutEffect(() => { + if (!elementRef.current) + return + + const updateRect = () => { + const elementRect = elementRef.current?.getBoundingClientRect() + + if (!elementRect){ + return + } + + const left = ((elementRect.left || 0) - canvasRect.left) / canvasZoom - 10; + const top = ((elementRect.top || 0) - canvasRect.top) / canvasZoom - 10; + const width = (elementRect.width / canvasZoom) + 20; + const height = (elementRect.height / canvasZoom) + 20; + setRect({ left, top, width, height }); + } + + clearTimeout(timeoutRef.current); + + if (!isResizingRef.current) + timeoutRef.current = setTimeout(updateRect, 16); // ~60fps + + else{ + updateRect() + } + + }, [elementRef, canvasZoom, canvasRect]) + + const handleResizing = (side) => { + + onWidgetResizing(side) + + if (side){ + isResizingRef.current = true + }else{ + isResizingRef.current = false + } + + } + + return ( +
+ +
{/* ${this.state.isDragging ? "tw-pointer-events-none" : "tw-pointer-events-auto"} */} + + +
{ + e.stopPropagation() + e.preventDefault() + handleResizing("nw") + }} + onMouseUp={() => handleResizing(null)} + /> +
{ + e.stopPropagation() + e.preventDefault() + handleResizing("ne") + }} + onMouseUp={() => handleResizing(null)} + /> +
{ + e.stopPropagation() + e.preventDefault() + handleResizing("sw") + }} + onMouseUp={() => handleResizing(null)} + /> +
{ + e.stopPropagation() + e.preventDefault() + handleResizing("se") + }} + onMouseUp={() => handleResizing(null)} + /> + +
+ +
+ + ) +} + export default Widget \ No newline at end of file diff --git a/src/frameworks/tkinter/widgets/base.js b/src/frameworks/tkinter/widgets/base.js index d373798..d156f67 100644 --- a/src/frameworks/tkinter/widgets/base.js +++ b/src/frameworks/tkinter/widgets/base.js @@ -18,8 +18,6 @@ export class TkinterBase extends Widget { this.getLayoutCode = this.getLayoutCode.bind(this) - console.log("constructor 1: ", this.__id, this.state) - this.state = { ...this.state, packAttrs: { @@ -177,47 +175,27 @@ export class TkinterBase extends Widget { flexManager: { label: "Pack Manager", display: "horizontal", - side: { - label: "Align Side", - tool: Tools.SELECT_DROPDOWN, - options: ["left", "right", "top", "bottom", ""].map(val => ({value: val, label: val})), - value: this.state.packAttrs.side, - onChange: (value) => { - // FIXME: force parent rerender because, here only child get rerendered, if only parent get rerendered the widget would move - this.setAttrValue("flexManager.side", value, () => { - this.updateState((prevState) => ({packAttrs: {...prevState.packAttrs, side: value}}), () => { - console.log("parent: ",this.props.parentWidgetRef.current.__id) - - this.props.requestWidgetDataUpdate(this.props.parentWidgetRef.current.__id) - this.stateChangeSubscriberCallback() // call this to notify the toolbar that the widget has changed state - // this.props.parentWidgetRef.current.forceRerender() - }) - }) - - - // console.log("updateing state: ", value, this.props.parentWidgetRef.current) - } - }, - anchor: { - label: "Anchor", - tool: Tools.SELECT_DROPDOWN, - options: ["nw", "ne", "sw", "se", "center"].map(val => ({value: val, label: val})), - value: this.state.packAttrs.anchor, - onChange: (value) => { - this.setAttrValue("flexManager.anchor", value, () => { - console.log("anchor updated") - this.updateState((prevState) => ({packAttrs: {...prevState.packAttrs, anchor: value}}), () => { + + // anchor: { + // label: "Anchor", + // tool: Tools.SELECT_DROPDOWN, + // options: ["nw", "ne", "sw", "se", "center"].map(val => ({value: val, label: val})), + // value: this.state.packAttrs.anchor, + // onChange: (value) => { + // this.setAttrValue("flexManager.anchor", value, () => { + // console.log("anchor updated") + // this.updateState((prevState) => ({packAttrs: {...prevState.packAttrs, anchor: value}}), () => { - this.props.requestWidgetDataUpdate(this.props.parentWidgetRef.current.__id) + // this.props.requestWidgetDataUpdate(this.props.parentWidgetRef.current.__id) - // this.props.requestWidgetDataUpdate(this.__id) - this.stateChangeSubscriberCallback() // call this to notify the toolbar that the widget has changed state - // this.props.parentWidgetRef.current.forceRerender() - }) - // this.props.parentWidgetRef.current.forceRerender() - }) - } - }, + // // this.props.requestWidgetDataUpdate(this.__id) + // this.stateChangeSubscriberCallback() // call this to notify the toolbar that the widget has changed state + // // this.props.parentWidgetRef.current.forceRerender() + // }) + // // this.props.parentWidgetRef.current.forceRerender() + // }) + // } + // }, fillX: { label: "Fill X", tool: Tools.CHECK_BUTTON, @@ -249,22 +227,40 @@ export class TkinterBase extends Widget { })) } }, - expand: { - label: "Expand", - tool: Tools.CHECK_BUTTON, - value: false, + side: { + label: "Align Side", + tool: Tools.SELECT_DROPDOWN, + options: ["left", "right", "top", "bottom", ""].map(val => ({value: val, label: val})), + value: this.state.packAttrs.side, onChange: (value) => { - this.setAttrValue("flexManager.expand", value) - - const widgetStyle = { - ...this.state.widgetOuterStyling, - flexGrow: value ? 1 : 0, - } - this.updateState({ - widgetOuterStyling: widgetStyle, + this.setAttrValue("flexManager.side", value, () => { + this.updateState((prevState) => ({packAttrs: {...prevState.packAttrs, side: value}}), () => { + this.props.requestWidgetDataUpdate(this.props.parentWidgetRef.current.__id) + this.stateChangeSubscriberCallback() // call this to notify the toolbar that the widget has changed state + // this.props.parentWidgetRef.current.forceRerender() + }) }) + + + // console.log("updateing state: ", value, this.props.parentWidgetRef.current) } }, + // expand: { + // label: "Expand", + // tool: Tools.CHECK_BUTTON, + // value: false, + // onChange: (value) => { + // this.setAttrValue("flexManager.expand", value) + + // const widgetStyle = { + // ...this.state.widgetOuterStyling, + // flexGrow: value ? 1 : 0, + // } + // this.updateState({ + // widgetOuterStyling: widgetStyle, + // }) + // } + // }, } } @@ -381,6 +377,7 @@ export class TkinterBase extends Widget { } getFlexLayoutStyle = (side, anchor) => { + // NOTE: may no longer be required // let baseStyle = { display: "flex", width: "100%", height: "100%", ...this.getPackAnchorStyle(anchor) } let baseStyle = { } @@ -417,6 +414,7 @@ export class TkinterBase extends Widget { * @param {*} anchor */ getPackAnchorStyle = (anchor, isColumn) => { + // NOTE: may no longer be required const styleMap = { nw: { justifyContent: "flex-start", alignItems: "flex-start" }, ne: { justifyContent: "flex-end", alignItems: "flex-start" }, @@ -455,9 +453,7 @@ export class TkinterBase extends Widget { * @returns */ renderPackWidgetsRecursively = (widgets, index = 0, lastSide="") => { - console.log("widgets: ", widgets, index) - //FIXME: when the first element is left, the second is top the second should also have its own container if (index >= widgets.length) return null @@ -510,22 +506,13 @@ export class TkinterBase extends Widget { flexDirection: currentWidgetDirection, width: "100%", height: "100%" - //TODO: if flex direction is top then width is 100% else height }}>
{widget}
- - {/*
-
*/} {this.renderPackWidgetsRecursively(widgets, index + 1, side)} - {/* FIXME: why is the pack widgets recursively outside container? */}
); @@ -661,9 +648,6 @@ export class TkinterBase extends Widget { data = {...data} // create a shallow copy - console.log("data reloaded: ", data) - - const {attrs={}, selected, pos={x: 0, y: 0}, parentLayout=null, ...restData} = data let layoutUpdates = { @@ -725,7 +709,6 @@ export class TkinterBase extends Widget { if (selected){ this.select() - console.log("selected again") } }) diff --git a/src/utils/hooks.js b/src/utils/hooks.js new file mode 100644 index 0000000..8f6bed6 --- /dev/null +++ b/src/utils/hooks.js @@ -0,0 +1,36 @@ +import { useLayoutEffect, useCallback, useState } from 'react'; + +export const useRect = (ref) => { + + const [rect, setRect] = useState({ width: 0, height: 0, top: 0, left: 0, right: 0, bottom: 0 }); + + const updateRect = useCallback(() => { + if (ref.current) { + setRect(ref.current.getBoundingClientRect()); + } + }, [ref]); + + useLayoutEffect(() => { + if (!ref.current) return; + + const timeout = setTimeout(updateRect, 0); // Delay to next event loop + + const observer = new ResizeObserver(updateRect); + observer.observe(ref.current); + + return () => { + observer.disconnect() + clearTimeout(timeout) + } + }, [updateRect, ref]); + + return rect; +}; + +/** + * Higher order component to make use of the useRect + */ +export function WithRect({ forwardedRef, children }) { + const rect = useRect(forwardedRef) + return children(rect) +} \ No newline at end of file