Skip to content

Commit 42510e7

Browse files
VTT-AI-Agentclaude96LawDawg
authored
Let dragLimit and snap grids describe their area with inequalities, not just a rectangle
* Let dropLimit be a dynamic expression and show what it amounts to A drop limit had to be a fixed number, so a game whose limit depends on the state (the number of seats taken, a difficulty setting, a counter) had to have a routine write the number into every widget that uses it. dropLimit now also takes an expression - "${PROPERTY seats OF board}", "${PROPERTY level} * 2", "min ${PROPERTY a} 5" - written the way a routine writes one and using the same operations (compute.js). It is evaluated wherever the limit is enforced, so it follows the state by itself, and a pile with showLimit shows what it currently amounts to, redrawn when a property the expression reads changes anywhere. An expression that does not evaluate to a number (a typo, a widget that is gone) reads as no limit, so it can never make a widget refuse everything. The JSON editor gets a "dynamic limit"/"fixed limit" button on the dropLimit line, and the validator accepts the new shape. The curated "Drop limit" number input cannot show an expression, so a widget that has one keeps dropLimit in the generic "Other properties" list until the properties sidebar can edit expressions - a design that is up for discussion in the PR. Co-Authored-By: Claude <noreply@anthropic.com> * Revert "Let dropLimit be a dynamic expression and show what it amounts to" This reverts commit 7abc623. The request behind this PR was about dragLimit - the area a widget can be dragged in - and about describing that area with an inequality like "2x^2+y>4", not about the limit on how many widgets can be dragged into a holder. The dropLimit take is kept in the branch history in case anyone wants it as its own PR. Co-Authored-By: Claude <noreply@anthropic.com> * Let dragLimit describe the drag area with inequalities like "2x^2+y>4" dragLimit could only be a rectangle of four fixed numbers, so a piece that has to stay on a board of any other shape - a disc, a triangle, a track, anything curved - could not be limited at all, and a rectangle that depends on the state had to be written by a routine. Each side (minX/maxX/minY/maxY) now also takes an expression, and a new "condition" - one inequality or a list of them, written in x and y - bounds the drag to any area that can be written down: "dragLimit": { "condition": "2x^2 + y > 4" } "dragLimit": { "condition": [ "y > x - 500", "y < 620" ] } "dragLimit": { "minX": 0, "maxX": "${PROPERTY width OF board} - 100" } client/js/expression.js is the small language behind both: ordinary infix maths (+ - * / % ^, brackets, comparisons, && || !, the usual functions), where x and y are the position being tested, any other name is a property of the widget, and ${PROPERTY name OF id} reads another widget's. A number in front of a name or a bracket multiplies, so "2x^2+y>4" means what it looks like. Nothing in it touches the room state or the DOM, so it is evaluated on every mouse move safely. A refused position falls back to moving on one axis only, so a widget slides along the edge of its area rather than sticking to it. A widget that is outside its area is not held in place, and an expression that cannot be read limits nothing, so neither can leave a piece stuck. The JSON editor gets a "condition" button on dragLimit, the validator knows the new shape, and the properties sidebar leaves a dragLimit that uses an expression or a condition to "Other properties" - its four number inputs cannot show one. Drawing the area in the editor is a design question and is proposed in the PR rather than invented here. Co-Authored-By: Claude <noreply@anthropic.com> * Keep a dragLimit editable when it turns into an expression mid-edit The curated "Drag limits" rectangle steps aside for a dragLimit that uses an expression or a condition, which the four number inputs cannot show. That decision was only made while the panel was built, so writing a condition in the JSON editor with the panel open left the inputs standing (empty, and overwriting the condition with the next keystroke) and left the property out of "Other properties", where it should appear instead. Both now follow the property. Co-Authored-By: Claude <noreply@anthropic.com> * Edit dragLimit conditions in the sidebar and reject expressions that lie Addresses the review on #3104. The "Drag limits" section no longer steps aside for an expression or a condition: each of the four sides takes a number or an expression, and a "Condition" field below them holds one inequality per line, so the whole area is editable without the JSON editor. The parser is stricter and cheaper where it was quietly wrong: - "0 < x < 500" read as "(0 < x) < 500", i.e. true wherever the widget is. Comparisons are no longer chainable and say so instead. - Implicit multiplication is only ever a number in front of a name or a bracket, so the stray space in "2 3x" is reported rather than meaning 6x. - && and || evaluate their right side only when it decides the result, so a condition can guard against the widget it reads being gone. - The validator parses every side and condition, so a typo shows up in edit mode instead of turning into a limit that limits nothing. - Tokens are cached per expression instead of being recomputed on every mouse move, and the function/constant tables no longer answer for inherited keys like "constructor". Also: the fallback a refused condition falls back to is put through the rectangle, so a widget outside its sides can never be handed back there, and a "condition" of null reads as no condition. * Keep the drag limit sides on one line and report a bad expression where it is typed Addresses the UI review on #3104. The four sides stretched to fit an expression whether they held one or not, so a plain rectangle wrapped into four ragged rows at laptop width. Only a side actually holding an expression takes a line of its own now, and each side carries its axis in its own label ("X min"/"X max") instead of a shared "X from/to" that a wrapped row leaves behind. A mistyped expression or condition was only reported in the Debug module's validation table, i.e. not where it was typed and not where it does nothing: property inputs take a "validate" option that puts the problem under the input, and both the sides and the conditions use it. The validator collects every problem of a dragLimit instead of returning at the first one. Smaller things from the same review: - an expression that does not fit its field is readable as a tooltip - arrow keys step a number again, which type=text had dropped - the conditions field is three lines tall, labelled "Conditions (one per line)", and says below it what x and y are and that every line has to hold - the section help text describes a condition instead of only the rectangle, the hint says "2x means 2 * x" instead of naming the parser rule, and the toggle hint says that switching it off drops the conditions too - the section summary counts the conditions, since a condition-only limit otherwise looks unset next to four empty sides - the JSON editor buttons read "add condition" / "add another condition" Co-Authored-By: Claude <noreply@anthropic.com> * Show the area a widget can be dragged in on the board while editing Asked for on Discord: "there should be a button on the editor that shows the drag limit area of a widget while on editor mode". The four sides and the conditions of a dragLimit describe an area that cannot be read off the numbers, so the "Drag limits" section gained a "Show on board" switch - the same control and wording the "Snap grid" section uses for its lattice - that shades every position the widget's top left corner can be dragged to. The shading is drawn into the widget's parent, i.e. the coordinate space the limit is measured in, so a piece inside a holder gets its area in the holder's coordinates and clipped to it. It appears with the limit, follows every edit of it, and goes away when the switch is off, the section is collapsed, another widget is selected or edit mode is left. Nothing is ever drawn in play. An area a condition describes is not computed but sampled: ~10000 points, each asking the widget itself whether a drag may end there. dragLimitAllows() is that question, and the part of dragLimitedCoord() that reads the limit moved into dragLimitRules() so both use the same rules - dragLimitedCoord() behaves exactly as before. The drawing waits for a pause in typing, and reads the four sides once for the whole canvas unless one of them is an expression that actually varies with the position. Co-Authored-By: Claude <noreply@anthropic.com> * Let a dragLimit carry a piece to the edge of its area and along it A drag that a dragLimit condition refused fell back to moving on one axis only, at the exact position the mouse reported. Against anything that is not an axis-parallel line that stops the piece early and in steps: it came to rest wherever the last mouse move before the boundary happened to fall - up to a whole mouse step short of a limit that looked much wider than the piece could reach - and once there it could only stay put or move along one axis, so an inclined edge made it move in a staircase and a curve caught it wherever the tangent was horizontal or vertical. The dragged position is now put where the area comes closest to the pointer instead: the movement asked for is turned away from the pointer by the smallest angle that lands it in the area, which is what sliding along an edge is, and repeating that follows an edge that curves away. Both the straight movement and the turned one are found by bisection, so the piece rests on the boundary itself whatever the mouse did in between, and the whole position it is finally given is the one of the four around that point that is nearest the pointer and still inside. Measured in a browser, with the pointer circling 500 units outside a limit of "|p - centre| < 300": the piece used to hop between four positions between 36 and 297 units from the centre, standing still for several mouse moves at a time, and now stays between 299.3 and 300.0 all the way round. Dragged into "y > x + 100" it used to stop dead at the edge and now follows it. * Tell a widget on the edge of its drag area from one outside it A refused drag looked for a position to start its search from by mixing one coordinate of the pointer with one of the widget, which says nothing about where the widget is: a widget outside a disc could be pulled onto it because the mixture happened to land in the middle, and a widget on a strict vertical edge was let go entirely because no mixture was inside. It now asks the whole positions around the widget instead, which distinguishes sitting on the boundary from being away from the area. The editor's area preview also read a side once for the whole drawing whenever it evaluated the same at two opposite corners of the parent, which "(x - 800)^2" does while differing everywhere in between, and it only redrew for a fixed list of properties. Both now go by the names an expression actually reads, for which expression.js gained expressionNames(). * Run the drag limit tests in CI and judge a dragLimit side where it applies Four things the fourth review of #3104 found: - tests/testcafe/draglimit.js was in neither TestCafe workflow matrix, so the three drags it makes never ran outside a local checkout. - The validator refused a side written as null although the engine reads it as "no limit on that side" - a false positive in edit mode on exactly the games the null -> 0 change affects. That change of meaning now also carries a FileUpdater step (v22), which writes down the 0 those saves always clamped to. - A side that reads x or y ("maxX": "y") was read once where the pointer is and then used to judge every candidate position, so a drag could end up outside the area the editor's drawing shades. Both now ask the widget the same question - dragLimitRules().varies - and re-read the sides per position. - The parser refused "0 < x < 500" but not "(0 < x) < 500", which is the same limit that limits nothing, and read "2^2x" as (2^2)*x where it is written to mean 2^(2x); both are now reported. The drawing is a canvas inside the parent widget, which drops it whenever it renders its own content, so every property of the parent redraws it. * Give dragLimit expressions the same functions and constants as var sin/cos/tan were radians in a dragLimit expression and degrees in a routine, "pi" was lowercase where var spells it PI, and asin/acos/atan/ cbrt/log10/log2/trunc were missing entirely - so a formula moved between the two languages could quietly compute something else. The expression language now has var's full numeric table, with var's meaning, plus === and !== so an expression copied out of a routine still parses. Left out of it are random/randInt/randRange: an area that is not the same twice can neither be slid along nor drawn, and everything that is about strings, arrays or colours rather than numbers. A new test holds every function and constant against the operation of the same name in compute_ops, so the two cannot drift apart again. * Add a "Properties - Drag Limits" tutorial with a rectangle and a shapes variant Two variants of a new tutorial in the public library. "Rectangle" is the four sides: one piece limited to a plain minX/maxX/minY/maxY - so its corner reaches the frame and the piece hangs out of it - and one whose sides are expressions read off the frame widget, with a button that resizes that frame so the limit visibly follows the state. "Any shape" is the condition: five buttons write a different dragLimit onto one piece and shade the area it describes - the same rectangle written as four conditions, a disc around the middle of the room, a regular hexagon that mixes minX/maxX with two abs() conditions, a box with a box cut out of it, and no limit at all. Each button also shows the JSON it just wrote and puts the piece back inside the new area. * Update tutorial library image and some of the text * Fix tutorial title * Give dragLimit an alignX/alignY reference point and keep refused drags on the mouse path alignX/alignY say which point of the widget the area holds, as the same fractions of its box a snap grid aligns to: 0.5/0.5 keeps its middle in the area, so a circle is written around where the middle can go instead of adding half the widget to x and y in every condition. They are edited with the 3x3 picker the grid uses ("Limited point"), and there is an "add alignX" button on dragLimit in the JSON editor. A widget property in a dragLimit expression is now read only where it is written the way routines write one - ${PROPERTY name}, ${PROPERTY name OF id}. A bare word other than x and y is a mistake the engine would read as nothing, so the validator and the sidebar report it as it is typed. A drag that is refused can now only reach positions it can get to without leaving the area on the way: the movement is walked rather than halved, so a widget limited to a ring or a track around the board follows the mouse round it instead of appearing on the far side of the hole the pointer crossed. The Drag limits section of the properties sidebar says all of that in its info hovers rather than in paragraphs above the fields. Tutorial: a new "Reference point" variant shows the same rectangle holding the corner, the middle and the bottom right corner of a piece; the "Any shape" variant is rewritten with alignX/alignY, so its conditions are the shapes themselves. Co-Authored-By: Claude <noreply@anthropic.com> * Update Shell Game with dragLimit for scoreboard * Report a dragLimit condition that is no condition, and an area nothing satisfies Closes the last member of the "a limit that silently limits nothing must be reported" family that this branch already catches for chained comparisons and for a bare property name: a condition written as maths rather than as an inequality. "condition": "x - 100" is a number, and a condition reads a number as true wherever it is not 0, so it used to pass the validator and the sidebar and then allow every position but the single line x == 100. expressionError() takes a requireCondition flag - whether an expression answers with a number or with true/false is decided by how it is written, not by what anything resolves to, so it can be judged once. The validator's condition branch and the sidebar's Conditions field both ask for it; the four sides keep the number-shaped check. An area that no position satisfies at all (two conditions that contradict each other, a property that turns out to be 0) is not an error in any single line, but it still limits nothing, because a widget outside its area is always let go so that it can never be stuck. The sampling behind the "Show on board" preview already knows: it now runs whenever the section is open, whether the area is drawn or not, and says so under the fields when it finds no position. Also: - the validator accepts "alignX": "0.5", which the engine reads with + and so has always honoured, instead of reporting a game that works; - the tutorial's "Other shapes" variant is laid out in top-down bands like its two siblings - title, overview, the five buttons as one row, the shape below them, the JSON and the note under that - rather than in the left/right text-vs-demo split docs/tutorials.md rules out; - the tutorial directory is renamed to "Properties - dragLimit" so it matches its own _meta.info.name and the "Properties - inheritFrom" spelling. Co-Authored-By: Claude <noreply@anthropic.com> * Start a drag from where the widget is, not from its holder's coordinates A widget dragged out of a holder used to be thrown to the far side of a dragLimit area on the very first mouse move: reported on the Shell Game's score track, where a marker sitting in the holder for 47 (bottom right) jumped to the other arm of the track as soon as it was nudged. moveStart() drops the widget's parent, and nothing converts x and y out of that parent's coordinates, because the first thing a drag does is overwrite them with the position it computed from the mouse. That was harmless until dragLimit began walking a drag from where the widget is, so that it follows the pointer around a hole instead of jumping over it: for that one move, "where the widget is" read as the holder's own "2, 2", which for the Shell Game's ring is the top left of the board - inside the area as well, so the walk started over there. moveStart() now writes down where the widget is in the coordinates of the room, taken while its parent still says what x and y mean, and dragLimitedCoord() walks from that. The first position the drag writes clears it again, and so does moveEnd, since a click is a moveStart and a moveEnd with no move between them. Measured on library/games/Shell Game/0.json with the marker in scoreHolder47 (middle at 1537,769), nudged 3 px right: it went to 699,188 and was dropped in scoreHolder2, and now moves to 1539,768 and is dropped back in scoreHolder47. Co-Authored-By: Claude <noreply@anthropic.com> * Let a snap grid apply only where a condition holds, not just in a rectangle A grid entry could be limited to the rectangle minX/maxX/minY/maxY; it now also takes a "condition" - one inequality in x and y (the position being dropped) or a list of them, in the same expression language a dragLimit condition is written in. Outside the area the grid is not one of the grids tried, so the widget snaps to whichever other grid covers the position and to nothing at all where none does, exactly as the rectangle already behaves. "grid": [ { "x": 100, "y": 100, "condition": "(x - 800)^2 + (y - 500)^2 < 300^2" } ] The properties sidebar edits it in a "Conditions (one per line)" field right under the X/Y from/to rows of "Only in part of the parent", reporting a line that does not parse under the field it was typed into; the area the conditions describe is traced onto the board in the same dashed line the rectangle is outlined with, and follows a property it reads while it is edited. The JSON editor offers "add condition" / "add another condition" on a grid entry, and the validator parses the expressions in one. The test room is a new variant of the Properties - Grid tutorial ("Condition Limit", 5.json): a round area, two grids split by a diagonal, and an area that follows a property while the game is played. Co-Authored-By: Claude <noreply@anthropic.com> * Snap onto a grid point inside a condition's area, not the one past its edge A grid entry limited by a condition was only asked about the position the widget was dropped at, and then snapped to the lattice point nearest that position wherever that lay. That point is up to half a cell away in each direction, and a boundary that runs wherever it likes is easily in between: dropping a piece well inside a round area could leave it sitting just outside the very area the grid is limited to. snapToGrid() now walks the lattice outwards from that point instead and lands the widget on the nearest one where the grid actually applies - inside its rectangle and inside the area its conditions describe - ranked by how far the widget itself moves rather than by the point alignX/alignY aligns. Where the area holds no lattice point at all (an area narrower than the grid step) the grid does not apply and the widget stays where it was let go, as it already does outside the area. A grid without a condition keeps snapping to the nearest lattice point wherever that is: that is what it has always done and what games are built on, so nothing existing changes. The Condition Limit variant of the Properties - Grid tutorial and the Conditions hint in the sidebar say where the widget lands. Co-Authored-By: Claude <noreply@anthropic.com> * Mark only the snap points a grid's condition holds at, not the whole rectangle The editor's snap-grid preview draws a dot at every position the widget can land on. It draws them as one repeating background across the grid's whole rectangle, which is every lattice point of it - so a grid limited to an area by a condition was still marked all over the rectangle, promising snap points on the far side of a boundary the widget can never be put past. The dots of a grid that carries a condition are drawn one by one instead, only where the grid applies (the same question snapToGrid asks of a point it weighs up), while the faint lattice lines stay as the mesh the area is read against. Each dot is a zero-length subpath with a round cap, so all of them are one path, and it is the same dot the background paints: the lattice's color inside a white ring, at the size it has at every zoom. A lattice with more points than a condition can be read at while it is being typed (4000) keeps the plain background, where the dots are barely apart anyway. Co-Authored-By: Claude <noreply@anthropic.com> * Tweaks to tutorials --------- Co-authored-by: VTT AI Agent <300563116+VTT-AI-Agent@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: 96LawDawg <76912527+96LawDawg@users.noreply.github.com>
1 parent 3a874a2 commit 42510e7

