Skip to content

Playground UI redesign#4792

Merged
shai-almog merged 56 commits intomasterfrom
playground-redesign
Apr 24, 2026
Merged

Playground UI redesign#4792
shai-almog merged 56 commits intomasterfrom
playground-redesign

Conversation

@shai-almog
Copy link
Copy Markdown
Collaborator

No description provided.

@github-actions
Copy link
Copy Markdown
Contributor

Cloudflare Preview

@shai-almog shai-almog force-pushed the playground-redesign branch 7 times, most recently from 55c6775 to 2f78cff Compare April 23, 2026 01:31
shai-almog and others added 22 commits April 24, 2026 12:19
- Switched the editor/preview layout to a SplitPane with 25/50/75 bounds,
  no expand/collapse arrows or drag-handle, a thin PlaygroundSplitDivider
  line, and matching dark variant.
- Status pill: 50% translucent background, pill border, BoxLayout.x so the
  dot is vertically aligned with the label regardless of font ascent.
- Share/Download: round-rect border with stronger share outline; download
  text stays white in dark mode.
- Code/CSS segmented group: rounded outer border + rounded selected pill.
- Playground wordmark uses MainRegular instead of MainBold.
- CSS toggle icon falls back to MATERIAL_BRUSH (the wireframe alternative
  to the filled MATERIAL_PALETTE, which CN1's font ships only in solid).
- Orientation segmented is now icons-only regardless of layout density.
- Button icon/text gap scales with DPI (1.3mm) instead of 2 raw pixels.
- Monaco gutter narrower and lighter (glyphMargin off, lineNumbersMinChars
  2, lineDecorationsWidth 4, softer line-number colors, matching
  editorGutter.background).
- Cleared the default BrowserComponent border so the editor sits flush.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Moved inspector and samples/history slots out of the SplitPane to
  center.EAST / center.WEST so opening Inspector no longer squishes the
  device skin; the SplitPane holds just editor and preview.
- previewContainer.setPreferredW(82mm) so BorderLayout keeps a minimum
  wide enough to fit the iPhone portrait skin even when the SplitPane
  right side is dragged inward.
- Stage scroll axis is now exclusive: portrait / no-skin uses scrollableY,
  landscape (skinned) uses scrollableX. Never both at once.
- Panel show/hide animation: drop the revalidate fallback so
  animateLayout(220) on the shared center container can actually
  interpolate between before/after layouts.
- Status pill:
    * Translucency bumped 0.5 -> 0.75 (darker Live background).
    * Pill border drawn only by the outer container; inner Label uses a
      new PlaygroundStatusLabel{,Error}{,Dark} UIID (transparent, no
      border) so the pill isn't drawn twice.
- Top-bar app icon: in dark mode switches to a white MATERIAL_WIDGETS
  glyph on an rgba(255,255,255,0.1) rounded pill (new PlaygroundAppIcon
  {,Dark} UIIDs) instead of the bundled icon.png, whose white
  background read poorly against the navy bar.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Inspector:
- Custom tree rows with chevrons for containers and outlined type icons
  (folder for containers, document for leaves). Depth-indented 3mm base
  plus 5mm per level, 7mm row height, flush rows.
- IDENTITY / CONTENT / APPEARANCE / LAYOUT section headers between field
  groups with 1px dividers above each non-first section.
- Single-column form: 22mm label + input-fills-rest, replaces the old
  two-column grid.
- Inputs get 1.5mm corner radius; dark #0F2A4D on 1px #1F3A5F border.
- Color fields pair a hex input with a 9x9mm live swatch.
- Bounds / Padding / Margin render as four separate inputs with a row
  of micro-labels (X Y W H or T R B L) directly beneath.
- Unit selection is now a horizontal segmented control (Dips / Pixels)
  in a standard form row rather than a floating button group.
- Added PlaygroundTreeRow{,Active}, PlaygroundTreeChevron,
  PlaygroundTreeType{,Active}, PlaygroundTreeBracket{,Active},
  PlaygroundInspectorSection, PlaygroundInspectorDivider,
  PlaygroundField{Row,Label,Input,ReadOnly,Micro},
  PlaygroundInspectorSwatch, PlaygroundInspectorSegment{,Active,Inactive},
  PlaygroundInspectorCheckbox UIIDs with dark variants and registered
  them in supportsDarkVariant().

Top bar:
- App-icon dark mode bug: setMaterialIcon bakes the glyph using the
  label's current FG color, so setUIID must be applied before the icon
  - otherwise the icon froze black and reappeared as a solid blob once
  the white-on-translucent UIID kicked in. Reordered + switched to
  MATERIAL_CODE which reads cleanly at 4.5mm.

Layout:
- previewContainer.setPreferredW 82mm -> 92mm so the bezel, corner mask,
  and SplitPane divider don't crowd the skin when Inspector is open.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tree rows:
- Replaced the narrow BorderLayout.EAST hit-Button (which only
  covered a sliver of the row) with a full-width Button body
  containing the type icon + combined label text, so clicking
  anywhere on the row's visible content selects the component.
  The chevron remains a separate Button on the west for
  expand/collapse only.
- Merged the type name and bracket into a single Button.setText
  since Button can't nest two differently-styled Labels.

Sizing:
- The inspector spec used "mm" as design-doc millimeters, which
  at CN1's physical-mm conversion render roughly 2x too large.
  Halved layout dimensions and font sizes across the panel:
  label column 22->12mm, tree indent 3/5->1.5/2.5mm, row height
  7->4mm, swatch 9->5mm, input corner 1.5->0.8mm, section header
  font 2.6->1.8mm, field label/input font 3->1.9mm, micro 2.2->
  1.4mm, segmented font 2.8->1.8mm, tree text 3.3->2mm, icon
  sizes 3.5/4->2.2/2.5mm, tree max height 70->38mm.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Unified the tree and property form into a single scrollable Y
  Container. Eliminates the dual bounce when scrolling nested
  scroll areas.
- Pass the user's script Component to the Inspector as the tree
  root directly, instead of the preview wrapper chain (content
  host / bezel / screen). Root now shows the user component.
- Removed the per-cell micro-labels (X/Y/W/H, T/R/B/L) below
  Bounds, Padding, and Margin fields. The same information is
  now exposed as a per-input tooltip (X, Y, Width, Height; Top,
  Right, Bottom, Left).
- CONTENT section only renders when the selected component has
  editable text (Label / TextField / TextArea); never renders an
  empty "Text: -" placeholder when the section is not applicable.
- Dropped the thin inter-section dividers. A single stronger
  divider separates the tree from the property form
  (PlaygroundInspectorTreeDivider) and reaches the panel edges
  via negative horizontal margins.
- Even spacing between multi-value fields: each sub-input has a
  symmetric 1 DIP horizontal margin and the surrounding grid
  uses negative outer margins so the overall row alignment is
  preserved and X-Y / T-R no longer touch.
- Sizes bumped from 50% back up to roughly 75% of the original
  spec (label column 16 DIP, tree row 5.2 mm, tree indent 2 +
  3.8 DIP per level, input padding 1/1.6 mm, swatch 5 mm, font
  sizes 2-2.5 mm).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tree rows:
- Replaced the Button-in-CENTER click target with a pointer-released
  listener on the row Container itself. Selection now fires on a
  release anywhere in the row (including the icon and label regions),
  except when the release is within the chevron's absolute X range -
  chevron retains its own ActionListener for expand/collapse.
- Row height bumped to 6 mm so the hit target is comfortably tall.

Divider between tree and property form:
- Previously the divider had negative horizontal margins to punch past
  the root's 2 mm inner padding; CN1 clipped the line so it never
  rendered. Moved the padding off PlaygroundInspectorRoot{,Dark} and
  applied it inline on treeContainer / propertiesContainer so the
  divider now spans the full panel width without needing any negative
  margin.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CN1 Container defaults to non-scrollable but this has burned us
before: the previous Tree-based tree view enabled Y scroll in its
own constructor, which produced the bouncy dual-scroll feel. Keep
scroll explicitly off on treeContainer and propertiesContainer so
only unifiedScroll handles pointer drags.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
propertiesContainer defaults to non-scrollable anyway - only the
tree needs the explicit disable since the old Tree-based parent
enabled scrolling in its own constructor. Collapsed the separate
X/Y calls into one setScrollable(false).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The pointer-released listener on the row Container does not
fire reliably inside the outer scrollable container: the scroll
container swallows the gesture when a drag begins. Reverted to
the explicit Button-in-CENTER approach which does get the click
events directly (and worked before). Button with Alignment.LEFT
still stretches to fill the row width because the UIID has
padding 0 and it sits in BorderLayout.CENTER.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Live dot used a 1px margin against its text, while Share
and Download buttons use a 1.3mm gap between their icon and
label. Aligned the dot's right margin to 1.3mm DIPS so the
spacing reads consistently across the top bar.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Clicking a tree row fired handleComponentSelected which ran
updatePropertyPanel and rebuildTree; each of those revalidated
their own Container. But CN1's revalidate() re-lays out the
called Container in place without propagating to the parent,
so the outer unifiedScroll was unaware of the new preferred
sizes - the updated rows and new property form stayed hidden
until an unrelated layout pass. Revalidate + repaint the root
inspector component at the end of selection to force the whole
subtree to re-render.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Swapped the unified scroll for a vertical SplitPane with 30/50/
70 insets, showing the same thin PlaygroundSplitDivider line as
the main editor/preview split. Tree (top) and properties
(bottom) are each independently scrollable Y, which keeps the
two panes' revalidate passes local so clicking a leaf actually
updates the property form - the old shared scroll didn't
propagate sizes up from the inner containers so selection
highlight showed but the form didn't repopulate.

Dropped the custom PlaygroundInspectorTreeDivider build helper
since the SplitPane provides the divider.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
propertiesContainer sits scrollableY inside a SplitPane. The
inner revalidate() in updatePropertyPanel fires but the new
children end up with zero measured size until the container's
ancestor chain (scroll wrapper + pane) re-lays out -- hence the
pane stayed blank until a manual resize. revalidateWithAnimationSafety
walks up from the container, which forces the pane to re-measure
and the field rows render immediately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Button-triggered selection fires while CN1's tactile press
animation is still running. Revalidating during the animation
caused AnimationManager to snap the pending layout to the
animation's final frame, leaving newly-added field rows at
measured size 0 until the next unrelated resize. Deferred the
panel rebuild + tree rebuild to the next EDT tick via
CN.callSerially, and revalidate the entire form afterwards so
the SplitPane and its inner scroll both re-measure with the
new content.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
shai-almog and others added 22 commits April 24, 2026 12:19
Inspector:
- propertiesContainer is now a plain non-scrollable BoxLayout.y
  that holds the field rows. A dedicated propertiesScroll wrapper
  (BorderLayout with the content at NORTH, setScrollableY(true))
  sits between it and the SplitPane's bottom pane. On selection,
  updatePropertyPanel mutates only the inner Container, so the
  scroll wrapper's layout state is never recreated and a plain
  propertiesContainer.revalidate() re-lays out the new children
  reliably - no more forceRevalidate / callSerially / revalidateLater
  trying to outrun a scroll wrapper that was being reset mid-update.

Activity icons:
- +1 mm more left padding (1.7 -> 2.7 mm) so each icon visually
  aligns with the title icon above.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Selection threw IllegalArgumentException from Style.setMargin - CN1
rejects negative margin values, but I was using (0, 0, -1, -1) on
the multi-value field grid to compensate for the symmetric per-cell
margins. Removed the negative outer compensation and trimmed the
per-cell margin to left-only, applied to every cell except the first,
so the four sub-inputs have a consistent inter-cell gap without the
outer grid edges needing any negative offset.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Switched the scroll wrapper's layout from BorderLayout with
content at NORTH to BoxLayout.y. BorderLayout.NORTH clamps its
child to its own viewport height, so scrollableY never engaged
when content overflowed. BoxLayout.y stacks by preferred height,
which lets the wrapper's scrollable state kick in as soon as the
content is taller than the pane.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mobile layout now differentiates from tablet-compact and actually
responds to bottom-nav taps.

Top bar (setMobile):
- Code/CSS mode toggle hidden (tab strip below takes its role).
- Status pill collapses to dot-only via new setCompactDot, leaving
  the text label hidden so only the coloured dot sits next to the
  app icon.

Preview column (setMobile):
- Preview toolbar (device / orientation / dimensions) hidden.
- Device forced to DEVICE_NO_SKIN; restored to the previously-active
  device when the layout reverts to tablet/desktop.
- Stage fills the full tab area without a synthetic bezel.

CN1Playground:
- assembleDesktopLayout and assembleMobileLayout call setMobile(false)
  and setMobile(true) respectively on both topBar and previewColumn.
- applyActivity branches on isMobileLayout(): on mobile, bottom-nav
  selection replaces the tab content with the chosen panel while
  the top bar + bottom nav stay in place; the panel's own close
  button deselects the nav item and restores the current tab.
- refreshMobileTabContent consults mobilePanelFor() first - if a
  bottom-nav panel is active it renders instead of the Code/CSS/
  Preview tab content.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- No-skin AND iPhone both default to iOS base theme in
  applyDeviceTheme (Pixel keeps Android). "No skin" meant "no
  bezel", not "no theme".
- PlaygroundTopBar.applyAppIconStyle sets the label FG color
  inline before creating the Material icon so the icon is baked
  in white in dark mode regardless of when UIID resolution runs.
- PlaygroundStatusPill.setCompactDot now hides both text AND its
  wrapping FlowLayout, so the pill collapses to just the coloured
  dot on mobile instead of retaining the wrap's width.
- Mobile top-tab strip and bottom-nav fonts shrunk (tab 2.8->2.4mm,
  nav 2.4->2mm) so "Preview" fits on tight phone widths.
- assembleMobileLayout defensively resets bottomNav's hidden /
  visible state and sets an explicit 12mm preferredH so the
  Samples / Inspector / History row is always visible even if
  an earlier flow (keyboard-show hook) toggled it off.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Top bar:
- setCompact now also setHidden(true) on the wordmark, not just
  setVisible(false). setVisible alone leaves the ~30mm preferred
  width reserved, pushing the status pill / Share / Download off
  screen on mobile. setHidden collapses it to 0 preferred width.
- setMobile shrinks the icon column from 11mm to 8mm, drops the
  Share/Download icon glyph size from 3mm to 2.2mm, so the title
  bar actually has room for the status dot on narrow viewports.

Tests:
- New PlaygroundLayoutHarness. Boots the Playground lifecycle
  headless via Display.init, lets the EDT tick, then walks the
  shown Form tree verifying the presence + non-zero size + in-
  bounds position of top-bar UIIDs, the activity bar (desktop)
  or top-tab strip + bottom nav (mobile). Runs for both desktop
  and mobile by toggling the cn1.desktop / cn1.tablet system
  properties. Hooked into run-playground-smoke-tests.sh so CI
  catches regressions where a chrome element is hidden, zero-
  sized, or rendered outside the form bounds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CN1Playground.runApp did Form.getToolbar().setUIID(...) unconditionally,
but getToolbar can return null in a Display.init(null) headless
context - hence the layout harness's "runApp did not complete within
3s" after the hidden NullPointerException. Guard the toolbar
configuration with a null check so the app works in both the
simulator and real HTML5/iOS/Android runtimes (where getToolbar
returns a real Toolbar instance).

Also surface any throwable from runApp in the harness's callSerially
runnable so CI logs the real exception instead of a timeout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Display.isDesktop / isTablet ignore external system properties in
the JavaSE simulator, so the "mobile" scenario was still getting
the desktop shell (hence "TopTabs NOT FOUND"). Added a package-
private testOnlyForceLayout hook on CN1Playground that bypasses
the Display checks when set. Harness sets it to LAYOUT_MOBILE /
LAYOUT_DESKTOP per scenario and resets to LAYOUT_NONE afterwards.

Also updated findByUiid to skip hidden / invisible subtrees. The
PlaygroundSegment UIID is used by BOTH the top bar's Code/CSS
toggle (setHidden on mobile) AND the mobile tab strip. Without
the hidden-skip the first match was the invisible mode toggle,
and the harness failed the size check.

Mobile checks now look for PlaygroundSegment{,Dark} (the tab
strip container) and PlaygroundBottomNav{,Dark}.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Layout breakpoint:
- Switched from Display.isDesktop() / isTablet() back to measuring
  viewport width via Display.getDisplayWidth() / convertToPixels(1f).
  isDesktop() always returns true on a desktop browser regardless
  of window width, so narrowing the browser never triggered the
  mobile shell. The spec is clear: < ~720 CSS px (190 mm) = mobile,
  < ~1100 CSS px (291 mm) = tablet, else desktop. d.isTablet() is
  still honoured as a secondary signal for native tablet builds.

Harness:
- Added an explicit "bottom nav must have three visible items"
  check. Previously the presence check on PlaygroundBottomNav only
  verified the container, which is why "3 buttons completely gone"
  slipped past CI. countVisibleItems walks the container and counts
  children with non-zero measured size and isVisible/!isHidden.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Added verifyBottomNavItems that, for each Button child of the
bottom nav, asserts it has at least an icon OR a non-empty text.
A Container with 3 empty Buttons would look "completely gone"
visually but pass the simple count check. Also checks that the
nav has exactly 3 Button children (Samples / Inspector / History).

Local harness: all checks pass in the JavaSE simulator. If the
HTML5 runtime still renders blank nav buttons after deployment
picks up this commit, the regression is on the rendering path
(icon font loading, peer z-order) rather than on construction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User report: "toolbar flickering in for a second below the HTML
component which means it's working but probably failing due to a
z-ordering or layout issue."

Root cause: the editor's BrowserComponent is a DOM iframe peer that
paints above the CN1 canvas. Its peer bounds track the editor
Component's bounds. With bottomNav inside bodyContainer -> mobileLayout
-> SOUTH, any time bodyContainer's height exceeded the expected
remaining-after-nav band (e.g. on first layout pass before SOUTH is
reserved), the iframe's bounds followed and covered the bottomNav.
Only after a resize would the Form-level layout reconcile, at which
point the bottomNav flashed through briefly before the iframe resized.

Fix: plant bottomNav at appForm.SOUTH directly, as a sibling of
bodyContainer. The Form-level BorderLayout carves out bottomNav's
preferredH BEFORE allocating CENTER, so bodyContainer can never
extend into the nav band - and neither can any peer iframe inside it.

assembleDesktopLayout also detaches bottomNav so the desktop shell
doesn't end up with both an activity bar and a leftover bottom nav.

Local harness confirms: PlaygroundBottomNav sits at pos=(5,1668)
size=2930x121, full form width, with 3 visible item buttons.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Added a 150ms UITimer after appForm.show() that resets the
cached layout key, re-runs applyLayoutForCurrentSize, and
revalidates the form. Browsers (especially mobile UA modes)
sometimes report a placeholder display size at the moment of
show, and the initial breakpoint / layout computation can
miss the real viewport. A second pass after the form has
settled guarantees the correct shell.

Also logging:
- Breakpoint computation: display width in px and mm, plus
  the chosen layout (MOBILE/TABLET/DESKTOP).
- bottomNav's final parent, absolute position, size, and
  visible/hidden flags after the post-show pass.

This gives us a deterministic way to tell from the browser
console whether the mobile shell is even being chosen, and
whether bottomNav ends up in a sane position. If it's in
the right spot but still visually covered, the issue is
the BrowserComponent peer iframe's z-order (canvas under
DOM) and needs a different fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The earlier diagnostic logs never appeared in the user's browser
console, which suggests the PR preview deploy hadn't picked up
the mobile-shell commits. Added a top-of-runApp Log.p marker so we
can tell at a glance whether the deployed build is the new one.

