diff --git a/webf/example/assets/bundle.html b/webf/example/assets/bundle.html index 431559ebb8..43ace700e0 100644 --- a/webf/example/assets/bundle.html +++ b/webf/example/assets/bundle.html @@ -1,51 +1,33 @@ - - - - -
- -
-
- - - -
- - - + } + + + + + +
+ Example DIV. +
+ + diff --git a/webf/example/lib/main.dart b/webf/example/lib/main.dart index dc3bd452ea..1ba792ca20 100644 --- a/webf/example/lib/main.dart +++ b/webf/example/lib/main.dart @@ -42,9 +42,10 @@ class FirstPageState extends State { super.didChangeDependencies(); controller = WebFController( context, + isDarkMode: MediaQuery.of(context).platformBrightness == Brightness.dark, devToolsService: ChromeDevToolsService(), ); - controller.preload(WebFBundle.fromUrl('assets:assets/bundle.html')); + controller.preload(WebFBundle.fromUrl('assets:assets/bundle.html'), viewportSize: MediaQuery.of(context).size); } @override diff --git a/webf/lib/src/css/css_rule.dart b/webf/lib/src/css/css_rule.dart index 0ff4412aef..077dc904fc 100644 --- a/webf/lib/src/css/css_rule.dart +++ b/webf/lib/src/css/css_rule.dart @@ -89,3 +89,161 @@ class CSSFontFaceRule extends CSSRule { return '@font-face'; } } + + +class CSSMediaDirective extends CSSRule { + final CSSMediaQuery? cssMediaQuery; + final List? rules; + + CSSMediaDirective(this.cssMediaQuery, this.rules) : super(); + + set rules(List? rules) { + this.rules = rules; + } + + @override + int get type => CSSRule.MEDIA_RULE; + + List? getValidMediaRules(double? windowWidth, double? windowHeight, bool isDarkMode) { + List? _mediaRules = []; + // print('--------- --------- --------- --------- CSSMediaDirective start--------- --------- --------- --------- '); + if (rules == null) { + return _mediaRules; + } + if (cssMediaQuery == null) { + return rules; + } + // bool isMediaTypeNotOp = cssMediaQuery._mediaUnary == TokenKind.MEDIA_OP_ONLY; + //w3c has media type screen/print/speech/all, but webf only work on screen and all + String? mediaType = cssMediaQuery!._mediaType?.name; + if (mediaType != null && mediaType != MediaType.SCREEN && mediaType != MediaType.ALL) { + return _mediaRules; + } + List conditions = []; + List ops = []; + for (CSSMediaExpression expression in cssMediaQuery!.expressions) { + // [max-width: 1800px, min-width: 450px] + if (expression.mediaStyle != null) { + dynamic maxAspectRatio = expression.mediaStyle!['max-aspect-ratio']; + if (maxAspectRatio != null && windowWidth != null && windowHeight != null) { + double? maxAPS; + if (maxAspectRatio is String) { + maxAPS = parseStringToDouble(maxAspectRatio); + } else if (maxAspectRatio is double) { + maxAPS = maxAspectRatio; + } + if (maxAPS != null) { + bool condition = windowWidth / windowHeight <= maxAPS; + conditions.add(condition); + ops.add(expression.op == MediaOperator.AND); + } + } + dynamic minAspectRatio = expression.mediaStyle!['min-aspect-ratio']; + if (minAspectRatio != null && windowWidth != null && windowHeight != null) { + double? minAPS; + if (minAspectRatio is String) { + minAPS = parseStringToDouble(minAspectRatio); + } else if (minAspectRatio is double) { + minAPS = minAspectRatio; + } + if (minAPS != null) { + bool condition = windowWidth / windowHeight >= minAPS; + conditions.add(condition); + ops.add(expression.op == MediaOperator.AND); + } + } + dynamic maxWidth = expression.mediaStyle!['max-width']; + if (windowWidth != null && maxWidth != null) { + double maxWidthValue = CSSLength.parseLength(maxWidth, null).value ?? -1; + bool condition = windowWidth < maxWidthValue; + conditions.add(condition); + ops.add(expression.op == MediaOperator.AND); + } + dynamic minWidth = expression.mediaStyle!['min-width']; + if (windowWidth != null && minWidth != null) { + double minWidthValue = CSSLength.parseLength(minWidth, null).value ?? -1; + bool condition = windowWidth > minWidthValue; + conditions.add(condition); + ops.add(expression.op == MediaOperator.AND); + } + dynamic prefersColorScheme = expression.mediaStyle!['prefers-color-scheme']; + if (prefersColorScheme != null) { + bool isMediaDarkMode = prefersColorScheme == 'dark'; + bool condition = isMediaDarkMode == isDarkMode; + conditions.add(condition); + ops.add(expression.op == MediaOperator.AND); + } + } + } + bool isValid = true; + for (int i = 0; i < conditions.length; i ++) { + bool con = conditions[i]; + bool isAnd = ops[i]; + if (isAnd) { + isValid = isValid && con; + } else { + isValid = isValid || con; + } + } + if (isValid) { + _mediaRules = rules; + } + // print('--------- --------- --------- --------- CSSMediaDirective end--------- --------- --------- --------- '); + return _mediaRules; + } + + double? parseStringToDouble(String str) { + try { + if (str.contains('/')) { // 8/5 + List parts = str.split('/'); + double num1 = double.parse(parts[0]); + double num2 = double.parse(parts[1]); + return num1 / num2; + } + return double.parse(str); + } catch (e) { + print('parseStringToDouble $e'); + } + return null; + } +} + +/// MediaQuery grammar: +/// +/// : [ONLY | NOT]? S* media_type S* [ AND S* media_expression ]* +/// | media_expression [ AND S* media_expression ]* +/// media_type +/// : IDENT +/// media_expression +/// : '(' S* media_feature S* [ ':' S* expr ]? ')' S* +/// media_feature +/// : IDENT +class CSSMediaQuery extends TreeNode { + /// not, only or no operator. + final int _mediaUnary; + final Identifier? _mediaType; + final List expressions; + + CSSMediaQuery( + this._mediaUnary, this._mediaType, this.expressions) + : super(); + + bool get hasMediaType => _mediaType != null; + String get mediaType => _mediaType!.name; + + bool get hasUnary => _mediaUnary != -1; + String get unary => + TokenKind.idToValue(TokenKind.MEDIA_OPERATORS, _mediaUnary)! + .toUpperCase(); +} + + +/// MediaExpression grammar: +/// +/// '(' S* media_feature S* [ ':' S* expr ]? ')' S* +class CSSMediaExpression extends TreeNode { + final String op; + final Map? mediaStyle; + + CSSMediaExpression(this.op, this.mediaStyle) : super(); +} diff --git a/webf/lib/src/css/parser/parser.dart b/webf/lib/src/css/parser/parser.dart index 8b96c9ca8c..4a7b5cd736 100644 --- a/webf/lib/src/css/parser/parser.dart +++ b/webf/lib/src/css/parser/parser.dart @@ -87,8 +87,8 @@ class CSSParser { } /// Main entry point for parsing an entire CSS file. - CSSStyleSheet parse() { - final rules = parseRules(); + CSSStyleSheet parse({double? windowWidth, double? windowHeight, bool? isDarkMode}) { + final rules = parseRules(windowWidth: windowWidth, windowHeight: windowHeight, isDarkMode: isDarkMode); return CSSStyleSheet(rules); } @@ -132,12 +132,21 @@ class CSSParser { return style; } - List parseRules({int startPosition = 0}) { + List parseRules({double? windowWidth, double? windowHeight, bool? isDarkMode}) { var rules = []; while (!_maybeEat(TokenKind.END_OF_FILE)) { final data = processRule(); if (data != null) { - rules.addAll(data); + for (CSSRule cssRule in data) { + if (cssRule is CSSMediaDirective) { + List? mediaRules = cssRule.getValidMediaRules(windowWidth, windowHeight, isDarkMode ?? false); + if (mediaRules != null) { + rules.addAll(mediaRules); + } + } else { + rules.add(cssRule); + } + } } else { _next(); } @@ -265,6 +274,80 @@ class CSSParser { return start.expand(_previousToken!.span); } + /////////////////////////////////////////////////////////////////// + // Top level productions + /////////////////////////////////////////////////////////////////// + + CSSMediaQuery? processMediaQuery() { + // Grammar: [ONLY | NOT]? S* media_type S* + // [ AND S* MediaExpr ]* | MediaExpr [ AND S* MediaExpr ]* + + var start = _peekToken.span; + + // Is it a unary media operator? + // @media only screen + var op = _peekToken.text; //only + var opLen = op.length; + var unaryOp = TokenKind.matchMediaOperator(op, 0, opLen); + if (unaryOp != -1) { + _next(); + start = _peekToken.span; + } + + Identifier? type; + // Get the media type. + if (_peekIdentifier()) type = identifier(); // screen + + var exprs = []; + + while (true) { + // Parse AND if query has a media_type or previous expression. + if (exprs.isEmpty && type == null) { + op = MediaOperator.AND; + } else { + var andOp = exprs.isNotEmpty || type != null; + if (andOp) { + op = _peekToken.text; // and + opLen = op.length; + int matchMOP = TokenKind.matchMediaOperator(op, 0, opLen); + if (matchMOP != TokenKind.MEDIA_OP_AND && matchMOP != TokenKind.MEDIA_OP_OR) { + break; + } + _next(); + } + } + var expr = processMediaExpression(op); + if (expr == null) break; + + exprs.add(expr); + } + + if (unaryOp != -1 || type != null || exprs.isNotEmpty) { + return CSSMediaQuery(unaryOp, type, exprs); + } + return null; + } + + CSSMediaExpression? processMediaExpression([String op = MediaOperator.AND]) { + var start = _peekToken.span; + // Grammar: '(' S* media_feature S* [ ':' S* expr ]? ')' S* + if (_maybeEat(TokenKind.LPAREN)) { + if (_peekIdentifier()) { + var feature = identifier().name; + String text = ''; + if (_maybeEat(TokenKind.COLON)) { + do { + text += _next().text; + } while(!_maybeEat(TokenKind.RPAREN)); + return CSSMediaExpression(op, {feature : text}); + } + } else if (isChecked) { + _warning('Missing media feature in media expression', location: _makeSpan(start)); + } + } + return null; + } + /// Directive grammar: /// /// import: '@import' [string | URI] media_list? @@ -293,10 +376,28 @@ class CSSParser { return null; case TokenKind.DIRECTIVE_MEDIA: - while (!_maybeEat(TokenKind.END_OF_FILE) && !_maybeEat(TokenKind.RBRACE)) { + _next(); + // print('processDirective CSSMediaDirective start ----- TokenKind.DIRECTIVE_MEDIA'); + CSSMediaQuery? cssMediaQuery = processMediaQuery(); + if (cssMediaQuery != null) { _next(); } - return null; + List? rules = []; + do { + List? rule = processRule(); + if (rule != null) { + rules.addAll(rule); + } + } while (!_maybeEat(TokenKind.RBRACE)); + // rules.forEach((rule) { + // if (rule is CSSStyleRule) { + // print(' ----> processDirective CSSMediaDirective forEach ${rule.selectorGroup.selectorText}, color ${rule.declaration.getPropertyValue('color')}'); + // } else { + // print(' ----> processDirective CSSMediaDirective forEach ${rule.runtimeType}'); + // } + // }); + // print('processDirective CSSMediaDirective end ----- rules ${rules.length} TokenKind.DIRECTIVE_MEDIA'); + return CSSMediaDirective(cssMediaQuery, rules); case TokenKind.DIRECTIVE_HOST: _next(); diff --git a/webf/lib/src/css/parser/token_kind.dart b/webf/lib/src/css/parser/token_kind.dart index 2761ff2736..3fe9ea4701 100644 --- a/webf/lib/src/css/parser/token_kind.dart +++ b/webf/lib/src/css/parser/token_kind.dart @@ -194,6 +194,7 @@ class TokenKind { static const int MEDIA_OP_ONLY = 665; // Unary. static const int MEDIA_OP_NOT = 666; // Unary. static const int MEDIA_OP_AND = 667; // Binary. + static const int MEDIA_OP_OR = 668; // Binary. // Directives inside of a @page (margin sym). static const int MARGIN_DIRECTIVE_TOPLEFTCORNER = 670; @@ -250,6 +251,8 @@ class TokenKind { {'type': TokenKind.MEDIA_OP_ONLY, 'value': 'only'}, {'type': TokenKind.MEDIA_OP_NOT, 'value': 'not'}, {'type': TokenKind.MEDIA_OP_AND, 'value': 'and'}, + {'type': TokenKind.MEDIA_OP_OR, 'value': 'or'}, + {'type': TokenKind.MEDIA_OP_OR, 'value': ','}, ]; static const List> MARGIN_DIRECTIVES = [ @@ -547,3 +550,17 @@ class TokenChar { static const int BACKSLASH = 0x5c; // "\".codeUnitAt(0) static const int AMPERSAND = 0x26; // "&".codeUnitAt(0) } + +class MediaType { + static const String ALL = 'all'; + static const String PRINT = 'print'; + static const String SCREEN = 'screen'; + static const String SPEECH = 'speech'; +} +class MediaOperator { + static const String NOT = 'not'; + static const String ONLY = 'only'; + static const String AND = 'and'; + static const String OR = 'or'; + static const String OR2 = ','; +} diff --git a/webf/lib/src/css/rule_set.dart b/webf/lib/src/css/rule_set.dart index 82925c8cfe..7d4603d838 100644 --- a/webf/lib/src/css/rule_set.dart +++ b/webf/lib/src/css/rule_set.dart @@ -49,6 +49,8 @@ class RuleSet { keyframesRules[rule.name] = rule; } else if (rule is CSSFontFaceRule) { CSSFontFace.resolveFontFaceRules(rule, ownerDocument.contextId!, baseHref); + } else if (rule is CSSMediaDirective) { + // doNothing } else { assert(false, 'Unsupported rule type: ${rule.runtimeType}'); } diff --git a/webf/lib/src/css/style_sheet.dart b/webf/lib/src/css/style_sheet.dart index ad8da1922c..135371c277 100644 --- a/webf/lib/src/css/style_sheet.dart +++ b/webf/lib/src/css/style_sheet.dart @@ -23,8 +23,8 @@ class CSSStyleSheet implements StyleSheet, Comparable { CSSStyleSheet(this.cssRules, {this.disabled = false, this.href}); - insertRule(String text, int index) { - List rules = CSSParser(text).parseRules(); + insertRule(String text, int index, {required double windowWidth, required double windowHeight, required bool isDarkMode}) { + List rules = CSSParser(text).parseRules(windowWidth: windowWidth, windowHeight: windowHeight, isDarkMode: isDarkMode); cssRules.addAll(rules); } @@ -34,15 +34,15 @@ class CSSStyleSheet implements StyleSheet, Comparable { } /// Synchronously replaces the content of the stylesheet with the content passed into it. - replaceSync(String text) { + replaceSync(String text, {required double windowWidth, required double windowHeight, required bool isDarkMode}) { cssRules.clear(); - List rules = CSSParser(text).parseRules(); + List rules = CSSParser(text).parseRules(windowWidth: windowWidth, windowHeight: windowHeight, isDarkMode: isDarkMode); cssRules.addAll(rules); } - Future replace(String text) async { + Future replace(String text, {required double windowWidth, required double windowHeight, required bool isDarkMode}) async { return Future(() { - replaceSync(text); + replaceSync(text, windowWidth: windowWidth, windowHeight: windowHeight, isDarkMode: isDarkMode); }); } diff --git a/webf/lib/src/dom/document.dart b/webf/lib/src/dom/document.dart index 1cff1c7b78..89bf6568cc 100644 --- a/webf/lib/src/dom/document.dart +++ b/webf/lib/src/dom/document.dart @@ -5,6 +5,7 @@ import 'dart:collection'; import 'dart:ffi'; import 'dart:io'; +import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:webf/css.dart'; @@ -146,6 +147,23 @@ class Document extends ContainerNode { RenderViewportBox? get viewport => controller.view.viewport; + ui.Size? _preloadViewportSize; + + set preloadViewportSize(viewportSize) { + _preloadViewportSize = viewportSize; + } + + ui.Size? get preloadViewportSize => _preloadViewportSize; + + bool? _preloadDarkMode; + + set preloadDarkMode(isDarkMode) { + _preloadDarkMode = isDarkMode; + } + + bool? get preloadDarkMode => _preloadDarkMode; + + @override Document get ownerDocument => this; diff --git a/webf/lib/src/html/head.dart b/webf/lib/src/html/head.dart index 0cdde11549..635bd5f151 100644 --- a/webf/lib/src/html/head.dart +++ b/webf/lib/src/html/head.dart @@ -57,6 +57,7 @@ class LinkElement extends Element { attributes['rel'] = ElementAttributeProperty(setter: (value) => rel = attributeToProperty(value)); attributes['href'] = ElementAttributeProperty(setter: (value) => href = attributeToProperty(value)); attributes['type'] = ElementAttributeProperty(setter: (value) => type = attributeToProperty(value)); + attributes['media'] = ElementAttributeProperty(setter: (value) => media = attributeToProperty(value)); } @override @@ -68,6 +69,7 @@ class LinkElement extends Element { properties['rel'] = BindingObjectProperty(getter: () => rel, setter: (value) => rel = castToType(value)); properties['href'] = BindingObjectProperty(getter: () => href, setter: (value) => href = castToType(value)); properties['type'] = BindingObjectProperty(getter: () => type, setter: (value) => type = castToType(value)); + properties['media'] = BindingObjectProperty(getter: () => media, setter: (value) => media = castToType(value)); } bool get disabled => getAttribute('disabled') != null; @@ -100,6 +102,13 @@ class LinkElement extends Element { set type(String value) { internalSetAttribute('type', value); } + + String get media => getAttribute('media') ?? ''; + + set media(String value) { + internalSetAttribute('media', value); + } + Future _resolveHyperlink() async { String? href = getAttribute('href'); String? rel = getAttribute('rel'); @@ -139,6 +148,9 @@ class LinkElement extends Element { rel == _REL_STYLESHEET && isConnected && !_stylesheetLoaded.containsKey(_resolvedHyperlink.toString())) { + if (!isValidMedia(media)) { + return; + } // Increase the pending count for preloading resources. if (ownerDocument.controller.preloadStatus != PreloadingStatus.none) { @@ -167,7 +179,8 @@ class LinkElement extends Element { WebFProfiler.instance.startTrackUICommandStep('Style.parseCSS'); } - _styleSheet = CSSParser(cssString, href: href).parse(); + _styleSheet = CSSParser(cssString, href: href) + .parse(windowWidth: windowWidth, windowHeight: windowHeight, isDarkMode: isDarkMode); _styleSheet?.href = href; if (enableWebFProfileTracking) { @@ -204,6 +217,18 @@ class LinkElement extends Element { } } + double get windowWidth { + return ownerDocument.viewport?.viewportSize.width ?? ownerDocument.preloadViewportSize?.width ?? -1; + } + + double get windowHeight { + return ownerDocument.viewport?.viewportSize.height ?? ownerDocument.preloadViewportSize?.height ?? -1; + } + + bool get isDarkMode { + return ownerDocument.viewport?.controller.isDarkMode ?? ownerDocument.preloadDarkMode ?? false; + } + @override void connectedCallback() { super.connectedCallback(); @@ -221,6 +246,72 @@ class LinkElement extends Element { } ownerDocument.styleNodeManager.removeStyleSheetCandidateNode(this); } + + //https://www.w3schools.com/cssref/css3_pr_mediaquery.php + //https://www.w3school.com.cn/cssref/pr_mediaquery.asp + Map mediaMap = {}; + bool isValidMedia(String media) { + bool isValid = true; + if (media.isEmpty) { + return isValid; + } + if (mediaMap.containsKey(media)) { + return mediaMap[media] ?? isValid; + } + media = media.toLowerCase(); + String mediaType = ''; + String lastOperator = ''; + Map andMap = {}; + Map notMap = {}; + Map onlyMap = {}; + int startIndex = 0; + int conditionStartIndex = 0; + for (int index = 0; index < media.length; index++) { + int code = media.codeUnitAt(index); + if (code == TokenChar.LPAREN) { + conditionStartIndex = index; + } else if (conditionStartIndex > 0) { + if (code == TokenChar.RPAREN) { + String condition = media.substring(conditionStartIndex + 1, index).replaceAll(' ', ''); + List cp = condition.split(':'); + if (lastOperator == MediaOperator.AND) { + andMap[cp[0]] = cp[1]; + } else if (lastOperator == MediaOperator.ONLY) { + onlyMap[cp[0]] = cp[1]; + } else if (lastOperator == MediaOperator.NOT) { + notMap[cp[0]] = cp[1]; + } + startIndex = index; + conditionStartIndex = -1; + } + } else if (code == TokenChar.SPACE) { + String key = media.substring(startIndex, index).replaceAll(' ', ''); + startIndex = index; + if (key == MediaType.ALL || key == MediaType.SCREEN) { + mediaType = key; + } else if (key == MediaOperator.AND || key == MediaOperator.NOT || key == MediaOperator.ONLY) { + lastOperator = key; + } + } + } + //mediaType:screen, lastOperator:and, andMap {max-width: 300px}, onlyMap {}, notMap {} + // print('mediaType:$mediaType, lastOperator:$lastOperator, andMap $andMap, onlyMap $onlyMap, notMap $notMap'); + if (mediaType == MediaType.ALL || mediaType == MediaType.SCREEN) { + //for onlyMap + + //for notMap + + //for andMap + double maxWidthValue = CSSLength.parseLength(andMap['max-width'] ?? '0px', null).value ?? -1; + double minWidthValue = CSSLength.parseLength(andMap['min-width'] ?? '0px', null).value ?? -1; + // print('windowWidth:$windowWidth, maxWidthValue:$maxWidthValue, minWidthValue:$minWidthValue, '); + if (maxWidthValue < windowWidth || minWidthValue > windowWidth) { + isValid = false; + } + } + mediaMap[media] = isValid; + return isValid; + } } class MetaElement extends Element { @@ -284,9 +375,9 @@ mixin StyleElementMixin on Element { String? text = collectElementChildText(); if (text != null) { if (_styleSheet != null) { - _styleSheet!.replace(text); + _styleSheet!.replaceSync(text, windowWidth: windowWidth, windowHeight: windowHeight, isDarkMode: isDarkMode); } else { - _styleSheet = CSSParser(text).parse(); + _styleSheet = CSSParser(text).parse(windowWidth: windowWidth, windowHeight: windowHeight, isDarkMode: isDarkMode); } if (_styleSheet != null) { ownerDocument.markElementStyleDirty(ownerDocument.documentElement!); @@ -300,6 +391,18 @@ mixin StyleElementMixin on Element { } } + double get windowWidth { + return ownerDocument.preloadViewportSize?.width ?? ownerDocument.viewport?.viewportSize.width ?? -1; + } + + double get windowHeight { + return ownerDocument.preloadViewportSize?.height ?? ownerDocument.viewport?.viewportSize.height ?? -1; + } + + bool get isDarkMode { + return ownerDocument.preloadDarkMode ?? ownerDocument.viewport?.controller.isDarkMode ?? false; + } + @override Node appendChild(Node child) { Node ret = super.appendChild(child); diff --git a/webf/lib/src/launcher/controller.dart b/webf/lib/src/launcher/controller.dart index 1f43e28145..2fa8919a28 100644 --- a/webf/lib/src/launcher/controller.dart +++ b/webf/lib/src/launcher/controller.dart @@ -903,6 +903,7 @@ class WebFController { final ui.FlutterView ownerFlutterView; bool resizeToAvoidBottomInsets; + bool? isDarkMode = false; String? _name; @@ -939,6 +940,8 @@ class WebFController { // The kraken view entrypoint bundle. WebFBundle? _entrypoint; WebFBundle? get entrypoint => _entrypoint; + ui.Size? _viewportSize; + ui.Size? get viewportSize => _viewportSize; final WebFThread runningThread; @@ -950,11 +953,7 @@ class WebFController { WebFController(BuildContext context, { String? name, - double? viewportWidth, - double? viewportHeight, - bool showPerformanceOverlay = false, bool enableDebug = false, - bool autoExecuteEntrypoint = true, Color? background, GestureListener? gestureListener, WebFNavigationDelegate? navigationDelegate, @@ -965,6 +964,7 @@ class WebFController { this.onCustomElementDetached, this.onLoad, this.onDOMContentLoaded, + this.isDarkMode, this.onLoadError, this.onJSError, this.httpClientInterceptor, @@ -1229,6 +1229,7 @@ class WebFController { if (_preloadStatus != PreloadingStatus.none) return; if (_preRenderingStatus != PreRenderingStatus.none) return; + _viewportSize = viewportSize; // Update entrypoint. _entrypoint = bundle; _replaceCurrentHistory(bundle); @@ -1245,6 +1246,8 @@ class WebFController { WebFProfiler.instance.startTrackUICommand(); } + view.document.preloadDarkMode = isDarkMode; + view.document.preloadViewportSize = _viewportSize; // Manually initialize the root element and create renderObjects for each elements. view.document.documentElement!.applyStyle(view.document.documentElement!.style); view.document.documentElement!.createRenderer(); diff --git a/webf/lib/src/rendering/viewport.dart b/webf/lib/src/rendering/viewport.dart index ec7a976eda..698f75b3ba 100644 --- a/webf/lib/src/rendering/viewport.dart +++ b/webf/lib/src/rendering/viewport.dart @@ -23,6 +23,10 @@ class RenderViewportBox extends RenderBox WebFController controller; + bool get isDarkMode { + return controller.isDarkMode ?? false; + } + @override void setupParentData(covariant RenderObject child) { child.parentData = RenderViewportParentData(); diff --git a/webf/lib/src/widget/webf.dart b/webf/lib/src/widget/webf.dart index fccda8b4b1..b6203aaa4a 100644 --- a/webf/lib/src/widget/webf.dart +++ b/webf/lib/src/widget/webf.dart @@ -23,10 +23,10 @@ class WebF extends StatefulWidget { final Color? background; /// the width of webFWidget - final double? viewportWidth; + double? viewportWidth; /// the height of webFWidget - final double? viewportHeight; + double? viewportHeight; /// The initial bundle to load. final WebFBundle? bundle; @@ -98,6 +98,8 @@ class WebF extends StatefulWidget { /// Use this property to reduce loading times when a WebF application attempts to load external resources on pages. final List? preloadedBundles; + bool? isDarkMode = false; + /// The initial cookies to set. final List? initialCookies; @@ -170,6 +172,7 @@ class WebF extends StatefulWidget { WebFThread? runningThread, this.routeObserver, this.initialCookies, + this.isDarkMode, this.preloadedBundles, WebFController? controller, // webf's viewportWidth options only works fine when viewportWidth is equal to window.physicalSize.width / window.devicePixelRatio. @@ -377,32 +380,36 @@ class WebFRootRenderObjectWidget extends MultiChildRenderObjectWidget { @override RenderObject createRenderObject(BuildContext context) { - WebFController controller = _webfWidget.controller ?? - WebFController(context, - name: shortHash(_webfWidget), - viewportWidth: _webfWidget.viewportWidth, - viewportHeight: _webfWidget.viewportHeight, - background: _webfWidget.background, - bundle: _webfWidget.bundle, - // Execute entrypoint when mount manually. - autoExecuteEntrypoint: false, - externalController: false, - onLoad: _webfWidget.onLoad, - onDOMContentLoaded: _webfWidget.onDOMContentLoaded, - onLoadError: _webfWidget.onLoadError, - onJSError: _webfWidget.onJSError, - runningThread: _webfWidget.runningThread, - methodChannel: _webfWidget.javaScriptChannel, - gestureListener: _webfWidget.gestureListener, - navigationDelegate: _webfWidget.navigationDelegate, - devToolsService: _webfWidget.devToolsService, - httpClientInterceptor: _webfWidget.httpClientInterceptor, - onCustomElementAttached: onCustomElementAttached, - onCustomElementDetached: onCustomElementDetached, - initialCookies: _webfWidget.initialCookies, - uriParser: _webfWidget.uriParser, - preloadedBundles: _webfWidget.preloadedBundles, - resizeToAvoidBottomInsets: resizeToAvoidBottomInsets); + WebFController? controller = _webfWidget.controller; + if (controller != null) { + _webfWidget.viewportWidth ??= controller.viewportSize?.width; + _webfWidget.viewportHeight ??= controller.viewportSize?.height; + _webfWidget.isDarkMode ??= controller.isDarkMode; + } else { + controller = WebFController(context, + name: shortHash(_webfWidget), + isDarkMode: _webfWidget.isDarkMode, + background: _webfWidget.background, + bundle: _webfWidget.bundle, + // Execute entrypoint when mount manually. + externalController: false, + onLoad: _webfWidget.onLoad, + onDOMContentLoaded: _webfWidget.onDOMContentLoaded, + onLoadError: _webfWidget.onLoadError, + onJSError: _webfWidget.onJSError, + runningThread: _webfWidget.runningThread, + methodChannel: _webfWidget.javaScriptChannel, + gestureListener: _webfWidget.gestureListener, + navigationDelegate: _webfWidget.navigationDelegate, + devToolsService: _webfWidget.devToolsService, + httpClientInterceptor: _webfWidget.httpClientInterceptor, + onCustomElementAttached: onCustomElementAttached, + onCustomElementDetached: onCustomElementDetached, + initialCookies: _webfWidget.initialCookies, + uriParser: _webfWidget.uriParser, + preloadedBundles: _webfWidget.preloadedBundles, + resizeToAvoidBottomInsets: resizeToAvoidBottomInsets); + } (context as _WebFRenderObjectElement).controller = controller; @@ -412,7 +419,7 @@ class WebFRootRenderObjectWidget extends MultiChildRenderObjectWidget { OnControllerCreated? onControllerCreated = _webfWidget.onControllerCreated; if (onControllerCreated != null) { controller.controlledInitCompleter.future.then((_) { - onControllerCreated(controller); + onControllerCreated(controller!); }); }