egui 0.1.1
egui: ^0.1.1 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.
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(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 _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 _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),
);
}
}