summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorMica White <botahamec@outlook.com>2025-12-08 19:54:36 -0500
committerMica White <botahamec@outlook.com>2025-12-08 19:54:36 -0500
commit3fad3812e117c6bc16b5007076803f498538e4c4 (patch)
tree197b7b27c3f1c1d2b8396e4c4150b37b4d3127e5 /lib
First commitHEADmain
Diffstat (limited to 'lib')
-rwxr-xr-xlib/console.dart157
-rwxr-xr-xlib/logs.dart242
-rwxr-xr-xlib/main.dart254
-rwxr-xr-xlib/profiler.dart76
-rwxr-xr-xlib/project.dart174
-rwxr-xr-xlib/serializer.dart93
-rwxr-xr-xlib/settings.dart23
7 files changed, 1019 insertions, 0 deletions
diff --git a/lib/console.dart b/lib/console.dart
new file mode 100755
index 0000000..e5bbc8b
--- /dev/null
+++ b/lib/console.dart
@@ -0,0 +1,157 @@
+import 'package:flutter/material.dart';
+
+class ConsoleEntry {
+ final String text;
+ final bool generatedByRuntime;
+ final DateTime timeGenerated;
+
+ ConsoleEntry({
+ required this.text,
+ required this.generatedByRuntime,
+ required this.timeGenerated,
+ });
+}
+
+class _ConsoleFilter extends StatelessWidget {
+ const _ConsoleFilter(this.filter, {required this.onChanged});
+
+ final String filter;
+ final Function(String filter) onChanged;
+
+ @override
+ Widget build(BuildContext context) {
+ return Row(
+ children: [
+ Expanded(
+ child: Padding(
+ padding: const EdgeInsets.only(bottom: 3),
+ child: TextFormField(
+ autocorrect: false,
+ enableSuggestions: false,
+ onChanged: onChanged,
+ style: const TextStyle(fontSize: 12),
+ decoration: const InputDecoration(
+ contentPadding: EdgeInsets.all(6),
+ labelText: "Filter: ",
+ ),
+ ),
+ ),
+ ),
+ const SizedBox(width: 16),
+ const Text("Console entries are only kept for several seconds"),
+ ],
+ );
+ }
+}
+
+class _ConsoleLine extends StatelessWidget {
+ const _ConsoleLine(
+ this.text, {
+ required this.runtime,
+ required this.time,
+ });
+
+ final String text;
+ final bool runtime;
+ final DateTime time;
+
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(vertical: 3),
+ child: DecoratedBox(
+ decoration: BoxDecoration(
+ color: this.runtime
+ ? Colors.blue.shade900.withAlpha(128)
+ : Colors.purple.shade900.withAlpha(128),
+ borderRadius: const BorderRadius.all(Radius.circular(4)),
+ border: Border.all(
+ color: this.runtime ? Colors.blue.shade700 : Colors.purple.shade700,
+ width: 3,
+ )),
+ child: Padding(
+ padding: const EdgeInsets.all(6),
+ child: Row(
+ children: [
+ Text(this.runtime ? "Runtime" : "Editor"),
+ Expanded(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16),
+ child: Text(
+ this.text,
+ style: const TextStyle(fontFamily: 'Consolas'),
+ ),
+ ),
+ ),
+ Text(
+ "${this.time.hour.toString().padLeft(2, '0')}:${this.time.minute.toString().padLeft(2, '0')}")
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+class ConsolePage extends StatefulWidget {
+ final List<ConsoleEntry> entries;
+ final void Function(String) messageFn;
+
+ const ConsolePage(this.entries, this.messageFn, {super.key});
+
+ @override
+ State<ConsolePage> createState() => _ConsolePageState();
+}
+
+class _ConsolePageState extends State<ConsolePage> {
+ String filter = "";
+
+ @override
+ Widget build(BuildContext context) {
+ final controller = TextEditingController();
+ final filteredMessages =
+ this.widget.entries.where((e) => e.text.contains(this.filter)).toList();
+
+ return Center(
+ child: Padding(
+ padding: const EdgeInsets.all(20),
+ child: Column(children: [
+ _ConsoleFilter(
+ filter,
+ onChanged: (newFilter) => this.setState(() => this.filter = newFilter),
+ ),
+ Expanded(
+ child: ListView.builder(
+ reverse: true,
+ itemCount: filteredMessages.length,
+ prototypeItem: _ConsoleLine(
+ "Loading...",
+ runtime: false,
+ time: DateTime(1970),
+ ),
+ itemBuilder: (context, index) {
+ final entry = filteredMessages[filteredMessages.length - index - 1];
+ return _ConsoleLine(
+ entry.text,
+ runtime: entry.generatedByRuntime,
+ time: entry.timeGenerated,
+ );
+ },
+ ),
+ ),
+ TextField(
+ autocorrect: false,
+ enableSuggestions: false,
+ style: const TextStyle(fontFamily: 'Consolas'),
+ controller: controller,
+ decoration: const InputDecoration(hintText: "Send a console message to the runtime"),
+ onSubmitted: (message) {
+ this.widget.messageFn(message);
+ controller.clear();
+ },
+ ),
+ ]),
+ ),
+ );
+ }
+}
diff --git a/lib/logs.dart b/lib/logs.dart
new file mode 100755
index 0000000..78f43c2
--- /dev/null
+++ b/lib/logs.dart
@@ -0,0 +1,242 @@
+import 'package:flutter/material.dart';
+import 'package:multiselect/multiselect.dart';
+
+enum LogType {
+ error,
+ warning,
+ info,
+ debug,
+ trace,
+}
+
+extension LogTypeExtension on LogType {
+ static LogType parse(String logType) {
+ final logTypeS = logType.toLowerCase().trim();
+ switch (logTypeS) {
+ case 'error':
+ return LogType.error;
+ case 'warning':
+ return LogType.warning;
+ case 'info':
+ return LogType.info;
+ case 'debug':
+ return LogType.debug;
+ case 'trace':
+ return LogType.trace;
+ default:
+ // there's no particular reason why this should be the default
+ // but hopefully this line is never needed
+ return LogType.error;
+ }
+ }
+
+ String name() {
+ switch (this) {
+ case LogType.error:
+ return "Error";
+ case LogType.warning:
+ return "Warning";
+ case LogType.info:
+ return "Info";
+ case LogType.debug:
+ return "Debug";
+ case LogType.trace:
+ return "Trace";
+ }
+ }
+}
+
+class LogEntry {
+ final bool scriptLog;
+ final LogType logType;
+ final String? file;
+ final int? lineNumber;
+ final String message;
+ final DateTime time;
+
+ LogEntry(
+ this.scriptLog,
+ this.logType,
+ this.file,
+ this.lineNumber,
+ this.message,
+ ) : this.time = DateTime.now();
+}
+
+class _LogFilter extends StatelessWidget {
+ const _LogFilter(this.label, this.onChanged, this.selectedValues);
+
+ final String label;
+ final Function(List<String>) onChanged;
+ final List<LogType> selectedValues;
+
+ @override
+ Widget build(BuildContext context) {
+ return Row(
+ children: [
+ Text(
+ this.label,
+ style: const TextStyle(fontSize: 16),
+ ),
+ const SizedBox(width: 10),
+ SizedBox(
+ width: 200,
+ height: 40,
+ child: DropDownMultiSelect(
+ options: LogType.values.map((e) => e.name()).toList(),
+ selectedValues: this.selectedValues.map((e) => e.name()).toList(),
+ whenEmpty: "Filter Script Logs",
+ isDense: true,
+ onChanged: this.onChanged,
+ ),
+ ),
+ ],
+ );
+ }
+}
+
+class _LogLine extends StatelessWidget {
+ const _LogLine(this.entry);
+
+ final LogEntry entry;
+
+ @override
+ Widget build(BuildContext context) {
+ Map<LogType, Widget> icons = {
+ LogType.error: Icon(
+ Icons.error,
+ color: Colors.red[300],
+ size: 24,
+ ),
+ LogType.warning: Icon(
+ Icons.warning,
+ color: Colors.yellow[300],
+ size: 24,
+ ),
+ LogType.info: Icon(
+ Icons.info,
+ color: Colors.blue[300],
+ size: 24,
+ ),
+ LogType.debug: Icon(
+ Icons.bug_report,
+ color: Colors.green[300],
+ size: 24,
+ ),
+ LogType.trace: Icon(
+ Icons.message,
+ color: Colors.grey[300],
+ size: 24,
+ ),
+ };
+
+ return Padding(
+ padding: const EdgeInsets.symmetric(vertical: 3),
+ child: DecoratedBox(
+ decoration: BoxDecoration(
+ color: Colors.purple.shade900.withAlpha(128),
+ border: Border.all(color: Colors.purple.shade800, width: 3),
+ borderRadius: const BorderRadius.all(Radius.circular(4)),
+ ),
+ child: Padding(
+ padding: const EdgeInsets.all(6),
+ child: Row(
+ children: [
+ icons[this.entry.logType]!,
+ const SizedBox(width: 5),
+ SizedBox(width: 70, child: Text(this.entry.logType.name())),
+ SizedBox(width: 54, child: Text(this.entry.scriptLog ? 'Script' : 'Runtime')),
+ const SizedBox(width: 20),
+ Expanded(
+ child: Text(
+ this.entry.message,
+ style: const TextStyle(fontFamily: 'Consolas'),
+ ),
+ ),
+ const SizedBox(width: 20),
+ Text("${this.entry.file}:${this.entry.lineNumber}"),
+ const SizedBox(width: 10),
+ Text(
+ "${this.entry.time.hour.toString().padLeft(2, '0')}:${this.entry.time.minute.toString().padLeft(2, '0')}")
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+class LogPage extends StatefulWidget {
+ const LogPage(this.logEntries, {super.key});
+
+ final List<LogEntry> logEntries;
+
+ @override
+ State<StatefulWidget> createState() => _LogPageState();
+}
+
+class _LogPageState extends State<LogPage> {
+ Set<LogType> runtimeLogsFilter = {LogType.error, LogType.warning};
+ Set<LogType> scriptLogsFilter = Set.from(LogType.values);
+ String searchFilter = '';
+
+ Function(List<String>) _generateOnFilter(Set<LogType> filter) {
+ return (values) {
+ setState(() {
+ filter.clear();
+ filter.addAll(values.map((e) => LogTypeExtension.parse(e)));
+ });
+ };
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Center(
+ child: Padding(
+ padding: const EdgeInsets.all(20),
+ child: Column(children: [
+ Row(children: [
+ Expanded(
+ child: SizedBox(
+ height: 40,
+ child: TextField(
+ autocorrect: false,
+ onChanged: (search) => setState(() => this.searchFilter = search),
+ decoration:
+ const InputDecoration(border: OutlineInputBorder(), labelText: 'Search'),
+ ),
+ ),
+ ),
+ const SizedBox(width: 80),
+ _LogFilter(
+ "Runtime Logs Filter:",
+ _generateOnFilter(this.runtimeLogsFilter),
+ runtimeLogsFilter.toList(),
+ ),
+ const SizedBox(width: 80),
+ _LogFilter(
+ "Script Logs Filter:",
+ _generateOnFilter(this.scriptLogsFilter),
+ scriptLogsFilter.toList(),
+ ),
+ ]),
+ Expanded(
+ child: SingleChildScrollView(
+ reverse: true,
+ child: Column(
+ children: this
+ .widget
+ .logEntries
+ .where((e) =>
+ ((e.scriptLog && this.scriptLogsFilter.contains(e.logType)) ||
+ (!e.scriptLog && this.runtimeLogsFilter.contains(e.logType))) &&
+ (this.searchFilter.isEmpty || e.message.contains(this.searchFilter)))
+ .map((e) => _LogLine(e))
+ .toList()),
+ ),
+ ),
+ ]),
+ ),
+ );
+ }
+}
diff --git a/lib/main.dart b/lib/main.dart
new file mode 100755
index 0000000..270eb34
--- /dev/null
+++ b/lib/main.dart
@@ -0,0 +1,254 @@
+import 'dart:io';
+
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+
+import 'console.dart';
+import 'logs.dart';
+import 'project.dart';
+import 'settings.dart';
+import 'profiler.dart';
+import 'serializer.dart';
+
+const maxConsoleEntries = 15000;
+const maxProfileFrames = 15000;
+
+void main() {
+ runApp(MultiProvider(
+ providers: [
+ ChangeNotifierProvider(create: (context) => ProjectConfig()),
+ ],
+ child: const AlligatorApp(),
+ ));
+}
+
+class AlligatorApp extends StatelessWidget {
+ const AlligatorApp({super.key});
+
+ // This widget is the root of your application.
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ title: 'Alligator Editor',
+ locale: const Locale('en', 'US'),
+ theme: ThemeData(
+ colorScheme: ColorScheme.fromSeed(
+ brightness: Brightness.dark,
+ seedColor: Colors.green,
+ ),
+ useMaterial3: true,
+ ),
+ home: const MyHomePage(),
+ );
+ }
+}
+
+class RunButtons extends StatelessWidget {
+ final bool isRunning;
+ final Function(BuildContext) onStart;
+ final Function(BuildContext) onStop;
+
+ const RunButtons(this.isRunning, {required this.onStart, required this.onStop, super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ if (!this.isRunning) {
+ return TextButton.icon(
+ onPressed: () => this.onStart(context),
+ icon: const Icon(Icons.play_arrow),
+ label: const Text('Run'),
+ style: TextButton.styleFrom(
+ shape: RoundedRectangleBorder(
+ borderRadius: const BorderRadius.all(Radius.circular(8)),
+ side: BorderSide(
+ color: Theme.of(context).colorScheme.primary,
+ width: 3,
+ ),
+ ),
+ ),
+ );
+ } else {
+ return TextButton.icon(
+ onPressed: () => this.onStop(context),
+ icon: const Icon(Icons.stop),
+ label: const Text('Stop'),
+ style: TextButton.styleFrom(
+ foregroundColor: Theme.of(context).colorScheme.error,
+ shape: RoundedRectangleBorder(
+ borderRadius: const BorderRadius.all(Radius.circular(8)),
+ side: BorderSide(
+ color: Theme.of(context).colorScheme.error,
+ width: 3,
+ ),
+ ),
+ ),
+ );
+ }
+ }
+}
+
+class MyHomePage extends StatefulWidget {
+ const MyHomePage({super.key});
+
+ @override
+ State<MyHomePage> createState() => _MyHomePageState();
+}
+
+class _MyHomePageState extends State<MyHomePage> {
+ final List<ConsoleEntry> _consoleEntries = [];
+
+ static const List<Tab> tabs = <Tab>[
+ Tab(text: 'Project'),
+ Tab(text: 'Log'),
+ Tab(text: 'Profiler'),
+ Tab(text: 'Console'),
+ Tab(text: 'Settings'),
+ ];
+
+ Process? _runningGame;
+ final StringBuffer _buffer = StringBuffer();
+
+ final List<LogEntry> _logEntries = [];
+
+ DateTime? _previousFrameTime;
+ int _nextFrameId = 0;
+ final List<ProfileFrame> _frames = [];
+
+ void _sendMessage(String msg) {
+ this._runningGame?.stdin.writeln(msg);
+ setState(() {
+ _consoleEntries.add(ConsoleEntry(
+ text: msg,
+ generatedByRuntime: false,
+ timeGenerated: DateTime.now(),
+ ));
+ });
+ }
+
+ void _parseMessage(String message) {
+ final args = message.split(' ');
+ if (args[0] == 'runtimelog') {
+ final logType = LogTypeExtension.parse(args[1]);
+ final [fileName, lineNumberS] = args[2].split(':');
+ final lineNumber = int.parse(lineNumberS);
+ final logMsg = args.sublist(3).join(' ');
+ setState(() => this._logEntries.add(LogEntry(false, logType, fileName, lineNumber, logMsg)));
+ } else if (args[0] == 'scriptlog') {
+ final logType = LogTypeExtension.parse(args[1]);
+ final [fileName, lineNumberS] = args[2].split(':');
+ final lineNumber = int.parse(lineNumberS);
+ final logMsg = args.sublist(3).join(' ');
+ setState(() => this._logEntries.add(LogEntry(true, logType, fileName, lineNumber, logMsg)));
+ } else if (args[0] == 'frametime') {
+ if (_previousFrameTime == null) {
+ _previousFrameTime = DateTime.fromMicrosecondsSinceEpoch(int.parse(args[1]));
+ return;
+ }
+
+ final id = _nextFrameId++;
+ final start = _previousFrameTime!;
+ final end = DateTime.fromMicrosecondsSinceEpoch(int.parse(args[1]));
+ _previousFrameTime = end;
+
+ setState(() {
+ _frames.add(ProfileFrame(id, start, end, []));
+ if (_frames.length >= maxProfileFrames) {
+ _frames.removeRange(0, _frames.length - maxProfileFrames);
+ }
+ });
+ }
+ }
+
+ void _startGame(BuildContext cx) async {
+ ProjectConfig projectConfig = Provider.of(cx, listen: false);
+ final gameConfig = AlligatorGame.fromConfig(projectConfig: projectConfig).toJson();
+
+ this._runningGame?.kill();
+
+ this._buffer.clear();
+ _consoleEntries.clear();
+ this._logEntries.clear();
+ this._previousFrameTime = null;
+ this._nextFrameId = 0;
+ this._frames.clear();
+
+ this._runningGame = await Process.start("alligator", ["--config", gameConfig, "--debug"]);
+ this._runningGame!.exitCode.then((value) => this._runningGame = null);
+ this._runningGame!.stdout.listen(
+ (event) async {
+ await Future.delayed(const Duration(milliseconds: 16));
+ for (final code in event) {
+ final char = String.fromCharCode(code);
+
+ if (char == "\n") {
+ final message = this._buffer.toString();
+ setState(
+ () => _consoleEntries.add(ConsoleEntry(
+ text: message,
+ generatedByRuntime: true,
+ timeGenerated: DateTime.now(),
+ )),
+ );
+
+ this._buffer.clear();
+ _parseMessage(message);
+ } else {
+ this._buffer.write(char);
+ }
+
+ setState(() {
+ if (_consoleEntries.length > maxConsoleEntries) {
+ _consoleEntries.removeRange(0, _consoleEntries.length - maxConsoleEntries);
+ }
+ });
+ }
+ },
+ onDone: () {
+ this.setState(() => this._runningGame = null);
+ },
+ );
+ }
+
+ @override
+ void initState() {
+ super.initState();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ // This method is rerun every time setState is called, for instance as done
+ // by the _incrementCounter method above.
+ //
+ // The Flutter framework has been optimized to make rerunning build methods
+ // fast, so that you can just rebuild anything that needs updating rather
+ // than having to individually change instances of widgets.
+ return DefaultTabController(
+ length: tabs.length,
+ child: Scaffold(
+ appBar: AppBar(
+ title: const Text("Alligator Editor"),
+ bottom: const TabBar(
+ tabs: tabs,
+ ),
+ actions: [
+ RunButtons(
+ this._runningGame != null,
+ onStart: this._startGame,
+ onStop: (_) => this._runningGame!.kill(),
+ ),
+ const SizedBox(width: 20),
+ ],
+ ),
+ body: TabBarView(
+ children: [
+ const ProjectPage(),
+ LogPage(this._logEntries),
+ ProfilerPage(_frames),
+ ConsolePage(_consoleEntries, this._sendMessage),
+ const SettingsPage(),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/profiler.dart b/lib/profiler.dart
new file mode 100755
index 0000000..495dc48
--- /dev/null
+++ b/lib/profiler.dart
@@ -0,0 +1,76 @@
+import 'package:flutter/material.dart';
+
+class ProfileScope {
+ final String name;
+ final DateTime start;
+ final DateTime end;
+
+ const ProfileScope(this.name, this.start, this.end);
+}
+
+class ProfileFrame {
+ final int id;
+ final DateTime start;
+ final DateTime end;
+ final List<ProfileScope> scopes;
+
+ const ProfileFrame(this.id, this.start, this.end, this.scopes);
+}
+
+extension DurationExt on Duration {
+ double get toSeconds => this.inMicroseconds.toDouble() / 1000000.0;
+}
+
+class ProfilerPage extends StatelessWidget {
+ final List<ProfileFrame> frames;
+
+ const ProfilerPage(this.frames, {super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ if (frames.isEmpty) {
+ return const Center(
+ child: Text(
+ "Run the project to see profiling information",
+ style: TextStyle(fontSize: 36),
+ ),
+ );
+ }
+
+ final frameTimes = this.frames.map((frame) => frame.end.difference(frame.start)).toList();
+ final totalFrameTime = frameTimes.reduce((a, b) => a + b);
+ final averageFrameTime = totalFrameTime.inMicroseconds / this.frames.length;
+ final averageFps = this.frames.length / totalFrameTime.toSeconds;
+
+ Duration totalFrameTimeForPreviousSecond = const Duration();
+ final List<Duration> frameTimesForPreviousSecond = [];
+ for (int i = frameTimes.length - 1; i >= 0; i--) {
+ final currentFrameTime = frameTimes[i];
+ totalFrameTimeForPreviousSecond += currentFrameTime;
+ frameTimesForPreviousSecond.add(currentFrameTime);
+
+ if (totalFrameTimeForPreviousSecond.toSeconds >= 1.0) {
+ break;
+ }
+ }
+ frameTimesForPreviousSecond.sort((a, b) => b.compareTo(a));
+ final fpsLow = 1 /
+ frameTimesForPreviousSecond[(frameTimesForPreviousSecond.length / 20).floor()].toSeconds;
+
+ return Padding(
+ padding: const EdgeInsets.all(20),
+ child: Column(
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text("Average Frame Time: ${averageFrameTime.round()} μs"),
+ Text("Average FPS: ${averageFps.round()} fps"),
+ Text("Low 5% FPS: ${fpsLow.round()}"),
+ ],
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/project.dart b/lib/project.dart
new file mode 100755
index 0000000..1f9b0ad
--- /dev/null
+++ b/lib/project.dart
@@ -0,0 +1,174 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:provider/provider.dart';
+
+enum WindowMode {
+ windowed,
+ borderlessFullscreen;
+
+ String get displayString {
+ switch (this) {
+ case WindowMode.windowed:
+ return "Windowed";
+ case WindowMode.borderlessFullscreen:
+ return "Borderless Fullscreen";
+ }
+ }
+
+ String get pascalCase {
+ switch (this) {
+ case WindowMode.windowed:
+ return "Windowed";
+ case WindowMode.borderlessFullscreen:
+ return "BorderlessFullscreen";
+ }
+ }
+
+ static WindowMode fromPascalCase(String pascalCase) {
+ return WindowMode.values.firstWhere((mode) => mode.pascalCase == pascalCase);
+ }
+}
+
+class ProjectConfig extends ChangeNotifier {
+ final TextEditingController _gameName;
+ final TextEditingController _defaultWindowWidth;
+ final TextEditingController _defaultWindowHeight;
+
+ bool _defaultVsync;
+ WindowMode _defaultWindowMode;
+
+ String get gameName => this._gameName.text;
+ set gameName(String gameName) {
+ this._gameName.text = gameName;
+ }
+
+ int get defaultWindowWidth => int.parse(this._defaultWindowWidth.text);
+
+ set defaultWindowWidth(int defaultWindowWidth) {
+ this._defaultWindowWidth.text = defaultWindowWidth.toString();
+ }
+
+ int get defaultWindowHeight => int.parse(this._defaultWindowHeight.text);
+
+ set defaultWindowHeight(int? defaultWindowHeight) {
+ this._defaultWindowHeight.text = defaultWindowHeight.toString();
+ }
+
+ bool get defaultVsync => this._defaultVsync;
+ set defaultVsync(bool defaultVsync) {
+ this._defaultVsync = defaultVsync;
+ notifyListeners();
+ }
+
+ WindowMode get defaultWindowMode => this._defaultWindowMode;
+ set defaultWindowMode(WindowMode defaultWindowMode) {
+ this._defaultWindowMode = defaultWindowMode;
+ notifyListeners();
+ }
+
+ ProjectConfig({
+ String gameName = "Black Screen",
+ int defaultWindowWidth = 1280,
+ int defaultWindowHeight = 720,
+ bool defaultVsync = true,
+ WindowMode defaultWindowMode = WindowMode.windowed,
+ }) : this._gameName = TextEditingController(text: gameName),
+ this._defaultWindowWidth = TextEditingController(text: defaultWindowWidth.toString()),
+ this._defaultWindowHeight = TextEditingController(text: defaultWindowHeight.toString()),
+ this._defaultVsync = defaultVsync,
+ this._defaultWindowMode = defaultWindowMode;
+}
+
+class ProjectPage extends StatelessWidget {
+ const ProjectPage({super.key});
+
+ int getWidth(ProjectConfig config) => int.tryParse(config._defaultWindowWidth.text) ?? 1280;
+ int getHeight(ProjectConfig config) => int.tryParse(config._defaultWindowHeight.text) ?? 720;
+
+ @override
+ Widget build(BuildContext context) {
+ var config = Provider.of<ProjectConfig>(context, listen: false);
+
+ return Padding(
+ padding: const EdgeInsets.all(20),
+ child: Form(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ SizedBox(
+ width: 600,
+ child: TextFormField(
+ decoration: const InputDecoration(
+ labelText: "Project Name",
+ //helperText: "This will appear as the game window's title",
+ border: OutlineInputBorder(),
+ ),
+ controller: config._gameName,
+ ),
+ ),
+ const SizedBox(height: 20),
+ DropdownMenu(
+ width: 600,
+ label: const Text("Default Window Mode"),
+ initialSelection: config.defaultWindowMode,
+ onSelected: (v) => config.defaultWindowMode = v!,
+ dropdownMenuEntries: WindowMode.values
+ .map(((mode) => DropdownMenuEntry(value: mode, label: mode.displayString)))
+ .toList(),
+ ),
+ const SizedBox(height: 20),
+ Consumer<ProjectConfig>(
+ builder: (context, config, child) => Row(
+ children: [
+ SizedBox(
+ width: 300,
+ child: TextFormField(
+ decoration: const InputDecoration(
+ labelText: "Default Resolution Width",
+ border: OutlineInputBorder(),
+ ),
+ controller: config._defaultWindowWidth,
+ keyboardType: TextInputType.number,
+ inputFormatters: [FilteringTextInputFormatter.digitsOnly],
+ ),
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 20),
+ Consumer<ProjectConfig>(
+ builder: (context, config, child) => Row(
+ children: [
+ SizedBox(
+ width: 300,
+ child: TextFormField(
+ decoration: const InputDecoration(
+ labelText: "Default Resolution Height",
+ border: OutlineInputBorder(),
+ ),
+ controller: config._defaultWindowHeight,
+ keyboardType: TextInputType.number,
+ inputFormatters: [FilteringTextInputFormatter.digitsOnly],
+ ),
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 20),
+ SizedBox(
+ width: 250,
+ child: Consumer<ProjectConfig>(
+ builder: (context, config, child) => CheckboxListTile(
+ value: config.defaultVsync,
+ onChanged: (v) => config.defaultVsync = v!,
+ title: const Text("Default Vsync", style: TextStyle(fontSize: 20)),
+ controlAffinity: ListTileControlAffinity.leading,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/serializer.dart b/lib/serializer.dart
new file mode 100755
index 0000000..d9d5dbe
--- /dev/null
+++ b/lib/serializer.dart
@@ -0,0 +1,93 @@
+import 'dart:convert';
+
+import 'package:alligator_editor/project.dart';
+
+const currentAlligatorVersion = "0.1";
+const currentRuntimeVersion = "0.1";
+
+class AlligatorProject {
+ String version;
+ ProjectConfig projectConfig;
+
+ AlligatorProject({
+ this.version = currentAlligatorVersion,
+ required this.projectConfig,
+ });
+
+ factory AlligatorProject.fromJson(String json) {
+ Map<String, dynamic> jsonMap = jsonDecode(json);
+ String version = jsonMap['version'];
+ ProjectConfig projectConfig = ProjectConfig(
+ gameName: jsonMap['project']['name'],
+ defaultWindowWidth: jsonMap['project']['defaultWidth'],
+ defaultWindowHeight: jsonMap['project']['defaultHeight'],
+ defaultWindowMode: WindowMode.fromPascalCase(jsonMap['project']['defaultWindowMode']),
+ defaultVsync: jsonMap['project']['vsync'],
+ );
+
+ return AlligatorProject(version: version, projectConfig: projectConfig);
+ }
+
+ String toJson() {
+ Map<String, dynamic> json = {
+ 'version': this.version,
+ 'project': {
+ 'name': this.projectConfig.gameName,
+ 'defaultWidth': this.projectConfig.defaultWindowWidth,
+ 'defaultHeight': this.projectConfig.defaultWindowHeight,
+ 'defaultWindowMode': this.projectConfig.defaultWindowMode,
+ 'vsync': this.projectConfig.defaultVsync,
+ }
+ };
+
+ return jsonEncode(json);
+ }
+}
+
+class AlligatorGame {
+ final String alligatorVersion;
+ final int? defaultWindowWidth;
+ final int? defaultWindowHeight;
+ final WindowMode defaultWindowMode;
+ final String windowTitle;
+ final bool vsync;
+
+ const AlligatorGame({
+ this.alligatorVersion = currentRuntimeVersion,
+ this.defaultWindowWidth = 1280,
+ this.defaultWindowHeight = 720,
+ this.defaultWindowMode = WindowMode.windowed,
+ this.windowTitle = "Alligator Game",
+ this.vsync = true,
+ });
+
+ AlligatorGame.fromConfig({
+ String alligatorVersion = currentRuntimeVersion,
+ required ProjectConfig projectConfig,
+ }) : this(
+ alligatorVersion: alligatorVersion,
+ defaultWindowWidth: projectConfig.defaultWindowWidth,
+ defaultWindowHeight: projectConfig.defaultWindowHeight,
+ defaultWindowMode: projectConfig.defaultWindowMode,
+ windowTitle: projectConfig.gameName,
+ vsync: projectConfig.defaultVsync,
+ );
+
+ String toJson() {
+ Map<String, dynamic> json = {
+ 'alligator_version': this.alligatorVersion,
+ 'scenes': {},
+ 'textures': {},
+ 'scripts': {},
+ 'default_scene': "",
+ 'sprite_manager_capacity': 0,
+ 'default_window_width': this.defaultWindowWidth,
+ 'default_window_height': this.defaultWindowHeight,
+ 'default_window_mode': this.defaultWindowMode.pascalCase,
+ 'window_title': this.windowTitle,
+ 'vsync': this.vsync
+ };
+
+ return jsonEncode(json);
+ }
+}
diff --git a/lib/settings.dart b/lib/settings.dart
new file mode 100755
index 0000000..b968502
--- /dev/null
+++ b/lib/settings.dart
@@ -0,0 +1,23 @@
+import 'package:flutter/material.dart';
+
+class Settings {
+ int tabPagePadding = 20;
+ double logRadius = 4;
+ double logBorderThickness = 2;
+ double logPadding = 6;
+ double logMessagePadding = 16;
+ String logMessageFont = 'Consolas';
+
+ Color consoleRuntimeEntry = Colors.blue.shade900;
+ Color consoleEditorEntry = Colors.purple.shade900;
+ Color consoleEntryBorderColor = Colors.white;
+}
+
+class SettingsPage extends StatelessWidget {
+ const SettingsPage({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Container();
+ }
+}