Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
6b4d148
iOS 27 tab bar: measured Liquid Glass motion, vibrancy, lens optics a…
shai-almog Sep 26, 2026
c63f08f
Fix CI: CLDC11 has no Math.exp, prose rule, close files in tabmotion.py
shai-almog Sep 26, 2026
2b384c1
VibrancyMatrix: closed forms replace the fitted light-alpha and grey …
shai-almog Sep 26, 2026
cc9c17d
Tabs: the platter colour matrix is 1.2 I - k * luminance, not 24 lite…
shai-almog Sep 26, 2026
586d03d
TabGlassMotion: the travel, lift, platter and glow are springs, not t…
shai-almog Sep 26, 2026
7d3643e
TabGlassGesture: the settle after a scrub is the tap's travel spring
shai-almog Sep 26, 2026
1ead5e6
TabGlassMotion: drop the blank lines the removed tables left behind
shai-almog Sep 26, 2026
f72cd42
Reseed the 7 TabsGlassMotion frames the spring curves moved
shai-almog Sep 26, 2026
dddf022
Tabs glass: fix the review findings on ports, placement, RTL and hit …
shai-almog Sep 26, 2026
3f07c75
Metal restarts keep clip and transform; vibrant tabs keep their own tint
shai-almog Sep 26, 2026
bdf0728
Glass tab titles never scroll; reseed the TabsGlassMotion frames
shai-almog Sep 26, 2026
163c465
Tabs: the iOS 26 drop may bulge past the bar in flight
shai-almog Sep 27, 2026
96b0aff
TabGlassGesture: test the finger state, not a float sentinel
shai-almog Sep 27, 2026
c6981b1
Tabs glass: presses under overlays, preset switches, style bookkeepin…
shai-almog Sep 27, 2026
54dd9dc
motion-probe: keep only the tint lists the fixture needs; README matc…
shai-almog Sep 27, 2026
c60d117
Tabs glass: grow the bar for tab content taller than native
shai-almog Sep 27, 2026
732b4ae
Reseed the iOS 27 TabsTheme screenshots
shai-almog Sep 27, 2026
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
85 changes: 85 additions & 0 deletions CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java
Original file line number Diff line number Diff line change
Expand Up @@ -4102,6 +4102,91 @@ public boolean glassRegion(Object graphics, int x, int y, int width, int height,
return glassRegion(graphics, x, y, width, height, radius, cornerRadius, sat, scale, offset, refract, specular);
}

/// True when `#colorMatrixRegion` works on this graphics context. Ports override.
///
/// #### Parameters
///
/// - `graphics`: the native graphics
///
/// #### Returns
///
/// false by default
public boolean isColorMatrixRegionSupported(Object graphics) {
return false;
}

/// Recolours the painted region through a colour matrix; see
/// `com.codename1.ui.Graphics#colorMatrixRegion`. Ports override; the default does nothing.
///
/// #### Parameters
///
/// - `graphics`: the native graphics
///
/// - `x`: region x
///
/// - `y`: region y
///
/// - `width`: region width
///
/// - `height`: region height
///
/// - `matrix`: 12-float colour matrix
///
/// - `mask`: optional coverage mask
///
/// - `cornerRadius`: corner radius, negative for a capsule
///
/// - `amount`: strength 0..1
///
/// #### Returns
///
/// false when unsupported
public boolean colorMatrixRegion(Object graphics, int x, int y, int width, int height, float[] matrix,
Image mask, float cornerRadius, float amount) {
return false;
}

/// True when `glassLensRegion` works on this graphics context.
///
/// #### Parameters
///
/// - `graphics`: the graphics context
///
/// #### Returns
///
/// false by default
public boolean isGlassLensRegionSupported(Object graphics) {
return false;
}

