From 19bfc36fbf5085fa27a0beadac367c374f7f4a72 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 5 Jun 2026 15:00:24 +0000 Subject: [PATCH 1/2] Make contraption builder palette professional: collapsible sections + polish Redesign the left palette (sidebar) of the Contraption Builder so it reads as intentional and is easier to scan, without changing any tool behaviour: - Collapsible accordion sections (SHAPES, TERRAIN, SPECIAL, VEHICLES, JOINTS) with clickable chevron headers. The scrollbar now appears only when the visible content actually overflows the viewport. - A fixed-width glyph gutter so every tool label lines up regardless of glyph width (the main "unpolished" tell), plus roomier rows and headers. - Selection-aware hover: idle/active state is derived from the current tool, not by matching a hardcoded pixel colour, so it never mis-reads an accent or fights the selected/collapsed styling. Easy wins folded in while reviewing the stack: - Unify the repeated chrome colours into one block of named constants (kCol*), routed through new setBtnIdle/setBtnActive helpers used by every styler. - Extract the remaining magic numbers (kDefaultMotorSpeed, kDefaultGravity). The UI runs only inside the LiveCode/OpenXTalk IDE, so this was verified by static review; runtime checks follow the manual checklist in the PR. https://claude.ai/code/session_01XpBcQg2DbncrBcFZLHqhMj --- ...box2dxt-contraption-builder.livecodescript | 222 ++++++++++++++---- 1 file changed, 178 insertions(+), 44 deletions(-) diff --git a/examples/box2dxt-contraption-builder.livecodescript b/examples/box2dxt-contraption-builder.livecodescript index 868e2bf..c4ee3dc 100644 --- a/examples/box2dxt-contraption-builder.livecodescript +++ b/examples/box2dxt-contraption-builder.livecodescript @@ -1324,6 +1324,7 @@ local gSelJoint -- selected joint index (1..gJN), empty i local gInspTab -- active inspector tab: shape | physics | motion | special local gGravityMode -- world gravity preset: normal|low|zero|reverse|sideways local gPreRun -- snapshot of the build, taken when Run starts +local gPalOpen -- CR-list of palette section titles currently expanded constant kShapeTools = "drag,box,ball,capsule,poly,image,anchor,delete,duplicate,multiply" constant kShapeLabels = "Drag,Box,Ball,Capsule,Poly,Image,Anchor,Delete,Duplicate,Multiply" @@ -1343,8 +1344,18 @@ constant kStackW = 1180, kStackH = 760 constant kTopBarH = 54, kAccentH = 3 constant kPaletteW = 196, kInspectorW = 248, kStatusH = 26 constant kCanvasInset = 2, kGroundBarH = 22 -constant kPalPadX = 12, kPalBtnH = 24, kPalBtnGap = 3 -constant kPalSectionGap = 8, kPalHeaderH = 18, kPalScrollW = 16 +constant kPalPadX = 12, kPalBtnH = 28, kPalBtnGap = 4 +constant kPalSectionGap = 8, kPalHeaderH = 22, kPalScrollW = 16, kPalGutterW = 22 + +-- Palette theme: one source of truth for the chrome colours (RGB strings) so the +-- idle / active / hover styling never drifts apart. Used by the setBtn*, highlight* +-- and hover helpers. (String literals are valid constants; expressions are not.) +constant kColPaletteBg = "26,28,35", kColBtnIdle = "44,48,58", kColBtnIdleText = "196,200,212" +constant kColBtnHover = "58,64,78", kColBtnText = "255,255,255" +constant kColAccentGreen = "72,190,130", kColAccentAmber = "232,150,72", kColToggleBlue = "64,132,232" +constant kColDanger = "214,92,92", kColHeaderText = "150,156,172", kColRule = "52,57,70" +-- Former magic numbers: starting motor speed (°/s) and world gravity (downward). +constant kDefaultMotorSpeed = 120, kDefaultGravity = -10 -- Materials, motors, feedback timing, and default part sizes (were magic numbers). constant kBounceOn = 0.7, kFlashMs = 150, kSelectLine = 3 @@ -1393,7 +1404,7 @@ on startCB put empty into gJointTool put false into gMotorOn put true into gMotorsRunning - put 120 into gMotorSpeed + put kDefaultMotorSpeed into gMotorSpeed put false into gBouncy put empty into gParts put empty into gFlash @@ -1407,11 +1418,12 @@ on startCB put "normal" into gGravityMode put empty into gHoverId put 0 into gStressCount + put "SHAPES" & cr & "TERRAIN" & cr & "SPECIAL" & cr & "VEHICLES" & cr & "JOINTS" into gPalOpen resetSceneState resetJointArrays resetPend buildUI - b2kSetup 0, -10 -- creates the world; the loop stays stopped + b2kSetup 0, kDefaultGravity -- creates the world; the loop stays stopped prepArena b2kFrameTarget the long id of this stack b2kContactTarget the long id of this stack @@ -1496,7 +1508,7 @@ on makeTopBar local tWide, tY, tX, tIds, tLbls, tWidths, tGap, tI, tId, tW put the width of this card into tWide makeBar "ui_header", 0, 0, tWide, kTopBarH, "30,33,42" - makeBar "ui_accent", 0, kTopBarH, tWide, kTopBarH + kAccentH, "72,190,130" + makeBar "ui_accent", 0, kTopBarH, tWide, kTopBarH + kAccentH, kColAccentGreen makeLabel "ui_title", 18, 7, 280, 33, "Box2Dxt", 22, "236,238,245", true makeLabel "ui_sub", 20, 32, 560, 50, "Contraption Builder — build a machine, then press Run to bring it to life", 11, "150,154,166", false put "mode,reset,clear,save,load,recipes,info" into tIds @@ -1522,7 +1534,7 @@ on makePalette local tWide, tHigh, tY put the width of this card into tWide put the height of this card into tHigh - makeBar "ui_palettebg", 0, kTopBarH + kAccentH, kPaletteW, tHigh - kStatusH, "26,28,35" + makeBar "ui_palettebg", 0, kTopBarH + kAccentH, kPaletteW, tHigh - kStatusH, kColPaletteBg put kTopBarH + kAccentH + 8 into tY put makeToolSection("SHAPES", kShapeTools, kShapeLabels, "tool", tY) into tY put makeToolSection("TERRAIN", kTerrainTools, kTerrainLabels, "tool", tY) into tY @@ -1530,6 +1542,7 @@ on makePalette put makeToolSection("VEHICLES", kVehicleTools, kVehicleLabels, "tool", tY) into tY put makeToolSection("JOINTS", kJointTools, kJointLabels, "joint", tY) into tY groupPalette + layoutPalette -- apply collapse/expand state, chevrons and scrollbar end makePalette -- Wrap the section headers + tool buttons in one scrolling group. The grouping @@ -1542,7 +1555,7 @@ on groupPalette try select empty repeat for each item tT in "SHAPES,TERRAIN,SPECIAL,VEHICLES,JOINTS" - if there is a field ("ui_hdr_" & tT) then set the selected of field ("ui_hdr_" & tT) to true + if there is a button ("ui_hdr_" & tT) then set the selected of button ("ui_hdr_" & tT) to true if there is a graphic ("ui_hdrrule_" & tT) then set the selected of graphic ("ui_hdrrule_" & tT) to true end repeat repeat for each item tId in (kShapeTools & comma & kTerrainTools & comma & kSpecialTools & comma & kVehicleTools) @@ -1551,6 +1564,9 @@ on groupPalette repeat for each item tId in kJointTools if there is a button ("ui_joint_" & tId) then set the selected of button ("ui_joint_" & tId) to true end repeat + repeat for each item tId in (kShapeTools & comma & kTerrainTools & comma & kSpecialTools & comma & kVehicleTools & comma & kJointTools) + if there is a field ("ui_g_" & tId) then set the selected of field ("ui_g_" & tId) to true + end repeat group set the name of it to "ui_palettescroll" select empty @@ -1569,35 +1585,135 @@ on groupPalette end try end groupPalette --- Build one labelled palette section: a bright header over a hairline rule, then --- the tool buttons. Returns the Y for the next section. +-- Build one labelled palette section: a clickable accordion header (chevron + title) +-- over a hairline rule, then the tool rows. Each row is a left-aligned text button +-- with its glyph in a fixed-width gutter on the left, so every label lines up no +-- matter how wide the glyph is. layoutPalette repositions/shows/hides these after. function makeToolSection pTitle, pIds, pLabels, pKind, pStartY - local tY, tI, tId, tLbl, tName, tW + local tY, tI, tId, tLbl, tName, tRight, tGut put pStartY into tY - makeLabel ("ui_hdr_" & pTitle), kPalPadX, tY, kPaletteW - kPalPadX, tY + kPalHeaderH, pTitle, 10, "150,156,172", true - makeBar ("ui_hdrrule_" & pTitle), kPalPadX, tY + kPalHeaderH, kPaletteW - kPalPadX - kPalScrollW, tY + kPalHeaderH + 1, "52,57,70" + put kPaletteW - kPalPadX - kPalScrollW into tRight -- shared right edge (leaves scrollbar room) + put kPalPadX + kPalGutterW into tGut -- labels start after the glyph gutter + makeButton ("ui_hdr_" & pTitle), (chevronGlyph(pTitle) && pTitle), kPalPadX, tY, tRight - kPalPadX, kPalHeaderH + set the textAlign of button ("ui_hdr_" & pTitle) to "left" + set the textStyle of button ("ui_hdr_" & pTitle) to "bold" + set the textSize of button ("ui_hdr_" & pTitle) to 10 + set the textColor of button ("ui_hdr_" & pTitle) to kColHeaderText + set the opaque of button ("ui_hdr_" & pTitle) to false -- reads as a label, still fully clickable + set the uHdr of button ("ui_hdr_" & pTitle) to pTitle + makeBar ("ui_hdrrule_" & pTitle), kPalPadX, tY + kPalHeaderH, tRight, tY + kPalHeaderH + 1, kColRule add kPalHeaderH + 6 to tY - put kPaletteW - 2 * kPalPadX - kPalScrollW into tW -- leave room for the scrollbar repeat with tI = 1 to the number of items of pIds put item tI of pIds into tId put item tI of pLabels into tLbl if pKind is "joint" then put "ui_joint_" & tId into tName - makeButton tName, (toolGlyph(tId) && tLbl), kPalPadX, tY, tW, kPalBtnH + makeButton tName, tLbl, tGut, tY, tRight - tGut, kPalBtnH set the uJointId of button tName to tId else put "ui_tool_" & tId into tName - makeButton tName, (toolGlyph(tId) && tLbl), kPalPadX, tY, tW, kPalBtnH + makeButton tName, tLbl, tGut, tY, tRight - tGut, kPalBtnH set the uToolId of button tName to tId end if set the textAlign of button tName to "left" -- menu-style: labels start at the left - set the margins of button tName to 10 + set the margins of button tName to 8 set the tooltip of button tName to line 1 of toolHelp(tId) -- set now; survives grouping + makeLabel ("ui_g_" & tId), kPalPadX, tY, tGut, tY + kPalBtnH, toolGlyph(tId), 13, kColBtnIdleText, false + set the textAlign of field ("ui_g_" & tId) to "center" + set the margins of field ("ui_g_" & tId) to 4 add kPalBtnH + kPalBtnGap to tY end repeat return tY + kPalSectionGap end makeToolSection +-- ▾ when a section is expanded, ▸ when collapsed (drives the header label). +function chevronGlyph pTitle + if pTitle is among the lines of gPalOpen then return "▾" + return "▸" +end chevronGlyph + +-- Title → its tool ids / kind. No new data: routes to the existing constant lists. +function sectionIds pTitle + switch pTitle + case "SHAPES": return kShapeTools + case "TERRAIN": return kTerrainTools + case "SPECIAL": return kSpecialTools + case "VEHICLES": return kVehicleTools + case "JOINTS": return kJointTools + end switch + return empty +end sectionIds + +function sectionKind pTitle + if pTitle is "JOINTS" then return "joint" + return "tool" +end sectionKind + +-- Lay the palette out from gPalOpen: headers always show (with a chevron); an +-- expanded section reveals its rule + glyph gutters + tool buttons, a collapsed one +-- hides them. Hidden controls leave the group's scroll extent, so the scrollbar only +-- appears when the *visible* content overflows the viewport. +on layoutPalette + if there is not a group "ui_palettescroll" then exit layoutPalette + local tY, tTitle, tIds, tPrefix, tId, tBtn, tGlyph, tOpen, tRight, tGut + lock screen + set the vScroll of group "ui_palettescroll" to 0 -- lay children out in unscrolled card coords + put kPaletteW - kPalPadX - kPalScrollW into tRight + put kPalPadX + kPalGutterW into tGut + put kTopBarH + kAccentH + 8 into tY + repeat for each item tTitle in "SHAPES,TERRAIN,SPECIAL,VEHICLES,JOINTS" + put sectionIds(tTitle) into tIds + if sectionKind(tTitle) is "joint" then put "ui_joint_" into tPrefix + else put "ui_tool_" into tPrefix + set the rect of button ("ui_hdr_" & tTitle) to kPalPadX, tY, tRight, tY + kPalHeaderH + set the label of button ("ui_hdr_" & tTitle) to (chevronGlyph(tTitle) && tTitle) + put (tTitle is among the lines of gPalOpen) into tOpen + add kPalHeaderH to tY + if tOpen then + set the visible of graphic ("ui_hdrrule_" & tTitle) to true + set the rect of graphic ("ui_hdrrule_" & tTitle) to kPalPadX, tY, tRight, tY + 1 + add 6 to tY + repeat for each item tId in tIds + put tPrefix & tId into tBtn + put "ui_g_" & tId into tGlyph + if there is a button tBtn then + set the visible of button tBtn to true + set the rect of button tBtn to tGut, tY, tRight, tY + kPalBtnH + end if + if there is a field tGlyph then + set the visible of field tGlyph to true + set the rect of field tGlyph to kPalPadX, tY, tGut, tY + kPalBtnH + end if + add kPalBtnH + kPalBtnGap to tY + end repeat + else + set the visible of graphic ("ui_hdrrule_" & tTitle) to false + repeat for each item tId in tIds + if there is a button (tPrefix & tId) then set the visible of button (tPrefix & tId) to false + if there is a field ("ui_g_" & tId) then set the visible of field ("ui_g_" & tId) to false + end repeat + end if + add kPalSectionGap to tY + end repeat + set the vScrollbar of group "ui_palettescroll" to ((tY - (kTopBarH + kAccentH)) > the height of group "ui_palettescroll") + unlock screen +end layoutPalette + +-- Collapse or expand a section, then relayout. Pure chrome: tool selection and the +-- HUD are deliberately left untouched (gPalOpen never carries a trailing newline, so +-- the delete/append idiom below stays clean across many toggles). +on togglePaletteSection pTitle + if pTitle is empty then exit togglePaletteSection + if pTitle is among the lines of gPalOpen then + delete line lineOffset(pTitle, gPalOpen) of gPalOpen + else if gPalOpen is empty then + put pTitle into gPalOpen + else + put pTitle into line (the number of lines of gPalOpen + 1) of gPalOpen + end if + layoutPalette +end togglePaletteSection + -- Right inspector: a title + plain-language help, a block of property adjusters -- (hidden until a part is selected), and the persistent build options. on makeInspector @@ -1606,7 +1722,7 @@ on makeInspector put the height of this card into tHigh put tWide - kInspectorW into tL put kTopBarH + kAccentH into tTop - makeBar "ui_inspbg", tL, tTop, tWide, tHigh - kStatusH, "26,28,35" + makeBar "ui_inspbg", tL, tTop, tWide, tHigh - kStatusH, kColPaletteBg makeLabel "ui_insp_title", tL + 12, tTop + 10, tWide - 12, tTop + 34, "Inspector", 14, "236,238,245", true makeField "ui_insp_body", tL + 12, tTop + 40, tWide - 12, tTop + 118, 11, "190,194,206" -- settings tabs: group a part's many settings so each pane stays short @@ -1649,8 +1765,7 @@ end makeInspector -- A prominent accent-green +/- button for adjusting a part's settings. on makePropBtn pId, pLabel, pX, pY, pW, pH makeAction pId, pLabel, pX, pY, pW, pH - set the backgroundColor of button ("ui_act_" & pId) to "72,190,130" - set the textColor of button ("ui_act_" & pId) to "255,255,255" + setBtnActive ("ui_act_" & pId), kColAccentGreen set the textStyle of button ("ui_act_" & pId) to "bold" end makePropBtn @@ -1676,8 +1791,7 @@ end makeStatusBar on makeAction pId, pLabel, pX, pY, pW, pH makeButton ("ui_act_" & pId), pLabel, pX, pY, pW, pH set the uActId of button ("ui_act_" & pId) to pId - set the backgroundColor of button ("ui_act_" & pId) to "44,48,58" - set the textColor of button ("ui_act_" & pId) to "196,200,212" + setBtnIdle ("ui_act_" & pId) end makeAction -- Delete the chrome. Collect first, then delete, and only touch *top-level* @@ -1766,9 +1880,23 @@ on makeButton pName, pLabel, pX, pY, pW, pH set the rect of button pName to pX, pY, pX + pW, pY + pH end makeButton +-- Idle / active button styling in one place, so the colours never drift apart and +-- the hover helper can reason about selection state instead of reading pixels. +on setBtnIdle pBtn + if there is not a button pBtn then exit setBtnIdle + set the backgroundColor of button pBtn to kColBtnIdle + set the textColor of button pBtn to kColBtnIdleText +end setBtnIdle + +on setBtnActive pBtn, pColor + if there is not a button pBtn then exit setBtnActive + set the backgroundColor of button pBtn to pColor + set the textColor of button pBtn to kColBtnText +end setBtnActive + on highlightAll - highlightGroup "ui_tool_", (kShapeTools & "," & kTerrainTools & "," & kSpecialTools & "," & kVehicleTools), gTool, (gJointTool is empty), "72,190,130" - highlightGroup "ui_joint_", kJointTools, gJointTool, true, "232,150,72" + highlightGroup "ui_tool_", (kShapeTools & "," & kTerrainTools & "," & kSpecialTools & "," & kVehicleTools), gTool, (gJointTool is empty), kColAccentGreen + highlightGroup "ui_joint_", kJointTools, gJointTool, true, kColAccentAmber highlightActions end highlightAll @@ -1780,14 +1908,11 @@ on highlightGroup pPrefix, pItems, pActiveId, pEnabled, pActiveColor if there is not a button tBtn then next repeat put (tItem is pActiveId and pEnabled) into tOn if tOn and tItem is "delete" then - set the backgroundColor of button tBtn to "214,92,92" - set the textColor of button tBtn to "255,255,255" + setBtnActive tBtn, kColDanger else if tOn then - set the backgroundColor of button tBtn to pActiveColor - set the textColor of button tBtn to "255,255,255" + setBtnActive tBtn, pActiveColor else - set the backgroundColor of button tBtn to "44,48,58" - set the textColor of button tBtn to "196,200,212" + setBtnIdle tBtn end if end repeat end highlightGroup @@ -1797,12 +1922,11 @@ on highlightActions if there is a button "ui_act_mode" then if gMode is "run" then set the label of button "ui_act_mode" to "Build" - set the backgroundColor of button "ui_act_mode" to "214,92,92" + setBtnActive "ui_act_mode", kColDanger else set the label of button "ui_act_mode" to "Run" - set the backgroundColor of button "ui_act_mode" to "72,190,130" + setBtnActive "ui_act_mode", kColAccentGreen end if - set the textColor of button "ui_act_mode" to "255,255,255" end if -- A clear, full-width cue for which mode you're in: green accent + neutral -- subtitle while building; amber accent + a "running" subtitle while simulating. @@ -1810,7 +1934,7 @@ on highlightActions put "232,150,60" into tAccent put "RUNNING — drag parts to play; press Build or Reset to edit" into tSub else - put "72,190,130" into tAccent + put kColAccentGreen into tAccent put "Contraption Builder — build a machine, then press Run to bring it to life" into tSub end if if there is a graphic "ui_accent" then set the backgroundColor of graphic "ui_accent" to tAccent @@ -1822,11 +1946,9 @@ end highlightActions on setToggle pBtn, pOn if there is not a button pBtn then exit setToggle if pOn then - set the backgroundColor of button pBtn to "64,132,232" - set the textColor of button pBtn to "255,255,255" + setBtnActive pBtn, kColToggleBlue else - set the backgroundColor of button pBtn to "44,48,58" - set the textColor of button pBtn to "196,200,212" + setBtnIdle pBtn end if end setToggle @@ -1967,6 +2089,11 @@ on handleUiClick editPropValue (the uPropKey of the target) exit handleUiClick end if + -- Click a palette section header to collapse / expand it (pure chrome, no selection). + if "ui_hdr_" is in tName then + togglePaletteSection (the uHdr of the target) + exit handleUiClick + end if -- Picking a build tool while the simulation runs drops you back into Build. if ("ui_tool_" is in tName or "ui_joint_" is in tName) and gMode is "run" then toggleMode if "ui_tool_" is in tName then @@ -4546,11 +4673,9 @@ on refreshInspTabs pAvail if tT is among the items of pAvail then set the visible of button ("ui_act_tab_" & tT) to true if tT is gInspTab then - set the backgroundColor of button ("ui_act_tab_" & tT) to "72,190,130" - set the textColor of button ("ui_act_tab_" & tT) to "255,255,255" + setBtnActive ("ui_act_tab_" & tT), kColAccentGreen else - set the backgroundColor of button ("ui_act_tab_" & tT) to "44,48,58" - set the textColor of button ("ui_act_tab_" & tT) to "196,200,212" + setBtnIdle ("ui_act_tab_" & tT) end if else set the visible of button ("ui_act_tab_" & tT) to false @@ -5487,10 +5612,19 @@ on mouseLeave end if end mouseLeave --- Lighten an *idle* palette button on hover (selected buttons keep their accent; --- mouseLeave repaints everything via highlightAll). +-- Lighten a palette button on hover, but only when it is the resting (idle) tool. +-- "Idle" is derived from the current selection, not the pixel colour, so it never +-- mis-reads an accent and never fights the selected / collapsed styling; mouseLeave +-- repaints everything via highlightAll. on hoverTool pBtn - if the backgroundColor of pBtn is "44,48,58" then set the backgroundColor of pBtn to "58,64,78" + if there is not a button pBtn then exit hoverTool + local tActive + if the uToolId of pBtn is not empty then + put (the uToolId of pBtn is gTool and gJointTool is empty) into tActive + else + put (the uJointId of pBtn is gJointTool) into tActive + end if + if not tActive then set the backgroundColor of pBtn to kColBtnHover end hoverTool -- ===================================================================== From 50b824c9a235dad42cc49ff3a6fe7453601890c4 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 5 Jun 2026 15:35:54 +0000 Subject: [PATCH 2/2] Fix invalid switch/case syntax in sectionIds LiveCode case labels don't use a colon and the action goes on the following line; the C-style `case "SHAPES": return ...` form fails to compile. Rewrite to the canonical bare-label form used elsewhere in the file (toolHelp, dirName). https://claude.ai/code/session_01XpBcQg2DbncrBcFZLHqhMj --- .../box2dxt-contraption-builder.livecodescript | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/examples/box2dxt-contraption-builder.livecodescript b/examples/box2dxt-contraption-builder.livecodescript index c4ee3dc..77920df 100644 --- a/examples/box2dxt-contraption-builder.livecodescript +++ b/examples/box2dxt-contraption-builder.livecodescript @@ -1635,11 +1635,16 @@ end chevronGlyph -- Title → its tool ids / kind. No new data: routes to the existing constant lists. function sectionIds pTitle switch pTitle - case "SHAPES": return kShapeTools - case "TERRAIN": return kTerrainTools - case "SPECIAL": return kSpecialTools - case "VEHICLES": return kVehicleTools - case "JOINTS": return kJointTools + case "SHAPES" + return kShapeTools + case "TERRAIN" + return kTerrainTools + case "SPECIAL" + return kSpecialTools + case "VEHICLES" + return kVehicleTools + case "JOINTS" + return kJointTools end switch return empty end sectionIds