Text Input fixes - Scrolling, highlighting - #53
Open
hoseyjoe wants to merge 11 commits into
Open
Conversation
ScreenImpl handed its listeners whatever was sitting in `event.screenX/Y`
when it ran. That is the single MouseEvent instance MouseHelper dispatches
to every listener in turn, and ComponentImpl.eventToCamera writes its own
transformed coordinates back into it - so what a Screen level listener saw
depended on whether a component handler happened to run first:
Toolkit.scale = 1.5, one synthetic move at window x = 600
with a Button on screen : Screen listener saw 400 (layout units)
with no component mouse
handlers registered : Screen listener saw 600 (raw window px)
The camera transform was never applied either, so a scene with an
interactiveCamera at any non-identity scale or offset got raw window
coordinates in both cases.
Everything that tracks a drag through Screen.instance - splitters,
sliders, scrollbar thumbs, menus, drag and drop - compares these
coordinates against component positions, which are in layout units, so
the wrong answer is a drag that runs away from the cursor.
Seeds from the untransformed hxd.Event (never from the shared event's
already-mutated screenX/Y, the same reason ComponentImpl.eventToCamera
does), applies the interactive camera, then divides by Toolkit.scaleX/Y -
so Screen level and component level events now arrive in the same space,
deterministically.
h2d.TextInput draws its selection by emitting a tile from the text itself,
which cannot work for a signed distance field font. The SDF shader is added to
the text drawable, so every tile that drawable emits goes through it, and the
shader ends
textureColor = vec4(1.0, 1.0, 1.0, smoothstep(...));
discarding the tile's colour and keeping only its coverage. The highlight
therefore arrives painted in the colour of the text, as a solid bar over
exactly the words it is meant to be showing. There is no translucent version
to settle for either: coverage is a smoothstep whose smoothing comes from the
gradient across the texture, and a flat colour has no gradient, so the bar is
fully opaque or entirely absent.
A separate object carries its own shaders and none of them is that one, so the
highlight is now a Graphics of its own, drawn from the same measurements and in
the same index space h2d.TextInput uses for its own - cursorIndex and
selectionRange count the wrapped text, breaks included - while heaps' tile is
left with no height and so covers nothing. It is re-emptied whenever the style
is validated, since that is where the font is set and h2d.Text.set_font builds
a fresh tile.
ComponentImpl puts it in the style graphics container, which is always the 0th
child and so is drawn before the text. Putting it in the component's own child
list would have put every component child one place out of step with
INDEX_OFFSET.
TextInputImpl.selectionColor sets the colour, defaulting to heaps' own blue so
that nothing which worked before looks any different.
…re laid out at Toolkit.scaleX magnifies the whole component tree — ScreenImpl sets it on every root component — but TextDisplayImpl sized the font from the style alone. A style of 11px was therefore built for an 11 pixel em and stretched over 16.5 physical pixels at a 1.5 scale: an 11 pixel rasterisation blown up, with glyph advances that were whole numbers landing on thirds of a pixel. Small text suffered most, which on a desktop tool is most of the text. The font is now built at round(fontSize * Toolkit.scaleX) and the text stands down by fontSize/deviceSize, so the same words occupy the same box out of a rasterisation that matches the screen. The ratio is exact rather than 1/scale, since the built size is a whole number: a 15pt label rounded to 23 device pixels is still 15pt wide. Three places read the text's own numbers and had to convert back to layout units through sprite.scaleX: the measured width and height, the baseline offset in validatePosition, and maxWidth in validateDisplay, which is where the words wrap and is in the text's units rather than the component's. At Toolkit.scaleX == 1 every line of this is an identity.
heaps renders a filtered object into a texture and composites it, sizing that texture at viewportScaleX * resolutionScale (h2d.Object.drawFilters). Toolkit.scaleX is neither of those - it magnifies the root COMPONENT - so at a display scale the viewport stays 1, the texture is rasterised at layout size, and the composite stretches it. Everything a clip rect touches was therefore soft at any scale but 1: every scrollview, list, tree and dropdown, while the text beside them that no filter touches stayed sharp. The same fault the glyph-size fix addresses for text drawn directly, one layer further out. Set on the group createFilterGroup builds, which is the one place every filter this backend makes comes from - and it is rebuilt by applyStyle and handleClipRect, so a scale set before the UI is built (which is when it has to be set anyway) is always the current one. Measured on Joeditor's tree column at 1.5: mean edge gradient 3.066 before, 3.204 after, against 3.197 for the same content unfiltered. The toolbar, which no filter touches, is 2.807 either way.
The two are different sums and they part company on a component whose
root is not at the origin.
cacheScreenPos adds every left up the chain and scales the TOTAL;
ComponentBase.get_screenBounds scales each left EXCEPT the root
component's own. For an ordinary root at left 0 those are the same
number, which is why everything inside a window hit-tests correctly and
this went unnoticed.
A dropdown's open list is not that. It is a root component of the
Screen, and DropDownHandler positions it with
_wrapper.left = _dropdown.screenLeft + componentOffset.x
- a value already in screen pixels. Scaling the total scales it a second
time, and screenX stops describing where the component is.
screenBounds is the one that agrees with the sprite: measured on a
dropdown row at Toolkit.scale 1.5, screenBounds and the drawn sprite both
said 739,755 while screenX said 370,400. It is also what
ComponentBase.hitTest already compares against, so this puts the two hit
paths of this backend on one number instead of two.
Unpatched: at any scale but 1 a press on a row of an open dropdown list
falls straight through to whatever is behind it. Verified with Joeditor
--dropdown, which opens a list and presses a row where it is drawn: the
row answers at 1, 1.25, 1.5 and 1.75 with this applied and at 1 only
without it.
An unconditional trace of two widths in the multiple-masks offset loop, left over from writing it - the identical loop for the height branch immediately below has none, which is the tell. It prints on every clip-rect update where a component has a sibling with a mask of a different width, so a tableview or a window of nested scrollviews would print it continuously. Nothing else in this file traces, and everything else that does says WARNING and means it.
It referred to a sibling fix by name, which means nothing to a reader who does not have that fix.
…'s own units A multiline field was handed the component's width and height as the window it clips its own drawing to, and the field draws in the units its glyphs are baked in. Since Text builds glyphs at DEVICE size and stands the sprite down to match, those are not the same units at any Toolkit scale but 1: at 1.5 the window came out two thirds the size of the box, so every line was cut off two thirds of the way across and the last third of the lines was not drawn at all. The box looked right; the words in it were a window onto themselves. The selection highlight had the mirror of it. Its bars are built from the field's own measurements - calcTextWidth, lineHeight, the wrapped lines - and were then drawn in the component's units, so at 1.5 they came out two thirds the width of the words they were under and a line low. So: one ratio (textScale), taken from the sprite the size of the glyphs already decided, and applied at every crossing - the view, the scroll position (from both the scrollbar and the caret-follow), the caret's line as it is read back, and the highlight, which is now drawn at the same scale as the words it belongs to. At scale 1 every one of those is an identity, which is why none of this showed until a tool followed the desktop: rendered at scale 1 before and after, the same screen is byte-identical.
… list update() measured __timers.length once and reused it for both the fire loop and the cleanup sweep. Timer callbacks are arbitrary application code — they can add timers, stop timers, or re-enter update() (a callback that rebuilds part of a UI commonly ends up pumping the backend again). When one shortened the list, the sweep indexed past the end and dereferenced null: Null access ._stopped at TimerImpl.hx:21.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Comments ion the commits