fixed anchor and side for flex layout

This commit is contained in:
paul
2025-03-26 15:08:25 +05:30
parent 92a967721a
commit c177a16b33
5 changed files with 312 additions and 191 deletions

View File

@@ -10,7 +10,7 @@ Let's start with the basics of UI
![Layout basics](./assets/basics.jpg)
1. The sidebar on the left will have multiple buttons, each button will provide you with necessary tools.
1. The sidebar on the left will have multiple tabs, each tabs will provide you with necessary tools.
2. The Place where you drag and drop widgets is the canvas
3. The toolbar will only appear if a widget is selected.
@@ -26,7 +26,7 @@ Things you can do on canvas.
4. Delete widgets using `del` key or right clicking on the widget
## Project name
By default all project's are named untitled project, you can change this from the header input next to export code.
By default all project's are named `"untitled project"`, you can change this from the header input next to export code.
## Selecting a UI library
You can select the UI library from the header dropdown. Once selected changing the UI library in between your work, will erase the canvas.

View File

@@ -1259,30 +1259,37 @@ class Canvas extends React.Component {
}
updateWidgetAndChildren = (widgetId) => {
const serializeWidgetRecursively = (widget) => {
const widgetObj = this.getWidgetById(widget.id)?.current;
if (!widgetObj) return widget; // If no widget reference found, return unchanged
return {
...widget,
initialData: {
...widget.initialData,
...widgetObj.serialize()
},
children: widget.children?.map(serializeWidgetRecursively) || [] // Recursively serialize children
const serializeWidgetRecursively = (widget) => {
const widgetObj = this.getWidgetById(widget.id)?.current;
if (!widgetObj) return widget; // If no widget reference found, return unchanged
return {
...widget,
initialData: {
...widget.initialData,
...widgetObj.serialize()
},
children: widget.children?.map(serializeWidgetRecursively) || [] // Recursively serialize children
};
};
};
this.setWidgets(prevWidgets => {
const updateWidgets = (widgets) => {
return widgets.map(widget =>
widget.id === widgetId ? serializeWidgetRecursively(widget) : widget
);
};
return widgets.map(widget => {
if (widget.id === widgetId) {
return serializeWidgetRecursively(widget)
}
// Search inside children recursively
return {
...widget,
children: updateWidgets(widget.children || [])
}
})
}
return updateWidgets(prevWidgets);
});
};
return updateWidgets(prevWidgets)
})
}
renderWidget = (widget) => {

View File

@@ -136,7 +136,7 @@ class Widget extends React.Component {
label: "Layout",
tool: Tools.LAYOUT_MANAGER, // the tool to display, can be either HTML ELement or a constant string
value: {
layout: "flex",
layout: Layouts.PLACE,
direction: "row",
// grid: {
// rows: 12,
@@ -243,6 +243,7 @@ class Widget extends React.Component {
}
componentDidUpdate(prevProps, prevState) {
if (prevProps !== this.props) {
this.canvasMetaData = this.props.canvasMetaData
}
@@ -704,7 +705,7 @@ class Widget extends React.Component {
getLayout(){
return this.state?.attrs?.layout?.value || Layouts.FLEX
return this.getAttrValue("layout") || Layouts.PLACE
}
setLayout(value) {
@@ -734,13 +735,13 @@ class Widget extends React.Component {
// widgetStyle["placeContent"] = "unset"
// }
this.updateState({
widgetInnerStyling: widgetStyle
this.setAttrValue("layout", value, () => {
this.updateState({
widgetInnerStyling: widgetStyle
})
this.props.onLayoutUpdate({parentId: this.__id, parentLayout: value})// inform children about the layout update
})
this.setAttrValue("layout", value)
this.props.onLayoutUpdate({parentId: this.__id, parentLayout: value})// inform children about the layout update
}
getWidgetInnerStyle = (key) => {

View File

@@ -8,7 +8,7 @@ import { convertObjectToKeyValueString, isNumeric, removeKeyFromObject } from ".
import { randomArrayChoice } from "../../../utils/random"
import { Tkinter_TO_WEB_CURSOR_MAPPING } from "../constants/cursor"
import { Tkinter_To_GFonts } from "../constants/fontFamily"
import { GRID_STICKY, JUSTIFY, RELIEF } from "../constants/styling"
import { ANCHOR, GRID_STICKY, JUSTIFY, RELIEF } from "../constants/styling"
// FIXME: grid sticky may clash with flex sticky when changing layout, check it once
@@ -27,7 +27,7 @@ export class TkinterBase extends Widget {
...this.state,
packAttrs: { // This is required as during flex layout change remount happens and the state updates my not function as expected
side: "top",
anchor: "nw",
anchor: "n",
}
}
@@ -68,6 +68,12 @@ export class TkinterBase extends Widget {
const packSide = this.getAttrValue("flexManager.side")
const marginX = this.getAttrValue("margin.marginX")
const marginY = this.getAttrValue("margin.marginY")
const paddingX = this.getAttrValue("padding.padX")
const paddingY = this.getAttrValue("padding.padY")
const config = {}
if (packSide === "" || packSide === "top"){
@@ -86,11 +92,27 @@ export class TkinterBase extends Widget {
config['side'] = `tk.BOTTOM`
}
if (gap > 0){
config["padx"] = gap
config["pady"] = gap
// if (gap > 0){
// config["padx"] = gap
// config["pady"] = gap
// }
if (marginX){
config["padx"] = marginX
}
if (marginY){
config["pady"] = marginY
}
if (paddingX){
config["ipadx"] = paddingX
}
if (paddingY){
config["ipady"] = paddingY
}
// if (align === "start"){
// config["anchor"] = "'nw'"
// }else if (align === "center"){
@@ -283,27 +305,6 @@ export class TkinterBase extends Widget {
flexManager: {
label: "Pack Manager",
display: "horizontal",
// 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.__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,
@@ -366,6 +367,26 @@ export class TkinterBase extends Widget {
// this.setWidgetOuterStyle(value ? 1 : 0)
}
},
anchor: {
label: "Anchor",
tool: Tools.SELECT_DROPDOWN,
options: ANCHOR.map(val => ({value: val, label: val})),
value: this.state.packAttrs.anchor,
onChange: (value) => {
this.setAttrValue("flexManager.anchor", value, () => {
console.log("set anchor: ", this.state.attrs.flexManager)
// this.props.parentWidgetRef.current.forceRerender()
})
this.updateState((prevState) => ({packAttrs: {...prevState.packAttrs, anchor: value}}), () => {
// 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()
})
}
},
}
}
@@ -582,106 +603,112 @@ export class TkinterBase extends Widget {
* @param {*} index
* @returns
*/
renderPackWidgetsRecursively = (widgets, index = 0, lastSide="", previousExpandValue=0) => {
if (index >= widgets.length) return null
const widget = widgets[index]
const widgetRef = widget.ref?.current
if (!widgetRef) return null // Ensure ref exists before accessing
const side = widgetRef.getPackAttrs()?.side || "top"
const expand = widgetRef.getPackAttrs()?.expand || false
console.log("rerendering; ", side, expand)
const direction = (s) => {
return (s === "bottom"
? "column-reverse"
: s === "top"
? "column"
: s === "right"
? "row-reverse"
: "row")
renderPackWidgetsRecursively = (widgets, index = 0, lastSide = "", previousExpandValue = 0) => {
if (index >= widgets.length) return null;
const widget = widgets[index];
const widgetRef = widget.ref?.current;
if (!widgetRef) return null; // Ensure ref exists before accessing
const { side = "top", expand = false, anchor } = widgetRef.getPackAttrs() || {};
console.log("rerendering:", side, expand);
const directionMap = {
top: "column",
bottom: "column-reverse",
left: "row",
right: "row-reverse",
}
const currentWidgetDirection = direction(side)
const isSameSide = lastSide === side
const currentWidgetDirection = directionMap[side] || "column"; // Default to "column"
const isSameSide = lastSide === side;
const isVertical = ["top", "bottom"].includes(side);
let expandValue = expand ? (isSameSide ? previousExpandValue : widgets.length - index) : 1;
if (expand && !isSameSide) previousExpandValue = expandValue;
lastSide = side; // Update last side for recursion
let expandValue = 0 // the first element will be given highest priority when expanding if expand is True
if (expand){
if (isSameSide){
expandValue = previousExpandValue // if its the same side then its value is same as the previous else widget length - index
}else{
expandValue = widgets.length - index
previousExpandValue = expandValue
}
}
// 🟢 Mapping Tkinter anchors to Flexbox styles
const anchorStyles = {
n: { alignItems: "flex-start", justifyContent: "center" }, // Top-center
s: { alignItems: "flex-end", justifyContent: "center" }, // Bottom-center
e: { alignItems: "center", justifyContent: "flex-end" }, // Right-center
w: { alignItems: "center", justifyContent: "flex-start" }, // Left-center
ne: { alignItems: "flex-start", justifyContent: "flex-end" }, // Top-right
nw: { alignItems: "flex-start", justifyContent: "flex-start" }, // Top-left
se: { alignItems: "flex-end", justifyContent: "flex-end" }, // Bottom-right
sw: { alignItems: "flex-end", justifyContent: "flex-start" }, // Bottom-left
center: { alignItems: "center", justifyContent: "center" }, // Fully centered
};
const { justifyContent, alignItems } = anchorStyles[anchor] || anchorStyles["n"]
lastSide = side; // Update last side for next recursion
// console.log("current widget direction: ", isSameSide, currentWidgetDirection)
const stretchClass = isVertical ? "tw-flex-grow" : "tw-h-full"; // Allow only horizontal growth for top/bottom
if (isSameSide) {
return (
<>
{/* <div style={{
display: "flex",
flexDirection: direction,
width: "100%"
}}>{widget}</div> */}
<div className="tw-flex tw-justify-center tw-items-center "
<div
className={`tw-flex tw-justify-center tw-items-center ${stretchClass}`}
style={{
flexGrow: expandValue,
// flexShrink: 0, // Prevent collapsing
flexGrow: expand ? expandValue : 0, // Prevent vertical stretching
flexShrink: expand ? 0 : 1, // Prevent collapse when expanding
flexBasis: "auto",
minWidth: isVertical ? "0" : "auto",
minHeight: !isVertical ? "0" : "auto",
// alignSelf,
// justifySelf,
alignItems,
justifyContent
}}
>
>
{widget}
</div>
{/* <WidgetOuter key={index}>{widget}</WidgetOuter> */}
{this.renderPackWidgetsRecursively(widgets, index + 1, side, previousExpandValue)}
</>
)
}
// FIXME: side and expand aren't working together
// If next widget has a different side, create a new container for it
return (
<div data-pack-container={side}
// className={`${expand ? "tw-h-full tw-w-full" : ""}
// `}
);
}
return (
<div
data-pack-container={side}
className={`tw-flex ${isVertical ? "!tw-h-full" : "!tw-w-full"}`}
style={{
display: "flex",
flexDirection: currentWidgetDirection,
flexGrow: expand ? expandValue : 1, //((widgets.length - 1) === index) ? 1 : 0, // last index will always have flex-grow 1
flexShrink: expand ? 0 : 1,
flexBasis: "auto",
minWidth: isVertical ? "0" : "auto",
minHeight: !isVertical ? "0" : "auto",
}}
>
<div
className={`tw-flex ${isVertical ? "tw-flex-grow" : "tw-h-full"}
tw-justify-center tw-items-center`}
style={{
display: "flex",
flexDirection: currentWidgetDirection,
// width: "100%",
// height: "100%",
flexGrow: expandValue
// FIXME: the flex should only grow when expand is true, by default every thing should grow, but when there a expand
}}>
<div className={`tw-flex
${(["top", "bottom"].includes(side)) ? "tw-justify-center" : "tw-items-center"}
`
}
style={{
flexGrow: expandValue,
}}
>
{widget}
</div>
{this.renderPackWidgetsRecursively(widgets, index + 1, side, previousExpandValue)}
flexGrow: expand ? expandValue : 0,
flexShrink: expand ? 0 : 1,
flexBasis: "auto",
minWidth: isVertical ? "0" : "auto",
minHeight: !isVertical ? "0" : "auto",
// alignSelf,
// justifySelf,
alignItems,
justifyContent
}}
>
{widget}
</div>
);
}
{this.renderPackWidgetsRecursively(widgets, index + 1, side, previousExpandValue)}
</div>
);
};
/**
@@ -707,21 +734,7 @@ export class TkinterBase extends Widget {
setLayout(value) {
const { layout, direction, grid = { rows: 1, cols: 1 }, gap = 10, align } = value
// console.log("layout value: ", value)
let widgetStyle = {
...this.state.widgetInnerStyling,
display: layout !== Layouts.PLACE ? (layout === "grid" ? "grid" : "flex" ) : "block",
flexDirection: "column",
// flexDirection: direction,
gap: `${gap}px`,
gridTemplateColumns: "repeat(3, max-content)",
gridTemplateRows: "repeat(3, max-content)",
// gridTemplateColumns: "repeat(auto-fill, minmax(100px, auto))",
// gridTemplateRows: "repeat(auto-fill, minmax(100px, auto))",
}
// FIXME: when the layout changes the data is lost
if (layout === Layouts.GRID){
@@ -752,14 +765,12 @@ export class TkinterBase extends Widget {
).join(" ") // creates "max-content max-content 1fr 3fr" depending on value
}
const widgetStyle = {
...this.state.widgetInnerStyling,
this.updateState((prev) => ({
widgetInnerStyling: {
...prev.widgetInnerStyling,
gridTemplateRows: gridTemplateRows
}
this.updateState({
widgetInnerStyling: widgetStyle
})
}
}))
}
},
noOfCols: {
@@ -770,14 +781,12 @@ export class TkinterBase extends Widget {
onChange: (value) => {
this.setAttrValue("gridConfig.noOfCols", value)
const widgetStyle = {
...this.state.widgetInnerStyling,
gridTemplateColumns: `repeat(${value}, max-content)`
}
this.updateState({
widgetInnerStyling: widgetStyle
})
this.updateState((prev) => ({
widgetInnerStyling: {
...prev.widgetInnerStyling,
gridTemplateColumns: `repeat(${value}, max-content)`
}
}))
}
},
@@ -850,13 +859,24 @@ export class TkinterBase extends Widget {
}
this.updateState({
widgetInnerStyling: widgetStyle
})
this.setAttrValue("layout", value)
this.props.onLayoutUpdate({parentId: this.__id, parentLayout: value})// inform children about the layout update
this.setAttrValue("layout", value)
this.updateState((prev) => ({
widgetInnerStyling: {
...prev.widgetInnerStyling,
display: layout !== Layouts.PLACE ? (layout === "grid" ? "grid" : "flex" ) : "block",
flexDirection: "column",
// flexDirection: direction,
gap: `${gap}px`,
gridTemplateColumns: "repeat(3, max-content)",
gridTemplateRows: "repeat(3, max-content)",
// gridTemplateColumns: "repeat(auto-fill, minmax(100px, auto))",
// gridTemplateRows: "repeat(auto-fill, minmax(100px, auto))",
}
}))
}
getInnerRenderStyling(){
@@ -1004,6 +1024,7 @@ export class TkinterBase extends Widget {
}
this.updateState({ attrs: newAttrs }, callback)
// FIXME: when changing layouts all the widgets are being selected
if (selected){
this.select()
}
@@ -1132,15 +1153,14 @@ export class TkinterWidgetBase extends TkinterBase{
value: null,
onChange: (value) => {
const widgetStyle = {
...this.state.widgetOuterStyling,
marginLeft: `${value}px`,
marginRight: `${value}px`
}
this.updateState({
widgetOuterStyling: widgetStyle,
})
this.updateState((prev) => ({
widgetOuterStyling: {
...prev.widgetOuterStyling,
marginLeft: `${value}px`,
marginRight: `${value}px`
},
}))
this.setAttrValue("margin.marginX", value)
}
},
@@ -1150,15 +1170,15 @@ export class TkinterWidgetBase extends TkinterBase{
toolProps: {min: 0, max: 140},
value: null,
onChange: (value) => {
const widgetStyle = {
...this.state.widgetOuterStyling,
marginTop: `${value}px`,
marginBottom: `${value}px`
}
this.updateState({
widgetOuterStyling: widgetStyle,
})
this.updateState((prev) => ({
widgetOuterStyling: {
...prev.widgetOuterStyling,
marginTop: `${value}px`,
marginBottom: `${value}px`
},
}))
this.setAttrValue("margin.marginY", value)
}
},

View File

@@ -1,3 +1,4 @@
import Tools from "../../../canvas/constants/tools"
import Widget from "../../../canvas/widgets/base"
import {TkinterBase} from "./base"
@@ -17,7 +18,99 @@ class Frame extends TkinterBase{
this.state = {
...this.state,
fitContent: {width: true, height: true},
widgetName: "Frame"
widgetName: "Frame",
attrs: {
...this.state.attrs,
padding: {
label: "padding",
padX: {
label: "Pad X",
tool: Tools.NUMBER_INPUT,
toolProps: {min: 0, max: 140},
value: null,
onChange: (value) => {
// this.setWidgetInnerStyle("paddingLeft", `${value}px`)
// this.setWidgetInnerStyle("paddingRight", `${value}px`)
// const widgetStyle = {
// }
this.setState((prevState) => ({
widgetInnerStyling: {
...prevState.widgetInnerStyling,
paddingLeft: `${value}px`,
paddingRight: `${value}px`
}
}))
this.setAttrValue("padding.padX", value)
}
},
padY: {
label: "Pad Y",
tool: Tools.NUMBER_INPUT,
toolProps: {min: 0, max: 140},
value: null,
onChange: (value) => {
this.setState((prevState) => ({
widgetInnerStyling: {
...prevState.widgetInnerStyling,
paddingTop: `${value}px`,
paddingBottom: `${value}px`
}
}))
// this.setState({
// widgetInnerStyling: widgetStyle
// })
this.setAttrValue("padding.padX", value)
}
},
},
margin: {
label: "Margin",
marginX: {
label: "Margin X",
tool: Tools.NUMBER_INPUT,
toolProps: {min: 0, max: 140},
value: null,
onChange: (value) => {
this.updateState((prev) => ({
widgetOuterStyling: {
...prev.widgetOuterStyling,
marginLeft: `${value}px`,
marginRight: `${value}px`
},
}))
this.setAttrValue("margin.marginX", value)
}
},
marginY: {
label: "Margin Y",
tool: Tools.NUMBER_INPUT,
toolProps: {min: 0, max: 140},
value: null,
onChange: (value) => {
this.updateState((prev) => ({
widgetOuterStyling: {
...prev.widgetOuterStyling,
marginTop: `${value}px`,
marginBottom: `${value}px`
},
}))
this.setAttrValue("margin.marginY", value)
}
},
},
}
}
}