31 files changed

Lines changed: 4238 additions & 94 deletions

File tree

.github/workflows/testcafe-chrome.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ jobs:
2121
- editor.js
2222
- toolbar.js
2323
- boardsize.js
24+
- draglimit.js
2425
- compute-1.js
2526
- compute-2.js
2627
- compute-3.js

.github/workflows/testcafe-firefox.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ jobs:
2121
- editor.js
2222
- toolbar.js
2323
- boardsize.js
24+
- draglimit.js
2425
- compute-1.js
2526
- compute-2.js
2627
- compute-3.js

client/css/editor/layout.css

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,9 @@ body.gridLines33 #topSurface {
173173
line width below is compensated */
174174
--gridPreviewDotRadius: calc(3px / (var(--scale, 1) * var(--zoom, 1)));
175175
--gridPreviewDots: radial-gradient(circle at center, var(--gridPreviewColor) 0 var(--gridPreviewDotRadius), #fff9 var(--gridPreviewDotRadius) calc(var(--gridPreviewDotRadius) + 1.5px), transparent calc(var(--gridPreviewDotRadius) + 1.5px));
176+
/* every lattice point of the rectangle, unless the dots are drawn one by one
177+
because a condition leaves only some of them (see .ownDots below) */
178+
--gridPreviewDotLayer: var(--gridPreviewDots);
176179
/* the overlay is drawn inside the scaled room, so a 1px line ends up thinner
177180
than one device pixel and the rasterizer drops some of the lines entirely
178181
instead of only dimming them - one device pixel is the thinnest that keeps
@@ -186,7 +189,7 @@ body.gridLines33 #topSurface {
186189
pointer-events: none;
187190
z-index: 2000000;
188191
background-image:
189-
var(--gridPreviewDots),
192+
var(--gridPreviewDotLayer),
190193
repeating-linear-gradient(to right, var(--gridPreviewLine) 0 var(--gridPreviewLineWidth), transparent var(--gridPreviewLineWidth) var(--gridPreviewX)),
191194
repeating-linear-gradient(to bottom, var(--gridPreviewLine) 0 var(--gridPreviewLineWidth), transparent var(--gridPreviewLineWidth) var(--gridPreviewY));
192195
background-size: var(--gridPreviewX) var(--gridPreviewY), auto, auto;
@@ -195,12 +198,90 @@ body.gridLines33 #topSurface {
195198

196199
/* several lattices at once (a hex grid) only agree on their snap points */
197200
.gridPreviewOverlay.dotsOnly {
198-
background-image: var(--gridPreviewDots);
201+
background-image: var(--gridPreviewDotLayer);
202+
}
203+
204+
/* a condition holds in part of the rectangle only, so the dots are drawn one
205+
by one where it does (.gridConditionDots below) and the layer that repeats
206+
one across the whole rectangle goes - the lattice lines stay, as the mesh
207+
the area is read against */
208+
.gridPreviewOverlay.ownDots {
209+
--gridPreviewDotLayer: none;
210+
}
211+
212+
/* what a grid's conditions add to the lattice overlay: the boundary of the area
213+
they describe, and the snap points inside it. Both are drawn into the overlay
214+
itself and in the coordinates it is placed in, so they are cleared and
215+
redrawn with it and sit exactly on the positions they are about. */
216+
.gridConditionOutline,
217+
.gridConditionDots {
218+
position: absolute;
219+
left: 0;
220+
top: 0;
221+
overflow: visible;
222+
pointer-events: none;
223+
}
224+
225+
/* the boundary, in the dashed line the rectangle around it is outlined with.
226+
Its stroke does not scale with the room (the outline's does not either), so
227+
it stays one device pixel wide at every zoom rather than disappearing at
228+
small ones. */
229+
.gridConditionOutline path {
230+
fill: none;
231+
/* the color the dots use rather than the fainter one of the lattice lines:
232+
a dash of a curve is antialiased across two device pixels where the
233+
rectangle's straight outline lands on one, which at the transparency of
234+
the lattice leaves nothing visible at all */
235+
stroke: var(--gridPreviewColor);
236+
stroke-width: 1px;
237+
stroke-dasharray: 6 4;
238+
vector-effect: non-scaling-stroke;
239+
}
240+
241+
/* the snap points of a grid a condition limits, drawn one by one where it
242+
holds instead of repeated across the whole rectangle. Each one is a
243+
zero-length subpath with a round cap, so a dot is a stroke width - the same
244+
dot the background layer paints, a core in the lattice's color of twice
245+
--gridPreviewDotRadius inside a white ring 1.5px wider on every side, and
246+
the same compensation of the room's scale so it keeps its size at every
247+
zoom (a stroke of the drawing itself, so non-scaling-stroke would leave the
248+
room's own transform on top of it). */
249+
.gridConditionDots path {
250+
fill: none;
251+
stroke-linecap: round;
252+
}
253+
254+
.gridConditionDots .dotHalo {
255+
stroke: #fff9;
256+
stroke-width: calc(2 * var(--gridPreviewDotRadius) + 3px / (var(--scale, 1) * var(--zoom, 1)));
257+
}
258+
259+
.gridConditionDots .dotCore {
260+
stroke: var(--gridPreviewColor);
261+
stroke-width: calc(2 * var(--gridPreviewDotRadius));
262+
}
263+
264+
/* live preview of the area a widget can be dragged in, drawn into its parent
265+
(the coordinate space dragLimitedCoord works in) while the "Drag limits"
266+
editor section is open. The canvas holds one pixel per sampled position and
267+
is blown up to the parent's box, so the shading is honest about how finely
268+
the area was tested rather than smoothing a shape that was never computed. */
269+
.dragLimitPreviewOverlay {
270+
position: absolute;
271+
left: 0;
272+
top: 0;
273+
width: 100%;
274+
height: 100%;
275+
opacity: 0.3;
276+
image-rendering: pixelated;
277+
pointer-events: none;
278+
z-index: 2000000;
199279
}
200280

201281
/* leaving edit mode from the toolbar tab keeps the editor loaded, so the
202282
preview has to disappear with the mode rather than only on editor close */
203-
body:not(.edit) .gridPreviewOverlay {
283+
body:not(.edit) .gridPreviewOverlay,
284+
body:not(.edit) .dragLimitPreviewOverlay {
204285
display: none;
205286
}
206287

client/css/editor/propertyInputs.css

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1914,6 +1914,45 @@ body.editorWidgetPicking, body.editorWidgetPicking .widget {
19141914
min-width: 0;
19151915
}
19161916

1917+
/* a side of the drag limit takes a number or an expression that computes one:
1918+
the two sides of an axis stay side by side at the width a number needs, and
1919+
only a side actually holding an expression takes a line of its own */
1920+
.editorModule .propertyInlineRow.dragLimitRow .propertyInput.nonNumericValue {
1921+
flex: 1 1 100%;
1922+
}
1923+
1924+
.editorModule .propertyInlineRow.dragLimitRow .propertyInput.nonNumericValue input[type=text] {
1925+
flex: 1 1 auto;
1926+
min-width: 0;
1927+
}
1928+
1929+
/* what is wrong with the value, under the input holding it */
1930+
.editorModule .propertyInput .propertyInputProblem:not(:empty) {
1931+
flex: 1 1 100%;
1932+
margin-left: 132px;
1933+
color: #e05d5d;
1934+
font-size: 12px;
1935+
}
1936+
1937+
.editorModule .propertyInlineRow .propertyInput .propertyInputProblem:not(:empty) {
1938+
margin-left: 0;
1939+
}
1940+
1941+
.editorModule .propertyInput.hasProblem input,
1942+
.editorModule .propertyInput.hasProblem textarea {
1943+
outline: 1px solid #e05d5d;
1944+
}
1945+
1946+
/* the sampling behind the drag limit preview found no position the conditions
1947+
allow. Not a red error - every line of it is written correctly, it is the
1948+
area they describe together that is not there - so it warns in its own
1949+
colour rather than in the one an unreadable expression uses. */
1950+
.editorModule .dragLimitEmptyArea:not(:empty) {
1951+
color: #e0a15d;
1952+
font-size: 12px;
1953+
margin: 2px 0 4px;
1954+
}
1955+
19171956
.editorModule .gridExtraRow {
19181957
display: flex;
19191958
align-items: center;

client/js/editor/propertyInputs.js

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -602,11 +602,24 @@ class PropertyInput {
602602
propertyInfoButton(this.dom, html(this.options.hint));
603603
}
604604
this.renderControl(this.dom);
605+
// options.validate: a problem with what was typed belongs under the input
606+
// it was typed into, not only in the validation table of another module
607+
if(this.options.validate)
608+
this.problemDOM = div(this.dom, 'propertyInputProblem');
605609
for(const property of this.listenProperties())
606-
this.module.addPropertyListener(this.widget, property, _=>this.update(this.getValue()));
610+
this.module.addPropertyListener(this.widget, property, _=>this.applyUpdate(this.getValue()));
607611
return this.dom;
608612
}
609613

614+
applyUpdate(value) {
615+
this.update(value);
616+
if(this.problemDOM) {
617+
const problem = value === null || propertyInputIsMulti(value) ? null : this.options.validate(value);
618+
this.problemDOM.textContent = problem || '';
619+
this.dom.classList.toggle('hasProblem', !!problem);
620+
}
621+
}
622+
610623
cssClass() {
611624
return '';
612625
}
@@ -627,6 +640,10 @@ class TextInput extends PropertyInput {
627640
this.input = document.createElement(this.options.multiline ? 'textarea' : 'input');
628641
if(this.options.placeholder)
629642
this.input.placeholder = this.options.placeholder;
643+
// a field holding a list is unreadable at the two rows a textarea defaults
644+
// to, so its height can be asked for in lines
645+
if(this.options.multiline && this.options.rows)
646+
this.input.rows = this.options.rows;
630647
this.input.oninput = _=>{
631648
const value = this.input.value;
632649
this.setValue(value === '' && this.options.nullIfEmpty ? null : value);
@@ -719,6 +736,19 @@ class NumberOrTextInput extends PropertyInput {
719736
this.input.type = 'text';
720737
if(this.options.placeholder !== undefined) this.input.placeholder = this.options.placeholder;
721738
this.input.oninput = _=>this.setValue(propertyInputNumberOrText(this.input.value, this.options.nullIfEmpty));
739+
// a text input has no arrow key stepping, which is the part of a number
740+
// input that still applies while the value happens to be a plain number
741+
this.input.onkeydown = e=>{
742+
if(e.key != 'ArrowUp' && e.key != 'ArrowDown')
743+
return;
744+
const value = propertyInputNumberOrText(this.input.value, false);
745+
if(typeof value != 'number')
746+
return;
747+
e.preventDefault();
748+
const stepped = +(value + (e.key == 'ArrowUp' ? 1 : -1) * (e.shiftKey ? 10 : 1)).toFixed(6);
749+
this.input.value = stepped;
750+
this.setValue(stepped);
751+
};
722752
target.appendChild(this.input);
723753
}
724754

@@ -729,6 +759,12 @@ class NumberOrTextInput extends PropertyInput {
729759
// old "e.g. 8, 8px, 50%" placeholder down to "e.g. 8," - reading like a
730760
// typo rather than a hint
731761
this.input.placeholder = multi ? '— multiple —' : (this.options.placeholder || '8px');
762+
// a plain number fits the compact width these inputs are laid out at,
763+
// anything else usually does not - so the layout can tell the two apart,
764+
// and what does not fit is at least readable as a tooltip
765+
const text = (value === null || multi) ? '' : String(value);
766+
this.dom.classList.toggle('nonNumericValue', text !== '' && typeof value != 'number');
767+
this.input.title = typeof value == 'number' ? '' : text;
732768
if(document.activeElement !== this.input)
733769
this.input.value = (value === null || multi) ? '' : value;
734770
}

0 commit comments

Comments
 (0)