Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions .github/workflows/scripts-fidelity.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,20 @@ name: Native theme fidelity
workflow_dispatch:
pull_request:
paths:
- 'native-themes/ios-modern/theme.css'
- 'native-themes/android-material/theme.css'
# One entry per THEME, not per file: ios-modern is built from
# common.css + gen26.css + gen27.css since the generation split, and this
# list still named the theme.css that split removed -- so from that merge
# until this fix, NO pull request could trigger the iOS fidelity suite at
# all. It went unnoticed because the splitting PR itself matched: deleting
# a path counts as touching it, so the last PR this gate ran on was the one
# that broke it.
#
# Globbed by directory so the next part file is covered on the day it is
# added rather than the day someone notices. native-themes/<theme>/target/
# is the concatenated build input and is gitignored, so it never appears in
# a pull request and cannot widen this.
- 'native-themes/ios-modern/**'
- 'native-themes/android-material/**'
push:
branches: [master]
paths:
Expand Down
89 changes: 87 additions & 2 deletions CodenameOne/src/com/codename1/ui/plaf/GlassRecipe.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@
/// button capsule (UIGlassEffect). The strongest material: heavy wash,
/// plus edge refraction and a specular rim so the glass reads as a layer
/// on top of the content rather than a flat hole.
/// - `chrome27`, `pill27`, `panel27` -- the same three materials as
/// iOS 27 retuned them. Light is measured as unchanged in chrome and pill;
/// dark moved in all three. See {@link #liquidPanel27(boolean)} for how the
/// constants were measured and why dark is approximate.
///
/// A theme assigns a recipe per UIID with the theme constant
/// `<UIID>GlassRecipe` (for example `ToolbarGlassRecipe: chrome`),
Expand Down Expand Up @@ -132,8 +136,80 @@ public static GlassRecipe liquidPanel(boolean dark) {
: new GlassRecipe(Kind.LIQUID_PANEL, 1.95f, 0.303f, 174.3f, 0.4f, 0.5f);
}

/// Looks up a recipe by its theme name (`blur`, `chrome`,
/// `pill` or `panel`). Unknown names fall back to the panel
/// The iOS 27 chrome-bar material.
///
/// LIGHT IS DELIBERATELY IDENTICAL to {@link #liquidChrome(boolean)}. Fitting
/// the transform against the iOS 27 capture returned sat 1.09, scale 0.860,
/// offset 18.9 where iOS 26 is 1.10 / 0.850 / 20.0 -- inside 1.3% on every
/// parameter, at an rms of 1.27/255. That is a measurement saying "unchanged",
/// so the iOS 26 numbers are reused verbatim rather than replaced by a
/// near-identical duplicate that would read as a real difference.
///
/// DARK is the best affine fit (rms 14.6/255) and is KNOWN NOT TO BE EXACT.
/// See {@link #liquidPanel27(boolean)} for why dark cannot be fitted properly.
///
/// @param dark true for the dark appearance
/// @return the iOS 27 chrome-bar recipe
public static GlassRecipe liquidChrome27(boolean dark) {
return dark
? new GlassRecipe(Kind.LIQUID_CHROME, 1.98f, 0.764f, 51.3f, 0f, 0f)
: new GlassRecipe(Kind.LIQUID_CHROME, 1.1f, 0.85f, 20f, 0f, 0f);
}

/// The iOS 27 floating-pill material.
///
/// LIGHT is again measured as unchanged -- the fit returned 1.86 / 0.984 /
/// 111.4 against iOS 26's 1.80 / 1.000 / 108.0 -- so the iOS 26 values are
/// reused. DARK is the best affine fit and, like chrome, is approximate.
///
/// @param dark true for the dark appearance
/// @return the iOS 27 floating-pill recipe
public static GlassRecipe liquidPill27(boolean dark) {
return dark
? new GlassRecipe(Kind.LIQUID_PILL, 3.04f, 0.244f, 45.5f, 0f, 0.2f)
: new GlassRecipe(Kind.LIQUID_PILL, 1.8f, 1.0f, 108f, 0f, 0.2f);
}