/// See Graphics.glassLensRegion; the default does nothing.
///
/// #### Parameters
///
/// - `graphics`: the graphics context
///
/// - `x`: lens left edge (translated)
///
/// - `y`: lens top edge (translated)
///
/// - `width`: lens width
///
/// - `height`: lens height
///
/// - `cornerRadius`: corner radius, negative for a capsule
///
/// - `optics`: the GlassLensBlend parameters
///
/// - `amount`: overall strength 0..1
///
/// #### Returns
///
/// false when unsupported
public boolean glassLensRegion(Object graphics, int x, int y, int width, int height, float cornerRadius,
float[] optics, float amount) {
return false;
}

/// In-place iOS 26 selection-drop LENS (magnify + chromatic aberration +
/// dark->accent tint over the painted content). Default unsupported; the iOS
/// port overrides it. Returns false so callers can fall back (e.g. to a tint).
Expand Down
35 changes: 34 additions & 1 deletion CodenameOne/src/com/codename1/ui/Button.java
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,14 @@ public class Button extends Label implements ReleasableComponent, ActionSource<A
private Image disabledIcon;
private Command cmd;
private boolean toggle;
/// Paint-time override used by Tabs for the Liquid Glass selection: the tab bar
/// paints every tab twice, once as unselected content and once as the accent
/// copy seen through the moving lens, regardless of which tab is selected.
/// GLASS_PAINT_NONE outside that paint; never set it anywhere else.
int glassPaintState = GLASS_PAINT_NONE;
static final int GLASS_PAINT_NONE = 0;
static final int GLASS_PAINT_DEFAULT = 1;
static final int GLASS_PAINT_SELECTED = 2;
private int releaseRadius;
private boolean autoRelease;
/// Duration in millis of the iOS-style release dim-out (0 disables it). Read
Expand Down Expand Up @@ -666,7 +674,10 @@ public Image getIconFromState() {
if (!isEnabled() && getDisabledIcon() != null) {
return getDisabledIcon();
}
if (isToggle() && isSelected()) {
if (glassPaintState == GLASS_PAINT_DEFAULT) {
return icon;
}
if (glassPaintState == GLASS_PAINT_SELECTED || (isToggle() && isSelected())) {
icon = rolloverPressedIcon;
if (icon == null) {
icon = getPressedIcon();
Expand Down Expand Up @@ -948,8 +959,30 @@ protected Border getBorder() {
return getStyle().getBorder();
}

/// {@inheritDoc}
///
/// While Tabs paints its Liquid Glass passes the forced state wins over focus
/// and selection: a focused tab would otherwise answer its selected style and
/// keep the accent colour outside the lens.
@Override
public Style getStyle() {
if (glassPaintState != GLASS_PAINT_NONE && isEnabled()) {
boolean selected = glassPaintState == GLASS_PAINT_SELECTED;
Style forced = selected ? getPressedStyle() : getUnselectedStyle();
// Keep Component.getStyle()'s bookkeeping: Label reuses a cached string
// width only while the unselected style is the one in force, and a
// selected style may carry a different font.
isUnselectedStyle = !selected;
return forced;
}
return super.getStyle();
}

@Override
boolean isPressedStyle() {
if (glassPaintState != GLASS_PAINT_NONE) {
return glassPaintState == GLASS_PAINT_SELECTED;
}
// if a toggle button has focus we should draw the selected state not the pressed state
// however if shouldRenderSelection is false the selected state won't be painted so
// we should draw the pressed state
Expand Down
82 changes: 59 additions & 23 deletions CodenameOne/src/com/codename1/ui/Component.java
Original file line number Diff line number Diff line change
Expand Up @@ -3080,29 +3080,39 @@ private void paintInternalImpl(Graphics g, boolean paintIntersects) {
}
}

void internalPaintImpl(Graphics g, boolean paintIntersects) {
g.clipRect(getX(), getY(), getWidth(), getHeight());
// CSS backdrop-filter:blur() -- the "liquid glass" effect. Blur whatever has
// already been painted behind this component (the clip confines it to our
// bounds) BEFORE our own translucent background and content paint on top. This
// runs regardless of opacity, since a glass surface is by definition
// translucent (opaque would be false and skip paintComponentBackground). The
// port blurs the destination region in place; an unsupported port returns
// false and the component simply paints without the blur.
// COST/CACHING POLICY (per paint path):
// * A glass surface only pays when it repaints; static chrome over static
// content costs nothing between repaints.
// * iOS live screen, selection lens: a pure GPU fragment shader on the
// frame's own command buffer -- no sync, no readback, no cache needed.
// * iOS live screen, glass material: the backdrop readback is required
// (the material is a function of the pixels behind the glass), but the
// composed patch is CACHED per rect+params+backdrop-hash in the port
// (METALView glass patch cache), so a repaint over an unchanged
// backdrop skips the colour transform + blur + optics; scrolling
// content under the glass recomposes that frame from the real bytes.
// * Offscreen mutable images (capture tooling) and the desktop simulator
// blur per paint -- capture renders once, and the simulator is not a
// shipping surface.
/// Paints the backdrop material (the CSS `backdrop-filter` glass or blur) and the
/// style background. Called by the paint pipeline right after the component clip
/// is set; a component whose visible surface is not its bounds (the Tabs bar,
/// whose glass pill scales inside reserved slack) overrides this and calls
/// {@link #paintBackgroundLayerAt}.
void paintBackgroundLayer(Graphics g) {
paintBackgroundLayerImpl(g);
}

/// Paints {@link #paintBackgroundLayer} into the given rect (parent coordinates,
/// like getX()/getY()) instead of the bounds. The bounds are swapped on the raw
/// rectangle so nothing observes the change: setX()/setWidth() would fire
/// accessibility bounds events on every animation frame.
final void paintBackgroundLayerAt(Graphics g, int x, int y, int w, int h) {
int ox = bounds.getX();
int oy = bounds.getY();
int ow = bounds.getSize().getWidth();
int oh = bounds.getSize().getHeight();
bounds.setX(x);
bounds.setY(y);
bounds.getSize().setWidth(w);
bounds.getSize().setHeight(h);
try {
paintBackgroundLayerImpl(g);
} finally {
bounds.setX(ox);
bounds.setY(oy);
bounds.getSize().setWidth(ow);
bounds.getSize().setHeight(oh);
}
}

private void paintBackgroundLayerImpl(Graphics g) {
float backdropBlur = getStyle().getBackdropFilterBlurRadius();
if (backdropBlur > 0) {
// The glass surface's material INTENT comes from a typed, named
Expand Down Expand Up @@ -3145,6 +3155,32 @@ void internalPaintImpl(Graphics g, boolean paintIntersects) {
}
}
paintComponentBackground(g);
}

void internalPaintImpl(Graphics g, boolean paintIntersects) {
g.clipRect(getX(), getY(), getWidth(), getHeight());
// CSS backdrop-filter:blur() -- the "liquid glass" effect. Blur whatever has
// already been painted behind this component (the clip confines it to our
// bounds) BEFORE our own translucent background and content paint on top. This
// runs regardless of opacity, since a glass surface is by definition
// translucent (opaque would be false and skip paintComponentBackground). The
// port blurs the destination region in place; an unsupported port returns
// false and the component simply paints without the blur.
// COST/CACHING POLICY (per paint path):
// * A glass surface only pays when it repaints; static chrome over static
// content costs nothing between repaints.
// * iOS live screen, selection lens: a pure GPU fragment shader on the
// frame's own command buffer -- no sync, no readback, no cache needed.
// * iOS live screen, glass material: the backdrop readback is required
// (the material is a function of the pixels behind the glass), but the
// composed patch is CACHED per rect+params+backdrop-hash in the port
// (METALView glass patch cache), so a repaint over an unchanged
// backdrop skips the colour transform + blur + optics; scrolling
// content under the glass recomposes that frame from the real bytes.
// * Offscreen mutable images (capture tooling) and the desktop simulator
// blur per paint -- capture renders once, and the simulator is not a
// shipping surface.
paintBackgroundLayer(g);

if (isScrollable()) {
if (refreshTask != null && !InfiniteProgress.isDefaultMaterialDesignMode() &&
Expand Down
9 changes: 9 additions & 0 deletions CodenameOne/src/com/codename1/ui/Container.java
Original file line number Diff line number Diff line change
Expand Up @@ -2534,6 +2534,15 @@ void markComponentsToBePaintedInElevatedPane(boolean shouldPaintInElevatedPane)
}
}

/// Runs the layout-on-paint step of {@link #paint(Graphics)} for a subclass that
/// paints its children itself (the Tabs bar paints each tab twice for the
/// Liquid Glass selection) and so cannot call super.paint().
final void layoutForPaint() {
if (allowEnableLayoutOnPaint && enableLayoutOnPaint) {
layoutContainer();
}
}

/// {@inheritDoc}
@Override
public void paint(Graphics g) {
Expand Down
87 changes: 87 additions & 0 deletions CodenameOne/src/com/codename1/ui/DampedSpring.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Codename One designates this
* particular file as subject to the "Classpath" exception as provided
* by Codename One in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/
package com.codename1.ui;

import com.codename1.util.MathUtil;

/// The free response of a damped harmonic oscillator -- the curve a
/// CASpringAnimation follows -- in closed form.
///
/// `x'' + 2 * zeta * omega * x' + omega^2 * x = 0`, starting at displacement `x0`
/// with velocity `v0`; the spring rests at 0. `omega` is the natural angular
/// frequency (rad/s, `sqrt(stiffness / mass)`) and `zeta` the damping ratio
/// (`damping / (2 * sqrt(stiffness * mass))`): under 1 it oscillates, 1 is
/// critical, over 1 it creeps back without crossing.
///
/// A spring animation from `a` to `b` is `b + displacement(omega, zeta, a - b, v0, t)`.
final class DampedSpring {
private DampedSpring() {
}

/// Displacement from rest at time `t` (seconds).
static double displacement(double omega, double zeta, double x0, double v0, double t) {
if (t <= 0) {
return x0;
}
double decay = MathUtil.exp(-zeta * omega * t);
if (zeta < 1) {
double wd = omega * Math.sqrt(1 - zeta * zeta);
double b = (v0 + zeta * omega * x0) / wd;
return decay * (x0 * Math.cos(wd * t) + b * Math.sin(wd * t));
}
if (zeta == 1) {
return decay * (x0 + (v0 + omega * x0) * t);
}
double s = omega * Math.sqrt(zeta * zeta - 1);
double r1 = -zeta * omega + s;
double r2 = -zeta * omega - s;
double c2 = (v0 - r1 * x0) / (r2 - r1);
double c1 = x0 - c2;
return c1 * MathUtil.exp(r1 * t) + c2 * MathUtil.exp(r2 * t);
}

/// Velocity at time `t` (units per second).
static double velocity(double omega, double zeta, double x0, double v0, double t) {
if (t <= 0) {
return v0;
}
double decay = MathUtil.exp(-zeta * omega * t);
if (zeta < 1) {
double wd = omega * Math.sqrt(1 - zeta * zeta);
double b = (v0 + zeta * omega * x0) / wd;
double c = Math.cos(wd * t);
double s = Math.sin(wd * t);
return decay * ((b * wd - zeta * omega * x0) * c - (x0 * wd + zeta * omega * b) * s);
}
if (zeta == 1) {
double k = v0 + omega * x0;
return decay * (k - omega * (x0 + k * t));
}
double s = omega * Math.sqrt(zeta * zeta - 1);
double r1 = -zeta * omega + s;
double r2 = -zeta * omega - s;
double c2 = (v0 - r1 * x0) / (r2 - r1);
double c1 = x0 - c2;
return c1 * r1 * MathUtil.exp(r1 * t) + c2 * r2 * MathUtil.exp(r2 * t);
}
}
Loading
Loading