Skip to content

Commit 0c523b0

Browse files
Canvas control (#1301)
* CustomPaint prototype * Paint class complete * DrawArc, DrawColor * pip install --upgrade pip virtualenv * DrawOval, DrawPaint, DrawColor * DrawPoints * DrawRect * Breaking change: `BorderRadius` field renamed * DrawPath * start renamed to offset * Drawing text with rotation and styling * `TextStyle.foreground` added * Clip content of container with rounded borders Fix #1225 * stroke_dash_pattern for Path and Line * Path.Arc, Path.ArcTo * Path.Oval, Path.Rect * Fix container's on_hover without on_click handler Fix #1315 * Moved canvas to a separate module * Typing anf cleanup * CustomPaint renamed to Canvas * Switch from offsets to x, y * canvas.Shadow * Canvas.content prop added * Typings cleanup * Fix some enum props * `Path.Path` renamed to `Path.SubPath` * Fix debug output * Preliminary RichText support * TextSpan as a control * TextStyle decorations * Fix ControlTreeViewModel for transitions * TextSpan hover and click events * Fix ControlTreeViewModel.children
1 parent 35f212b commit 0c523b0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+2319
-128
lines changed

.appveyor.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,7 @@ for:
421421
install:
422422
- python --version
423423
- cd sdk/python
424-
- pip install --upgrade pip
424+
- pip install --upgrade pip virtualenv
425425
- pip install poetry
426426
- poetry install
427427

Lines changed: 370 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,370 @@
1+
import 'dart:convert';
2+
import 'dart:ui' as ui;
3+
4+
import 'package:collection/collection.dart';
5+
import 'package:flutter/material.dart';
6+
import 'package:flutter_redux/flutter_redux.dart';
7+
8+
import '../flet_app_services.dart';
9+
import '../models/app_state.dart';
10+
import '../models/canvas_view_model.dart';
11+
import '../models/control.dart';
12+
import '../models/control_tree_view_model.dart';
13+
import '../utils/alignment.dart';
14+
import '../utils/borders.dart';
15+
import '../utils/colors.dart';
16+
import '../utils/dash_path.dart';
17+
import '../utils/drawing.dart';
18+
import '../utils/numbers.dart';
19+
import '../utils/text.dart';
20+
import '../utils/transforms.dart';
21+
import 'create_control.dart';
22+
23+
typedef CanvasControlOnPaintCallback = void Function(Size size);
24+
25+
class CanvasControl extends StatefulWidget {
26+
final Control? parent;
27+
final Control control;
28+
final List<Control> children;
29+
final bool parentDisabled;
30+
31+
const CanvasControl(
32+
{Key? key,
33+
this.parent,
34+
required this.control,
35+
required this.children,
36+
required this.parentDisabled})
37+
: super(key: key);
38+
39+
@override
40+
State<CanvasControl> createState() => _CanvasControlState();
41+
}
42+
43+
class _CanvasControlState extends State<CanvasControl> {
44+
int _lastResize = DateTime.now().millisecondsSinceEpoch;
45+
Size? _lastSize;
46+
47+
@override
48+
Widget build(BuildContext context) {
49+
debugPrint("CustomPaint build: ${widget.control.id}");
50+
51+
var result = StoreConnector<AppState, CanvasViewModel>(
52+
distinct: true,
53+
converter: (store) =>
54+
CanvasViewModel.fromStore(store, widget.control, widget.children),
55+
builder: (context, viewModel) {
56+
var onResize = viewModel.control.attrBool("onResize", false)!;
57+
var resizeInterval = viewModel.control.attrInt("resizeInterval", 10)!;
58+
59+
var paint = CustomPaint(
60+
painter: FletCustomPainter(
61+
theme: Theme.of(context),
62+
shapes: viewModel.shapes,
63+
onPaintCallback: (size) {
64+
if (onResize) {
65+
var now = DateTime.now().millisecondsSinceEpoch;
66+
if ((now - _lastResize > resizeInterval &&
67+
_lastSize != size) ||
68+
_lastSize == null) {
69+
_lastResize = now;
70+
_lastSize = size;
71+
FletAppServices.of(context).server.sendPageEvent(
72+
eventTarget: viewModel.control.id,
73+
eventName: "resize",
74+
eventData:
75+
json.encode({"w": size.width, "h": size.height}));
76+
}
77+
}
78+
},
79+
),
80+
child: viewModel.child != null
81+
? createControl(viewModel.control, viewModel.child!.id,
82+
viewModel.control.isDisabled)
83+
: null,
84+
);
85+
86+
return paint;
87+
});
88+
89+
return constrainedControl(context, result, widget.parent, widget.control);
90+
}
91+
}
92+
93+
class FletCustomPainter extends CustomPainter {
94+
final ThemeData theme;
95+
final List<ControlTreeViewModel> shapes;
96+
final CanvasControlOnPaintCallback onPaintCallback;
97+
98+
const FletCustomPainter(
99+
{required this.theme,
100+
required this.shapes,
101+
required this.onPaintCallback});
102+
103+
@override
104+
void paint(Canvas canvas, Size size) {
105+
onPaintCallback(size);
106+
107+
//debugPrint("SHAPE CONTROLS: $shapes");
108+
109+
for (var shape in shapes) {
110+
if (shape.control.type == "line") {
111+
drawLine(canvas, shape);
112+
} else if (shape.control.type == "circle") {
113+
drawCircle(canvas, shape);
114+
} else if (shape.control.type == "arc") {
115+
drawArc(canvas, shape);
116+
} else if (shape.control.type == "color") {
117+
drawColor(canvas, shape);
118+
} else if (shape.control.type == "oval") {
119+
drawOval(canvas, shape);
120+
} else if (shape.control.type == "fill") {
121+
drawFill(canvas, shape);
122+
} else if (shape.control.type == "points") {
123+
drawPoints(canvas, shape);
124+
} else if (shape.control.type == "rect") {
125+
drawRect(canvas, shape);
126+
} else if (shape.control.type == "path") {
127+
drawPath(canvas, shape);
128+
} else if (shape.control.type == "shadow") {
129+
drawShadow(canvas, shape);
130+
} else if (shape.control.type == "text") {
131+
drawText(canvas, shape);
132+
}
133+
}
134+
}
135+
136+
@override
137+
bool shouldRepaint(FletCustomPainter oldDelegate) {
138+
return true;
139+
}
140+
141+
void drawLine(Canvas canvas, ControlTreeViewModel shape) {
142+
Paint paint = parsePaint(theme, shape.control, "paint");
143+
var dashPattern = parsePaintStrokeDashPattern(shape.control, "paint");
144+
paint.style = ui.PaintingStyle.stroke;
145+
var path = ui.Path();
146+
path.moveTo(
147+
shape.control.attrDouble("x1")!, shape.control.attrDouble("y1")!);
148+
path.lineTo(
149+
shape.control.attrDouble("x2")!, shape.control.attrDouble("y2")!);
150+
151+
if (dashPattern != null) {
152+
path = dashPath(path, dashArray: CircularIntervalList(dashPattern));
153+
}
154+
canvas.drawPath(path, paint);
155+
}
156+
157+
void drawCircle(Canvas canvas, ControlTreeViewModel shape) {
158+
var radius = shape.control.attrDouble("radius", 0)!;
159+
Paint paint = parsePaint(theme, shape.control, "paint");
160+
canvas.drawCircle(
161+
Offset(shape.control.attrDouble("x")!, shape.control.attrDouble("y")!),
162+
radius,
163+
paint);
164+
}
165+
166+
void drawOval(Canvas canvas, ControlTreeViewModel shape) {
167+
var width = shape.control.attrDouble("width", 0)!;
168+
var height = shape.control.attrDouble("height", 0)!;
169+
Paint paint = parsePaint(theme, shape.control, "paint");
170+
canvas.drawOval(
171+
Rect.fromLTWH(shape.control.attrDouble("x")!,
172+
shape.control.attrDouble("y")!, width, height),
173+
paint);
174+
}
175+
176+
void drawArc(Canvas canvas, ControlTreeViewModel shape) {
177+
var width = shape.control.attrDouble("width", 0)!;
178+
var height = shape.control.attrDouble("height", 0)!;
179+
var startAngle = shape.control.attrDouble("startAngle", 0)!;
180+
var sweepAngle = shape.control.attrDouble("sweepAngle", 0)!;
181+
var useCenter = shape.control.attrBool("useCenter", false)!;
182+
Paint paint = parsePaint(theme, shape.control, "paint");
183+
canvas.drawArc(
184+
Rect.fromLTWH(shape.control.attrDouble("x")!,
185+
shape.control.attrDouble("y")!, width, height),
186+
startAngle,
187+
sweepAngle,
188+
useCenter,
189+
paint);
190+
}
191+
192+
void drawFill(Canvas canvas, ControlTreeViewModel shape) {
193+
Paint paint = parsePaint(theme, shape.control, "paint");
194+
canvas.drawPaint(paint);
195+
}
196+
197+
void drawColor(Canvas canvas, ControlTreeViewModel shape) {
198+
var color =
199+
HexColor.fromString(theme, shape.control.attrString("color", "")!) ??
200+
Colors.black;
201+
var blendMode = BlendMode.values.firstWhere(
202+
(e) =>
203+
e.name.toLowerCase() ==
204+
shape.control.attrString("blendMode", "")!.toLowerCase(),
205+
orElse: () => BlendMode.srcOver);
206+
canvas.drawColor(color, blendMode);
207+
}
208+
209+
void drawPoints(Canvas canvas, ControlTreeViewModel shape) {
210+
var points = parseOffsetList(shape.control, "points")!;
211+
var pointMode = ui.PointMode.values.firstWhere(
212+
(e) =>
213+
e.name.toLowerCase() ==
214+
shape.control.attrString("pointMode", "")!.toLowerCase(),
215+
orElse: () => ui.PointMode.points);
216+
Paint paint = parsePaint(theme, shape.control, "paint");
217+
canvas.drawPoints(pointMode, points, paint);
218+
}
219+
220+
void drawRect(Canvas canvas, ControlTreeViewModel shape) {
221+
var width = shape.control.attrDouble("width", 0)!;
222+
var height = shape.control.attrDouble("height", 0)!;
223+
var borderRadius = parseBorderRadius(shape.control, "borderRadius");
224+
Paint paint = parsePaint(theme, shape.control, "paint");
225+
canvas.drawRRect(
226+
RRect.fromRectAndCorners(
227+
Rect.fromLTWH(shape.control.attrDouble("x")!,
228+
shape.control.attrDouble("y")!, width, height),
229+
topLeft: borderRadius?.topLeft ?? Radius.zero,
230+
topRight: borderRadius?.topRight ?? Radius.zero,
231+
bottomLeft: borderRadius?.bottomLeft ?? Radius.zero,
232+
bottomRight: borderRadius?.bottomRight ?? Radius.zero),
233+
paint);
234+
}
235+
236+
void drawText(Canvas canvas, ControlTreeViewModel shape) {
237+
var offset =
238+
Offset(shape.control.attrDouble("x")!, shape.control.attrDouble("y")!);
239+
var alignment =
240+
parseAlignment(shape.control, "alignment") ?? Alignment.topLeft;
241+
var text = shape.control.attrString("text", "")!;
242+
TextStyle style = parseTextStyle(theme, shape.control, "style") ??
243+
theme.textTheme.bodyMedium!;
244+
245+
if (style.color == null) {
246+
style = style.copyWith(color: theme.textTheme.bodyMedium!.color);
247+
}
248+
249+
TextAlign? textAlign = TextAlign.values.firstWhereOrNull((a) =>
250+
a.name.toLowerCase() ==
251+
shape.control.attrString("textAlign", "")!.toLowerCase());
252+
TextSpan span = TextSpan(
253+
text: text,
254+
style: style,
255+
children: parseTextSpans(theme, shape, false, null));
256+
257+
var maxLines = shape.control.attrInt("maxLines");
258+
var maxWidth = shape.control.attrDouble("maxWidth");
259+
var ellipsis = shape.control.attrString("ellipsis");
260+
261+
// paint
262+
TextPainter textPainter = TextPainter(
263+
text: span,
264+
textAlign: textAlign ?? TextAlign.start,
265+
maxLines: maxLines,
266+
ellipsis: ellipsis);
267+
textPainter.layout(maxWidth: maxWidth ?? double.infinity);
268+
269+
var angle = shape.control.attrDouble("rotate", 0)!;
270+
271+
final delta = Offset(
272+
offset.dx - textPainter.size.width / 2 * (alignment.x + 1.0),
273+
offset.dy - textPainter.size.height / 2 * (alignment.y + 1.0));
274+
275+
// rotate the text around offset point
276+
canvas.save();
277+
canvas.translate(offset.dx, offset.dy);
278+
canvas.rotate(angle);
279+
canvas.translate(-offset.dx, -offset.dy);
280+
textPainter.paint(canvas, delta);
281+
canvas.restore();
282+
}
283+
284+
void drawPath(Canvas canvas, ControlTreeViewModel shape) {
285+
var path =
286+
buildPath(json.decode(shape.control.attrString("elements", "[]")!));
287+
Paint paint = parsePaint(theme, shape.control, "paint");
288+
var dashPattern = parsePaintStrokeDashPattern(shape.control, "paint");
289+
if (dashPattern != null) {
290+
path = dashPath(path, dashArray: CircularIntervalList(dashPattern));
291+
}
292+
canvas.drawPath(path, paint);
293+
}
294+
295+
void drawShadow(Canvas canvas, ControlTreeViewModel shape) {
296+
var path = buildPath(json.decode(shape.control.attrString("path", "[]")!));
297+
var color =
298+
HexColor.fromString(theme, shape.control.attrString("color", "")!) ??
299+
Colors.black;
300+
var elevation = shape.control.attrDouble("elevation", 0)!;
301+
var transparentOccluder =
302+
shape.control.attrBool("transparentOccluder", false)!;
303+
canvas.drawShadow(path, color, elevation, transparentOccluder);
304+
}
305+
306+
ui.Path buildPath(dynamic j) {
307+
var path = ui.Path();
308+
if (j == null) {
309+
return path;
310+
}
311+
for (var elem in (j as List)) {
312+
var type = elem["type"];
313+
if (type == "moveto") {
314+
path.moveTo(parseDouble(elem["x"]), parseDouble(elem["y"]));
315+
} else if (type == "lineto") {
316+
path.lineTo(parseDouble(elem["x"]), parseDouble(elem["y"]));
317+
} else if (type == "arc") {
318+
path.addArc(
319+
Rect.fromLTWH(parseDouble(elem["x"]), parseDouble(elem["y"]),
320+
parseDouble(elem["width"]), parseDouble(elem["height"])),
321+
parseDouble(elem["start_angle"]),
322+
parseDouble(elem["sweep_angle"]));
323+
} else if (type == "arcto") {
324+
path.arcToPoint(Offset(parseDouble(elem["x"]), parseDouble(elem["y"])),
325+
radius: Radius.circular(parseDouble(elem["radius"])),
326+
rotation: parseDouble(elem["rotation"]),
327+
largeArc: parseBool(elem["large_arc"]),
328+
clockwise: parseBool(elem["clockwise"]));
329+
} else if (type == "oval") {
330+
path.addOval(Rect.fromLTWH(
331+
parseDouble(elem["x"]),
332+
parseDouble(elem["y"]),
333+
parseDouble(elem["width"]),
334+
parseDouble(elem["height"])));
335+
} else if (type == "rect") {
336+
var borderRadius = elem["border_radius"] != null
337+
? borderRadiusFromJSON(elem["border_radius"])
338+
: null;
339+
path.addRRect(RRect.fromRectAndCorners(
340+
Rect.fromLTWH(parseDouble(elem["x"]), parseDouble(elem["y"]),
341+
parseDouble(elem["width"]), parseDouble(elem["height"])),
342+
topLeft: borderRadius?.topLeft ?? Radius.zero,
343+
topRight: borderRadius?.topRight ?? Radius.zero,
344+
bottomLeft: borderRadius?.bottomLeft ?? Radius.zero,
345+
bottomRight: borderRadius?.bottomRight ?? Radius.zero));
346+
} else if (type == "conicto") {
347+
path.conicTo(
348+
parseDouble(elem["cp1x"]),
349+
parseDouble(elem["cp1y"]),
350+
parseDouble(elem["x"]),
351+
parseDouble(elem["y"]),
352+
parseDouble(elem["w"]));
353+
} else if (type == "cubicto") {
354+
path.cubicTo(
355+
parseDouble(elem["cp1x"]),
356+
parseDouble(elem["cp1y"]),
357+
parseDouble(elem["cp2x"]),
358+
parseDouble(elem["cp2y"]),
359+
parseDouble(elem["x"]),
360+
parseDouble(elem["y"]));
361+
} else if (type == "subpath") {
362+
path.addPath(buildPath(elem["elements"]),
363+
Offset(parseDouble(elem["x"]), parseDouble(elem["y"])));
364+
} else if (type == "close") {
365+
path.close();
366+
}
367+
}
368+
return path;
369+
}
370+
}

0 commit comments

Comments
 (0)