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

Flutter widget kit mirroring Rust egui's compact immediate-mode UI style. Dark-mode-first, zero Material widgets, Catppuccin palettes built in.

example/lib/main.dart

import 'package:egui/egui.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'egui showcase',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF89b4fa),
          brightness: Brightness.dark,
        ),
        splashFactory: NoSplash.splashFactory,
        highlightColor: Colors.transparent,
      ),
      home: const ShowcasePage(),
    );
  }
}

class ShowcasePage extends StatefulWidget {
  const ShowcasePage({super.key});
  @override
  State<ShowcasePage> createState() => _ShowcasePageState();
}

class _ShowcasePageState extends State<ShowcasePage>
    with SingleTickerProviderStateMixin {
  EguiPalette _palette = EguiPalette.mocha;
  double _fontSize = kEgFontSize;

  double _posX = 0.0;
  double _posY = 0.5;
  double _posZ = -2.0;
  double _rotX = 0.0;
  double _rotY = 0.0;
  double _rotZ = 0.0;
  double _scale = 1.0;

  double _roughness = 0.4;
  double _metallic = 0.0;
  double _emissive = 0.0;
  Color _albedo = const Color(0xFFD4A07A);
  Color _emissionColor = const Color(0xFF000000);
  String _shadingModel = 'PBR';
  final _shadingModels = ['PBR', 'Unlit', 'Toon', 'Wireframe'];

  double _exposure = 1.0;
  double _fov = 60.0;
  bool _shadowsEnabled = true;
  bool _ssaoEnabled = false;
  bool _bloomEnabled = true;
  bool _motionBlur = false;
  String _tonemapper = 'ACES';
  final _tonemappers = ['ACES', 'Filmic', 'Reinhard', 'AgX'];
  int _tonemapIdx = 0;

  double _gravity = -9.81;
  bool _physicsEnabled = true;
  String _solver = 'Impulse';
  final _solvers = ['Impulse', 'PGS', 'TGS'];
  int _substeps = 4;
  int _iterations = 10;

  bool _showWireframe = false;
  bool _showBVH = false;
  bool _showNormals = false;
  double _fps = 0;

  String? _selectedNode;

  int? _selectedComponent;

  String _entityName = 'Mesh_001';
  String _entityTag = 'player';
  String _entityNote = '';
  String _castShadows = 'Dynamic';
  static const _shadowModes = ['Off', 'Static', 'Dynamic'];

  final List<double> _fpsHistory = [];
  final List<double> _frameHistory = [];

  late final Ticker _ticker;
  int _frameCount = 0;
  int _lastMs = 0;

  @override
  void initState() {
    super.initState();
    _ticker = createTicker(_onTick)..start();
  }

  void _onTick(Duration elapsed) {
    _frameCount++;
    final ms = elapsed.inMilliseconds;
    if (ms - _lastMs >= 500) {
      final dt = (ms - _lastMs) / 1000.0;
      final fps = _frameCount / dt;
      setState(() {
        _fps = fps;
        _fpsHistory.add(fps);
        if (_fpsHistory.length > 80) _fpsHistory.removeAt(0);
        _frameHistory.add(1000 / fps.clamp(1, 9999));
        if (_frameHistory.length > 80) _frameHistory.removeAt(0);
      });
      _frameCount = 0;
      _lastMs = ms;
    }
  }

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

  String _f(double v, {int d = 2}) => v.toStringAsFixed(d);
  String _deg(double v) => '${v.toStringAsFixed(1)}°';

  void _cyclePrev<T>(List<T> list, T current, ValueChanged<T> onChanged) {
    final i = list.indexOf(current);
    onChanged(list[(i - 1 + list.length) % list.length]);
  }

  void _cycleNext<T>(List<T> list, T current, ValueChanged<T> onChanged) {
    final i = list.indexOf(current);
    onChanged(list[(i + 1) % list.length]);
  }

  @override
  Widget build(BuildContext context) {
    final effectivePalette = _palette.withFont(
      kEgFontFamily,
      fontSize: _fontSize,
    );
    return EguiScope(
      palette: effectivePalette,
      child: _Bg(
        palette: _palette,
        child: SafeArea(
          child: Stack(
            children: [
              const Center(child: _ViewportHint()),

              Positioned(top: 12, left: 12, child: _leftColumn()),

              Positioned(top: 12, right: 12, child: _rightColumn()),

              Positioned(top: 12, left: 280, child: _hierarchyPanel()),

              Positioned(top: 12, left: 480, child: _assetsPanel()),

              Positioned(top: 12, left: 700, child: _perfPanel()),

              Positioned(top: 390, left: 560, child: _nodeGraphPanel()),

              Positioned(bottom: 12, left: 12, right: 12, child: _bottomBar()),
            ],
          ),
        ),
      ),
    );
  }

  Widget _leftColumn() => EguiColumn(
    maxWidth: 250,
    children: [
      EguiPane(title: 'Transform', child: _transformContent()),
      EguiTabBar(
        tabs: const ['Material', 'Entity'],
        children: [_materialContent(), _entityContent()],
      ),
    ],
  );

  Widget _rightColumn() => EguiColumn(
    maxWidth: 250,
    children: [
      EguiTabBar(
        tabs: const ['Render', 'Physics'],
        children: [_renderContent(), _physicsContent()],
      ),
    ],
  );

  Widget _transformContent() => Column(
    crossAxisAlignment: CrossAxisAlignment.stretch,
    mainAxisSize: MainAxisSize.min,
    children: [
      EguiSection(
        title: 'Position',
        children: [
          EguiVec3(
            x: _posX,
            y: _posY,
            z: _posZ,
            speed: 0.05,
            format: _f,
            onChanged: (x, y, z) => setState(() {
              _posX = x;
              _posY = y;
              _posZ = z;
            }),
          ),
        ],
      ),
      EguiSection(
        title: 'Rotation',
        initiallyExpanded: false,
        children: [
          EguiVec3(
            x: _rotX,
            y: _rotY,
            z: _rotZ,
            speed: 1.0,
            min: -180,
            max: 180,
            suffix: '°',
            format: (v) => v.toStringAsFixed(1),
            onChanged: (x, y, z) => setState(() {
              _rotX = x;
              _rotY = y;
              _rotZ = z;
            }),
          ),
        ],
      ),
      EguiSection(
        title: 'Scale',
        children: [
          EguiSlider(
            label: 'Uniform',
            value: _scale,
            min: 0.01,
            max: 5,
            valueLabel: _f(_scale),
            onChanged: (v) => setState(() => _scale = v),
            tooltip: 'Scales all axes uniformly. Drag right to enlarge.',
          ),
        ],
      ),
      Wrap(
        spacing: 3,
        children: [
          EguiButton(
            label: 'Reset',
            tooltip: 'Reset position, rotation and scale to defaults',
            onPressed: () => setState(() {
              _posX = _posY = _posZ = 0;
              _rotX = _rotY = _rotZ = 0;
              _scale = 1;
            }),
          ),
          EguiButton(
            label: 'Copy',
            tooltip: 'Copy transform to clipboard',
            onPressed: () {},
          ),
          EguiButton(
            label: 'Paste',
            tooltip: 'Paste transform from clipboard',
            onPressed: () {},
          ),
        ],
      ),
    ],
  );

  Widget _materialContent() => Column(
    crossAxisAlignment: CrossAxisAlignment.stretch,
    mainAxisSize: MainAxisSize.min,
    children: [
      EguiCombo<String>(
        label: 'Shading',
        value: _shadingModel,
        items: _shadingModels,
        itemLabel: (s) => s,
        onChanged: (v) => setState(() => _shadingModel = v),
        onPrev: () => _cyclePrev(
          _shadingModels,
          _shadingModel,
          (v) => setState(() => _shadingModel = v),
        ),
        onNext: () => _cycleNext(
          _shadingModels,
          _shadingModel,
          (v) => setState(() => _shadingModel = v),
        ),
        tooltip: 'Surface shading model applied to this mesh',
      ),
      const SizedBox(height: 4),
      EguiSection(
        title: 'PBR Properties',
        children: [
          EguiColorPicker(
            label: 'Albedo',
            color: _albedo,
            onChanged: (c) => setState(() => _albedo = c),
            tooltip: 'Surface base color (sRGB)',
          ),
          const SizedBox(height: 3),
          EguiColorPicker(
            label: 'Emission',
            color: _emissionColor,
            onChanged: (c) => setState(() => _emissionColor = c),
            tooltip: 'Emission color (multiplied by Emissive intensity)',
          ),
          const SizedBox(height: 3),
          EguiSlider(
            label: 'Roughness',
            value: _roughness,
            min: 0,
            max: 1,
            valueLabel: _f(_roughness),
            onChanged: (v) => setState(() => _roughness = v),
            tooltip: '0 = mirror-smooth, 1 = fully diffuse',
          ),
          EguiSlider(
            label: 'Metallic',
            value: _metallic,
            min: 0,
            max: 1,
            valueLabel: _f(_metallic),
            onChanged: (v) => setState(() => _metallic = v),
            tooltip: 'Blend between dielectric (0) and metallic (1) response',
          ),
          EguiSlider(
            label: 'Emissive',
            value: _emissive,
            min: 0,
            max: 8,
            valueLabel: _f(_emissive),
            onChanged: (v) => setState(() => _emissive = v),
            tooltip: 'Emissive intensity in EV. Values > 1 bloom in HDR.',
          ),
        ],
      ),
    ],
  );

  Widget _renderContent() => Column(
    crossAxisAlignment: CrossAxisAlignment.stretch,
    mainAxisSize: MainAxisSize.min,
    children: [
      EguiSection(
        title: 'Camera',
        children: [
          EguiSlider(
            label: 'FOV',
            value: _fov,
            min: 20,
            max: 120,
            valueLabel: _deg(_fov),
            onChanged: (v) => setState(() => _fov = v),
            tooltip: 'Vertical field of view in degrees',
          ),
          EguiSlider(
            label: 'Exposure',
            value: _exposure,
            min: 0.1,
            max: 4.0,
            valueLabel: _f(_exposure),
            onChanged: (v) => setState(() => _exposure = v),
            tooltip: 'Scene exposure multiplier before tonemapping',
          ),
        ],
      ),
      EguiSection(
        title: 'Post Process',
        children: [
          EguiCombo<String>(
            label: 'Tonemapper',
            value: _tonemapper,
            items: _tonemappers,
            itemLabel: (s) => s,
            onChanged: (v) => setState(() => _tonemapper = v),
            onPrev: () {
              setState(() {
                _tonemapIdx =
                    (_tonemapIdx - 1 + _tonemappers.length) %
                    _tonemappers.length;
                _tonemapper = _tonemappers[_tonemapIdx];
              });
            },
            onNext: () {
              setState(() {
                _tonemapIdx = (_tonemapIdx + 1) % _tonemappers.length;
                _tonemapper = _tonemappers[_tonemapIdx];
              });
            },
          ),
          const SizedBox(height: 3),
          EguiCheckbox(
            label: 'Bloom',
            value: _bloomEnabled,
            onChanged: (v) => setState(() => _bloomEnabled = v),
            tooltip: 'Scatter bright pixels to simulate lens bloom',
          ),
          EguiCheckbox(
            label: 'Motion Blur',
            value: _motionBlur,
            onChanged: (v) => setState(() => _motionBlur = v),
            tooltip: 'Per-object velocity-based motion blur',
          ),
        ],
      ),
      EguiSection(
        title: 'Shadows',
        children: [
          EguiCheckbox(
            label: 'Enable',
            value: _shadowsEnabled,
            onChanged: (v) => setState(() => _shadowsEnabled = v),
            tooltip: 'Toggle shadow casting for all shadow-enabled objects',
          ),
          EguiCheckbox(
            label: 'SSAO',
            value: _ssaoEnabled,
            onChanged: (v) => setState(() => _ssaoEnabled = v),
            tooltip: 'Screen-space ambient occlusion — adds contact shadows',
          ),
        ],
      ),
      EguiSection(
        title: 'Budget',
        children: [
          EguiProgressBar(
            label: 'VRAM',
            value: 0.61,
            valueLabel: '1.2 / 2.0 GB',
          ),
          const SizedBox(height: 3),
          EguiProgressBar(
            label: 'Frame',
            value: (_fps > 0 ? (1000 / _fps / 16.67).clamp(0, 1) : 0),
            valueLabel: _fps > 0
                ? '${(1000 / _fps).toStringAsFixed(1)} ms'
                : '—',
          ),
          const SizedBox(height: 3),
          EguiProgressBar(
            label: 'Draw',
            value: 0.38,
            valueLabel: '${(_fps * 1.3).round()} calls',
          ),
        ],
      ),
    ],
  );

  Widget _physicsContent() => Column(
    crossAxisAlignment: CrossAxisAlignment.stretch,
    mainAxisSize: MainAxisSize.min,
    children: [
      EguiToggle(
        label: 'Simulation',
        value: _physicsEnabled,
        onChanged: (v) => setState(() => _physicsEnabled = v),
        tooltip: 'Enable or pause the physics simulation',
      ),
      const SizedBox(height: 4),
      EguiCombo<String>(
        label: 'Solver',
        value: _solver,
        items: _solvers,
        itemLabel: (s) => s,
        onChanged: (v) => setState(() => _solver = v),
      ),
      const SizedBox(height: 4),
      EguiSlider(
        label: 'Gravity',
        value: _gravity,
        min: -20,
        max: 0,
        valueLabel: '${_f(_gravity)} m/s²',
        onChanged: (v) => setState(() => _gravity = v),
        tooltip: 'Gravitational acceleration along −Y (m/s²)',
      ),
      const SizedBox(height: 4),
      EguiNumberInput(
        label: 'Substeps',
        value: _substeps,
        min: 1,
        max: 16,
        onChanged: (v) => setState(() => _substeps = v),
        tooltip: 'Physics substeps per frame — higher = more stable',
      ),
      const SizedBox(height: 3),
      EguiNumberInput(
        label: 'Iterations',
        value: _iterations,
        min: 1,
        max: 64,
        step: 2,
        onChanged: (v) => setState(() => _iterations = v),
        tooltip: 'Solver iterations per substep',
      ),
      const SizedBox(height: 4),
      EguiButton(
        label: _physicsEnabled ? 'Pause Sim' : 'Start Sim',
        toggled: _physicsEnabled,
        tooltip: _physicsEnabled
            ? 'Pause the simulation'
            : 'Resume the simulation',
        onPressed: () => setState(() => _physicsEnabled = !_physicsEnabled),
      ),
    ],
  );

  Widget _entityContent() => Column(
    crossAxisAlignment: CrossAxisAlignment.stretch,
    mainAxisSize: MainAxisSize.min,
    children: [
      EguiSection(
        title: 'Info',
        children: [
          EguiLabel.row(label: 'ID', value: '#4812'),
          EguiLabel.row(label: 'Vertices', value: '14 208'),
          EguiLabel.row(label: 'Triangles', value: '28 416'),
        ],
      ),
      EguiSection(
        title: 'Properties',
        children: [
          EguiTextField(
            label: 'Name',
            hint: 'entity name…',
            value: _entityName,
            onChanged: (v) => setState(() => _entityName = v),
          ),
          const SizedBox(height: 3),
          EguiTextField(
            label: 'Tag',
            hint: 'tag…',
            value: _entityTag,
            onChanged: (v) => setState(() => _entityTag = v),
          ),
        ],
      ),
      EguiSection(
        title: 'Shadows',
        children: [
          EguiRadioGroup<String>(
            value: _castShadows,
            items: _shadowModes,
            itemLabel: (s) => s,
            onChanged: (v) => setState(() => _castShadows = v),
          ),
        ],
      ),
      EguiSection(
        title: 'Notes',
        initiallyExpanded: false,
        children: [
          EguiTextField(
            hint: 'notes…',
            value: _entityNote,
            onChanged: (v) => setState(() => _entityNote = v),
          ),
          if (_entityNote.isNotEmpty) ...[
            const SizedBox(height: 3),
            EguiLabel(_entityNote, muted: true),
          ],
        ],
      ),
    ],
  );

  EguiTreeNode _node(String label, {List<EguiTreeNode> children = const []}) =>
      EguiTreeNode(
        label: label,
        selected: _selectedNode == label,
        onTap: () => setState(() => _selectedNode = label),
        children: children,
      );

  Widget _hierarchyPanel() => EguiWindow(
    title: 'Hierarchy',
    collapsible: true,
    draggable: true,
    maxWidth: 180,
    child: EguiTree(
      children: [
        _node(
          'Scene Root',
          children: [
            _node('Directional Light'),
            _node('Camera_Main'),
            _node(
              'Characters',
              children: [_node('Player'), _node('Enemy_01'), _node('Enemy_02')],
            ),
            EguiTreeNode(
              label: 'Environment',
              initiallyExpanded: false,
              selected: _selectedNode == 'Environment',
              onTap: () => setState(() => _selectedNode = 'Environment'),
              children: [
                _node('Terrain'),
                _node('Trees_Group'),
                _node('Rocks_Group'),
              ],
            ),
          ],
        ),
      ],
    ),
  );

  Widget _assetsPanel() => EguiWindow(
    title: 'Assets',
    collapsible: true,
    draggable: true,
    maxWidth: 210,
    child: EguiScrollArea(
      maxHeight: 120,
      child: EguiTable(
        columns: const ['Name', 'Type', 'Size'],
        columnWidths: const [3, 2, 2],
        rows: const [
          ['player.glb', 'Mesh', '2.4 MB'],
          ['environment.glb', 'Mesh', '18 MB'],
          ['rock_01.glb', 'Mesh', '340 KB'],
          ['grass.png', 'Texture', '512 KB'],
          ['skybox_hdr', 'Texture', '8 MB'],
          ['footstep.wav', 'Audio', '128 KB'],
          ['theme.ogg', 'Audio', '4.2 MB'],
        ],
        selectedRow: _selectedComponent,
        onRowTap: (i) => setState(() => _selectedComponent = i),
      ),
    ),
  );

  Widget _perfPanel() => EguiWindow(
    title: 'Performance',
    collapsible: true,
    draggable: true,
    maxWidth: 220,
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      mainAxisSize: MainAxisSize.min,
      children: [
        EguiSparkline(
          label: 'FPS',
          values: _fpsHistory,
          min: 0,
          max: 120,
          valueLabel: '${_fps.round()}',
          tooltip: 'Frames per second — target is 60 or 120',
        ),
        const SizedBox(height: 4),
        EguiSparkline(
          label: 'ms',
          values: _frameHistory,
          min: 0,
          max: 33,
          valueLabel: _fps > 0 ? (1000 / _fps).toStringAsFixed(1) : '—',
          tooltip: 'Frame time in milliseconds — 16.7 ms = 60 fps',
        ),
        const SizedBox(height: 8),
        EguiPlot(
          height: 90,
          yMin: 0,
          series: [
            EguiSeries(
              name: 'FPS',
              points: List.generate(
                _fpsHistory.length,
                (i) => Offset(i.toDouble(), _fpsHistory[i]),
              ),
            ),
            EguiSeries(
              name: 'ms',
              points: List.generate(
                _frameHistory.length,
                (i) => Offset(i.toDouble(), _frameHistory[i]),
              ),
            ),
          ],
        ),
      ],
    ),
  );

  Widget _nodeGraphPanel() => EguiWindow(
    title: 'Node Graph',
    collapsible: true,
    draggable: true,
    resizable: true,
    initialWidth: 360,
    initialHeight: 240,
    minWidth: 260,
    minHeight: 160,
    maxWidth: 560,
    maxHeight: 420,
    contentPadding: EdgeInsets.zero,
    child: const _NodeGraphMock(),
  );

  Widget _bottomBar() {
    final p = _palette;
    return SingleChildScrollView(
      scrollDirection: Axis.horizontal,
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.end,
        children: [
          EguiWindow(
            title: 'Debug View',
            maxWidth: 180,
            child: Column(
              mainAxisSize: MainAxisSize.min,
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                EguiCheckbox(
                  label: 'Wireframe',
                  value: _showWireframe,
                  onChanged: (v) => setState(() => _showWireframe = v),
                  tooltip: 'Render meshes as triangle wireframes',
                ),
                const SizedBox(height: 3),
                EguiCheckbox(
                  label: 'BVH Bounds',
                  value: _showBVH,
                  onChanged: (v) => setState(() => _showBVH = v),
                  tooltip: 'Visualise bounding volume hierarchy boxes',
                ),
                const SizedBox(height: 3),
                EguiCheckbox(
                  label: 'Normals',
                  value: _showNormals,
                  onChanged: (v) => setState(() => _showNormals = v),
                  tooltip: 'Draw vertex normals as coloured line segments',
                ),
              ],
            ),
          ),
          const SizedBox(width: 8),
          EguiWindow(
            title: 'Theme',
            maxWidth: 220,
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              mainAxisSize: MainAxisSize.min,
              children: [
                EguiThemeChooser(
                  current: _palette,
                  onChanged: (p) => setState(() => _palette = p),
                ),
                const SizedBox(height: 4),
                const EguiSeparator(label: 'Size'),
                const SizedBox(height: 2),
                Wrap(
                  spacing: 3,
                  children: [
                    EguiButton(
                      label: 'S',
                      toggled: _fontSize == 9.0,
                      onPressed: () => setState(() => _fontSize = 9.0),
                    ),
                    EguiButton(
                      label: 'M',
                      toggled: _fontSize == kEgFontSize,
                      onPressed: () => setState(() => _fontSize = kEgFontSize),
                    ),
                    EguiButton(
                      label: 'L',
                      toggled: _fontSize == 13.0,
                      onPressed: () => setState(() => _fontSize = 13.0),
                    ),
                    EguiButton(
                      label: 'XL',
                      toggled: _fontSize == 16.0,
                      onPressed: () => setState(() => _fontSize = 16.0),
                    ),
                  ],
                ),
              ],
            ),
          ),
          const SizedBox(width: 8),
          EguiPanel(
            maxWidth: 180,
            child: Padding(
              padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
              child: Column(
                mainAxisSize: MainAxisSize.min,
                crossAxisAlignment: CrossAxisAlignment.end,
                children: [
                  EguiStatusText('${_fps.round()} fps'),
                  EguiStatusText('draw calls: ${(_fps * 1.3).round()}'),
                  EguiStatusText('vram: 1.2 GB'),
                  EguiStatusText('theme: ${p.name}'),
                  EguiStatusText('font: $kEgFontFamily'),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class _NodeGraphMock extends StatelessWidget {
  const _NodeGraphMock();

  @override
  Widget build(BuildContext context) {
    final p = EguiScope.of(context);
    return ColoredBox(
      color: Color.lerp(p.panelBg.withAlpha(0xFF), Colors.black, 0.28)!,
      child: Stack(
        children: [
          Positioned.fill(child: CustomPaint(painter: _NodeGraphPainter(p))),
          const Positioned(
            left: 24,
            top: 28,
            child: _GraphNodeCard(
              title: 'Texture',
              rows: ['albedo.png', 'normal.png'],
            ),
          ),
          const Positioned(
            left: 188,
            top: 86,
            child: _GraphNodeCard(
              title: 'Blend',
              rows: ['multiply', 'roughness 0.40'],
            ),
          ),
          const Positioned(
            left: 360,
            top: 48,
            child: _GraphNodeCard(
              title: 'Output',
              rows: ['base color', 'material'],
            ),
          ),
          Positioned(
            left: 8,
            bottom: 8,
            child: EguiStatusText('drag title bar, resize bottom-right'),
          ),
        ],
      ),
    );
  }
}

class _GraphNodeCard extends StatelessWidget {
  const _GraphNodeCard({required this.title, required this.rows});

  final String title;
  final List<String> rows;

  @override
  Widget build(BuildContext context) {
    final p = EguiScope.of(context);
    return Container(
      width: 128,
      decoration: BoxDecoration(
        color: p.widgetBg,
        border: Border.all(color: p.border),
        borderRadius: BorderRadius.circular(3),
        boxShadow: p.shadows
            ? const [
                BoxShadow(
                  color: Color(0x88000000),
                  blurRadius: 14,
                  offset: Offset(0, 8),
                ),
              ]
            : null,
      ),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          DecoratedBox(
            decoration: BoxDecoration(color: p.widgetHov),
            child: Padding(
              padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 4),
              child: Text(
                title,
                style: p.textSt.copyWith(color: p.heading),
                overflow: TextOverflow.ellipsis,
              ),
            ),
          ),
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 6),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                for (final row in rows)
                  Padding(
                    padding: const EdgeInsets.only(bottom: 3),
                    child: Text(
                      row,
                      style: p.subSt,
                      overflow: TextOverflow.ellipsis,
                    ),
                  ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

class _NodeGraphPainter extends CustomPainter {
  const _NodeGraphPainter(this.palette);

  final EguiPalette palette;

  @override
  void paint(Canvas canvas, Size size) {
    final gridPaint = Paint()
      ..color = palette.separator.withAlpha(0x70)
      ..strokeWidth = 1;
    const step = 24.0;
    for (var x = 0.0; x <= size.width; x += step) {
      canvas.drawLine(Offset(x, 0), Offset(x, size.height), gridPaint);
    }
    for (var y = 0.0; y <= size.height; y += step) {
      canvas.drawLine(Offset(0, y), Offset(size.width, y), gridPaint);
    }

    final edgePaint = Paint()
      ..color = palette.sliderFill
      ..style = PaintingStyle.stroke
      ..strokeWidth = 2;
    _drawEdge(canvas, edgePaint, const Offset(152, 78), const Offset(188, 116));
    _drawEdge(canvas, edgePaint, const Offset(316, 136), const Offset(360, 80));
  }

  void _drawEdge(Canvas canvas, Paint paint, Offset from, Offset to) {
    final path = Path()
      ..moveTo(from.dx, from.dy)
      ..cubicTo(from.dx + 48, from.dy, to.dx - 48, to.dy, to.dx, to.dy);
    canvas.drawPath(path, paint);
  }

  @override
  bool shouldRepaint(_NodeGraphPainter oldDelegate) {
    return oldDelegate.palette != palette;
  }
}

class _ViewportHint extends StatelessWidget {
  const _ViewportHint();

  @override
  Widget build(BuildContext context) {
    final p = EguiScope.of(context);
    return Text(
      '[ 3D VIEWPORT ]',
      style: TextStyle(
        fontFamily: p.fontFamily,
        fontSize: p.fontSize + 0.5,
        color: const Color(0x44FFFFFF),
        height: 1,
        letterSpacing: 2,
      ),
    );
  }
}

class _Bg extends StatelessWidget {
  const _Bg({required this.palette, required this.child});
  final EguiPalette palette;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    final bg = Color.lerp(
      palette.panelBg.withAlpha(0xFF),
      const Color(0xFF000000),
      0.35,
    )!;

    return DecoratedBox(
      decoration: BoxDecoration(
        gradient: RadialGradient(
          center: const Alignment(0, -0.2),
          radius: 1.2,
          colors: [Color.lerp(bg, const Color(0xFF1a1a2e), 0.4)!, bg],
        ),
      ),
      child: SizedBox.expand(child: child),
    );
  }
}
0
likes
160
points
107
downloads

Documentation

API reference

Publisher

verified publishercodealchemist.dev

Weekly Downloads

Flutter widget kit mirroring Rust egui's compact immediate-mode UI style. Dark-mode-first, zero Material widgets, Catppuccin palettes built in.

Repository (GitHub)
View/report issues

Topics

#widget #ui #inspector #debug-ui #egui

License

MIT (license)

Dependencies

flutter

More

Packages that depend on egui