On the next page reload the console should show:
  CN1Playground build marker: mobile-shell-v2 @ 2026-04-24
  Playground layout: width=... (...mm) -> MOBILE|TABLET|DESKTOP
  Form: WxH
  bottomNav: parent=... pos=(...) size=... visible=... hidden=...

If the marker is missing, the preview hasn't rebuilt yet; wait a
couple of minutes and hard-reload. If the marker appears but
subsequent lines don't, runApp is failing mid-way.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diagnostic output confirmed: on mobile the layout is correct --
form 1544x2628, bottomNav at y=2242 size=1480x354 with
visible=true, hidden=false. The problem is CN1 HTML5Peer's DOM
iframe (Monaco editor) extending past its Java component's
bounds and covering the canvas-drawn bottom nav.

Mitigation: installIframeBottomGuard injects a <style> tag into
document.head with:

  @media (max-width: 720px) {
    iframe { max-height: calc(100vh - 64px) !important; }
  }

so no iframe can extend into the bottom 64px band regardless of
what the HTML5Peer positioning does. Runs once after
appForm.show() via the shared JS context.

This is a band-aid: the real fix would be making the nav a peer
itself, but the CSS cap is a deterministic way to prove the
diagnosis and give a working mobile UX while that larger change
is scoped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
You were right - peer components respect their bounds when they
are properly attached and sized. The covering peer was not the
ACTIVE editor but its idle sibling:

- We construct two BrowserComponents in runApp (editor for Java,
  cssEditor for CSS). Both create their iframes immediately in
  their constructors - CN1 HTML5 adds the iframe to the DOM up
  front, and we set HTML5Peer.removeOnDeinitialize=false to
  preserve Monaco state across dialog show/hide.
- Only the editor matching the current mode is ever attached to
  the CN1 tree via attachEditorsToHost. The other BrowserComponent
  is never in the tree, so its peer has no Java bounds to sync
  against. Its iframe sits at whatever default CSS size it was
  given (typically full-viewport) and paints above everything
  including the canvas-drawn bottom nav.

Fix: explicitly setVisible(false) on both editors at construction.
attachEditorsToHost flips the active editor's setVisible(true),
leaving the idle one hidden. refreshMobileTabContent calls
hideAllEditors() when Preview is active or a bottom-nav panel
covers the tab, since neither editor should paint in those cases.
CN1 HTML5 propagates setVisible(false) to the peer, which sets
display:none on the iframe and removes it from the compositor
until reattached.

Removed the installIframeBottomGuard CSS band-aid and its call
since the real fix is in place.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This reverts commit 2d4afd2.

The idle-editor-covering hypothesis was wrong: tabs already hide the
inactive component, so setVisible(false) on a non-attached
BrowserComponent has no impact on what the browser composites.
Restoring the previous behaviour so the next diagnostic pass has a
clean baseline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The bottom nav's Java bounds look correct in every log (pos, size,
visible, hidden all sane), yet users don't see it on the HTML5
deploy. Before guessing again, add a browser-side diagnostic: after
the post-show revalidate, read window.innerWidth/innerHeight,
visualViewport.width/height, documentElement.clientHeight, and
devicePixelRatio from JS and log the result via the existing CN
javascript context.

