fix(ui): correct glass shader coordinates on scaled tablets

Use native window metrics for glass shaders while preserving scaled layout metrics for their content. Cover fullscreen tablet orientations and resizing to a compact window.

Fixes #597
This commit is contained in:
zarzet committed 2026-09-25 21:11:30 +07:00
1 parent b40ba27cd6
commit 55a390d679
4 files changed
+292 -147

No files matched your search

+8 -5
View File
@@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:liquid_glass_easy/liquid_glass_easy.dart';
import 'package:spotiflac_android/providers/runtime_profile_provider.dart';
import 'package:spotiflac_android/theme/mornye_theme.dart';
import 'package:spotiflac_android/widgets/native_glass_metrics.dart';
/// One switch style for settings, extension controls and editing sheets.
/// Mornye keeps its wide oval thumb even when glass or motion is disabled.
@@ -48,11 +49,13 @@ class AppSwitch extends StatelessWidget {
if (useGlass) {
control = Transform.flip(
flipX: Directionality.of(context) == TextDirection.rtl,
child: LiquidGlassSwitch(
value: value,
onChanged: onChanged!,
activeColor: active,
inactiveColor: inactive,
child: NativeGlassMetrics(
child: LiquidGlassSwitch(
value: value,
onChanged: onChanged!,
activeColor: active,
inactiveColor: inactive,
),
),
);
} else {
+153 -142
View File
@@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:liquid_glass_easy/liquid_glass_easy.dart';
import 'package:spotiflac_android/providers/runtime_profile_provider.dart';
import 'package:spotiflac_android/theme/mornye_theme.dart';
import 'package:spotiflac_android/widgets/native_glass_metrics.dart';
class MornyeSegmentedControl extends ConsumerWidget {
const MornyeSegmentedControl({
@@ -53,59 +54,61 @@ class MornyeSegmentedControl extends ConsumerWidget {
child: const SizedBox.expand(),
),
),
LiquidGlassTabBar.withImpeller(
width: constraints.maxWidth,
height: height,
margin: const EdgeInsets.only(bottom: 8),
itemPadding: 4,
selectedIndex: logicalIndex(selectedIndex),
onChanged: (index) => onChanged(logicalIndex(index)),
style: const LiquidGlassStyle(
shape: LiquidGlassShape.continuousRoundedRectangle(
cornerRadius: 32,
borderWidth: 0,
lightIntensity: 0,
NativeGlassMetrics(
child: LiquidGlassTabBar.withImpeller(
width: constraints.maxWidth,
height: height,
margin: const EdgeInsets.only(bottom: 8),
itemPadding: 4,
selectedIndex: logicalIndex(selectedIndex),
onChanged: (index) => onChanged(logicalIndex(index)),
style: const LiquidGlassStyle(
shape: LiquidGlassShape.continuousRoundedRectangle(
cornerRadius: 32,
borderWidth: 0,
lightIntensity: 0,
),
appearance: LiquidGlassAppearance(),
refraction: LiquidGlassRefraction(
distortion: 0,
chromaticAberration: 0,
),
),
appearance: LiquidGlassAppearance(),
refraction: LiquidGlassRefraction(
distortion: 0,
chromaticAberration: 0,
pillStyle: LiquidGlassTabPillStyle(
mode: blur && animate
? LiquidGlassPillMode.impellerOnly
: LiquidGlassPillMode.none,
animated: animate,
),
),
pillStyle: LiquidGlassTabPillStyle(
mode: blur && animate
? LiquidGlassPillMode.impellerOnly
: LiquidGlassPillMode.none,
animated: animate,
),
itemStyle: LiquidGlassTabItemStyle(
selectedColor: scheme.onSurface,
unselectedColor: scheme.onSurface,
iconSize: 0,
iconLabelGap: 0,
labelFontSize: 15,
),
items: [
for (final label in visualLabels)
LiquidGlassTabBarItem(
label: label,
iconBuilder: (_, _) => const SizedBox.shrink(),
labelBuilder: (context, state) => Text(
label,
textDirection: rtl
? TextDirection.rtl
: TextDirection.ltr,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodyMedium?.copyWith(
color: scheme.onSurface,
fontWeight: state.selected
? FontWeight.w600
: FontWeight.w400,
itemStyle: LiquidGlassTabItemStyle(
selectedColor: scheme.onSurface,
unselectedColor: scheme.onSurface,
iconSize: 0,
iconLabelGap: 0,
labelFontSize: 15,
),
items: [
for (final label in visualLabels)
LiquidGlassTabBarItem(
label: label,
iconBuilder: (_, _) => const SizedBox.shrink(),
labelBuilder: (context, state) => Text(
label,
textDirection: rtl
? TextDirection.rtl
: TextDirection.ltr,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodyMedium?.copyWith(
color: scheme.onSurface,
fontWeight: state.selected
? FontWeight.w600
: FontWeight.w400,
),
),
),
),
],
],
),
),
],
),
@@ -235,32 +238,36 @@ class MornyeGlass extends StatelessWidget {
tintOpacity: tintOpacity,
tintColor: tintColor,
child: useGlass
? LiquidGlassLens(
style: LiquidGlassStyle(
shape: LiquidGlassShape.continuousRoundedRectangle(
cornerRadius: radius,
borderWidth: dark && tintOpacity == null ? 0 : 0.6,
lightIntensity: dark && tintOpacity == null ? 0 : 0.18,
),
appearance: LiquidGlassAppearance(
color: tintOpacity != null
? Colors.transparent
: dark
? scheme.surfaceContainerHigh.withValues(alpha: 0.28)
: Colors.white.withValues(
alpha: strongTint ? 0.20 : 0.55,
),
// The surface already blurs the backdrop. Refracting that
// frosted result needs no second Gaussian blur pass.
blur: const LiquidGlassBlur(),
),
refraction: const LiquidGlassRefraction(
distortion: 0.02,
distortionWidth: 8,
chromaticAberration: 0,
? NativeGlassMetrics(
child: LiquidGlassLens(
style: LiquidGlassStyle(
shape: LiquidGlassShape.continuousRoundedRectangle(
cornerRadius: radius,
borderWidth: dark && tintOpacity == null ? 0 : 0.6,
lightIntensity: dark && tintOpacity == null ? 0 : 0.18,
),
appearance: LiquidGlassAppearance(
color: tintOpacity != null
? Colors.transparent
: dark
? scheme.surfaceContainerHigh.withValues(alpha: 0.28)
: Colors.white.withValues(
alpha: strongTint ? 0.20 : 0.55,
),
// The surface already blurs the backdrop. Refracting that
// frosted result needs no second Gaussian blur pass.
blur: const LiquidGlassBlur(),
),
refraction: const LiquidGlassRefraction(
distortion: 0.02,
distortionWidth: 8,
chromaticAberration: 0,
),
),
// Only the shader uses window metrics; responsive content and
// decoded artwork retain the surrounding tablet layout scale.
child: MediaQuery(data: MediaQuery.of(context), child: child),
),
child: child,
)
: child,
);
@@ -494,76 +501,80 @@ class MornyeTabBar extends StatelessWidget {
child: const SizedBox.expand(),
),
),
LiquidGlassTabBar.withImpeller(
width: constraints.maxWidth,
height: 64,
margin: const EdgeInsets.only(bottom: 8),
selectedIndex: selectedIndex < 0 ? 0 : selectedIndex,
onChanged: onSelected,
style: LiquidGlassTabBar.defaultStyle.copyWith(
// The frosted base owns the subtle outline. Disable the
// package's default specular rim around the whole capsule.
shape: const LiquidGlassShape.continuousRoundedRectangle(
cornerRadius: 32,
borderWidth: 0,
lightIntensity: 0,
),
// The sibling surface already supplies tint and blur. Keep
// the lens clear so the moving pill can refract the icons.
appearance: const LiquidGlassAppearance(),
refraction: const LiquidGlassRefraction(
distortion: 0,
chromaticAberration: 0,
),
),
itemStyle: LiquidGlassTabItemStyle(
selectedColor: scheme.primary,
unselectedColor: scheme.onSurface,
iconSize: 25,
labelFontSize: 11,
),
pillStyle: LiquidGlassTabPillStyle(
mode: LiquidGlassPillMode.impellerOnly,
show: selectedIndex >= 0,
color: selectionFill,
animated: true,
// Keep the moving refractive pill, without stacking the
// package's second magnifier lens beneath it.
magnifierPill: const LiquidGlassTabMagnifierPillStyle(
enabled: false,
),
),
items: [
for (final (index, destination) in destinations.indexed)
LiquidGlassTabBarItem(
label: destination.label,
iconBuilder: (context, icon) => IconTheme(
data: IconThemeData(
size: icon.size,
color: icon.selected && selectedIndex >= 0
? scheme.primary
: inactiveIconColor,
),
child: Opacity(
opacity: hiddenIconIndices.contains(index) ? 0 : 1,
child: destination.icon,
),
),
labelBuilder: (context, label) => Text(
destination.label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.labelSmall
?.copyWith(
fontSize: label.textStyle.fontSize,
fontWeight: FontWeight.w600,
color: selectedIndex < 0
? scheme.onSurface
: label.textStyle.color,
),
),
NativeGlassMetrics(
child: LiquidGlassTabBar.withImpeller(
width: constraints.maxWidth,
height: 64,
margin: const EdgeInsets.only(bottom: 8),
selectedIndex: selectedIndex < 0 ? 0 : selectedIndex,
onChanged: onSelected,
style: LiquidGlassTabBar.defaultStyle.copyWith(
// The frosted base owns the subtle outline. Disable the
// package's default specular rim around the whole capsule.
shape: const LiquidGlassShape.continuousRoundedRectangle(
cornerRadius: 32,
borderWidth: 0,
lightIntensity: 0,
),
],
// The sibling surface already supplies tint and blur. Keep
// the lens clear so the moving pill can refract the icons.
appearance: const LiquidGlassAppearance(),
refraction: const LiquidGlassRefraction(
distortion: 0,
chromaticAberration: 0,
),
),
itemStyle: LiquidGlassTabItemStyle(
selectedColor: scheme.primary,
unselectedColor: scheme.onSurface,
iconSize: 25,
labelFontSize: 11,
),
pillStyle: LiquidGlassTabPillStyle(
mode: LiquidGlassPillMode.impellerOnly,
show: selectedIndex >= 0,
color: selectionFill,
animated: true,
// Keep the moving refractive pill, without stacking the
// package's second magnifier lens beneath it.
magnifierPill: const LiquidGlassTabMagnifierPillStyle(
enabled: false,
),
),
items: [
for (final (index, destination) in destinations.indexed)
LiquidGlassTabBarItem(
label: destination.label,
iconBuilder: (context, icon) => IconTheme(
data: IconThemeData(
size: icon.size,
color: icon.selected && selectedIndex >= 0
? scheme.primary
: inactiveIconColor,
),
child: Opacity(
opacity: hiddenIconIndices.contains(index)
? 0
: 1,
child: destination.icon,
),
),
labelBuilder: (context, label) => Text(
destination.label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.labelSmall
?.copyWith(
fontSize: label.textStyle.fontSize,
fontWeight: FontWeight.w600,
color: selectedIndex < 0
? scheme.onSurface
: label.textStyle.color,
),
),
),
],
),
),
// With no selected tab the package still needs an internal
// index; handle taps here so
+25
View File
@@ -0,0 +1,25 @@
import 'package:flutter/widgets.dart';
/// Gives glass shaders the actual window's coordinate system.
///
/// AdaptiveUiScaler changes MediaQuery's size and density for layout and image
/// decoding. The glass renderer already includes that ancestor transform when
/// positioning its shader, so using the scaled density again displaces the
/// effect from its clip (especially near the bottom of a tablet screen).
class NativeGlassMetrics extends StatelessWidget {
const NativeGlassMetrics({super.key, required this.child});
final Widget child;
@override
Widget build(BuildContext context) {
final view = View.of(context);
return MediaQuery(
data: MediaQuery.of(context).copyWith(
size: view.physicalSize / view.devicePixelRatio,
devicePixelRatio: view.devicePixelRatio,
),
child: child,
);
}
}