Skip to content

Commit 0d2bc15

Browse files
Custom fonts (#22)
* Allow numeric BorderRadius values * Custom fonts and fontFamily * Remove typing warning
1 parent 79ea819 commit 0d2bc15

File tree

15 files changed

+234
-97
lines changed

15 files changed

+234
-97
lines changed

client/lib/controls/image.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class ImageControl extends StatelessWidget {
4646
builder: (context, pageUri) {
4747
return baseControl(
4848
_clipCorners(
49-
Image.network(getAssetUrl(pageUri!, src),
49+
Image.network(getAssetUri(pageUri!, src).toString(),
5050
width: width, height: height, repeat: repeat, fit: fit),
5151
control),
5252
parent,

client/lib/controls/page.dart

Lines changed: 103 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'package:flet_view/utils/user_fonts.dart';
12
import 'package:flutter/material.dart';
23
import 'package:flutter_redux/flutter_redux.dart';
34

@@ -11,6 +12,7 @@ import '../utils/colors.dart';
1112
import '../utils/desktop.dart';
1213
import '../utils/edge_insets.dart';
1314
import '../utils/theme.dart';
15+
import '../utils/uri.dart';
1416
import '../widgets/page_media.dart';
1517
import 'app_bar.dart';
1618
import 'create_control.dart';
@@ -110,89 +112,109 @@ class PageControl extends StatelessWidget {
110112
childIds.add(appBar.id);
111113
}
112114

113-
return StoreConnector<AppState, PageMediaViewModel>(
115+
return StoreConnector<AppState, Uri?>(
114116
distinct: true,
115-
converter: (store) => PageMediaViewModel.fromStore(store),
116-
builder: (context, media) {
117-
var theme = themeMode == ThemeMode.light ||
118-
(themeMode == ThemeMode.system &&
119-
media.displayBrightness == Brightness.light)
120-
? lightTheme
121-
: darkTheme;
122-
123-
return StoreConnector<AppState, ControlsViewModel>(
117+
converter: (store) => store.state.pageUri,
118+
builder: (context, pageUri) {
119+
// load custom fonts
120+
parseFonts(control, "fonts").forEach((fontFamily, fontUrl) {
121+
var fontUri = Uri.parse(fontUrl);
122+
if (!fontUri.hasAuthority) {
123+
fontUri = getAssetUri(pageUri!, fontUrl);
124+
}
125+
debugPrint("fontUri: $fontUri");
126+
UserFonts.loadFont(fontFamily, fontUri);
127+
});
128+
129+
return StoreConnector<AppState, PageMediaViewModel>(
124130
distinct: true,
125-
converter: (store) =>
126-
ControlsViewModel.fromStore(store, childIds),
127-
builder: (context, childrenViews) {
128-
debugPrint("Offstage StoreConnector build");
129-
130-
// offstage
131-
List<Widget> offstageWidgets = offstage != null
132-
? childrenViews.controlViews.first.children
133-
.where((c) =>
134-
c.isVisible &&
135-
c.type != ControlType.floatingActionButton)
136-
.map((c) => createControl(offstage, c.id, disabled))
137-
.toList()
138-
: [];
139-
140-
List<Control> fab = offstage != null
141-
? childrenViews.controlViews.first.children
142-
.where((c) =>
143-
c.isVisible &&
144-
c.type == ControlType.floatingActionButton)
145-
.toList()
146-
: [];
147-
148-
var appBarView =
149-
appBar != null ? childrenViews.controlViews.last : null;
150-
151-
var column = Column(
152-
mainAxisAlignment: mainAlignment,
153-
crossAxisAlignment: crossAlignment,
154-
children: controls);
155-
156-
return MaterialApp(
157-
title: title,
158-
theme: lightTheme,
159-
darkTheme: darkTheme,
160-
themeMode: themeMode,
161-
home: Scaffold(
162-
appBar: appBarView != null
163-
? AppBarControl(
164-
parent: control,
165-
control: appBarView.control,
166-
children: appBarView.children,
167-
parentDisabled: disabled,
168-
height: appBarView.control
169-
.attrDouble("toolbarHeight", kToolbarHeight)!,
170-
theme: theme)
171-
: null,
172-
body: Stack(children: [
173-
SizedBox.expand(
174-
child: Container(
175-
padding: parseEdgeInsets(control, "padding") ??
176-
const EdgeInsets.all(10),
177-
decoration: BoxDecoration(
178-
color: HexColor.fromString(theme,
179-
control.attrString("bgcolor", "")!)),
180-
child: scrollMode != ScrollMode.none
181-
? ScrollableControl(
182-
child: column,
183-
scrollDirection: Axis.vertical,
184-
scrollMode: scrollMode,
185-
autoScroll: autoScroll,
186-
)
187-
: column)),
188-
...offstageWidgets,
189-
const PageMedia()
190-
]),
191-
floatingActionButton: fab.isNotEmpty
192-
? createControl(offstage, fab.first.id, disabled)
193-
: null,
194-
),
195-
);
131+
converter: (store) => PageMediaViewModel.fromStore(store),
132+
builder: (context, media) {
133+
var theme = themeMode == ThemeMode.light ||
134+
(themeMode == ThemeMode.system &&
135+
media.displayBrightness == Brightness.light)
136+
? lightTheme
137+
: darkTheme;
138+
139+
return StoreConnector<AppState, ControlsViewModel>(
140+
distinct: true,
141+
converter: (store) =>
142+
ControlsViewModel.fromStore(store, childIds),
143+
builder: (context, childrenViews) {
144+
debugPrint("Offstage StoreConnector build");
145+
146+
// offstage
147+
List<Widget> offstageWidgets = offstage != null
148+
? childrenViews.controlViews.first.children
149+
.where((c) =>
150+
c.isVisible &&
151+
c.type != ControlType.floatingActionButton)
152+
.map((c) =>
153+
createControl(offstage, c.id, disabled))
154+
.toList()
155+
: [];
156+
157+
List<Control> fab = offstage != null
158+
? childrenViews.controlViews.first.children
159+
.where((c) =>
160+
c.isVisible &&
161+
c.type == ControlType.floatingActionButton)
162+
.toList()
163+
: [];
164+
165+
var appBarView = appBar != null
166+
? childrenViews.controlViews.last
167+
: null;
168+
169+
var column = Column(
170+
mainAxisAlignment: mainAlignment,
171+
crossAxisAlignment: crossAlignment,
172+
children: controls);
173+
174+
return MaterialApp(
175+
title: title,
176+
theme: lightTheme,
177+
darkTheme: darkTheme,
178+
themeMode: themeMode,
179+
home: Scaffold(
180+
appBar: appBarView != null
181+
? AppBarControl(
182+
parent: control,
183+
control: appBarView.control,
184+
children: appBarView.children,
185+
parentDisabled: disabled,
186+
height: appBarView.control.attrDouble(
187+
"toolbarHeight", kToolbarHeight)!,
188+
theme: theme)
189+
: null,
190+
body: Stack(children: [
191+
SizedBox.expand(
192+
child: Container(
193+
padding:
194+
parseEdgeInsets(control, "padding") ??
195+
const EdgeInsets.all(10),
196+
decoration: BoxDecoration(
197+
color: HexColor.fromString(
198+
theme,
199+
control.attrString(
200+
"bgcolor", "")!)),
201+
child: scrollMode != ScrollMode.none
202+
? ScrollableControl(
203+
child: column,
204+
scrollDirection: Axis.vertical,
205+
scrollMode: scrollMode,
206+
autoScroll: autoScroll,
207+
)
208+
: column)),
209+
...offstageWidgets,
210+
const PageMedia()
211+
]),
212+
floatingActionButton: fab.isNotEmpty
213+
? createControl(offstage, fab.first.id, disabled)
214+
: null,
215+
),
216+
);
217+
});
196218
});
197219
});
198220
}

client/lib/controls/text.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class TextControl extends StatelessWidget {
3030
fontSize: control.attrDouble("size", null),
3131
fontWeight: getFontWeight(control.attrString("weight", "")!),
3232
fontStyle: control.attrBool("italic", false)! ? FontStyle.italic : null,
33+
fontFamily: control.attrString("fontFamily"),
3334
color: HexColor.fromString(
3435
Theme.of(context), control.attrString("color", "")!),
3536
backgroundColor: HexColor.fromString(

client/lib/utils/theme.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ ThemeData themeFromJson(Map<String, dynamic> json) {
2020
brightness: Brightness.values.firstWhere(
2121
(b) => b.name.toLowerCase() == json["brightness"],
2222
orElse: () => Brightness.light),
23-
colorSchemeSeed: HexColor.fromString(null, json["color_scheme_seed"]),
23+
colorSchemeSeed:
24+
HexColor.fromString(null, json["color_scheme_seed"] ?? ""),
25+
fontFamily: json["font_family"],
2426
useMaterial3: json["use_material3"]);
2527
}

client/lib/utils/uri.dart

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,10 @@ String getWebSocketEndpoint(Uri uri) {
99
return "$wsScheme://${uri.authority}/ws";
1010
}
1111

12-
String getAssetUrl(Uri pageUri, String assetPath) {
12+
Uri getAssetUri(Uri pageUri, String assetPath) {
1313
return Uri(
14-
scheme: pageUri.scheme,
15-
host: pageUri.host,
16-
port: pageUri.port,
17-
path: assetPath)
18-
.toString();
14+
scheme: pageUri.scheme,
15+
host: pageUri.host,
16+
port: pageUri.port,
17+
path: assetPath);
1918
}

client/lib/utils/user_fonts.dart

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import 'dart:convert';
2+
3+
import 'package:flutter/services.dart';
4+
import 'package:http/http.dart' as http;
5+
6+
import '../models/control.dart';
7+
8+
class UserFonts {
9+
static Map<String, FontLoader> fontLoaders = {};
10+
11+
static void loadFont(String fontFamily, Uri fontUri) {
12+
var key = "$fontFamily$fontUri";
13+
if (fontLoaders.containsKey(key)) {
14+
return;
15+
}
16+
var fontLoader = FontLoader(fontFamily);
17+
fontLoaders[key] = fontLoader;
18+
fontLoader.addFont(fetchFont(fontUri));
19+
fontLoader.load();
20+
}
21+
22+
static Future<ByteData> fetchFont(Uri uri) async {
23+
final response = await http.get(uri);
24+
25+
if (response.statusCode == 200) {
26+
return ByteData.view(response.bodyBytes.buffer);
27+
} else {
28+
// If that call was not successful, throw an error.
29+
throw Exception('Failed to load font $uri');
30+
}
31+
}
32+
}
33+
34+
Map<String, String> parseFonts(Control control, String propName) {
35+
var v = control.attrString(propName, null);
36+
if (v == null) {
37+
return {};
38+
}
39+
40+
final j1 = json.decode(v);
41+
return fontsFromJson(j1);
42+
}
43+
44+
Map<String, String> fontsFromJson(Map<String, dynamic> json) {
45+
return json.map((key, value) => MapEntry(key, value));
46+
}

client/pubspec.lock

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,20 @@ packages:
116116
description: flutter
117117
source: sdk
118118
version: "0.0.0"
119+
http:
120+
dependency: "direct main"
121+
description:
122+
name: http
123+
url: "https://pub.dartlang.org"
124+
source: hosted
125+
version: "0.13.4"
126+
http_parser:
127+
dependency: transitive
128+
description:
129+
name: http_parser
130+
url: "https://pub.dartlang.org"
131+
source: hosted
132+
version: "4.0.1"
119133
image:
120134
dependency: transitive
121135
description:

client/pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ dependencies:
3838
equatable: ^2.0.3
3939
web_socket_channel: ^2.1.0
4040
window_manager: ^0.2.1
41+
http: ^0.13.3
4142

4243
dev_dependencies:
4344
flutter_test:
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import 'dart:convert';
2+
3+
import 'package:flet_view/utils/user_fonts.dart';
4+
import 'package:flutter_test/flutter_test.dart';
5+
6+
void main() {
7+
test("Custom fonts are parsed from JSON", () {
8+
const t1 = '''{
9+
"font1": "https://fonts.com/font1.ttf",
10+
"font2": "https://fonts.com/font2.ttf"
11+
}''';
12+
13+
final j1 = json.decode(t1);
14+
var fonts = fontsFromJson(j1);
15+
16+
expect(fonts.length, 2);
17+
expect(fonts["font1"], "https://fonts.com/font1.ttf");
18+
expect(fonts["font2"], "https://fonts.com/font2.ttf");
19+
});
20+
}

sdk/python/flet/container.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@
77
from flet.border import Border
88
from flet.border_radius import BorderRadius
99
from flet.constrained_control import ConstrainedControl
10-
from flet.control import BorderStyle, Control, MarginValue, OptionalNumber, PaddingValue
10+
from flet.control import (
11+
BorderRadiusValue,
12+
Control,
13+
MarginValue,
14+
OptionalNumber,
15+
PaddingValue,
16+
)
1117
from flet.ref import Ref
1218

1319
try:
@@ -37,7 +43,7 @@ def __init__(
3743
alignment: Alignment = None,
3844
bgcolor: str = None,
3945
border: Border = None,
40-
border_radius: BorderRadius = None,
46+
border_radius: BorderRadiusValue = None,
4147
):
4248
ConstrainedControl.__init__(
4349
self,
@@ -134,7 +140,7 @@ def border_radius(self):
134140

135141
@border_radius.setter
136142
@beartype
137-
def border_radius(self, value: Optional[BorderRadius]):
143+
def border_radius(self, value: BorderRadiusValue):
138144
self.__border_radius = value
139145
if value and isinstance(value, (int, float)):
140146
value = border_radius.all(value)

sdk/python/flet/control.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from beartype import beartype
88
from beartype.typing import List, Optional
99

10+
from flet.border_radius import BorderRadius
1011
from flet.embed_json_encoder import EmbedJsonEncoder
1112
from flet.margin import Margin
1213
from flet.padding import Padding
@@ -53,6 +54,8 @@
5354

5455
MarginValue = Union[None, int, float, Margin]
5556

57+
BorderRadiusValue = Union[None, int, float, BorderRadius]
58+
5659
ScrollMode = Literal[None, True, False, "none", "auto", "adaptive", "always"]
5760

5861

0 commit comments

Comments
 (0)