Mobile Safari in particular reports innerHeight as the layout
viewport (tall) while visualViewport.height reflects the area that
is actually not occluded by the dynamic URL bar - CN1 sizes the
canvas to innerHeight, so if visualViewport.height is materially
smaller, the bottom ~100px of the canvas is drawn behind browser
chrome and the bottom nav would never be visible no matter how
correct the CN1 layout is.

The log line lands next to the existing "Form:" and "bottomNav:"
lines so the three can be compared against each other on the next
deploy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The viewport log settled the browser-chrome question - window and
visualViewport matched exactly, so the nav is not hidden behind
iOS Safari's URL bar. Yet the nav is still not visible.

Replace the viewport probe with a DOM probe that answers three
questions directly from the browser:

1. Where is the CN1 canvas, really? Its rect + z-index tells us
   whether the canvas itself is full-height or clipped.
2. Where are all iframes, with z-index + visibility + display?
   Lists every iframe so we can see if any extends into the nav
   band, and whether any is display:none or visibility:hidden.
3. What element does elementFromPoint(x, navCenterY) return at the
   centre of each of the three expected nav cells (left, middle,
   right)? Returns the topmost paintable element with its bounding
   rect and z-index. If it's a CANVAS, the nav is drawn but the
   style is somehow invisible. If it's an IFRAME, a peer is
   covering it. If it's HTML/BODY, the canvas is clipped short.

