vitreum 0.1.2 copy "vitreum: ^0.1.2" to clipboard
vitreum: ^0.1.2 copied to clipboard

Adaptive native and simulated glass surfaces for Flutter.

example/lib/main.dart

import 'dart:async';
import 'dart:math' as math;

import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:vitreum/vitreum.dart';

const _showPerformanceOverlay = bool.fromEnvironment(
  'VITREUM_PERFORMANCE_OVERLAY',
);

void main() => runApp(const VitreumExample());

abstract final class _NativeTabReference {
  static const _channel = MethodChannel('dev.vitreum/native_tab_scaffold');

  static Future<void> present({
    required String content,
    required String minimizeBehavior,
    bool automate = false,
  }) => _channel.invokeMethod<void>('present', <String, Object>{
    'content': content,
    'minimizeBehavior': minimizeBehavior,
    'automate': automate,
  });
}

@pragma('vm:entry-point')
void vitreumShowcaseTabMain() =>
    runApp(const _NativeHostedTabApp(page: _HostedTabPage.showcase));

@pragma('vm:entry-point')
void vitreumDiagnosticsTabMain() =>
    runApp(const _NativeHostedTabApp(page: _HostedTabPage.diagnostics));

enum _HostedTabPage { showcase, diagnostics }

class _NativeHostedTabApp extends StatelessWidget {
  const _NativeHostedTabApp({required this.page});

  final _HostedTabPage page;

  @override
  Widget build(BuildContext context) => CupertinoApp(
    debugShowCheckedModeBanner: false,
    builder: (context, child) => VitreumPerformanceOverlay(
      enabled: _showPerformanceOverlay,
      label: 'Native host · ${page.name}',
      child: child ?? const SizedBox.shrink(),
    ),
    theme: const CupertinoThemeData(
      brightness: Brightness.dark,
      primaryColor: CupertinoColors.systemBlue,
      scaffoldBackgroundColor: Color(0xFF050814),
    ),
    home: _NativeHostedTabPage(page: page),
  );
}

class _NativeHostedTabPage extends StatelessWidget {
  const _NativeHostedTabPage({required this.page});

  final _HostedTabPage page;