/// The iOS 27 glass-panel material, and the recipe that carries the method
/// these three were measured with.
///
/// HOW THE NUMBERS WERE OBTAINED. The material is the documented affine
/// transform `c' = clamp((lum + (c - lum) * sat) * scale + offset)`. The iOS
/// 26 constants are known, so every interior pixel of a committed
/// goldens/ios-26-metal tile can be inverted back to the backdrop that
/// produced it, and the matching goldens/ios-27-metal pixel then fitted
/// against that same backdrop. Nothing about the backdrop has to be assumed,
/// and because the transform is linear it commutes with the Gaussian blur, so
/// the blur does not bias the fit. Clipping does NOT commute, so a pixel is
/// used only when nothing within the blur's reach clipped -- without that
/// erosion the photo-backdrop tile fitted at rms 14 against rms 1 for the flat
/// ones, and it dragged every parameter with it.
///
/// The method self-checks: run against the iOS 26 set it recovers the iOS 26
/// chrome constants it was never told, to within 1.3%.
///
/// WHY DARK IS APPROXIMATE, IN ALL THREE RECIPES. After the best affine fit,
/// the light residual is flat across the whole backdrop-luma range (within
/// +/-0.9/255). The dark residual is not: on the chrome bar it runs +8.9 at
/// low luma, -9.3 through the middle and +10.3 at high luma -- a systematic
/// curve, not noise. iOS 27's dark glass therefore applies a NON-LINEAR
/// luminance response, and no choice of sat/scale/offset can express it,
/// because this transform is affine by construction. Matching dark properly
/// needs a curve term in the material model and in every port's shader, which
/// is a larger change than new constants; these values are the closest an
/// affine material gets until then.
///
/// @param dark true for the dark appearance
/// @return the iOS 27 glass-panel recipe
public static GlassRecipe liquidPanel27(boolean dark) {
return dark
? new GlassRecipe(Kind.LIQUID_PANEL, 2.84f, 0.378f, 79.8f, 0.4f, 0.5f)
: new GlassRecipe(Kind.LIQUID_PANEL, 2.08f, 0.457f, 137.4f, 0.4f, 0.5f);
}

/// Looks up a recipe by its theme name: `blur`, `chrome`,
/// `pill`, `panel`, or their iOS 27 variants `chrome27`,
/// `pill27` and `panel27`. Unknown names fall back to the panel
/// recipe -- the safest default for a free-standing glass surface.
///
/// @param name the recipe name from the theme
Expand All @@ -150,6 +226,15 @@ public static GlassRecipe named(String name, boolean dark) {
if ("pill".equals(n)) {
return liquidPill(dark);
}
if ("chrome27".equals(n)) {
return liquidChrome27(dark);
}
if ("pill27".equals(n)) {
return liquidPill27(dark);
}
if ("panel27".equals(n)) {
return liquidPanel27(dark);
}
return liquidPanel(dark);
}

Expand Down
Binary file modified Ports/iOSPort/nativeSources/iOSModern27Theme.res
Binary file not shown.
44 changes: 44 additions & 0 deletions Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java
Original file line number Diff line number Diff line change
Expand Up @@ -3127,6 +3127,22 @@ private void injectListFocusStyle(Hashtable tp) {
/// the whole time, and installNativeTheme()'s modern branch read that null
/// as "the theme was never built" and fell back to iOS 7 -- silently, on
/// every target, which is why ios.themeMode=modern appeared to do nothing.
/// True when the app bundle carries this resource. Opens and closes rather
/// than keeping the stream: the caller wants the NAME, and a stream left open
/// here would leak once per query.
private boolean hasResource(String name) {
InputStream in = getResourceAsStream(name);
if(in == null) {
return false;
}
try {
in.close();
} catch(IOException err) {
// Nothing to do: the question was whether it exists, and it does.
}
return true;
}

private InputStream getResourceAsStream(String name) {
return getResourceAsStream(IOSImplementation.class, name);
}
Expand Down Expand Up @@ -10626,6 +10642,34 @@ public String getProperty(String key, String defaultValue) {
if(key.equalsIgnoreCase("cn1.nativeRedirects")) {
return "true";
}
// The ios.themeGeneration build hint, read back. iOS has no generic
// build-hint bridge -- getProperty answers a fixed key list and the
// generated stub's static setter is the only way a hint reaches the
// device -- so a hint the port already stores is invisible to the
// application unless it is answered here. The fidelity harness needs
// exactly this: it installs the native theme itself rather than through
// installNativeTheme, so without a readable generation it always scored
// the iOS 26 theme, including against the iOS 27 goldens.
if(key.equalsIgnoreCase("ios.themeGeneration")) {
return iosThemeGeneration;
}
// The theme resource that generation selects, so a caller that wants the
// file rather than the number does not have to re-derive the mapping and
// risk disagreeing with installNativeTheme about it.
//
// Answers the resource that is actually PRESENT, applying the same
// generation-26 fallback installNativeTheme applies. Reporting the
// requested name unconditionally would hand a caller a path that is not
// in the bundle, and a caller that trusts the answer -- the fidelity
// runner does, and gives up when the stream is null -- would then install
// no theme at all, which is worse than the fallback this exists beside.
if(key.equalsIgnoreCase("cn1.nativeThemeResource")) {
String want = "/" + modernThemeResourceName() + ".res";
if(!"/iOSModernTheme.res".equals(want) && !hasResource(want)) {
return "/iOSModernTheme.res";
}
return want;
}
if(key.equalsIgnoreCase("OS")) {
return "iOS";
}
Expand Down
Binary file modified Themes/iOSModern27Theme.res
Binary file not shown.
Loading
Loading