Converts navY from device pixels to CSS pixels via dpr so the JS
coordinates match browser coordinate space.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous probe found cn1-peers-container on top at the nav
coordinates, but CN1's HTML5 port marks the canvas with
pointer-events:none (via codenameone-canvas setProperty in the
TeaVM-compiled classes.js) so elementFromPoint just skips the
canvas - that doesn't mean the nav is hidden, only that the canvas
can't be hit-tested. Visually the canvas is still on top (z=auto,
DOM-ordered AFTER peers-container which is z=-1000).

To actually know whether the nav paints, read back the pixel at
each of the three expected nav-cell X positions at the nav's Y on
the canvas via getImageData. If the pixels are the expected nav
UIID colour, CN1 is painting the nav and something else (still TBD)
is hiding it; if they are white/transparent, CN1 never draws the
nav band and the bug is on the Java side.

Also widens the DOM probe to return computed bg/opacity/pointer-
events for peers-container and canvas, plus the elementsFromPoint
stack at the nav centre for a full view of the compositing order.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Canvas pixel read proved CN1 paints the nav (navy rgba(16,43,102,255)
at the expected Y). So the bug is compositing, not CN1 layout. Two
oddities to pin down:

1. BODY is position:fixed with bg=rgb(255,255,255). If it outranks
   the canvas in stacking order, its opaque white background could
   paint over the canvas-drawn nav.

