Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion webf/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import 'package:webf_bluetooth/webf_bluetooth.dart';
final RouteObserver<ModalRoute<void>> routeObserver = RouteObserver<ModalRoute<void>>();
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

const String demoEntryUrl = 'http://localhost:5173/';
const String demoEntryUrl = 'https://usecase.openwebf.com';
const String demoControllerName = 'demo';
const String demoInitialRoute = '/';
const Map<String, dynamic>? demoInitialState = null;
Expand Down
2 changes: 0 additions & 2 deletions webf/example/macos/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,6 @@
"${BUILT_PRODUCTS_DIR}/example_app/example_app.framework",
"${BUILT_PRODUCTS_DIR}/file_picker/file_picker.framework",
"${BUILT_PRODUCTS_DIR}/file_selector_macos/file_selector_macos.framework",
"${BUILT_PRODUCTS_DIR}/objective_c/objective_c.framework",
"${BUILT_PRODUCTS_DIR}/flutter_blue_plus_darwin/flutter_blue_plus_darwin.framework",
"${BUILT_PRODUCTS_DIR}/path_provider_foundation/path_provider_foundation.framework",
"${BUILT_PRODUCTS_DIR}/share_plus/share_plus.framework",
Expand All @@ -287,7 +286,6 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_picker.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_selector_macos.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_blue_plus_darwin.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/objective_c.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_foundation.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/share_plus.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences_foundation.framework",
Expand Down
156 changes: 144 additions & 12 deletions webf/lib/src/devtools/panel/inspector_panel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2191,24 +2191,156 @@ class _WebFInspectorBottomSheetState extends State<_WebFInspectorBottomSheet> wi
onPressed: () async {
// Get current route path if available
String? routePath = controller.currentBuildContext?.path;
final bool isDesktop = Platform.isMacOS || Platform.isWindows || Platform.isLinux;

showDialog(
context: context,
barrierDismissible: false,
builder: (context) => Center(
child: Container(
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.black87,
borderRadius: BorderRadius.circular(8),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(color: Colors.white),
SizedBox(height: 16),
Text(
'Dumping render tree...',
style: TextStyle(color: Colors.white),
),
],
),
),
),
);

RenderObjectTreeDumpResult? dumpResult;
Object? dumpError;
try {
dumpResult = await controller.dumpRenderObjectTree(
routePath,
writeToFile: isDesktop,
);
} catch (e) {
dumpError = e;
} finally {
if (context.mounted) {
Navigator.of(context, rootNavigator: true).pop();
}
}

// Print render object tree
await controller.printRenderObjectTree(routePath);
if (!context.mounted) return;

// Show feedback
final message = Platform.isMacOS
? 'Render object tree printed to console and saved to ~/Documents/WebF_Debug/'
: 'Render object tree printed to console';
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
duration: Duration(seconds: 3),
backgroundColor: Colors.blue,
if (dumpError != null) {
await showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Color(0xFF1E1E1E),
title: Text('Failed to dump render tree', style: TextStyle(color: Colors.white)),
content: Text(dumpError.toString(), style: TextStyle(color: Colors.white70)),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
style: TextButton.styleFrom(foregroundColor: Colors.white),
child: Text('OK', style: TextStyle(color: Colors.white)),
),
],
),
);
return;
}

if (dumpResult == null) {
await showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Color(0xFF1E1E1E),
title: Text('Render tree is empty', style: TextStyle(color: Colors.white)),
content: Text(
'No render object tree available for this route.',
style: TextStyle(color: Colors.white70),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
style: TextButton.styleFrom(foregroundColor: Colors.white),
child: Text('OK', style: TextStyle(color: Colors.white)),
),
],
),
);
return;
}

Object? clipboardError;
final String? savedFilePath = dumpResult.savedFilePath;
final bool shouldCopyFilePath = isDesktop && savedFilePath != null && savedFilePath.isNotEmpty;
try {
await Clipboard.setData(ClipboardData(text: shouldCopyFilePath ? savedFilePath : dumpResult.text));
} catch (e) {
clipboardError = e;
}

if (!context.mounted) return;

if (clipboardError != null) {
await showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Color(0xFF1E1E1E),
title: Text('Failed to copy to clipboard', style: TextStyle(color: Colors.white)),
content: Text(clipboardError.toString(), style: TextStyle(color: Colors.white70)),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
style: TextButton.styleFrom(foregroundColor: Colors.white),
child: Text('OK', style: TextStyle(color: Colors.white)),
),
],
),
);
return;
}

final summary = shouldCopyFilePath
? 'Render object tree saved to file.\n\nFile path copied to clipboard:\n$savedFilePath'
: 'Render object tree copied to clipboard.';

await showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Color(0xFF1E1E1E),
title: Text('Copied', style: TextStyle(color: Colors.white)),
content: Text(summary, style: TextStyle(color: Colors.white70)),
actions: [
if (shouldCopyFilePath)
TextButton(
onPressed: () async {
try {
await Clipboard.setData(ClipboardData(text: dumpResult!.text));
} finally {
if (context.mounted) {
Navigator.of(context).pop();
}
}
},
style: TextButton.styleFrom(foregroundColor: Colors.white),
child: Text('Copy Tree', style: TextStyle(color: Colors.white)),
),
TextButton(
onPressed: () => Navigator.of(context).pop(),
style: TextButton.styleFrom(foregroundColor: Colors.white),
child: Text('OK', style: TextStyle(color: Colors.white)),
),
],
),
);
},
tooltip: 'Print Render Object Tree',
tooltip: 'Copy Render Object Tree',
),
SizedBox(width: 8),
IconButton(
Expand Down
88 changes: 66 additions & 22 deletions webf/lib/src/launcher/controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import 'package:flutter/widgets.dart'
RouteObserver,
StatefulElement,
View;
import 'package:path_provider/path_provider.dart';
import 'package:webf/css.dart';
import 'package:dio/dio.dart' show Interceptor; // For custom Dio configuration
import 'package:webf/dom.dart';
Expand Down Expand Up @@ -113,6 +114,16 @@ class HybridRoutePageContext {
HybridRoutePageContext(this.path, this.context, this.state);
}

class RenderObjectTreeDumpResult {
final String text;
final String? savedFilePath;

const RenderObjectTreeDumpResult({
required this.text,
this.savedFilePath,
});
}

enum WebFLoadingMode {
/// This mode preloads remote resources into memory and begins execution when the WebF widget is mounted into the Flutter tree.
/// If the entrypoint is an HTML file, the HTML will be parsed, and its elements will be organized into a DOM tree.
Expand Down Expand Up @@ -460,47 +471,80 @@ class WebFController with Diagnosticable {
/// If null or matches initialRoute, prints the root render object tree.
/// Otherwise prints the render tree of the specified hybrid route view.
///
/// On macOS platform, this method also writes the render object tree to a file
/// in the user's Documents directory with a timestamp.
/// On desktop platforms (macOS/Windows/Linux), this method also writes the render object tree to a file
/// in the user's Documents directory (or app documents directory fallback) with a timestamp.
Future<void> printRenderObjectTree(String? routePath) async {
final result = await dumpRenderObjectTree(
routePath,
writeToFile: Platform.isMacOS || Platform.isWindows || Platform.isLinux,
printToConsole: true,
);
if (result == null) {
debugPrint('Render object tree is empty.');
return;
}
if (result.savedFilePath != null) {
debugPrint('Render object tree written to: ${result.savedFilePath}');
}
}

Future<RenderObjectTreeDumpResult?> dumpRenderObjectTree(
String? routePath, {
bool writeToFile = false,
bool printToConsole = false,
}) async {
String? renderObjectTreeString;

if (routePath == null || routePath == initialRoute) {
renderObjectTreeString = view.getRootRenderObject()?.toStringDeep();
} else {
RouterLinkElement? routeLinkElement = view.getHybridRouterView(routePath);
final RouterLinkElement? routeLinkElement = view.getHybridRouterView(routePath);
renderObjectTreeString = routeLinkElement?.getRenderObjectTree();
}

// On macOS, also write to file
if (Platform.isMacOS && renderObjectTreeString != null) {
try {
// Get the Documents directory
final documentsDir = Directory('${Platform.environment['HOME']}/Documents');
final webfDebugDir = Directory('${documentsDir.path}/WebF_Debug');
if (renderObjectTreeString == null || renderObjectTreeString.isEmpty) {
return null;
}

// Create WebF_Debug directory if it doesn't exist
if (!await webfDebugDir.exists()) {
await webfDebugDir.create();
String? savedFilePath;
if (writeToFile && (Platform.isMacOS || Platform.isWindows || Platform.isLinux)) {
try {
Directory? documentsDir;
if (Platform.isMacOS || Platform.isLinux) {
final String? home = Platform.environment['HOME'];
if (home != null && home.isNotEmpty) {
documentsDir = Directory('$home/Documents');
}
} else if (Platform.isWindows) {
final String? userProfile = Platform.environment['USERPROFILE'];
if (userProfile != null && userProfile.isNotEmpty) {
documentsDir = Directory('$userProfile\\Documents');
}
}

// Generate filename with timestamp and route info
final timestamp = DateTime.now().toIso8601String().replaceAll(':', '-').replaceAll('.', '-');
final routeInfo = routePath != null ? '_route_${routePath.replaceAll('/', '_')}' : '_root';
final filename = 'render_tree${routeInfo}_$timestamp.txt';
documentsDir ??= await getApplicationDocumentsDirectory();
final webfDebugDir = Directory('${documentsDir.path}${Platform.pathSeparator}WebF_Debug');
await webfDebugDir.create(recursive: true);

// Write to file
final file = File('${webfDebugDir.path}/$filename');
final timestamp = DateTime.now().toIso8601String().replaceAll(':', '-').replaceAll('.', '-');
final sanitizedRoute = (routePath ?? 'root').replaceAll(RegExp(r'[^a-zA-Z0-9_-]+'), '_');
final filename = 'render_tree_${sanitizedRoute}_$timestamp.txt';
final file = File('${webfDebugDir.path}${Platform.pathSeparator}$filename');
await file.writeAsString(renderObjectTreeString);

debugPrint('Render object tree written to: ${file.path}');
savedFilePath = file.path;
} catch (e) {
debugPrint('Failed to write render object tree to file: $e');
}
} else {
// Always print to console
}

if (printToConsole) {
debugPrint(renderObjectTreeString);
}

return RenderObjectTreeDumpResult(
text: renderObjectTreeString,
savedFilePath: savedFilePath,
);
}

/// Prints the render object tree for debugging purposes.
Expand Down
Loading