Widgets in the scene: the source
Open the live demo · Read the guide · View on GitHub
Regions the guide quotes: surface (line 47), tick (line 96), hit (line 122).
1/// A live Flutter widget drawn as a mesh in the scene: a plane whose texture
2/// is the widget's own pixels, redrawn when the widget changes. This one is a
3/// little console with a clock and a progress bar, standing on a floor.
4///
5/// Quoted by `widget_surface.md` and shown whole in the Source tab.
6library;
8import 'dart:async';
9import 'dart:math' as math;
11import 'package:flutter/foundation.dart' show ValueListenable;
12import 'package:flutter/widgets.dart';
13import 'package:flutter3d/flutter3d.dart' hide Material;
14import 'package:flutter3d/flutter3d.dart' as f3d show Material;
15import 'package:flutter3d_app/flutter3d_app.dart';
16import 'package:flutter3d_showcase/src/demo/demo.dart';
17import 'package:vector_math/vector_math.dart' hide Colors;
19final class WidgetSurfaceDemo extends ShowcaseDemo {
20 double speed = 1.0;
22 late final WidgetSurface _surface;
23 final ValueNotifier<double> _seconds = ValueNotifier<double>(0.0);
24 bool _uploading = false;
26 /// Where the middle of the surface stands, in metres.
27 static const double _height = 0.9;
30 void configureView(DemoContext context) {
31 context.orbit
32 ..distance = 3.4
33 ..pitch = 0.15
34 // At -Z, the side the plane's front faces at yaw zero.
35 ..yaw = math.pi + 0.4;
36 context.orbit.target.setValues(0.0, _height, 0.0);
37 }
40 Scene build(DemoContext context) {
41 // A widget surface renders a widget off the visible tree, which needs a
42 // binding to exist. `runApp` has already made one by the time a real
43 // session opens this page; idempotent here for the software-device test
44 // that builds a page directly.
45 WidgetsFlutterBinding.ensureInitialized();
47 _surface = WidgetSurface(
48 device: context.device,
49 width: 1.6,
50 height: 1.0,
51 // A surface's content lands on its mesh turned half way round, so the
52 // widget is turned half way round to meet it. A workaround the
53 // pendulum lab applies for the same reason, not a property of the
54 // widget: without it the clock reads upside down.
55 child: Transform.flip(
56 flipX: true,
57 flipY: true,
58 child: _Console(seconds: _seconds),
59 ),
60 )..setPosition(Vector3(0.0, _height, 0.0));
61 // The plane's front faces -Z at yaw zero, which is where this page's
62 // camera stands. Drawn from both sides, so orbiting behind it shows the
63 // back of the screen rather than nothing.
64 _surface.node.material.doubleSided = true;
66 final floor = MeshNode(
67 DeviceMesh.upload(
68 context.device,
69 CuboidShape(size: Vector3(5, 0.2, 5)).build(),
70 ),
71 f3d.Material(name: 'floor', baseColor: Vector4(0.4, 0.44, 0.42, 1.0)),
72 name: 'floor',
73 )..setPosition(0, -0.1, 0);
74 // A stand for it, so it reads as a screen and not a card in mid-air.
75 final stand = MeshNode(
76 DeviceMesh.upload(
77 context.device,
78 CuboidShape(size: Vector3(0.12, _height - 0.4, 0.12)).build(),
79 ),
80 f3d.Material(name: 'stand', baseColor: Vector4(0.25, 0.27, 0.3, 1.0)),
81 name: 'stand',
82 )..setPosition(0, (_height - 0.4) / 2, 0.06);
83 return Scene()
84 ..add(_surface.node)
85 ..add(floor)
86 ..add(stand)
87 ..add(
88 LightNode(name: 'sun', intensity: 3.0)
89 ..setLocalForward(Vector3(-0.4, -1.0, -0.3)),
90 );
91 }
94 void update(DemoContext context, double dt) {
95 _seconds.value += dt * speed;
96 // Called once a game frame; redraws only when the widget's own pixels
97 // have changed since the last call. One upload at a time: a slow one is
98 // not queued behind by another.
99 if (!_uploading) {
100 _uploading = true;
101 unawaited(_surface.tick().whenComplete(() => _uploading = false));
102 }
103 }
106 List<DemoControl> controls(DemoContext context) => <DemoControl>[
107 SliderControl(
108 'Clock speed',
109 min: 0,
110 max: 4,
111 value: () => speed,
112 onChanged: (double v) => speed = v,
113 format: (double v) => '${v.toStringAsFixed(1)}x',
114 ),
115 ];
118 void verify(Scene scene, FrameResult frame) {
119 if (frame.drawCalls < 1) {
120 throw StateError('the widget surface was not drawn');
121 }
122 // A point in the middle of the surface's own plane maps back to the
123 // middle of its UV space.
124 final uv = _surface.uvAt(Vector3(0.0, _height, 0.0));
125 if (uv == null ||
126 (uv.dx - 0.5).abs() > 0.01 ||
127 (uv.dy - 0.5).abs() > 0.01) {
128 throw StateError(
129 'the plane\'s own centre should map to uv (0.5, 0.5), '
130 'got $uv',
131 );
132 }
133 // The widget is drawn to the texture as soon as it is built, so the
134 // surface has redrawn by the time the clock has moved on.
135 _seconds.value += 1.0;
136 if (!_surface.pipeline.isDirty) {
137 throw StateError('changing what the widget shows should mark it dirty');
138 }
139 }
140}
142/// What appears on the surface: a title, a running clock and a bar that fills
143/// every four seconds. An ordinary widget tree, drawn off the visible one.
144class _Console extends StatelessWidget {
145 const _Console({required this.seconds});
147 final ValueListenable<double> seconds;
149 static const TextStyle _mono = TextStyle(
150 fontFamily: 'RobotoMono',
151 color: Color(0xFFFFFFFF),
152 );
155 Widget build(BuildContext context) {
156 return ValueListenableBuilder<double>(
157 valueListenable: seconds,
158 builder: (BuildContext context, double t, Widget? _) {
159 final int whole = t.floor();
160 final String clock =
161 '${(whole ~/ 60).toString().padLeft(2, '0')}:'
162 '${(whole % 60).toString().padLeft(2, '0')}.'
163 '${((t - whole) * 10).floor()}';
164 return DecoratedBox(
165 decoration: const BoxDecoration(
166 gradient: LinearGradient(
167 begin: Alignment.topLeft,
168 end: Alignment.bottomRight,
169 colors: <Color>[Color(0xFF14204A), Color(0xFF3355AA)],
170 ),
171 ),
172 child: Padding(
173 padding: const EdgeInsets.all(44),
174 child: Column(
175 crossAxisAlignment: CrossAxisAlignment.start,
176 children: <Widget>[
177 Text(
178 'FLUTTER WIDGET, ON A MESH',
179 style: _mono.copyWith(
180 fontSize: 30,
181 color: const Color(0xFFB8C7FF),
182 ),
183 ),
184 const Spacer(),
185 Text(clock, style: _mono.copyWith(fontSize: 150)),
186 const SizedBox(height: 28),
187 Container(
188 height: 34,
189 decoration: BoxDecoration(
190 color: const Color(0x33FFFFFF),
191 borderRadius: BorderRadius.circular(17),
192 ),
193 alignment: Alignment.centerLeft,
194 child: FractionallySizedBox(
195 widthFactor: ((t % 4.0) / 4.0).clamp(0.02, 1.0),
196 child: DecoratedBox(
197 decoration: BoxDecoration(
198 color: const Color(0xFFFFB03B),
199 borderRadius: BorderRadius.circular(17),
200 ),
201 ),
202 ),
203 ),
204 ],
205 ),
206 ),
207 );
208 },
209 );
210 }
211}