2. Some other fixed/absolute/sticky element may overlap the nav
   band. Walk every element, filter to positioned ones whose rect
   intersects [navTop, navBot], and dump id/class/z-index/bg/etc.
   Skip the canvas and the peers-container (already understood).

Also paint a 8px red band directly on the canvas at navTop and a
green band at navBot. If the user sees red+green but no nav, the
canvas IS visible - the nav paint got overpainted later. If the
user sees no red+green, the canvas as a whole is being occluded.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI compliance check failed with 9 violations of the form:
  bsh/cn1/gen/GeneratedAccess_com_codename1_util#invoke10(
      Lcom/codename1/util/Simd;Ljava/lang/String;[Ljava/lang/Object;
  ) -> SIMD alloca value returned from method

com.codename1.util.Simd exposes allocaByte / allocaInt /
allocaFloat (+ *Zeroed / *Filled variants) that return method-local
SIMD scratch arrays. On ParparVM these lower to C-stack allocations;
CN1's compliance check forbids letting the array escape the method
that allocated it, and the auto-generated reflection bridge
inherently does exactly that by returning the value from invokeN.

Fix: add Simd to the existing INTERNAL_CN1_TYPES set so the
generator skips it the same way it already skips Accessor and
IOAccessor. Simd is a low-level SIMD primitives API that bean-shell
playground scripts are extremely unlikely to need, so the blast
radius is tiny. If a user does need it later we can generate a
hand-written bridge that respects the method-local constraint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The rebase brought in #4801 (CN1 version bump 7.0.234 -> 7.0.235),
#4794 (Simd warnings fix), and #4798 (zoomFonts). Regenerate
GeneratedCN1Access + helper classes so the bean-shell registry
matches the new API surface: picks up UIManager.zoomFonts,
additional components/ui/plaf members, and the cleaned-up util
package (no more alloca* escapes from Simd, which is also excluded
explicitly by the preceding commit).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
shai-almog and others added 6 commits April 24, 2026 13:07
The JavaScript cloud build fails at runtime with
  com.codename1.ui.plaf.UIManager.zoomFonts(F)V was not found
