UIManager: add zoomFonts(factor) to scale theme font sizes#4798
Merged
shai-almog merged 3 commits intomasterfrom Apr 23, 2026
Merged
UIManager: add zoomFonts(factor) to scale theme font sizes#4798shai-almog merged 3 commits intomasterfrom
shai-almog merged 3 commits intomasterfrom
Conversation
Introduces a public API that multiplies every scalable font in the current theme (and the default styles) by a given factor, e.g. 1.2 enlarges by 20% and 0.8 shrinks by 20%. System fonts are skipped via Font.isTTFNativeFont() since their size is fixed by the platform. The styles cache is cleared and the theme refreshed so live components pick up the change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Javadoc now spells out that zoomFonts updates the theme but does not repaint live forms; callers need Form.refreshTheme() to apply the new sizes to an already-displayed form. - New UIManagerZoomFontsTest covers: TTF zoom in/out, system-font skip, compounding calls, reciprocal restoration, factor-of-one no-op, invalid factor, and default-style scaling. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Collaborator
Android screenshot updatesCompared 37 screenshots: 36 matched, 1 error.
Native Android coverage
Benchmark ResultsDetailed Performance Metrics
|
List.fireActionEvent silently drops events when Display.hasDragOccured() is true, so a prior test that simulated a drag (DraggableTabsTest, DnDRegression3048Test, PointerEventsTest, etc.) could make the next test's fireActionEvent a no-op. This surfaced as a flaky RSSReaderCoverageTest.testEventHandler where the EventHandler never ran and the model stayed empty. Reset dragOccured, pointerPressedAndNotReleasedOrDragged, and dragPathLength in the shared @AfterEach via reflection so subsequent tests start from a clean pointer state. Verified: injecting dragOccured=true before the test reproduces the CI failure; the reset eliminates it. Full core-unittests suite (2366 tests) stays green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
✅ Continuous Quality ReportTest & Coverage
Static Analysis
Generated automatically by the PR CI workflow. |
Collaborator
liannacasper
pushed a commit
that referenced
this pull request
Apr 24, 2026
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>
liannacasper
pushed a commit
that referenced
this pull request
Apr 24, 2026
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>
shai-almog
added a commit
that referenced
this pull request
Apr 24, 2026
* Playground UI redesign
* Fixed compilation issue
* Improved layout size calculation
* Improved device display
* Skin now uses a border to bring round corners in
* Added Android theme and fixed no-skin mode
* Better fidelity of selection and better sidebar positioning
* Improved title/button UX and restored the split pane
* Refined top-bar chrome, split pane, and editor gutter
- 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>
* Side-panel redistribution, scroll axis, and status-pill polish
- 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>
* Rebuild inspector, fix dark-mode app icon, widen preview reserve
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>
* Fix inspector tree selection and halve design-mm values
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>
* Inspector polish: unified scroll, tooltips, conditional sections
- 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>
* Inspector: reliable row selection, visible divider
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>
* Explicitly disable scroll on inner inspector containers
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>
* Simplify: just setScrollable(false) on tree
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>
* Restore Button-in-CENTER selection for tree rows
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>
* Match dot-to-label gap to the top-bar icon/text spacing
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>
* Inspector: revalidate root after selection so row + panel refresh
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>
* Inspector: vertical SplitPane between tree and property form
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>
* Force property pane re-layout on selection
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>
* Defer inspector repopulation so property pane renders on click
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>
* Inspector: strip selection back to the minimum
Rolling back the rebuildTree + form-wide revalidate + callSerially
layering - each fix fought the previous one and the property pane
still didn't render reliably on click. Keep just the three things
that must happen on selection:
1. selectedComponent = c
2. highlightComponent(c)
3. updatePropertyPanel(c)
updatePropertyPanel already calls propertiesContainer.revalidate()
once internally, which is the single layout pass we actually need.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Inspector: revalidateLater + misc polish
Inspector:
- Replace the inner-pane revalidate() with revalidateLater(), which
queues via the Form's paint-cycle revalidate queue instead of
firing mid-callback. Calling revalidate() from inside the Button
ActionListener (while the tactile press animation was in flight)
had CN1's AnimationManager snap the pending layout to the
animation's final frame, leaving the freshly-added rows at
measured size 0 until a subsequent resize kicked a fresh layout.
Live indicator:
- Gave the dot 2 mm DIPS of right padding (inside the wrapper)
instead of the 1.3 mm margin, which was still reading as too
tight at the edge.
Activity bar:
- Symmetric horizontal margin (0 both sides) and right-padding
bumped 0.2 mm to compensate for the 0.8 mm reserved border-left
rail, so icons sit visually centred in the column rather than
slightly left.
Samples panel:
- Hint reads "Search" and the hint Label gets a magnifying-glass
MATERIAL_SEARCH icon (via TextField.getHintLabel() + setIcon).
History panel:
- Row wrapper padded (3 mm left) so the two text lines align with
the panel header's 3 mm left padding.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Selection order, history alignment, icon column
Inspector:
- Update property panel BEFORE highlighting the selected
component. highlightComponent sets the form's glass pane
and calls form.repaint(), which was consuming the pending
revalidateLater() queued by updatePropertyPanel - hence
the blank pane on first click and the fix working after
a manual resize. Expanding a container (chevron click)
never called highlight and always worked - that was the
hint.
History panel:
- setPadding takes (top, bottom, left, right); I had the
values in the wrong slots so the 3mm inset ended up on
the right rather than the left. Fixed so entries line up
under the HISTORY title.
Top bar:
- Wrapped the app icon in a fixed-width 13mm column matching
the activity bar width, and removed the 3mm left padding
on the top bar UIID. The title icon now sits exactly above
the activity bar icons and the wordmark starts where each
left-side panel's title would start, so the whole left edge
reads as a single aligned column.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Selection: defer panel rebuild via callSerially; tighten icon column
Inspector selection:
- Wrap updatePropertyPanel in CN.callSerially so the panel rebuild
runs on the next EDT tick, after the Button's tactile press /
release handling has fully settled. revalidate() called inside
the Button's own ActionListener was fighting the press animation,
which produced the intermittent click-does-nothing behaviour.
- Remove the explicit form.repaint() call in highlightComponent
(and the matching one in clearHighlight); CN1 paints a new glass
pane on the next natural frame, and the extra repaint was racing
with the press animation + the queued property-panel revalidate.
Icon column alignment:
- Activity bar and the top-bar app-icon column both narrowed from
13 mm to 11 mm so the left-column icons occupy more of their
width and feel less empty on the right.
- Activity button padding trimmed from 2/2.8/2/2 to 2/1.5/2/0.7
to visually center the icon against the 0.8 mm reserved border
rail without the 1-mm right bias previously perceived.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Inspector: forceRevalidate + scroll reset; icons +1mm left padding
Inspector:
- Drop the callSerially deferral (it didn't fix the stale pane)
and the revalidateLater(). Use forceRevalidate() instead, which
walks the entire sub-tree marking every descendant for a fresh
preferredSize calc before re-laying out -- plain revalidate was
leaving descendants at stale size 0 from the previous pass.
- Reset propertiesContainer's scroll to the top on rebuild so the
first rows of the new content aren't above the visible viewport
when a prior selection had scrolled the pane down.
Activity icons:
- +1 mm of left padding on each activity button (0.7 -> 1.7 mm)
per your suggestion so the icon visually lines up with the
top-bar app-icon column.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Fix compile: setScrollY is protected, drop scroll reset
CN1 Component.setScrollY is protected, so keep just forceRevalidate
without the scroll reset - forceRevalidate's layout pass positions
the children starting at y=0 relative to the container already.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Inspector: isolate content from scroll; icons +1mm more left pad
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>
* Fix IllegalArgumentException: margin cannot be negative
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>
* Inspector: make the property pane actually scroll
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 adaptation: strip chrome, wire bottom nav
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>
* Mobile: iOS theme by default, smaller text, robust bottom nav
- 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: shrink on mobile; add layout harness
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>
* Harness: guard null toolbar + surface runApp exceptions
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>
* Harness: force layout via hook + skip hidden when searching
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>
* Breakpoint by viewport width; harness checks bottom-nav items
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>
* Harness: assert bottom-nav items have icon or text
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>
* Mobile: bottom nav at Form.SOUTH, not bodyContainer
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>
* Mobile: post-show relayout + diagnostic logging
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>
* Add build marker log at runApp start
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>
* Mobile: inject iframe max-height guard via JS
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>
* Hide inactive editor iframes so they don't cover bottom nav
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>
* Revert "Hide inactive editor iframes so they don't cover bottom nav"
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>
* Mobile: log visualViewport vs window size after first layout
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>
* Mobile: probe DOM at bottom-nav coordinates, not just viewport
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>
* Mobile: read canvas pixels at nav Y to see if CN1 painted the nav
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>
* Mobile: scan whole DOM for overlappers + draw red marker on canvas
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>
* Playground: exclude com.codename1.util.Simd from bean-shell registry
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>
* Playground: regenerate bean-shell registry against CN1 7.0.235
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>
* Playground: exclude UIManager.zoomFonts from bean-shell registry
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>
* Playground: pin bean-shell registry one CN1 release behind cn1.version
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>
* CI: pin lychee-action to v0.23.0 to dodge asset-rename regression
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>
* Mobile: log each nav child's geometry + sample canvas at its centre
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>
* Mobile: force bottom-nav item FG before baking Material icon
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>
* Mobile: centre bottom-nav icons, drop diagnostic scaffolding
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>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

Summary
UIManager.zoomFonts(float factor)— multiplies every scalable font in the current theme (and the default styles) byfactor, e.g.1.2enlarges by 20% and0.8shrinks by 20%.Font.isTTFNativeFont()), since their size is fixed by the underlying platform andFont.deriveonly supports TTF/native fonts.Fontvalues and unparsedStringentries inthemeProps; parses strings viaparseFontand only scales the result when it's a TTF/native font.IllegalArgumentException;1fis a no-op. Zoom is relative — successive calls compound (call with the reciprocal to undo).current.refreshTheme(false)so live components pick up the new sizes, matching the pattern used byaddThemeProps.Test plan
mvn compileinmaven/core— BUILD SUCCESS.UIManager.getInstance().zoomFonts(1.2f), verify TTF-based UIIDs render larger and system-font UIIDs are unchanged.zoomFonts(1f / 1.2f)afterwards and confirm sizes return to approximately their original pixel size.zoomFonts(0f)/ negative value and confirmIllegalArgumentException.🤖 Generated with Claude Code