  @override
  Widget build(BuildContext context) {
    final diagnostics = page == _HostedTabPage.diagnostics;
    final colors = diagnostics
        ? const <Color>[
            Color(0xFF161B2D),
            Color(0xFF3156D8),
            Color(0xFF0D8075),
            Color(0xFFD85A44),
          ]
        : const <Color>[
            Color(0xFFD85A44),
            Color(0xFF3156D8),
            Color(0xFF0D8075),
            Color(0xFF11182A),
          ];
    return CupertinoPageScaffold(
      child: CustomScrollView(
        slivers: <Widget>[
          SliverPadding(
            padding: const EdgeInsets.fromLTRB(20, 24, 20, 120),
            sliver: SliverList.separated(
              itemCount: 12,
              separatorBuilder: (_, _) => const SizedBox(height: 14),
              itemBuilder: (context, index) => Container(
                height: 176,
                padding: const EdgeInsets.all(22),
                decoration: BoxDecoration(
                  color: colors[index % colors.length],
                  borderRadius: BorderRadius.circular(28),
                ),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    Icon(
                      diagnostics
                          ? CupertinoIcons.gauge
                          : CupertinoIcons.sparkles,
                      size: 26,
                    ),
                    const Spacer(),
                    Text(
                      diagnostics
                          ? 'Diagnostic sample ${index + 1}'
                          : 'Showcase space ${index + 1}',
                      style: const TextStyle(
                        fontSize: 22,
                        fontWeight: FontWeight.w600,
                      ),
                    ),
                    const SizedBox(height: 4),
                    const Text(
                      'Flutter content hosted by UITabBarController',
                      style: TextStyle(color: CupertinoColors.systemGrey2),
                    ),
                  ],
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class VitreumExample extends StatelessWidget {
  const VitreumExample({super.key});

  @override
  Widget build(BuildContext context) {
    const scene = String.fromEnvironment('VITREUM_DIAGNOSTIC_SCENE');
    const tabReference = String.fromEnvironment('VITREUM_TAB_REFERENCE');
    const automateTabReference = bool.fromEnvironment('VITREUM_TAB_AUTOMATION');
    final home = switch (scene) {
      'pure' => const PureNativeDiagnosticPage(),
      'native_flutter' => const NativeOverFlutterDiagnosticPage(),
      'simulated' => const SimulatedDiagnosticPage(),
      _ when tabReference == 'simulated' => const SimulatedTabBarReferencePage(
        automate: automateTabReference,
      ),
      _ => const ExampleHome(),
    };
    return CupertinoApp(
      debugShowCheckedModeBanner: false,
      title: 'Vitreum',
      builder: (context, child) => VitreumPerformanceOverlay(
        enabled: _showPerformanceOverlay,
        label: 'Example · rolling 120 frames',
        child: child ?? const SizedBox.shrink(),
      ),
      theme: const CupertinoThemeData(
        brightness: Brightness.dark,
        primaryColor: CupertinoColors.systemCyan,
        scaffoldBackgroundColor: Color(0xFF050814),
      ),
      home: home,
    );
  }
}

class ExampleHome extends StatefulWidget {
  const ExampleHome({super.key});

  @override
  State<ExampleHome> createState() => _ExampleHomeState();
}

class _ExampleHomeState extends State<ExampleHome> {
  static const _initialTab = String.fromEnvironment('VITREUM_INITIAL_TAB');
  static const _tabReference = String.fromEnvironment('VITREUM_TAB_REFERENCE');
  static const _tabMinimizeBehavior = String.fromEnvironment(
    'VITREUM_TAB_MINIMIZE',
    defaultValue: 'onScrollDown',
  );
  static const _automateTabReference = bool.fromEnvironment(
    'VITREUM_TAB_AUTOMATION',
  );
  static const _showNavigationBounds = bool.fromEnvironment(
    'VITREUM_SHOW_NAV_BOUNDS',
  );

  late int _selectedIndex = _initialTab == 'diagnostics' ? 1 : 0;

  @override
  void initState() {
    super.initState();
    if (_tabReference.isNotEmpty &&
        defaultTargetPlatform == TargetPlatform.iOS &&
        !kIsWeb) {
      WidgetsBinding.instance.addPostFrameCallback((_) {
        _NativeTabReference.present(
          content: _tabReference,
          minimizeBehavior: _tabMinimizeBehavior,
          automate: _automateTabReference,
        );
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    final bottomSafeArea = MediaQuery.paddingOf(context).bottom;
    final topSafeArea = MediaQuery.paddingOf(context).top;
    return CupertinoPageScaffold(
      child: Stack(
        children: <Widget>[
          Positioned.fill(
            child: IndexedStack(
              index: _selectedIndex,
              children: const <Widget>[ShowcasePage(), DiagnosticsPage()],
            ),
          ),
          Positioned(
            left: 20,
            right: 20,
            bottom: bottomSafeArea + 8,
            child: VitreumGlassNavigationBar(
              selectedIndex: _selectedIndex,
              onDestinationSelected: (index) =>
                  setState(() => _selectedIndex = index),
              showDebugBounds: _showNavigationBounds,
              fallbackStyle: const VitreumFallbackStyle(
                surfaceOpacity: 0.08,
                borderOpacity: 0.18,
                shadowStrength: 0.10,
              ),
              destinations: const <VitreumNavigationDestination>[
                VitreumNavigationDestination(
                  icon: CupertinoIcons.sparkles,
                  label: 'Showcase',
                ),
                VitreumNavigationDestination(
                  icon: CupertinoIcons.gauge,
                  label: 'Diagnostics',
                ),
              ],
            ),
          ),
          Positioned(
            left: 12,
            top: topSafeArea + 4,
            child: const IgnorePointer(
              child: _ModeLabel(text: 'VITREUM SIMULATED'),
            ),
          ),
        ],
      ),
    );
  }
}

class _ModeLabel extends StatelessWidget {
  const _ModeLabel({required this.text});

  final String text;

  @override
  Widget build(BuildContext context) => DecoratedBox(
    decoration: BoxDecoration(
      color: CupertinoColors.black.withValues(alpha: 0.72),
      borderRadius: BorderRadius.circular(7),
    ),
    child: Padding(
      padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
      child: Text(
        text,
        style: const TextStyle(
          color: CupertinoColors.white,
          fontSize: 9,
          fontWeight: FontWeight.w700,
          letterSpacing: 0.4,
        ),
      ),
    ),
  );
}

class ShowcasePage extends StatefulWidget {
  const ShowcasePage({super.key});

  @override
  State<ShowcasePage> createState() => _ShowcasePageState();
}

class _ShowcasePageState extends State<ShowcasePage>
    with SingleTickerProviderStateMixin {
  late final AnimationController _motion;
  late final ScrollController _scrollController;

  @override
  void initState() {
    super.initState();
    final initialOffset =
        double.tryParse(
          const String.fromEnvironment(
            'VITREUM_SHOWCASE_SCROLL_OFFSET',
            defaultValue: '0',
          ),
        ) ??
        0;
    _scrollController = ScrollController(initialScrollOffset: initialOffset);
    _motion = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 10),
    )..repeat();
  }

  @override
  void dispose() {
    _motion.dispose();
    _scrollController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final bottomSafeArea = MediaQuery.paddingOf(context).bottom;
    final bottomContentPadding = 64 + bottomSafeArea + 24;
    return CupertinoPageScaffold(
      child: Stack(
        children: <Widget>[
          Positioned.fill(child: _EditorialBackground(animation: _motion)),
          SafeArea(
            bottom: false,
            child: CustomScrollView(
              controller: _scrollController,
              slivers: <Widget>[
                const SliverPadding(
                  padding: EdgeInsets.fromLTRB(22, 100, 22, 16),
                  sliver: SliverToBoxAdapter(
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: <Widget>[
                        Text(
                          'Today',
                          style: TextStyle(
                            fontSize: 34,
                            fontWeight: FontWeight.w700,
                            letterSpacing: -1,
                          ),
                        ),
                        SizedBox(height: 8),
                        Text(
                          'Your spaces, quietly organised.',
                          style: TextStyle(
                            color: CupertinoColors.systemGrey2,
                            fontSize: 17,
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
                SliverPadding(
                  padding: EdgeInsets.fromLTRB(
                    22,
                    18,
                    22,
                    bottomContentPadding,
                  ),
                  sliver: SliverList.list(
                    children: <Widget>[
                      _contentCard(
                        'Coastal studio',
                        'Warm light · 24°',
                        CupertinoIcons.sun_max_fill,
                        const Color(0xFFDA5A44),
                      ),
                      const SizedBox(height: 16),
                      _contentCard(
                        'Evening focus',
                        'Three scenes ready',
                        CupertinoIcons.moon_stars_fill,
                        const Color(0xFF344BC2),
                      ),
                      const SizedBox(height: 16),
                      _contentCard(
                        'Listening room',
                        'Now playing · Still Corners',
                        CupertinoIcons.music_note_2,
                        const Color(0xFF137F77),
                      ),
                      const SizedBox(height: 16),
                      _contentCard(
                        'Sunset arrival',
                        'Hallway lights · 38%',
                        CupertinoIcons.sunset_fill,
                        const Color(0xFFDA5A44),
                      ),
                      const SizedBox(height: 16),
                      _contentCard(
                        'Deep work',
                        'Focus scene · 42 minutes',
                        CupertinoIcons.timer,
                        const Color(0xFF344BC2),
                      ),
                      const SizedBox(height: 28),
                      const Text(
                        'Controls float above content; ordinary content stays '
                        'in the content layer.',
                        style: TextStyle(
                          color: CupertinoColors.systemGrey2,
                          fontSize: 15,
                        ),
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ),
          Positioned(
            left: 20,
            right: 20,
            top: MediaQuery.paddingOf(context).top + 12,
            child: VitreumGlass(
              mode: VitreumMode.simulated,
              semanticLabel: 'Search spaces',
              shape: const VitreumShape.capsule(),
              style: VitreumStyle.clear,
              fallbackStyle: const VitreumFallbackStyle(
                surfaceOpacity: 0.07,
                borderOpacity: 0.14,
                shadowStrength: 0.06,
              ),
              child: const Padding(
                padding: EdgeInsets.symmetric(horizontal: 16, vertical: 15),
                child: Row(
                  children: <Widget>[
                    Icon(
                      CupertinoIcons.search,
                      size: 23,
                      color: CupertinoColors.systemGrey2,
                    ),
                    SizedBox(width: 11),
                    Text(
                      'Search spaces',
                      style: TextStyle(
                        color: CupertinoColors.systemGrey2,
                        fontSize: 16,
                      ),
                    ),
                    Spacer(),
                    Icon(
                      CupertinoIcons.mic_fill,
                      size: 22,
                      color: CupertinoColors.systemGrey2,
                    ),
                  ],
                ),
              ),
            ),
          ),
          Positioned(
            right: 24,
            bottom: bottomSafeArea + 96,
            child: SizedBox.square(
              dimension: 50,
              child: VitreumGlassButton(
                mode: VitreumMode.simulated,
                semanticLabel: 'Add space',
                onPressed: () {},
                padding: EdgeInsets.zero,
                fallbackStyle: const VitreumFallbackStyle(
                  surfaceOpacity: 0.08,
                  borderOpacity: 0.18,
                  shadowStrength: 0.10,
                ),
                child: const Icon(CupertinoIcons.add, size: 24),
              ),
            ),
          ),
        ],
      ),
    );
  }

  Widget _contentCard(
    String title,
    String subtitle,
    IconData icon,
    Color color,
  ) => Container(
    height: 180,
    padding: const EdgeInsets.all(22),
    decoration: BoxDecoration(
      color: color,
      borderRadius: BorderRadius.circular(30),
      gradient: LinearGradient(
        begin: Alignment.topLeft,
        end: Alignment.bottomRight,
        colors: <Color>[
          color,
          Color.lerp(color, const Color(0xFF090B14), 0.5)!,
        ],
      ),
    ),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        Icon(icon, size: 28),
        const Spacer(),
        Text(
          title,
          style: const TextStyle(fontSize: 22, fontWeight: FontWeight.w600),
        ),
        const SizedBox(height: 4),
        Text(
          subtitle,
          style: const TextStyle(
            color: CupertinoColors.systemGrey2,
            fontSize: 15,
          ),
        ),
      ],
    ),
  );
}

class DiagnosticsPage extends StatefulWidget {
  const DiagnosticsPage({super.key});

  @override
  State<DiagnosticsPage> createState() => _DiagnosticsPageState();
}

class _DiagnosticsPageState extends State<DiagnosticsPage> {
  VitreumCapabilities? _capabilities;
  double _stressCount = 8;
  bool _highQuality = false;
  String _minimizeBehavior = 'onScrollDown';

  @override
  void initState() {
    super.initState();
    Vitreum.getCapabilities().then((value) {
      if (mounted) setState(() => _capabilities = value);
    });
  }

  @override
  Widget build(BuildContext context) {
    final capabilities = _capabilities;
    final bottomContentPadding = 64 + MediaQuery.paddingOf(context).bottom + 24;
    return CupertinoPageScaffold(
      navigationBar: const CupertinoNavigationBar(middle: Text('Diagnostics')),
      child: SafeArea(
        child: ListView(
          padding: EdgeInsets.fromLTRB(16, 18, 16, bottomContentPadding),
          children: <Widget>[
            CupertinoListSection.insetGrouped(
              header: const Text('COMPOSITION'),
              children: <Widget>[
                _valueRow(
                  'Native API',
                  capabilities?.nativeApiExists == true
                      ? 'available'
                      : 'unavailable',
                ),
                _valueRow(
                  'Native view',
                  capabilities?.nativeViewCanBeCreated == true
                      ? 'created'
                      : 'unavailable',
                ),
                _valueRow(
                  'Interactive effect',
                  capabilities?.nativeInteractiveEffectAvailable == true
                      ? 'available'
                      : 'unavailable',
                ),
                _valueRow(
                  'Flutter backdrop composition',
                  capabilities?.nativeBackdropCompositionValidated == true
                      ? 'validated'
                      : 'failed',
                  destructive:
                      capabilities?.nativeBackdropCompositionValidated != true,
                ),
                _valueRow(
                  'Single native overlay',
                  capabilities?.nativeSingleOverlayCompositionValidated == true
                      ? 'validated'
                      : 'unavailable',
                ),
                _valueRow(
                  'Selected backend',
                  capabilities?.selectedAutomaticBackend.name ?? 'loading',
                ),
              ],
            ),
            CupertinoListSection.insetGrouped(
              header: const Text('SCENES'),
              children: <Widget>[
                _sceneButton(
                  context,
                  'Pure native UIKit',
                  const PureNativeDiagnosticPage(),
                ),
                _sceneButton(
                  context,
                  'Native glass over Flutter',
                  const NativeOverFlutterDiagnosticPage(),
                ),
                _sceneButton(
                  context,
                  'Flutter simulated',
                  const SimulatedDiagnosticPage(),
                ),
              ],
            ),
            CupertinoListSection.insetGrouped(
              header: const Text('TAB BAR COMPARISON'),
              children: <Widget>[
                Padding(
                  padding: const EdgeInsets.fromLTRB(12, 10, 12, 8),
                  child: CupertinoSlidingSegmentedControl<String>(
                    groupValue: _minimizeBehavior,
                    children: const <String, Widget>{
                      'automatic': Text('Auto', style: TextStyle(fontSize: 12)),
                      'never': Text('Never', style: TextStyle(fontSize: 12)),
                      'onScrollDown': Text(
                        'Down',
                        style: TextStyle(fontSize: 12),
                      ),
                      'onScrollUp': Text('Up', style: TextStyle(fontSize: 12)),
                    },
                    onValueChanged: (value) {
                      if (value != null) {
                        setState(() => _minimizeBehavior = value);
                      }
                    },
                  ),
                ),
                _nativeTabButton('System native tab bar', content: 'native'),
                _nativeTabButton(
                  'Native tab bar with Flutter content',
                  content: 'flutter',
                ),
                _sceneButton(
                  context,
                  'Vitreum simulated tab bar',
                  const SimulatedTabBarReferencePage(),
                ),
              ],
            ),
            CupertinoListSection.insetGrouped(
              header: const Text('STRESS TEST'),
              children: <Widget>[
                Padding(
                  padding: const EdgeInsets.all(14),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: <Widget>[
                      Text('${_stressCount.round()} surfaces'),
                      CupertinoSlider(
                        value: _stressCount,
                        min: 1,
                        max: 24,
                        divisions: 23,
                        onChanged: (value) =>
                            setState(() => _stressCount = value),
                      ),
                    ],
                  ),
                ),
                Padding(
                  padding: const EdgeInsets.all(14),
                  child: Row(
                    children: <Widget>[
                      const Expanded(child: Text('High quality')),
                      CupertinoSwitch(
                        value: _highQuality,
                        onChanged: (value) =>
                            setState(() => _highQuality = value),
                      ),
                    ],
                  ),
                ),
                Padding(
                  padding: const EdgeInsets.all(14),
                  child: Wrap(
                    spacing: 8,
                    runSpacing: 8,
                    children: List<Widget>.generate(
                      _stressCount.round(),
                      (index) => SizedBox(
                        width: 54,
                        height: 38,
                        child: VitreumGlass(
                          mode: VitreumMode.simulated,
                          quality: _highQuality
                              ? VitreumQuality.high
                              : VitreumQuality.low,
                          shape: const VitreumShape.capsule(),
                          child: Center(child: Text('$index')),
                        ),
                      ),
                    ),
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  Widget _valueRow(String label, String value, {bool destructive = false}) =>
      Padding(
        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 13),
        child: Row(
          children: <Widget>[
            Expanded(child: Text(label)),
            Text(
              value,
              style: TextStyle(
                color: destructive
                    ? CupertinoColors.systemRed
                    : CupertinoColors.systemGrey,
              ),
            ),
          ],
        ),
      );

  Widget _sceneButton(BuildContext context, String label, Widget page) =>
      CupertinoButton(
        alignment: Alignment.centerLeft,
        onPressed: () => Navigator.of(
          context,
        ).push(CupertinoPageRoute<void>(builder: (_) => page)),
        child: Row(
          children: <Widget>[
            Expanded(child: Text(label)),
            const Icon(CupertinoIcons.chevron_forward, size: 18),
          ],
        ),
      );

  Widget _nativeTabButton(String label, {required String content}) =>
      CupertinoButton(
        alignment: Alignment.centerLeft,
        onPressed: defaultTargetPlatform == TargetPlatform.iOS && !kIsWeb
            ? () => _presentNativeTabs(content)
            : null,
        child: Row(
          children: <Widget>[
            Expanded(child: Text(label)),
            const Icon(CupertinoIcons.arrow_up_right_square, size: 18),
          ],
        ),
      );

  Future<void> _presentNativeTabs(String content) async {
    try {
      await _NativeTabReference.present(
        content: content,
        minimizeBehavior: _minimizeBehavior,
      );
    } on PlatformException catch (error) {
      if (!mounted) return;
      await showCupertinoDialog<void>(
        context: context,
        builder: (context) => CupertinoAlertDialog(
          title: const Text('Native host unavailable'),
          content: Text(error.message ?? error.code),
          actions: <Widget>[
            CupertinoDialogAction(
              onPressed: () => Navigator.of(context).pop(),
              child: const Text('OK'),
            ),
          ],
        ),
      );
    }
  }
}

class SimulatedTabBarReferencePage extends StatefulWidget {
  const SimulatedTabBarReferencePage({this.automate = false, super.key});

  final bool automate;

  @override
  State<SimulatedTabBarReferencePage> createState() =>
      _SimulatedTabBarReferencePageState();
}

class _SimulatedTabBarReferencePageState
    extends State<SimulatedTabBarReferencePage> {
  late final ScrollController _scrollController;
  final List<Timer> _automationTimers = <Timer>[];
  int _selectedIndex = 0;

  @override
  void initState() {
    super.initState();
    _scrollController = ScrollController();
    if (widget.automate) {
      _automationTimers.add(
        Timer.periodic(const Duration(seconds: 1), (timer) {
          switch (timer.tick % 10) {
            case 3:
              if (mounted) setState(() => _selectedIndex = 1);
            case 4:
              if (mounted) setState(() => _selectedIndex = 0);
            case 5:
              if (_scrollController.hasClients) {
                _scrollController.animateTo(
                  940,
                  duration: const Duration(milliseconds: 900),
                  curve: Curves.easeInOutCubic,
                );
              }
            case 7:
              if (_scrollController.hasClients) {
                _scrollController.animateTo(
                  0,
                  duration: const Duration(milliseconds: 900),
                  curve: Curves.easeInOutCubic,
                );
              }
          }
        }),
      );
    }
  }

  @override
  void dispose() {
    for (final timer in _automationTimers) {
      timer.cancel();
    }
    _scrollController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final mediaPadding = MediaQuery.paddingOf(context);
    final diagnostics = _selectedIndex == 1;
    final colors = diagnostics
        ? const <Color>[Color(0xFF11182A), Color(0xFF3156D8), Color(0xFF0D8075)]
        : const <Color>[
            Color(0xFFD85A44),
            Color(0xFF3156D8),
            Color(0xFF0D8075),
          ];
    return CupertinoPageScaffold(
      child: Stack(
        children: <Widget>[
          Positioned.fill(
            child: ListView.separated(
              controller: _scrollController,
              padding: EdgeInsets.fromLTRB(
                20,
                mediaPadding.top + 64,
                20,
                64 + mediaPadding.bottom + 24,
              ),
              itemCount: 10,
              separatorBuilder: (_, _) => const SizedBox(height: 14),
              itemBuilder: (context, index) => Container(
                height: 180,
                padding: const EdgeInsets.all(22),
                decoration: BoxDecoration(
                  color: colors[index % colors.length],
                  borderRadius: BorderRadius.circular(28),
                ),
                alignment: Alignment.bottomLeft,
                child: Text(
                  diagnostics
                      ? 'Diagnostic sample ${index + 1}'
                      : 'Showcase sample ${index + 1}',
                  style: const TextStyle(
                    fontSize: 22,
                    fontWeight: FontWeight.w600,
                  ),
                ),
              ),
            ),
          ),
          Positioned(
            left: 20,
            right: 20,
            bottom: mediaPadding.bottom + 8,
            child: VitreumGlassNavigationBar(
              selectedIndex: _selectedIndex,
              onDestinationSelected: (value) =>
                  setState(() => _selectedIndex = value),
              destinations: const <VitreumNavigationDestination>[
                VitreumNavigationDestination(
                  icon: CupertinoIcons.sparkles,
                  label: 'Showcase',
                ),
                VitreumNavigationDestination(
                  icon: CupertinoIcons.gauge,
                  label: 'Diagnostics',
                ),
              ],
            ),
          ),
          Positioned(
            left: 12,
            top: mediaPadding.top + 8,
            child: const _ModeLabel(text: 'VITREUM SIMULATED'),
          ),
        ],
      ),
    );
  }
}

class PureNativeDiagnosticPage extends StatelessWidget {
  const PureNativeDiagnosticPage({super.key});

  @override
  Widget build(BuildContext context) => CupertinoPageScaffold(
    child: _supportsUIKitViews
        ? const UiKitView(viewType: 'dev.vitreum/native_diagnostic')
        : const Center(child: Text('PURE NATIVE — iOS only')),
  );
}

class NativeOverFlutterDiagnosticPage extends StatelessWidget {
  const NativeOverFlutterDiagnosticPage({super.key});

  @override
  Widget build(BuildContext context) => CupertinoPageScaffold(
    child: _DiagnosticScene(
      label: 'NATIVE OVER FLUTTER',
      surface: _supportsUIKitViews
          ? const _RawNativeDiagnosticGlass()
          : const Center(child: Text('Native platform view unavailable')),
    ),
  );
}

class SimulatedDiagnosticPage extends StatelessWidget {
  const SimulatedDiagnosticPage({super.key});

  @override
  Widget build(BuildContext context) => const CupertinoPageScaffold(
    child: _DiagnosticScene(
      label: 'FLUTTER SIMULATED',
      surface: VitreumGlass(
        mode: VitreumMode.simulated,
        quality: VitreumQuality.high,
        style: VitreumStyle.regular,
        shape: VitreumShape.roundedRectangle(radius: 36),
        child: Center(
          child: Text(
            'FLUTTER FILTER',
            style: TextStyle(fontWeight: FontWeight.w600),
          ),
        ),
      ),
    ),
  );
}

class _RawNativeDiagnosticGlass extends StatelessWidget {
  const _RawNativeDiagnosticGlass();

  @override
  Widget build(BuildContext context) => Stack(
    fit: StackFit.expand,
    children: <Widget>[
      const UiKitView(
        viewType: 'dev.vitreum/native_glass',
        creationParams: <String, Object?>{
          'shape': <String, Object>{'kind': 'roundedRectangle', 'radius': 36.0},
          'style': 'regular',
          'interactive': false,
          'tint': null,
        },
        creationParamsCodec: StandardMessageCodec(),
      ),
      const IgnorePointer(
        child: Center(
          child: Text(
            'FLUTTER FOREGROUND',
            style: TextStyle(fontWeight: FontWeight.w600),
          ),
        ),
      ),
    ],
  );
}

class _DiagnosticScene extends StatefulWidget {
  const _DiagnosticScene({required this.label, required this.surface});

  final String label;
  final Widget surface;

  @override
  State<_DiagnosticScene> createState() => _DiagnosticSceneState();
}

class _DiagnosticSceneState extends State<_DiagnosticScene>
    with SingleTickerProviderStateMixin {
  late final AnimationController _motion;

  @override
  void initState() {
    super.initState();
    _motion = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 4),
    )..repeat();
  }

  @override
  void dispose() {
    _motion.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => Stack(
    children: <Widget>[
      Positioned.fill(child: _DiagnosticBackground(animation: _motion)),
      Center(child: SizedBox(width: 310, height: 112, child: widget.surface)),
      Positioned(
        left: 0,
        right: 0,
        top: MediaQuery.paddingOf(context).top + 18,
        child: Center(
          child: Container(
            padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 11),
            decoration: BoxDecoration(
              color: CupertinoColors.systemRed,
              borderRadius: BorderRadius.circular(12),
            ),
            child: Text(
              widget.label,
              style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
            ),
          ),
        ),
      ),
    ],
  );
}

class _DiagnosticBackground extends StatelessWidget {
  const _DiagnosticBackground({required this.animation});

  final Animation<double> animation;

  @override
  Widget build(BuildContext context) => AnimatedBuilder(
    animation: animation,
    builder: (context, child) => CustomPaint(
      painter: _DiagnosticPainter(animation.value),
      child: Stack(
        children: <Widget>[
          Positioned(
            left: animation.value * MediaQuery.sizeOf(context).width - 120,
            top: MediaQuery.sizeOf(context).height * 0.68,
            child: const Text(
              'MOVING FLUTTER TEXT',
              style: TextStyle(
                fontFamily: 'monospace',
                fontSize: 18,
                fontWeight: FontWeight.bold,
              ),
            ),
          ),
          Positioned(
            left: 18,
            bottom: 28,
            child: Text(
              'FRAME ${(animation.value * 100000).round().toString().padLeft(6, '0')}',
              style: const TextStyle(
                fontFamily: 'monospace',
                fontWeight: FontWeight.bold,
              ),
            ),
          ),
        ],
      ),
    ),
  );
}

class _DiagnosticPainter extends CustomPainter {
  const _DiagnosticPainter(this.phase);

  final double phase;

  @override
  void paint(Canvas canvas, Size size) {
    final rect = Offset.zero & size;
    canvas.drawRect(
      rect,
      Paint()
        ..shader = LinearGradient(
          begin: Alignment(math.sin(phase * math.pi * 2), -1),
          end: Alignment(-math.cos(phase * math.pi * 2), 1),
          colors: const <Color>[
            Color(0xFF071633),
            Color(0xFF1678E8),
            Color(0xFFDD326F),
            Color(0xFF29D1A4),
          ],
        ).createShader(rect),
    );

    const cell = 28.0;
    final checker = Paint();
    for (var y = 0.0; y < size.height; y += cell) {
      for (var x = 0.0; x < size.width; x += cell) {
        checker.color = ((x / cell + y / cell).round().isEven)
            ? const Color(0x33000000)
            : const Color(0x22FFFFFF);
        canvas.drawRect(Rect.fromLTWH(x, y, cell, cell), checker);
      }
    }

    final linePaint = Paint()
      ..color = const Color(0xCCFFFFFF)
      ..strokeWidth = 3;
    final offset = phase * 52;
    for (var x = -size.height + offset; x < size.width; x += 52) {
      canvas.drawLine(
        Offset(x, 0),
        Offset(x + size.height, size.height),
        linePaint,
      );
    }
    for (var y = 90.0; y < size.height; y += 96) {
      canvas.drawLine(Offset(0, y), Offset(size.width, y), linePaint);
    }
  }

  @override
  bool shouldRepaint(_DiagnosticPainter oldDelegate) =>
      oldDelegate.phase != phase;
}

class _EditorialBackground extends StatelessWidget {
  const _EditorialBackground({required this.animation});

  final Animation<double> animation;

  @override
  Widget build(BuildContext context) => AnimatedBuilder(
    animation: animation,
    builder: (context, child) {
      final angle = animation.value * math.pi * 2;
      return Stack(
        fit: StackFit.expand,
        children: <Widget>[
          DecoratedBox(
            decoration: BoxDecoration(
              gradient: LinearGradient(
                begin: Alignment(math.sin(angle) * 0.6, -1),
                end: Alignment(-math.cos(angle) * 0.4, 1),
                colors: const <Color>[
                  Color(0xFF070A14),
                  Color(0xFF101B35),
                  Color(0xFF241539),
                  Color(0xFF071B24),
                ],
              ),
            ),
          ),
          Positioned(
            top: 36 + math.sin(angle) * 20,
            right: -60 + math.cos(angle) * 24,
            child: const SizedBox.square(
              dimension: 280,
              child: DecoratedBox(
                decoration: BoxDecoration(
                  shape: BoxShape.circle,
                  gradient: RadialGradient(
                    colors: <Color>[
                      Color(0x8A16BDE8),
                      Color(0x3D5C65E8),
                      Color(0x005C65E8),
                    ],
                  ),
                ),
              ),
            ),
          ),
          Positioned(
            top: 120 + math.cos(angle) * 18,
            left: -100 + math.sin(angle) * 20,
            child: const SizedBox.square(
              dimension: 240,
              child: DecoratedBox(
                decoration: BoxDecoration(
                  shape: BoxShape.circle,
                  gradient: RadialGradient(
                    colors: <Color>[
                      Color(0x66E53C86),
                      Color(0x205C65E8),
                      Color(0x005C65E8),
                    ],
                  ),
                ),
              ),
            ),
          ),
        ],
      );
    },
  );
}

bool get _supportsUIKitViews =>
    !kIsWeb && defaultTargetPlatform == TargetPlatform.iOS;
0
likes
160
points
163
downloads
screenshot

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Adaptive native and simulated glass surfaces for Flutter.

Repository (GitHub)
View/report issues
Contributing

Topics

#glass #ui #ios #android

License

MIT (license)

Dependencies

flutter

More

Packages that depend on vitreum

Packages that implement vitreum