because zoomFonts was added in CN1 7.0.235 (#4798) but the JS port
deployed to the cloud doesn't have it yet. The bean-shell access
registry references it directly, which turns a missing method into
a startup failure for every playground session.

Add a qualified-name method blacklist to the generator and seed it
with com.codename1.ui.plaf.UIManager.zoomFonts. Kept separate from
the name-only blacklist so the exclusion is surgical (other classes
that happen to define a zoomFonts method, if any appear later, are
unaffected). Once the JS port ships zoomFonts this entry can come
straight out.

Regenerated registry reflects the exclusion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Every new API that lands in the CN1 release channel but not yet in
the JavaScript cloud build bricks the playground with NoSuchMethod
at shell-eval time - first it was UIManager.zoomFonts, now
isSimdOptimizationsEnabled, and more will keep appearing on every
version bump. The one-off qualified-name blacklist doesn't scale.

Fix: introduce <cn1.registry.version> in the pom (pinned to 7.0.234,
one release behind cn1.version=7.0.235) and have the generator
prefer it when downloading release source jars. Falls back to
cn1.version if the registry version isn't set, so consumers outside
this project aren't broken. Bump <cn1.registry.version> deliberately
once the JS cloud build catches up, not implicitly with cn1.version.

With the pin in place, UIManager.zoomFonts is not discovered at all,
so the ad-hoc qualified-method blacklist can go. Kept the generator
scaffolding empty-but-ready in case a port-specific regression
needs a surgical workaround later.

Regenerated registry reflects 7.0.234 API surface: zoomFonts,
isSimdOptimizationsEnabled, and the rest of the post-7.0.234 API
additions are absent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The website-docs workflow's "Validate internal links and images"
step fails in CI with:
  Downloading from: https://github.com/lycheeverse/lychee/releases/
    latest/download/lychee-x86_64-unknown-linux-gnu.tar.gz
  ##[error]Process completed with exit code 22.

Root cause: lychee v0.24.0 renamed release assets from
  lychee-<arch>-unknown-linux-gnu.tar.gz
to
  lychee-lychee-v0.24.0-<arch>-unknown-linux-gnu.tar.gz
but lychee-action@v2 still constructs the old URL, so
`lycheeVersion: latest` now resolves to a 404 and curl bails out
with exit 22.

Pin to v0.23.0, the last release with the stable asset naming.
This is orthogonal to the Playground work - the actual Playground
cloud JS build + smoke tests succeed in the same job; only the
downstream link check trips on the upstream action bug. Bump back
to `latest` once lychee-action@v2 catches up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User reports a thin bar visible at the nav position that clicks as
the correct tab but has no icons or text. Canvas pixels at nav
centre were confirmed opaque navy in the previous probe, and
nothing else in the DOM overlaps the nav region. So the Button
children of the nav are the missing piece - either zero-sized, or
painting only their UIID background with no glyph rendering.

Two diagnostics:

1. For each nav child, log its absolute position, size, preferred
   size, visibility, UIID, text, and icon dimensions. If any of
   these are zero the layout pipeline broke; if they look right,
   the paint pipeline broke.

2. Sample the canvas pixel at the CENTRE of each button, roughly
   where the Material icon glyph should be painted (offset by
   height/3 from top so we land on the icon, not the text). If all
   three pixels are the nav UIID colour, no glyph got painted at
   all. If they vary (lighter/darker shades, foreground-colour
   hits), glyphs are being painted and something else is hiding
   them.

Removes the now-redundant DOM-overlapper scan and the red/green
canvas markers - both answered their questions already.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Root cause identified. The navBtnPixels probe came back as solid
navy UIID bg at every icon centre - no glyph pixels at all. The
navChild probe showed buttons with correct size/position/icon-dims
and real text. So the icons were baked in a colour that matches
the nav's background, making them invisible on top of it.

FontImage.setMaterialIcon bakes the glyph using whatever foreground
colour the component's style has at the instant of the call.
setUIID application can be deferred on the HTML5 port, so the bake
captures the default FG, which collides with the nav's navy bg.

PlaygroundTopBar.applyAppIconStyle already documents this pitfall
and works around it by calling getAllStyles().setFgColor(...) before
FontImage.setMaterialIcon. Mirror the same pattern for the bottom
nav:

  - createBottomNavButton: setUIID -> force FG from a table that
    mirrors the `color:` values in theme.css's
    PlaygroundBottomNavItem{Dark,Active,ActiveDark} UIIDs -> bake.
  - refreshBottomNav: on state flip, re-set UIID, re-force FG, and
    re-bake the icon with the new colour. The icon is cached on
    the Button as a baked image, so just changing the UIID wouldn't
    update it.
  - Stash the icon char on the Button via `navIcon` client property
    so refreshBottomNav can rebake without the creation-time char
    in scope.

Also remove the diagnostic probes (navChild/navBtnPixels logs, the
canvas pixel sampler) that were only useful for narrowing down the
root cause.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Icon centering
  Set Button alignment to CENTER in createBottomNavButton (default
  is LEFT, which leaves the glyph hugging the left padding on the
  HTML5 port once the FG bake fix made it actually paint) and add
  `text-align: center` to the four PlaygroundBottomNavItem* UIIDs
  in theme.css so the same placement survives a UIID-level restyle.

Cleanup for merge
  - Remove the `CN1Playground build marker: mobile-shell-v2` boot
    log, the per-relayout `Playground layout:` log, and the comment
    block that describes them.
  - Remove installIframeBottomGuard() and MOBILE_NAV_GUARD_PX. It
    was a speculative CSS max-height injection for iframe peers,
    added before the real cause of the missing nav was known; the
    actual fix (FG bake colour) is now in place and the guard adds
    nothing.
  - Rewrite the Form.SOUTH nav comment: the structural choice still
    stands, but the "iframe covers the nav" rationale was wrong.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@shai-almog shai-almog merged commit 8e736b3 into master Apr 24, 2026
12 of 13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant