Skip to content

Fix Dark/Light Mode toggle: preserve app theme, stop growing canvas, refresh l10n#4934

Merged
shai-almog merged 2 commits into
masterfrom
fix-darkmode-toggle-and-l10n-staleness
May 13, 2026
Merged

Fix Dark/Light Mode toggle: preserve app theme, stop growing canvas, refresh l10n#4934
shai-almog merged 2 commits into
masterfrom
fix-darkmode-toggle-and-l10n-staleness

Conversation

@shai-almog
Copy link
Copy Markdown
Collaborator

Summary

Three connected fixes after testing the recently added Dark/Light Mode menu:

  • JavaSEPort.refreshThemeOnly() replaces refreshSkin for the Dark/Light/Unsupported and Larger Text menu items. refreshSkin was calling Display.installNativeTheme() -- which is UIManager.setThemeProps(nativeProps) and wipes the installed theme -- so a CSS-compiled app theme (custom UIID fonts, margins, colors) silently reverted to the bare native theme after one click. refreshSkin also recomputed the canvas as skin * retinaScale on HiDPI, growing it to ~2× while drawing stayed at zoomLevel == 1, which read as "the simulator activated zoom mode for no reason". The new helper uses UIManager.refreshTheme() (preserves themeProps, clears the styles cache so shouldUseDarkStyle flips correctly) and leaves the canvas alone.

  • CompileCSSMojo.getLocalizationModificationTime() rolls the l10n directory's recursive mtime into the up-to-date check. The shared getCSSSourcesModificationTime only walked src/main/css, but the CSS compiler also reads src/main/l10n (passed via -l) and bakes the bundle into the same theme.res. Without this, editing Messages.properties looked like "no change", the mojo skipped the compile, and stale strings stayed in theme.res until mvn clean.

  • Three new CompileCSSMojoTest cases (one is the actual reproducer that fails before the fix) and two new UIManagerDarkModeStyleTest cases that pin the contract refreshThemeOnly() relies on: cached light-mode styles must give way to dark variants after refreshTheme, and app-only UIIDs / overridden colors must survive a dark-mode flip.

Test plan

  • mvn test -Dtest=CompileCSSMojoTest (7/7 pass; the new recompilesWhenLocalizationFileNewerThanThemeRes fails on master and passes here)
  • mvn test -Dtest=UIManagerDarkModeStyleTest (6/6 pass)
  • Full codenameone-javase test suite green
  • Manual: toggle Dark/Light Mode in a project with a CSS theme, confirm fonts/colors from theme.css still apply and the simulator window doesn't grow
  • Manual: edit a .properties localization bundle, restart the simulator, confirm the change appears without mvn clean

🤖 Generated with Claude Code

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 13, 2026

Compared 7 screenshots: 7 matched.
✅ JavaSE simulator integration screenshots matched stored baselines.

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 13, 2026

Compared 16 screenshots: 16 matched.
✅ JavaScript-port screenshot tests passed.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 13, 2026

✅ Continuous Quality Report

Test & Coverage

Static Analysis

  • SpotBugs [Report archive]
    • ByteCodeTranslator: 0 findings (no issues)
    • android: 0 findings (no issues)
    • codenameone-maven-plugin: 0 findings (no issues)
    • core-unittests: 0 findings (no issues)
    • ios: 0 findings (no issues)
  • PMD: 0 findings (no issues) [Report archive]
  • Checkstyle: 0 findings (no issues) [Report archive]

Generated automatically by the PR CI workflow.

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 13, 2026

Compared 106 screenshots: 106 matched.
✅ Native iOS screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 156 seconds

Build and Run Timing

Metric Duration
Simulator Boot 95000 ms
Simulator Boot (Run) 1000 ms
App Install 13000 ms
App Launch 23000 ms
Test Execution 298000 ms

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 1777.000 ms
Base64 CN1 encode 1601.000 ms
Base64 encode ratio (CN1/native) 0.901x (9.9% faster)
Base64 native decode 961.000 ms
Base64 CN1 decode 1434.000 ms
Base64 decode ratio (CN1/native) 1.492x (49.2% slower)
Base64 SIMD encode 907.000 ms
Base64 encode ratio (SIMD/native) 0.510x (49.0% faster)
Base64 encode ratio (SIMD/CN1) 0.567x (43.3% faster)
Base64 SIMD decode 504.000 ms
Base64 decode ratio (SIMD/native) 0.524x (47.6% faster)
Base64 decode ratio (SIMD/CN1) 0.351x (64.9% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 99.000 ms
Image createMask (SIMD on) 11.000 ms
Image createMask ratio (SIMD on/off) 0.111x (88.9% faster)
Image applyMask (SIMD off) 176.000 ms
Image applyMask (SIMD on) 113.000 ms
Image applyMask ratio (SIMD on/off) 0.642x (35.8% faster)
Image modifyAlpha (SIMD off) 225.000 ms
Image modifyAlpha (SIMD on) 66.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.293x (70.7% faster)
Image modifyAlpha removeColor (SIMD off) 213.000 ms
Image modifyAlpha removeColor (SIMD on) 122.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.573x (42.7% faster)
Image PNG encode (SIMD off) 1425.000 ms
Image PNG encode (SIMD on) 1107.000 ms
Image PNG encode ratio (SIMD on/off) 0.777x (22.3% faster)
Image JPEG encode 577.000 ms

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 13, 2026

Compared 106 screenshots: 106 matched.
✅ Native iOS Metal screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 339 seconds

Build and Run Timing

Metric Duration
Simulator Boot 110000 ms
Simulator Boot (Run) 1000 ms
App Install 14000 ms
App Launch 8000 ms
Test Execution 269000 ms

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 1717.000 ms
Base64 CN1 encode 1715.000 ms
Base64 encode ratio (CN1/native) 0.999x (0.1% faster)
Base64 native decode 857.000 ms
Base64 CN1 decode 1405.000 ms
Base64 decode ratio (CN1/native) 1.639x (63.9% slower)
Base64 SIMD encode 551.000 ms
Base64 encode ratio (SIMD/native) 0.321x (67.9% faster)
Base64 encode ratio (SIMD/CN1) 0.321x (67.9% faster)
Base64 SIMD decode 536.000 ms
Base64 decode ratio (SIMD/native) 0.625x (37.5% faster)
Base64 decode ratio (SIMD/CN1) 0.381x (61.9% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 57.000 ms
Image createMask (SIMD on) 9.000 ms
Image createMask ratio (SIMD on/off) 0.158x (84.2% faster)
Image applyMask (SIMD off) 140.000 ms
Image applyMask (SIMD on) 60.000 ms
Image applyMask ratio (SIMD on/off) 0.429x (57.1% faster)
Image modifyAlpha (SIMD off) 134.000 ms
Image modifyAlpha (SIMD on) 65.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.485x (51.5% faster)
Image modifyAlpha removeColor (SIMD off) 236.000 ms
Image modifyAlpha removeColor (SIMD on) 137.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.581x (41.9% faster)
Image PNG encode (SIMD off) 1428.000 ms
Image PNG encode (SIMD on) 1383.000 ms
Image PNG encode ratio (SIMD on/off) 0.968x (3.2% faster)
Image JPEG encode 735.000 ms

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 13, 2026

Compared 106 screenshots: 106 matched.

Native Android coverage

  • 📊 Line coverage: 11.34% (6270/55313 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 9.02% (31076/344567), branch 3.91% (1282/32768), complexity 5.01% (1573/31420), method 8.75% (1287/14701), class 14.76% (294/1992)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6327 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.ClassReader – 0.00% (0/1519 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1148 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.MethodWriter – 0.00% (0/923 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/730 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/623 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.Frame – 0.00% (0/564 lines covered)
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysJvmKt – 0.00% (0/495 lines covered)
      • kotlinx.coroutines.kotlinx.coroutines.JobSupport – 0.00% (0/423 lines covered)

✅ Native Android screenshot tests passed.

Native Android coverage

  • 📊 Line coverage: 11.34% (6270/55313 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 9.02% (31076/344567), branch 3.91% (1282/32768), complexity 5.01% (1573/31420), method 8.75% (1287/14701), class 14.76% (294/1992)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6327 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.ClassReader – 0.00% (0/1519 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1148 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.MethodWriter – 0.00% (0/923 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/730 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/623 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.Frame – 0.00% (0/564 lines covered)
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysJvmKt – 0.00% (0/495 lines covered)
      • kotlinx.coroutines.kotlinx.coroutines.JobSupport – 0.00% (0/423 lines covered)

Benchmark Results

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 929.000 ms
Base64 CN1 encode 188.000 ms
Base64 encode ratio (CN1/native) 0.202x (79.8% faster)
Base64 native decode 1034.000 ms
Base64 CN1 decode 314.000 ms
Base64 decode ratio (CN1/native) 0.304x (69.6% faster)
Image encode benchmark status skipped (SIMD unsupported)

shai-almog and others added 2 commits May 13, 2026 06:43
…anvas

Two regressions in the simulator's Dark/Light Mode menu (and the
adjacent Larger Text menu), plus a related stale-compile bug behind
"my localization edits don't take effect until I run mvn clean":

1. JavaSEPort: dark-mode / larger-text toggles now route through a new
   refreshThemeOnly() helper instead of refreshSkin. refreshSkin called
   Display.installNativeTheme(), which is UIManager.setThemeProps(
   nativeProps) and **replaces** the installed theme wholesale -- so a
   CSS-compiled app theme (custom UIID fonts, custom margins, overridden
   colors) was wiped to the bare native theme after one click. Visually
   the user saw "fonts are completely wrong" until the simulator was
   relaunched.

   refreshSkin also recomputed zoomLevel as
   canvas.width * retinaScale / skin.width, which equals retinaScale on
   a HiDPI display whose skin already fits the screen. The canvas then
   grew to skin * retinaScale while subsequent drawing still ran at
   zoomLevel == 1, so the simulator appeared to "activate zoom mode"
   for no reason after a Dark/Light click.

   refreshThemeOnly() uses UIManager.refreshTheme() (re-applies the
   currently-installed themeProps, clears the styles cache so
   shouldUseDarkStyle resolves $Dark<UIID> correctly) and leaves the
   canvas alone.

2. CompileCSSMojo: edits to a .properties localization bundle now
   invalidate the up-to-date check. The shared
   AbstractCN1Mojo.getCSSSourcesModificationTime only walks
   src/main/css, but the CSS compiler also reads the l10n directory
   (passed via -l) and bakes the bundle into the same theme.res. With
   the old check, l10n edits looked like "no change", the mojo skipped
   the compiler, and the stale theme.res hid the .properties update
   until a mvn clean. The new getLocalizationModificationTime() rolls
   the l10n directory's recursive modtime into the comparison.

Tests:

- CompileCSSMojoTest gains three cases:
  recompilesWhenLocalizationFileNewerThanThemeRes (the actual
  reproducer -- failed before the fix, passes after);
  skipsCompilationWhenAllInputsOlderThanThemeRes (guards against an
  over-eager fix that just disables the up-to-date check);
  recompilesWhenCssFileNewerThanThemeRes (locks down the CSS path so
  it can't regress alongside the l10n fix). A new
  setupMultiModuleCommonProject helper builds the canonical
  parent/common/ layout the production check is written against; the
  pre-existing single-module helper put CSS at project/src/main/css
  which left getCSSSourcesModificationTime returning zero and silently
  masked the staleness bug.

- UIManagerDarkModeStyleTest gains two cases:
  testRefreshThemeAppliesDarkStyleAfterCachedLightLookup (the cached
  light style must not win after a dark-mode flip + refreshTheme);
  testRefreshThemePreservesAppThemeCustomizationsAcrossDarkModeFlip
  (app-only UIIDs and overridden colors must survive the refresh --
  this is the contract refreshThemeOnly() relies on).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…every run

Follow-up to the l10n staleness fix in this PR. Reviewer caught a
feedback loop: when the user opts in to "Auto Update Default Bundle",
AutoLocalizationBundle persists to disk on every missing-key lookup
(see `get` -> `storeEntry` -> `persistIfNeeded(true)` -> `persist`).
Each persist advanced the bundle file's mtime, so
CompileCSSMojo.getLocalizationModificationTime -- which now factors l10n
mtime into the staleness comparison -- would treat the runtime cache
flush as a fresh user edit and force a ~30s CSS recompile on every
subsequent `cn1:run`.

Reproducer: opt in to auto-bundle, add `getString("hello")` to the app,
run twice. The second `cn1:run` recompiles CSS even though the user
changed nothing.

Fix: snapshot the bundle file's mtime before each persist and restore
it after the write. If an external editor advanced the mtime since our
last restoration (i.e. current mtime != preservedMtime), adopt the
user's mtime as the new preserved target so genuine user edits still
propagate through the staleness check. `setLastModified` failure is
treated as best-effort -- worst case is one extra CSS recompile, which
is the pre-fix behavior, not a regression.

Why neither the original code review nor the test plan caught this:

- I scoped my review to CompileCSSMojo in isolation and reasoned "the
  compiler reads `-l <l10nDir>`, so l10n edits must invalidate the
  cache." That logic is correct for user-driven edits. I never asked
  who else writes to `src/main/l10n/*.properties` at runtime.

- The auto-bundle lives in Ports/JavaSE while the staleness check
  lives in maven/codenameone-maven-plugin -- different modules, so the
  cross-cutting writer didn't surface naturally during the mojo-level
  review.

- The new CompileCSSMojoTest cases are pure mtime arithmetic
  (Files.write + setLastModified). The auto-bundle never executes in
  those tests, so its runtime write pattern can't show up. There was
  no integration test of "compile -> simulate -> compile" that would
  have asserted the second compile is a no-op.

Tests:

- AutoLocalizationBundleMtimeTest is new and covers four scenarios:
  runtimePersistKeepsOriginalFileMtime (the actual reproducer);
  multipleRuntimePersistsKeepOriginalFileMtime (no drift across many
  persists); externalUserEditPropagatesToMtimeEvenAfterAutoBundleWrite
  (a user edit between auto-bundle writes still propagates); and
  freshFilePersistEstablishesPreservedMtime (no baseline yet -> first
  write becomes the baseline). All four fail before this commit and
  pass after.

The bundle is a private inner class of JavaSEPort so the new test uses
reflection, mirroring the established pattern in
tests/core/test/com/codename1/impl/javase/AutoLocalizationBundleTest.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@shai-almog shai-almog force-pushed the fix-darkmode-toggle-and-l10n-staleness branch from 0cf991d to a229d8c Compare May 13, 2026 03:44
@shai-almog shai-almog merged commit 3fb1ec7 into master May 13, 2026
23 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