diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000..79c113f9b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,45 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.build/
+.buildlog/
+.history
+.svn/
+.swiftpm/
+migrate_working_dir/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+**/ios/Flutter/.last_build_id
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.pub-cache/
+.pub/
+/build/
+
+# Symbolication related
+app.*.symbols
+
+# Obfuscation related
+app.*.map.json
+
+# Android Studio will place build artifacts here
+/android/app/debug
+/android/app/profile
+/android/app/release
diff --git a/.metadata b/.metadata
new file mode 100644
index 000000000..39a6af965
--- /dev/null
+++ b/.metadata
@@ -0,0 +1,30 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+ revision: "edada7c56edf4a183c1735310e123c7f923584f1"
+ channel: "stable"
+
+project_type: app
+
+# Tracks metadata for the flutter migrate command
+migration:
+ platforms:
+ - platform: root
+ create_revision: edada7c56edf4a183c1735310e123c7f923584f1
+ base_revision: edada7c56edf4a183c1735310e123c7f923584f1
+ - platform: web
+ create_revision: edada7c56edf4a183c1735310e123c7f923584f1
+ base_revision: edada7c56edf4a183c1735310e123c7f923584f1
+
+ # User provided section
+
+ # List of Local paths (relative to this file) that should be
+ # ignored by the migrate tool.
+ #
+ # Files that are not part of the templates will be ignored by default.
+ unmanaged_files:
+ - 'lib/main.dart'
+ - 'ios/Runner.xcodeproj/project.pbxproj'
diff --git a/README.md b/README.md
index 597b39b44..b71494306 100644
--- a/README.md
+++ b/README.md
@@ -1,96 +1,292 @@
-# [Project Name] 🎯
+# ScamPad 🎯
## Basic Details
-### Team Name: [Name]
+### Team Name: Vanguard
### Team Members
-- Team Lead: [Name] - [College]
-- Member 2: [Name] - [College]
-- Member 3: [Name] - [College]
+- Team Lead: Jeffin Basil - SCMS SCHOOL OF ENGINEERING & TECHNOLOGY
+- Member 2: Adithya Manghat - SCMS SCHOOL OF ENGINEERING & TECHNOLOGY
### Project Description
-[2-3 lines about what your project does]
+"ScamPad" could be envisioned as a notepad application that looks normal but gradually scrambles, distorts, or modifies saved notes subtly (and sometimes not so subtly) over time!
### The Problem (that doesn't exist)
-[What ridiculous problem are you solving?]
+Notepad app is quite simple and convenient to use, but that's no fun right?
### The Solution (that nobody asked for)
-[How are you solving it? Keep it fun!]
+Give it a little personality and it'll help you "rephrase" your notes really well.
## Technical Details
### Technologies/Components Used
For Software:
-- [Languages used]
-- [Frameworks used]
-- [Libraries used]
-- [Tools used]
+- Dart, C++
+- Flutter, Material Design 3
+- flutter/material.dart, flutter/services.dart, file_picker, dart:io, dart:async, dart:math
+- Visual Studio Code, Flutter SDK, CMake
+
-For Hardware:
-- [List main components]
-- [List specifications]
-- [List tools required]
### Implementation
For Software:
# Installation
-[commands]
+```bash
+# Install Flutter SDK
+flutter doctor
-# Run
-[commands]
+# Clone the repository
+git clone https://github.com/FALLEN-01/vanguard.git
+cd vanguard
-### Project Documentation
-For Software:
+# Get dependencies
+flutter pub get
-# Screenshots (Add at least 3)
-
-*Add caption explaining what this shows*
+# Enable Windows desktop
+flutter config --enable-windows-desktop
-
-*Add caption explaining what this shows*
+# Verify setup
+flutter doctor -v
+```
-
-*Add caption explaining what this shows*
+# Run
+```bash
+# Development mode
+flutter run -d windows
-# Diagrams
-
-*Add caption explaining your workflow*
+# Release build
+flutter build windows
-For Hardware:
+# Run executable
+./build/windows/runner/Release/scampad.exe
+```
-# Schematic & Circuit
-
-*Add caption explaining connections*
+### Project Documentation
+For Software:
-
-*Add caption explaining the schematic*
+## Features
+
+### Core Functionality
+- **Multi-tab Text Editor**: Supports multiple documents with tab-based navigation
+- **Native Windows Integration**: File dialogs, keyboard shortcuts, and system clipboard support
+- **Real-time Statistics**: Live word count, character count, and line tracking
+- **Auto-save Warning**: Prompts users before losing unsaved work
+
+### Chaos Features 🎲
+- **Progressive Chaos System**: Chaos intensity increases based on document word count
+ - Under 40 words: Gentle disruptions (punctuation, spaces, cursor movement)
+ - 40-59 words: Character manipulation begins (letter deletion, swapping)
+ - 60+ words: Full chaos mode (word deletion, word swapping)
+
+- **Speed-based Escalation**: Typing speed monitoring with escalating consequences
+ - 65+ WPM triggers speed warnings
+ - 4 violation system with sarcastic messages
+ - Automatic shutdown after 4th violation
+
+- **Idle Punishment**: After 10 seconds of inactivity, rapid word swapping begins
+ - Continuous text scrambling until user resumes typing
+ - 500ms interval rapid swapping for maximum disruption
+
+### Chaos Actions
+- **Letter Operations**: Random deletion, swapping within words
+- **Word Operations**: Word deletion, inter-line word swapping
+- **Punctuation Injection**: Smart placement at word boundaries only
+- **Cursor Teleportation**: Random cursor repositioning
+- **Indentation Chaos**: Random spacing and tab insertion
+- **Space Addition**: Random whitespace injection
+
+### User Interface
+- **Modern Design**: Clean, Material Design 3 interface
+- **Dark Theme**: Easy on the eyes during chaos events
+- **Status Bar**: Real-time document statistics and modification indicators
+- **Dialog Systems**: Speed warnings, subscription prompts, and shutdown notifications
+
+## Architecture
+
+### Class Structure
+```
+ChaosManager
+├── Timer-based chaos scheduling (4-8 second intervals)
+├── Word count-based action selection
+├── Idle detection and rapid swapping
+└── 13 different chaos methods
+
+TypingSpeedMonitor
+├── Real-time keystroke tracking
+├── Speed calculation (CPM/WPM)
+├── Violation counter with persistence
+└── Escalating warning system
+
+ModernNotepadPage
+├── Multi-tab document management
+├── File operations (New, Open, Save, Save As)
+├── Statistics tracking
+└── UI state management
+
+TabData
+├── Individual document state
+├── TextEditingController management
+├── File path and modification tracking
+└── Focus node handling
+```
+
+### Chaos Progression Logic
+1. **Document Analysis**: Word count determines available chaos actions
+2. **Random Selection**: Weighted probability system for action selection
+3. **Speed Monitoring**: Continuous typing speed analysis
+4. **Idle Detection**: 10-second countdown with rapid punishment
+5. **State Persistence**: Violation counts persist throughout session
+
+## Technical Implementation
+
+### Key Components
+- **Flutter Desktop**: Cross-platform UI framework targeting Windows
+- **Native File System**: Direct Windows API integration for file operations
+- **Timer Management**: Multiple concurrent timers for different chaos systems
+- **Text Manipulation**: Advanced string processing for surgical text modifications
+- **Event Handling**: Keyboard shortcuts and text change monitoring
+
+### Performance Optimizations
+- **Non-blocking Chaos**: All chaos operations run on separate timer threads
+- **Efficient Text Operations**: Minimal string allocations during modifications
+- **Smart Action Selection**: Context-aware chaos to avoid breaking functionality
+- **Memory Management**: Proper timer cleanup and resource disposal
+
+## Usage Scenarios
+
+### Normal Usage (If Possible)
+1. Open ScamPad like any text editor
+2. Create or open text files
+3. Experience "minor" inconveniences during typing
+4. Save work frequently (before chaos strikes)
+
+### Chaos Experience
+1. **Early Stage**: Gentle disruptions, mostly cosmetic
+2. **Mid Stage**: Noticeable text modifications, typing becomes challenging
+3. **Late Stage**: Full chaos mode, document becomes increasingly unstable
+4. **Speed Violations**: Aggressive warnings, potential shutdown
+5. **Idle Periods**: Relentless text scrambling until activity resumes
+
+## Configuration
+
+### Adjustable Parameters
+- Chaos interval: 4-8 seconds (configurable in code)
+- Speed threshold: 325 CPM / 65 WPM
+- Idle timeout: 10 seconds
+- Rapid swap interval: 500ms
+- Word count thresholds: 40, 60 words
+
+### Chaos Probabilities
+**Stage 1 (< 40 words):**
+- 40% Punctuation injection
+- 30% Random spaces
+- 20% Cursor teleportation
+- 10% Random indentation
+
+**Stage 2 (40-59 words):**
+- 35% Letter deletion
+- 25% Letter swapping
+- 15% Punctuation injection
+- 12% Cursor teleportation
+- 8% Random spaces
+- 5% Random indentation
+
+**Stage 3 (60+ words):**
+- 30% Letter deletion
+- 25% Letter swapping
+- 20% Word deletion
+- 10% Word swapping
+- 8% Punctuation injection
+- 5% Random spaces
+- 2% Random indentation
+
+# Screenshots
+
+*ScamPad's clean interface on startup - appears as a normal notepad application with multiple tabs, file operations, and a professional layout*
+
+
+*After an idle period - text becomes scrambled, words are swapped and deleted, demonstrating the progressive chaos system in action*
-# Build Photos
-
-*List out all components shown*
+# Diagrams
-
-*Explain the build steps*
+## ScamPad Chaos Workflow
+```
+ ┌─────────────────┐
+ │ User Opens │
+ │ ScamPad │
+ └─────────┬───────┘
+ │
+ ▼
+ ┌─────────────────┐
+ │ Initialize UI │
+ │ & Systems │
+ └─────────┬───────┘
+ │
+ ▼
+ ┌─────────────────────────────────────────────┐
+ │ Start Three Parallel Systems │
+ └─────────┬───────────┬───────────┬───────────┘
+ │ │ │
+ ▼ ▼ ▼
+ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
+ │ Chaos │ │ Speed │ │ Idle │
+ │ Manager │ │ Monitor │ │ Detector │
+ │ (4-8 sec) │ │ (Real-time) │ │ (10 sec) │
+ └─────────────┘ └─────────────┘ └─────────────┘
+ │ │ │
+ ▼ ▼ ▼
+ ┌─────────────────────────────────────────────┐
+ │ USER TYPING ACTIVITY │
+ └─────────────────┬───────────────────────────┘
+ │
+ ▼
+ ┌─────────────────┐ ┌─────────────────┐
+ │ Word Count │ │ Speed Check │
+ │ Analysis │◄─────┤ ≥65 WPM? │
+ └─────────┬───────┘ └─────────┬───────┘
+ │ │
+ ┌─────────▼───────┐ ┌─────────▼───────┐
+ │ < 40 Words? │ │ Speed Warning │
+ │ Stage 1 │ │ Escalation │
+ │ • Punctuation │ │ • 4 Violations │
+ │ • Spaces │ │ • Shutdown │
+ │ • Cursor Move │ └─────────────────┘
+ └─────────┬───────┘
+ │
+ ┌─────────▼───────┐ ┌─────────────────┐
+ │ 40-59 Words? │ │ Idle Timer │
+ │ Stage 2 │◄─────┤ 10 seconds │
+ │ • Letter Ops │ └─────────┬───────┘
+ │ • + Stage 1 │ │
+ └─────────┬───────┘ ┌─────────▼───────┐
+ │ │ Rapid Swapping │
+ ┌─────────▼───────┐ │ Every 500ms │
+ │ 60+ Words? │ │ Until Activity │
+ │ Stage 3 │ └─────────────────┘
+ │ • Word Delete │
+ │ • Word Swap │
+ │ • Full Chaos │
+ └─────────────────┘
+```
+*This diagram shows how ScamPad's three parallel chaos systems work together to progressively disrupt the user experience based on document length, typing speed, and activity level*
-
-*Explain the final build*
### Project Demo
# Video
-[Add your demo video link here]
-*Explain what the video demonstrates*
-# Additional Demos
-[Add any extra demo materials/links]
+
+
+**Full Video**: [Download complete demo](Screenshots/ScamPad_final.mp4)
+
+*This GIF demonstrates ScamPad's complete chaos system in action - from normal notepad functionality to progressive text scrambling, speed-based warnings, and idle-based rapid swapping*
+
## Team Contributions
-- [Name 1]: [Specific contributions]
-- [Name 2]: [Specific contributions]
-- [Name 3]: [Specific contributions]
+- Jeffin Basil: Flutter app architecture, chaos system implementation, UI design, file operations
+- Adithya Manghat: Chaos algorithms, speed monitoring system, testing, documentation
---
Made with ❤️ at TinkerHub Useless Projects
diff --git a/Screenshots/ScamPad_final.gif b/Screenshots/ScamPad_final.gif
new file mode 100644
index 000000000..0bb13a884
Binary files /dev/null and b/Screenshots/ScamPad_final.gif differ
diff --git a/Screenshots/ScamPad_final.mp4 b/Screenshots/ScamPad_final.mp4
new file mode 100644
index 000000000..a398e0ffe
Binary files /dev/null and b/Screenshots/ScamPad_final.mp4 differ
diff --git a/Screenshots/final_state.png b/Screenshots/final_state.png
new file mode 100644
index 000000000..2ae25c448
Binary files /dev/null and b/Screenshots/final_state.png differ
diff --git a/Screenshots/initial_state.png b/Screenshots/initial_state.png
new file mode 100644
index 000000000..eff1dd58b
Binary files /dev/null and b/Screenshots/initial_state.png differ
diff --git a/analysis_options.yaml b/analysis_options.yaml
new file mode 100644
index 000000000..0d2902135
--- /dev/null
+++ b/analysis_options.yaml
@@ -0,0 +1,28 @@
+# This file configures the analyzer, which statically analyzes Dart code to
+# check for errors, warnings, and lints.
+#
+# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
+# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
+# invoked from the command line by running `flutter analyze`.
+
+# The following line activates a set of recommended lints for Flutter apps,
+# packages, and plugins designed to encourage good coding practices.
+include: package:flutter_lints/flutter.yaml
+
+linter:
+ # The lint rules applied to this project can be customized in the
+ # section below to disable rules from the `package:flutter_lints/flutter.yaml`
+ # included above or to enable additional rules. A list of all available lints
+ # and their documentation is published at https://dart.dev/lints.
+ #
+ # Instead of disabling a lint rule for the entire project in the
+ # section below, it can also be suppressed for a single line of code
+ # or a specific dart file by using the `// ignore: name_of_lint` and
+ # `// ignore_for_file: name_of_lint` syntax on the line or in the file
+ # producing the lint.
+ rules:
+ # avoid_print: false # Uncomment to disable the `avoid_print` rule
+ # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
+
+# Additional information about this file can be found at
+# https://dart.dev/guides/language/analysis-options
diff --git a/assets/icons/app_icon.ico b/assets/icons/app_icon.ico
new file mode 100644
index 000000000..74e20810d
Binary files /dev/null and b/assets/icons/app_icon.ico differ
diff --git a/lib/chaos/chaos_controller.dart b/lib/chaos/chaos_controller.dart
new file mode 100644
index 000000000..3ad44e177
--- /dev/null
+++ b/lib/chaos/chaos_controller.dart
@@ -0,0 +1,320 @@
+import 'dart:async';
+import 'dart:math';
+import 'package:flutter/material.dart';
+
+/// The chaotic text editor system that randomly interferes with user input
+class ChaosController {
+ final TextEditingController textController;
+ final Function(String) onTextChanged;
+ final Function(TextSelection) onSelectionChanged;
+ final Function() onSave;
+
+ Timer? _chaosTimer;
+ final Random _random = Random();
+ bool _isActive = true;
+
+ // Chaos behaviors weighted by frequency
+ final List _chaosActions;
+
+ ChaosController({
+ required this.textController,
+ required this.onTextChanged,
+ required this.onSelectionChanged,
+ required this.onSave,
+ }) : _chaosActions = [
+ ChaosAction(ChaosType.cursorDisplacement, weight: 25),
+ ChaosAction(ChaosType.randomDeletion, weight: 20),
+ ChaosAction(ChaosType.letterSwapping, weight: 15),
+ ChaosAction(ChaosType.wordSwapping, weight: 10),
+ ChaosAction(ChaosType.punctuationInsert, weight: 20),
+ ChaosAction(ChaosType.punctuationDuplicate, weight: 10),
+ ];
+
+ /// Start the chaos cycle
+ void startChaos() {
+ _isActive = true;
+ _scheduleNextChaos();
+ }
+
+ /// Stop the chaos cycle
+ void stopChaos() {
+ _isActive = false;
+ _chaosTimer?.cancel();
+ }
+
+ /// Schedule the next chaotic event
+ void _scheduleNextChaos() {
+ if (!_isActive) return;
+
+ // Random delay between 8-15 seconds
+ final delaySeconds = 8 + _random.nextInt(8);
+
+ _chaosTimer = Timer(Duration(seconds: delaySeconds), () {
+ _executeChaos();
+ _scheduleNextChaos();
+ });
+ }
+
+ /// Execute a random chaotic behavior
+ void _executeChaos() {
+ if (!_isActive || textController.text.isEmpty) return;
+
+ final action = _selectRandomAction();
+ _performChaosAction(action.type);
+ }
+
+ /// Select a random chaos action based on weights
+ ChaosAction _selectRandomAction() {
+ final totalWeight = _chaosActions.fold(
+ 0,
+ (sum, action) => sum + action.weight,
+ );
+ final randomValue = _random.nextInt(totalWeight);
+
+ int currentWeight = 0;
+ for (final action in _chaosActions) {
+ currentWeight += action.weight;
+ if (randomValue < currentWeight) {
+ return action;
+ }
+ }
+
+ return _chaosActions.first; // fallback
+ }
+
+ /// Perform the specified chaos action
+ void _performChaosAction(ChaosType type) {
+ final text = textController.text;
+ if (text.isEmpty) return;
+
+ switch (type) {
+ case ChaosType.cursorDisplacement:
+ _displaceCursor();
+ break;
+ case ChaosType.randomDeletion:
+ _performRandomDeletion();
+ break;
+ case ChaosType.letterSwapping:
+ _swapLetters();
+ break;
+ case ChaosType.wordSwapping:
+ _swapWords();
+ break;
+ case ChaosType.punctuationInsert:
+ _insertRandomPunctuation();
+ break;
+ case ChaosType.punctuationDuplicate:
+ _duplicatePunctuation();
+ break;
+ case ChaosType.randomIndentation:
+ _addRandomIndentation();
+ break;
+ }
+ }
+
+ /// Move cursor to random position
+ void _displaceCursor() {
+ final text = textController.text;
+ if (text.isEmpty) return;
+
+ final newPosition = _random.nextInt(text.length + 1);
+ final newSelection = TextSelection.collapsed(offset: newPosition);
+
+ textController.selection = newSelection;
+ onSelectionChanged(newSelection);
+ }
+
+ /// Delete random character or word
+ void _performRandomDeletion() {
+ final text = textController.text;
+ if (text.isEmpty) return;
+
+ final shouldDeleteWord = _random.nextBool() && text.contains(' ');
+
+ if (shouldDeleteWord) {
+ _deleteRandomWord();
+ } else {
+ _deleteRandomCharacter();
+ }
+ }
+
+ /// Delete a random character
+ void _deleteRandomCharacter() {
+ final text = textController.text;
+ if (text.isEmpty) return;
+
+ final deleteIndex = _random.nextInt(text.length);
+ final newText =
+ text.substring(0, deleteIndex) + text.substring(deleteIndex + 1);
+
+ textController.text = newText;
+ textController.selection = TextSelection.collapsed(
+ offset: deleteIndex.clamp(0, newText.length),
+ );
+ onTextChanged(newText);
+ }
+
+ /// Delete a random word
+ void _deleteRandomWord() {
+ final text = textController.text;
+ final words = text.split(RegExp(r'\s+'));
+ if (words.length <= 1) return;
+
+ final wordIndex = _random.nextInt(words.length);
+ words.removeAt(wordIndex);
+
+ final newText = words.join(' ');
+ textController.text = newText;
+ textController.selection = TextSelection.collapsed(offset: newText.length);
+ onTextChanged(newText);
+ }
+
+ /// Swap two random letters within a word
+ void _swapLetters() {
+ final text = textController.text;
+ final words = text.split(' ');
+
+ // Find a word with at least 2 characters
+ final validWords = words.where((word) => word.length >= 2).toList();
+ if (validWords.isEmpty) return;
+
+ final word = validWords[_random.nextInt(validWords.length)];
+ final wordIndex = words.indexOf(word);
+
+ // Swap two random letters
+ final letterIndices = List.generate(word.length, (i) => i);
+ letterIndices.shuffle(_random);
+ final index1 = letterIndices[0];
+ final index2 = letterIndices[1];
+
+ final chars = word.split('');
+ final temp = chars[index1];
+ chars[index1] = chars[index2];
+ chars[index2] = temp;
+
+ words[wordIndex] = chars.join('');
+ final newText = words.join(' ');
+
+ textController.text = newText;
+ onTextChanged(newText);
+ }
+
+ /// Swap two random words
+ void _swapWords() {
+ final text = textController.text;
+ final words = text.split(' ');
+ if (words.length < 2) return;
+
+ final indices = List.generate(words.length, (i) => i);
+ indices.shuffle(_random);
+ final index1 = indices[0];
+ final index2 = indices[1];
+
+ final temp = words[index1];
+ words[index1] = words[index2];
+ words[index2] = temp;
+
+ final newText = words.join(' ');
+ textController.text = newText;
+ onTextChanged(newText);
+ }
+
+ /// Insert random punctuation at random position
+ void _insertRandomPunctuation() {
+ final text = textController.text;
+ if (text.isEmpty) return;
+
+ final punctuation = ['.', ',', '!', '?', ';', ':', '-'];
+ final randomPunct = punctuation[_random.nextInt(punctuation.length)];
+ final insertIndex = _random.nextInt(text.length + 1);
+
+ final newText =
+ text.substring(0, insertIndex) +
+ randomPunct +
+ text.substring(insertIndex);
+
+ textController.text = newText;
+ textController.selection = TextSelection.collapsed(offset: insertIndex + 1);
+ onTextChanged(newText);
+ }
+
+ /// Duplicate existing punctuation
+ void _duplicatePunctuation() {
+ final text = textController.text;
+ final punctuationRegex = RegExp(r'[.!?,;:]');
+ final matches = punctuationRegex.allMatches(text).toList();
+
+ if (matches.isEmpty) return;
+
+ final match = matches[_random.nextInt(matches.length)];
+ final punctuation = match.group(0)!;
+ final position = match.start;
+
+ final newText =
+ text.substring(0, position + 1) +
+ punctuation +
+ text.substring(position + 1);
+
+ textController.text = newText;
+ onTextChanged(newText);
+ }
+
+ /// Add random indentation to lines (triggered on save)
+ void _addRandomIndentation() {
+ final text = textController.text;
+ final lines = text.split('\n');
+
+ // Randomly indent 1-3 lines
+ final linesToIndent = _random.nextInt(3) + 1;
+ final selectedLines = {};
+
+ while (selectedLines.length < linesToIndent &&
+ selectedLines.length < lines.length) {
+ selectedLines.add(_random.nextInt(lines.length));
+ }
+
+ for (final lineIndex in selectedLines) {
+ final isTab = _random.nextBool();
+ final indentCount = _random.nextInt(4) + 1;
+ final indent = isTab ? '\t' * indentCount : ' ' * indentCount;
+ lines[lineIndex] = indent + lines[lineIndex];
+ }
+
+ final newText = lines.join('\n');
+ textController.text = newText;
+ onTextChanged(newText);
+ }
+
+ /// Handle save action with random indentation
+ void handleSave() {
+ // 70% chance to add random indentation on save
+ if (_random.nextInt(10) < 7) {
+ _addRandomIndentation();
+ }
+ onSave();
+ }
+
+ /// Dispose resources
+ void dispose() {
+ stopChaos();
+ }
+}
+
+/// Types of chaotic behaviors
+enum ChaosType {
+ cursorDisplacement,
+ randomDeletion,
+ letterSwapping,
+ wordSwapping,
+ punctuationInsert,
+ punctuationDuplicate,
+ randomIndentation,
+}
+
+/// Chaos action with weight for random selection
+class ChaosAction {
+ final ChaosType type;
+ final int weight;
+
+ ChaosAction(this.type, {required this.weight});
+}
diff --git a/lib/main.dart b/lib/main.dart
new file mode 100644
index 000000000..7721da7f7
--- /dev/null
+++ b/lib/main.dart
@@ -0,0 +1,2957 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'dart:io';
+import 'dart:async';
+import 'dart:math';
+import 'package:file_picker/file_picker.dart';
+
+// ChaosManager handles all the chaos behaviors
+class ChaosManager {
+ late Timer _timer;
+ Timer? _idleTimer;
+ Timer? _rapidSwapTimer;
+ final Random _random = Random();
+ final TextEditingController textController;
+ final VoidCallback updateState;
+ bool _isRapidSwapping = false;
+
+ ChaosManager({required this.textController, required this.updateState});
+
+ void startChaos() {
+ _scheduleNextChaos();
+ _startIdleDetection();
+ }
+
+ void stopChaos() {
+ _timer.cancel();
+ _idleTimer?.cancel();
+ _rapidSwapTimer?.cancel();
+ }
+
+ void _startIdleDetection() {
+ _resetIdleTimer();
+ }
+
+ void _resetIdleTimer() {
+ _idleTimer?.cancel();
+ // Track activity time
+
+ // Stop rapid swapping if user becomes active
+ if (_isRapidSwapping) {
+ _stopRapidSwapping();
+ }
+
+ // Start 10-second idle timer
+ _idleTimer = Timer(const Duration(seconds: 10), () {
+ _startRapidSwapping();
+ });
+ }
+
+ void _startRapidSwapping() {
+ if (_isRapidSwapping) return; // Prevent multiple timers
+
+ _isRapidSwapping = true;
+ _rapidSwapTimer = Timer.periodic(const Duration(milliseconds: 500), (
+ timer,
+ ) {
+ // Rapid word swapping every 500ms while idle
+ wordSwapping();
+ updateState();
+ });
+ }
+
+ void _stopRapidSwapping() {
+ _isRapidSwapping = false;
+ _rapidSwapTimer?.cancel();
+ _rapidSwapTimer = null;
+ }
+
+ // Call this method whenever user types to reset idle detection
+ void onUserActivity() {
+ _resetIdleTimer();
+ }
+
+ void _scheduleNextChaos() {
+ // Random interval between 4-8 seconds
+ final int seconds = 4 + _random.nextInt(5); // 4 to 8 seconds
+ _timer = Timer(Duration(seconds: seconds), () {
+ _executeChaosAction();
+ _scheduleNextChaos(); // Schedule the next chaos event
+ });
+ }
+
+ void _executeChaosAction() {
+ // Get current word count from the text
+ final text = textController.text;
+ final wordCount = text.trim().isEmpty
+ ? 0
+ : text.trim().split(RegExp(r'\s+')).length;
+
+ // Different chaos actions based on word count thresholds
+ Map chaosActions;
+
+ if (wordCount < 40) {
+ // Early stage: Only gentle chaos (no character replacement yet)
+ chaosActions = {
+ 0.40: () => punctuationInjection(), // 40% - Add punctuation
+ 0.30: () => randomSpaceAddition(), // 30% - Add spaces
+ 0.20: () => cursorTeleportation(), // 20% - Move cursor
+ 0.10: () => randomIndentationChaos(), // 10% - Random indentation
+ };
+ } else if (wordCount < 60) {
+ // Mid stage: Character manipulation begins (40+ words)
+ chaosActions = {
+ 0.35: () => randomLetterDeletion(), // 35% - Remove letters
+ 0.25: () => letterSwapping(), // 25% - Swap letters
+ 0.15: () => punctuationInjection(), // 15% - Add punctuation
+ 0.12: () => cursorTeleportation(), // 12% - Move cursor
+ 0.08: () => randomSpaceAddition(), // 8% - Add spaces
+ 0.05: () => randomIndentationChaos(), // 5% - Random indentation
+ };
+ } else {
+ // Advanced stage: Full chaos including word deletion (60+ words)
+ chaosActions = {
+ 0.30: () => randomLetterDeletion(), // 30% - Remove letters
+ 0.25: () => letterSwapping(), // 25% - Swap letters
+ 0.20: () => wordDeletion(), // 20% - Remove words (now active)
+ 0.10: () => wordSwapping(), // 10% - Swap words
+ 0.08: () => punctuationInjection(), // 8% - Add punctuation
+ 0.05: () => randomSpaceAddition(), // 5% - Add spaces
+ 0.02: () => randomIndentationChaos(), // 2% - Random indentation
+ };
+ }
+
+ // Generate random number and select action based on cumulative probability
+ final randomValue = _random.nextDouble();
+ double cumulativeProbability = 0.0;
+
+ for (final entry in chaosActions.entries) {
+ cumulativeProbability += entry.key;
+ if (randomValue <= cumulativeProbability) {
+ entry.value();
+ updateState();
+ return;
+ }
+ }
+ }
+
+ // Random Indentation on Save - will be called from save method
+ void randomIndentationOnSave() {
+ final text = textController.text;
+ if (text.isEmpty) return;
+
+ final lines = text.split('\n');
+ final linesToModify = _random.nextInt(3) + 1; // Modify 1-3 lines
+
+ for (int i = 0; i < linesToModify; i++) {
+ final lineIndex = _random.nextInt(lines.length);
+ final indentType = _random.nextBool() ? '\t' : ' '; // Tab or 2 spaces
+ final indentCount = _random.nextInt(3) + 1; // 1-3 indentations
+ lines[lineIndex] = (indentType * indentCount) + lines[lineIndex];
+ }
+
+ textController.text = lines.join('\n');
+ }
+
+ void cursorTeleportation() {
+ final text = textController.text;
+ if (text.isEmpty) return;
+
+ final newPosition = _random.nextInt(text.length);
+ textController.selection = TextSelection.collapsed(offset: newPosition);
+ }
+
+ void randomDeletion() {
+ final text = textController.text;
+ if (text.isEmpty) return;
+
+ if (_random.nextBool()) {
+ // Delete a random letter
+ final position = _random.nextInt(text.length);
+ final newText =
+ text.substring(0, position) + text.substring(position + 1);
+ textController.text = newText;
+ } else {
+ // Delete a random word
+ final words = text.split(RegExp(r'\s+'));
+ if (words.isNotEmpty) {
+ final wordIndex = _random.nextInt(words.length);
+ words.removeAt(wordIndex);
+ textController.text = words.join(' ');
+ }
+ }
+ }
+
+ void letterOrWordSwapping() {
+ final text = textController.text;
+ if (text.length < 2) return;
+
+ if (_random.nextBool() && text.length > 1) {
+ // Swap letters within a word
+ final lines = text.split('\n');
+ final words = [];
+ final lineIndices = [];
+
+ for (int i = 0; i < lines.length; i++) {
+ final lineWords = lines[i].split(RegExp(r'\s+'));
+ for (final word in lineWords) {
+ if (word.length > 1) {
+ words.add(word);
+ lineIndices.add(i);
+ }
+ }
+ }
+
+ if (words.isNotEmpty) {
+ final wordIndex = _random.nextInt(words.length);
+ final word = words[wordIndex];
+ final chars = word.split('');
+
+ if (chars.length > 1) {
+ final pos1 = _random.nextInt(chars.length);
+ int pos2 = _random.nextInt(chars.length);
+ while (pos2 == pos1) {
+ pos2 = _random.nextInt(chars.length);
+ }
+
+ final temp = chars[pos1];
+ chars[pos1] = chars[pos2];
+ chars[pos2] = temp;
+
+ final newWord = chars.join('');
+ textController.text = text.replaceFirst(word, newWord);
+ }
+ }
+ } else {
+ // Swap entire words between lines
+ final lines = text.split('\n');
+ if (lines.length < 2) return;
+
+ final line1Index = _random.nextInt(lines.length);
+ int line2Index = _random.nextInt(lines.length);
+ while (line2Index == line1Index) {
+ line2Index = _random.nextInt(lines.length);
+ }
+
+ final words1 = lines[line1Index].split(RegExp(r'\s+'));
+ final words2 = lines[line2Index].split(RegExp(r'\s+'));
+
+ if (words1.isNotEmpty && words2.isNotEmpty) {
+ final word1Index = _random.nextInt(words1.length);
+ final word2Index = _random.nextInt(words2.length);
+
+ final temp = words1[word1Index];
+ words1[word1Index] = words2[word2Index];
+ words2[word2Index] = temp;
+
+ lines[line1Index] = words1.join(' ');
+ lines[line2Index] = words2.join(' ');
+
+ textController.text = lines.join('\n');
+ }
+ }
+ }
+
+ void punctuationInjection() {
+ final text = textController.text;
+ if (text.isEmpty) return;
+
+ final punctuations = ['.', ',', '!', '?', ';', ':'];
+ final punctuation = punctuations[_random.nextInt(punctuations.length)];
+
+ // Find all word boundaries and sentence endings
+ List validPositions = [];
+
+ for (int i = 0; i < text.length; i++) {
+ // Add position if it's at the end of a word (followed by space or punctuation)
+ if (i < text.length - 1 &&
+ text[i].contains(RegExp(r'[a-zA-Z0-9]')) &&
+ text[i + 1].contains(RegExp(r'[\s\.,!?;:]'))) {
+ validPositions.add(i + 1);
+ }
+ // Add position if it's at the very end of text
+ else if (i == text.length - 1 &&
+ text[i].contains(RegExp(r'[a-zA-Z0-9]'))) {
+ validPositions.add(i + 1);
+ }
+ }
+
+ // If no valid positions found, add at the end
+ if (validPositions.isEmpty) {
+ validPositions.add(text.length);
+ }
+
+ final position = validPositions[_random.nextInt(validPositions.length)];
+ final newText =
+ text.substring(0, position) + punctuation + text.substring(position);
+ textController.text = newText;
+ }
+
+ // Specialized chaos methods with specific probabilities
+ void randomLetterDeletion() {
+ final text = textController.text;
+ if (text.isEmpty) return;
+
+ final position = _random.nextInt(text.length);
+ final newText = text.substring(0, position) + text.substring(position + 1);
+ textController.text = newText;
+ }
+
+ void letterSwapping() {
+ final text = textController.text;
+ if (text.length < 2) return;
+
+ // Find words with more than 1 character
+ final lines = text.split('\n');
+ final words = [];
+ final positions = [];
+ int currentPos = 0;
+
+ for (final line in lines) {
+ final lineWords = line.split(RegExp(r'\s+'));
+ for (final word in lineWords) {
+ if (word.length > 1) {
+ words.add(word);
+ positions.add(currentPos + line.indexOf(word));
+ }
+ }
+ currentPos += line.length + 1; // +1 for newline
+ }
+
+ if (words.isNotEmpty) {
+ final wordIndex = _random.nextInt(words.length);
+ final word = words[wordIndex];
+ final chars = word.split('');
+
+ if (chars.length > 1) {
+ final pos1 = _random.nextInt(chars.length);
+ int pos2 = _random.nextInt(chars.length);
+ while (pos2 == pos1) {
+ pos2 = _random.nextInt(chars.length);
+ }
+
+ final temp = chars[pos1];
+ chars[pos1] = chars[pos2];
+ chars[pos2] = temp;
+
+ final newWord = chars.join('');
+ textController.text = text.replaceFirst(word, newWord);
+ }
+ }
+ }
+
+ void wordDeletion() {
+ final text = textController.text;
+ if (text.isEmpty) return;
+
+ final words = text.split(RegExp(r'\s+'));
+ if (words.isNotEmpty) {
+ final wordIndex = _random.nextInt(words.length);
+ words.removeAt(wordIndex);
+ textController.text = words.join(' ');
+ }
+ }
+
+ void wordSwapping() {
+ final text = textController.text;
+ if (text.trim().isEmpty) return;
+
+ // Split into all words across the entire text
+ final allWords = text.split(RegExp(r'\s+'));
+
+ // Need at least 2 words to swap
+ if (allWords.length < 2) return;
+
+ // Find two different word indices
+ final index1 = _random.nextInt(allWords.length);
+ int index2 = _random.nextInt(allWords.length);
+ while (index2 == index1 && allWords.length > 1) {
+ index2 = _random.nextInt(allWords.length);
+ }
+
+ // Swap the words
+ final temp = allWords[index1];
+ allWords[index1] = allWords[index2];
+ allWords[index2] = temp;
+
+ // Reconstruct the text maintaining original spacing structure
+ textController.text = allWords.join(' ');
+ }
+
+ void randomSpaceAddition() {
+ final text = textController.text;
+ if (text.isEmpty) return;
+
+ final position = _random.nextInt(text.length + 1);
+ final spacesToAdd = _random.nextInt(3) + 1; // Add 1-3 spaces
+ final spaces = ' ' * spacesToAdd;
+
+ final newText =
+ text.substring(0, position) + spaces + text.substring(position);
+ textController.text = newText;
+ }
+
+ void randomIndentationChaos() {
+ final text = textController.text;
+ if (text.isEmpty) return;
+
+ final lines = text.split('\n');
+ if (lines.isEmpty) return;
+
+ // Pick a random line to add indentation to
+ final targetLineIndex = _random.nextInt(lines.length);
+
+ // Generate random indentation (1-6 spaces or tabs)
+ final indentationType = _random.nextBool() ? ' ' : '\t';
+ final indentationCount = _random.nextInt(6) + 1;
+ final indentation = indentationType * indentationCount;
+
+ // Add indentation to the beginning of the selected line
+ lines[targetLineIndex] = indentation + lines[targetLineIndex];
+
+ textController.text = lines.join('\n');
+ }
+
+ // New gentler chaos methods for better user experience
+ void gentleLetterSwapping() {
+ final text = textController.text;
+ if (text.length < 2) return;
+
+ // Only swap adjacent letters within words (much more recoverable)
+ final lines = text.split('\n');
+ final words = [];
+ final positions = [];
+ int currentPos = 0;
+
+ for (final line in lines) {
+ final lineWords = line.split(RegExp(r'\s+'));
+ for (final word in lineWords) {
+ if (word.length > 2) {
+ // Only swap in words with 3+ characters
+ words.add(word);
+ positions.add(currentPos + line.indexOf(word));
+ }
+ }
+ currentPos += line.length + 1; // +1 for newline
+ }
+
+ if (words.isNotEmpty) {
+ final wordIndex = _random.nextInt(words.length);
+ final word = words[wordIndex];
+ final chars = word.split('');
+
+ if (chars.length > 2) {
+ // Only swap adjacent characters, not random ones
+ final pos1 =
+ 1 + _random.nextInt(chars.length - 2); // Avoid first and last char
+ final pos2 = pos1 + 1;
+
+ final temp = chars[pos1];
+ chars[pos1] = chars[pos2];
+ chars[pos2] = temp;
+
+ final newWord = chars.join('');
+ textController.text = text.replaceFirst(word, newWord);
+ }
+ }
+ }
+
+ void cursorNudge() {
+ final text = textController.text;
+ if (text.isEmpty) return;
+
+ final currentPosition = textController.selection.baseOffset;
+ // Small nudge of 1-5 characters, not full teleportation
+ final nudgeDistance =
+ (_random.nextInt(5) + 1) * (_random.nextBool() ? 1 : -1);
+ final newPosition = (currentPosition + nudgeDistance).clamp(0, text.length);
+
+ textController.selection = TextSelection.collapsed(offset: newPosition);
+ }
+
+ void gentlePunctuationInjection() {
+ final text = textController.text;
+ if (text.isEmpty) return;
+
+ // Only add gentle punctuation, no aggressive ones
+ final punctuations = ['.', ',', ';']; // Removed !, ?, :
+ final punctuation = punctuations[_random.nextInt(punctuations.length)];
+
+ // Find all word boundaries and sentence endings (same logic as main punctuation method)
+ List validPositions = [];
+
+ for (int i = 0; i < text.length; i++) {
+ // Add position if it's at the end of a word (followed by space or punctuation)
+ if (i < text.length - 1 &&
+ text[i].contains(RegExp(r'[a-zA-Z0-9]')) &&
+ text[i + 1].contains(RegExp(r'[\s\.,!?;:]'))) {
+ validPositions.add(i + 1);
+ }
+ // Add position if it's at the very end of text
+ else if (i == text.length - 1 &&
+ text[i].contains(RegExp(r'[a-zA-Z0-9]'))) {
+ validPositions.add(i + 1);
+ }
+ }
+
+ // If no valid positions found, add at the end
+ if (validPositions.isEmpty) {
+ validPositions.add(text.length);
+ }
+
+ final position = validPositions[_random.nextInt(validPositions.length)];
+ final newText =
+ text.substring(0, position) + punctuation + text.substring(position);
+ textController.text = newText;
+ }
+
+ void randomCapitalization() {
+ final text = textController.text;
+ if (text.isEmpty) return;
+
+ // Find letters to capitalize/decapitalize
+ final letters = [];
+ for (int i = 0; i < text.length; i++) {
+ if (RegExp(r'[a-zA-Z]').hasMatch(text[i])) {
+ letters.add(i);
+ }
+ }
+
+ if (letters.isNotEmpty) {
+ final position = letters[_random.nextInt(letters.length)];
+ final char = text[position];
+ final newChar = char == char.toUpperCase()
+ ? char.toLowerCase()
+ : char.toUpperCase();
+
+ final newText =
+ text.substring(0, position) + newChar + text.substring(position + 1);
+ textController.text = newText;
+ }
+ }
+
+ void doubleSpaceInsertion() {
+ final text = textController.text;
+ if (text.isEmpty) return;
+
+ // Find existing spaces and make them double spaces
+ final spacePositions = [];
+ for (int i = 0; i < text.length; i++) {
+ if (text[i] == ' ' && (i == 0 || text[i - 1] != ' ')) {
+ spacePositions.add(i);
+ }
+ }
+
+ if (spacePositions.isNotEmpty) {
+ final position = spacePositions[_random.nextInt(spacePositions.length)];
+ final newText =
+ '${text.substring(0, position + 1)} ${text.substring(position + 1)}';
+ textController.text = newText;
+ } else {
+ // If no spaces, just add one somewhere reasonable
+ final position = _random.nextInt(text.length + 1);
+ final newText =
+ '${text.substring(0, position)} ${text.substring(position)}';
+ textController.text = newText;
+ }
+ }
+
+ void gentleLetterDeletion() {
+ final text = textController.text;
+ if (text.length < 10) return; // Only delete if there's plenty of text
+
+ // Avoid deleting from small words or important positions
+ final safePositions = [];
+ for (int i = 1; i < text.length - 1; i++) {
+ if (text[i] != ' ' && text[i - 1] != ' ' && text[i + 1] != ' ') {
+ safePositions.add(i);
+ }
+ }
+
+ if (safePositions.isNotEmpty) {
+ final position = safePositions[_random.nextInt(safePositions.length)];
+ final newText =
+ text.substring(0, position) + text.substring(position + 1);
+ textController.text = newText;
+ }
+ }
+
+ void autocorrectMischief() {
+ final text = textController.text;
+ if (text.isEmpty) return;
+
+ // Simple "autocorrect" style replacements that are obvious and easily fixed
+ final autocorrects = {
+ 'the ': 'teh ',
+ 'and ': 'adn ',
+ 'you ': 'yuo ',
+ 'that ': 'taht ',
+ 'with ': 'wiht ',
+ 'have ': 'ahve ',
+ 'this ': 'thsi ',
+ 'will ': 'wlil ',
+ 'your ': 'yuor ',
+ 'from ': 'form ',
+ };
+
+ for (final entry in autocorrects.entries) {
+ if (text.contains(entry.key) && _random.nextDouble() < 0.3) {
+ textController.text = text.replaceFirst(entry.key, entry.value);
+ break;
+ }
+ }
+ }
+}
+
+// TypingSpeedMonitor handles speed-based chaos escalation
+class TypingSpeedMonitor {
+ final Function(int) showSpeedWarning; // Pass violation count
+ final VoidCallback forceShutdown;
+ final VoidCallback immediateShutdown; // New immediate shutdown callback
+ final VoidCallback triggerSpeedChaos;
+ final VoidCallback showSubscriptionPrompt; // New subscription prompt
+ final bool Function() isDialogVisible; // Check if any dialog is visible
+
+ // Typing speed tracking
+ final List _keystrokes = [];
+ final Random _random = Random();
+ Timer? _speedCheckTimer;
+ int _speedViolationCount = 0;
+ DateTime? _lastWarningTime; // Track when last warning was shown
+ bool _currentViolationAcknowledged =
+ true; // Track if current violation level has been acknowledged
+
+ // Speed thresholds (words per minute - assuming 5 chars per word)
+ static const int _warningThreshold = 325; // 65 WPM (65 * 5 chars)
+
+ TypingSpeedMonitor({
+ required this.showSpeedWarning,
+ required this.forceShutdown,
+ required this.immediateShutdown,
+ required this.triggerSpeedChaos,
+ required this.showSubscriptionPrompt,
+ required this.isDialogVisible,
+ });
+
+ void startMonitoring() {
+ _speedCheckTimer = Timer.periodic(const Duration(seconds: 2), (_) {
+ _checkTypingSpeed();
+ });
+ }
+
+ void stopMonitoring() {
+ _speedCheckTimer?.cancel();
+ _keystrokes.clear();
+ _speedViolationCount = 0;
+ _lastWarningTime = null; // Reset warning timer
+ _currentViolationAcknowledged = true; // Reset acknowledgment flag
+ }
+
+ void acknowledgeCurrentViolation() {
+ _currentViolationAcknowledged = true;
+ }
+
+ void recordKeystroke() {
+ final now = DateTime.now();
+ _keystrokes.add(now);
+
+ // Keep only keystrokes from the last 10 seconds
+ _keystrokes.removeWhere((time) => now.difference(time).inSeconds > 10);
+ }
+
+ void _checkTypingSpeed() {
+ if (_keystrokes.isEmpty) return;
+
+ final now = DateTime.now();
+ final recentKeystrokes = _keystrokes
+ .where((time) => now.difference(time).inSeconds <= 6)
+ .length;
+
+ // Calculate characters per minute
+ final cpm = (recentKeystrokes * 10); // Rough approximation
+
+ // Only act if speed is above threshold and no dialog is currently visible
+ if (cpm >= _warningThreshold) {
+ // Check if enough time has passed since last warning (5-second cooldown)
+ final canShowWarning =
+ _lastWarningTime == null ||
+ now.difference(_lastWarningTime!).inSeconds >= 5;
+
+ // Only increment violation count when we can show a warning and current violation hasn't been acknowledged
+ if (!isDialogVisible() &&
+ canShowWarning &&
+ _currentViolationAcknowledged) {
+ _speedViolationCount++;
+ _lastWarningTime = now;
+ _currentViolationAcknowledged =
+ false; // Mark current violation as not acknowledged
+
+ if (_speedViolationCount <= 3) {
+ // Violations 1-3: show warnings with increasing intensity
+ if (_speedViolationCount == 2 && _random.nextBool()) {
+ showSubscriptionPrompt(); // 50% chance for subscription on violation 2
+ } else {
+ showSpeedWarning(
+ _speedViolationCount,
+ ); // Show warning with current count
+ }
+ triggerSpeedChaos();
+ } else if (_speedViolationCount >= 4) {
+ // Fourth violation - shutdown
+ forceShutdown();
+ return;
+ }
+ }
+ }
+ // Speed violation count only goes UP - no forgiveness!
+ }
+}
+
+// Intent classes for keyboard shortcuts
+class SelectAllIntent extends Intent {
+ const SelectAllIntent();
+}
+
+class SaveIntent extends Intent {
+ const SaveIntent();
+}
+
+class NewFileIntent extends Intent {
+ const NewFileIntent();
+}
+
+class OpenFileIntent extends Intent {
+ const OpenFileIntent();
+}
+
+class SaveAsIntent extends Intent {
+ const SaveAsIntent();
+}
+
+class FindIntent extends Intent {
+ const FindIntent();
+}
+
+class ReplaceIntent extends Intent {
+ const ReplaceIntent();
+}
+
+class FindNextIntent extends Intent {
+ const FindNextIntent();
+}
+
+class FindPreviousIntent extends Intent {
+ const FindPreviousIntent();
+}
+
+class GoToIntent extends Intent {
+ const GoToIntent();
+}
+
+class InsertDateTimeIntent extends Intent {
+ const InsertDateTimeIntent();
+}
+
+// TabData class to manage individual tab state
+class TabData {
+ final TextEditingController controller;
+ final FocusNode focusNode;
+ String fileName;
+ String? filePath;
+ bool isModified;
+ int lineNumber;
+ int columnNumber;
+ int wordCount;
+ int charCount;
+
+ TabData({
+ required this.controller,
+ required this.focusNode,
+ this.fileName = 'Untitled.txt',
+ this.filePath,
+ this.isModified = false,
+ this.lineNumber = 1,
+ this.columnNumber = 30,
+ this.wordCount = 0,
+ this.charCount = 0,
+ });
+
+ void dispose() {
+ controller.dispose();
+ focusNode.dispose();
+ }
+}
+
+void main() {
+ runApp(const MyApp());
+}
+
+class MyApp extends StatelessWidget {
+ const MyApp({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ title: 'ScamPad',
+ debugShowCheckedModeBanner: false,
+ theme: ThemeData(
+ brightness: Brightness.light,
+ colorScheme: const ColorScheme.light(
+ primary: Colors.black87,
+ surface: Colors.white,
+ ),
+ scaffoldBackgroundColor: Colors.grey.shade50,
+ fontFamily: 'Segoe UI',
+ ),
+ home: const ModernNotepadPage(),
+ );
+ }
+}
+
+class ModernNotepadPage extends StatefulWidget {
+ const ModernNotepadPage({super.key});
+
+ @override
+ State createState() => _ModernNotepadPageState();
+}
+
+class _ModernNotepadPageState extends State {
+ // Tab management
+ final List _tabs = [];
+ int _currentTabIndex = 0;
+
+ final TextEditingController _searchController = TextEditingController();
+ final TextEditingController _replaceController = TextEditingController();
+
+ // Find/Replace functionality
+ int _currentSearchIndex = -1;
+ final List _searchResults = [];
+
+ // Text formatting state
+ bool _isBold = false;
+ bool _isItalic = false;
+ bool _isUnderline = false;
+
+ // Chaos mode state
+ final bool _isChaosEnabled = false;
+ ChaosManager? _chaosManager;
+ TypingSpeedMonitor? _typingSpeedMonitor;
+
+ // Speed violation tracking
+ int _speedViolationCount = 0;
+ bool _isSpeedWarningVisible = false;
+ bool _isSubscriptionPromptVisible = false;
+
+ // Computed properties for current tab
+ TabData get _currentTab => _tabs[_currentTabIndex];
+ TextEditingController get _controller => _currentTab.controller;
+ FocusNode get _focusNode => _currentTab.focusNode;
+ String get _currentFileName => _currentTab.fileName;
+ set _currentFileName(String value) => _currentTab.fileName = value;
+ String? get _currentFilePath => _currentTab.filePath;
+ set _currentFilePath(String? value) => _currentTab.filePath = value;
+ bool get _isModified => _currentTab.isModified;
+ set _isModified(bool value) => _currentTab.isModified = value;
+ int get _lineNumber => _currentTab.lineNumber;
+ set _lineNumber(int value) => _currentTab.lineNumber = value;
+ int get _columnNumber => _currentTab.columnNumber;
+ set _columnNumber(int value) => _currentTab.columnNumber = value;
+ int get _wordCount => _currentTab.wordCount;
+ set _wordCount(int value) => _currentTab.wordCount = value;
+ int get _charCount => _currentTab.charCount;
+ set _charCount(int value) => _currentTab.charCount = value;
+
+ @override
+ void initState() {
+ super.initState();
+
+ // Create the first tab
+ _createNewTab();
+
+ _controller.addListener(_updateStats);
+
+ // Initialize ChaosManager with the current controller
+ _initializeChaosManager();
+ _initializeTypingSpeedMonitor();
+
+ // Set initial cursor position to column 30
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ _controller.text = '';
+ _controller.selection = const TextSelection.collapsed(offset: 0);
+ _focusNode.requestFocus();
+ });
+ }
+
+ void _initializeChaosManager() {
+ // Stop existing chaos manager if it exists
+ _chaosManager?.stopChaos();
+
+ // Initialize ChaosManager with current tab's controller
+ _chaosManager = ChaosManager(
+ textController: _controller,
+ updateState: () => setState(() {}),
+ );
+
+ // Start chaos immediately (always active)
+ _chaosManager?.startChaos();
+ }
+
+ void _initializeTypingSpeedMonitor() {
+ // Stop existing monitor if it exists
+ _typingSpeedMonitor?.stopMonitoring();
+
+ // Initialize TypingSpeedMonitor
+ _typingSpeedMonitor = TypingSpeedMonitor(
+ showSpeedWarning: _showSpeedWarning,
+ forceShutdown: _forceShutdown,
+ immediateShutdown: _immediateShutdown,
+ triggerSpeedChaos: _triggerSpeedChaos,
+ showSubscriptionPrompt: _showSubscriptionPrompt,
+ isDialogVisible: () =>
+ _isSpeedWarningVisible || _isSubscriptionPromptVisible,
+ );
+
+ // Start monitoring
+ _typingSpeedMonitor?.startMonitoring();
+ }
+
+ void _showSpeedWarning(int violationCount) {
+ // Don't show new dialog if one is already visible
+ if (_isSpeedWarningVisible) return;
+
+ // Escalating messages with increasing mockery and intensity
+ final List> escalatingMessages = [
+ // Level 1: Gentle and playful warnings (violation 1-2)
+ [
+ "� SPEEDY GONZALES DETECTED! �\nWhoa there, speed racer! Your fingers are on fire! Maybe take a tiny break?",
+ "⚡ FAST FINGERS ALERT! ⚡\nImpressive typing speed! But remember, this isn't a race... or is it? 😏",
+ "🏃♂️ ZOOM ZOOM! 🏃♂️\nYour keyboard is getting a workout! Give those keys a breather!",
+ ],
+ // Level 2: More insistent but still friendly (violation 3-4)
+ [
+ "� OK, NOW I'M IMPRESSED! 😅\nSeriously though, violation #$violationCount! Maybe slow down just a smidge?",
+ "🎯 TARGETING SPEED DEMON! 🎯\nYou're really going for it! But I'm starting to get a bit dizzy watching...",
+ "🎪 LADIES AND GENTLEMEN! 🎪\nWitness the incredible typing human! Violation #$violationCount and counting!",
+ "🤖 PROCESSING... PROCESSING... 🤖\nMy circuits are struggling to keep up with you! Slow down, please!",
+ ],
+ // Level 3: Peak mockery and final warnings (violation 4+)
+ [
+ "😱 HOUSTON, WE HAVE A PROBLEM! 😱\nViolation #$violationCount! You're approaching typing light speed!",
+ "� DEFCON 1: FINGER EMERGENCY! �\nThis is your final courtesy warning! One more and I'm taking a nap!",
+ "� THE DRAMA! THE SUSPENSE! �\nViolation #$violationCount! Will they slow down? Will I survive? Find out next!",
+ "💀 FINAL WARNING, SPEED FREAK! �\nViolation #$violationCount! Next time I'm pulling the plug!",
+ "🚫 I CAN'T EVEN... 🚫\nYou've violated $violationCount times! Do you WANT me to crash?!",
+ ],
+ ];
+
+ // Select message tier based on violation count
+ int tier = 0;
+ if (violationCount >= 2) {
+ tier = 2; // Final warnings on 2nd violation
+ } else if (violationCount >= 1) {
+ tier = 0; // Initial warning on 1st violation
+ }
+
+ final messages = escalatingMessages[tier];
+
+ // Many sarcastic messages based on violation count
+ List sarcasticMessages = [];
+ final random = Random(); // Use local random instance
+
+ if (violationCount == 1) {
+ // Violation 1 - Sarcastic first warnings
+ sarcasticMessages = [
+ "Oh wow, SPEED RACER!\nSlow down there, Lightning McQueen! Your keyboard isn't built for NASCAR!",
+ "TURBO FINGERS DETECTED!\nWhat's the rush? Are you late for a very important date with your text editor?",
+ "BREAKING NEWS!\nLocal human thinks they're a typing machine! More at 11!",
+ "ZOOM ZOOM!\nEasy there, Speed Demon! This isn't the Olympics of typing!",
+ "TARGET ACQUIRED!\nWe've got a hot shot typist over here! Everyone look out!",
+ "BEEP BEEP!\nError 404: Chill not found. Please slow down and try again!",
+ "FIRE IN THE HOLE!\nYour fingers are literally smoking! Maybe give them a breather?",
+ "LADIES AND GENTLEMEN!\nStep right up and witness the incredible FAST FINGER phenomenon!",
+ "CAUTION: WET PAINT!\nOh wait, that's just your keyboard melting from the heat!",
+ "HOUSTON, WE HAVE LIFTOFF!\nYour typing speed has reached escape velocity!",
+ ];
+ } else if (violationCount == 2) {
+ // Violation 2 - More aggressive sarcasm
+ sarcasticMessages = [
+ "SERIOUSLY?! AGAIN?!\nI LITERALLY just told you to slow down! Are you even listening?!",
+ "CONGRATULATIONS!\nYou've won the award for 'Most Likely to Ignore Warnings'!",
+ "DEATH WISH MUCH?\nViolation #2! Do you WANT me to have a nervous breakdown?!",
+ "THE AUDACITY!\nThe sheer NERVE of this human! Violation #2 and still going strong!",
+ "THIS IS FINE!\nEverything is TOTALLY fine! Just my sanity slowly leaving my body!",
+ "SHOCKING!\nAbsolutely SHOCKING that you didn't learn from violation #1!",
+ "BULLSEYE!\nYou hit the target of maximum annoyance! Violation #2 achieved!",
+ "RED ALERT!\nAll hands on deck! We have a repeat offender! This is not a drill!",
+ "DOES NOT COMPUTE!\nError: Human refuses to follow simple instructions. System overloading!",
+ "KABOOM!\nThere goes my last nerve! Violation #2 and my patience is GONE!",
+ ];
+ } else if (violationCount == 3) {
+ // Violation 3 - Final warnings before shutdown
+ sarcasticMessages = [
+ "THIS IS YOUR FINAL WARNING!\nOne more violation and I'm pulling the plug! I'm NOT kidding around!",
+ "DEFCON 1! MAXIMUM THREAT LEVEL!\nViolation #3! You're literally one keystroke away from TOTAL SHUTDOWN!",
+ "I'VE HAD IT UP TO HERE!\nThree strikes and you're OUT! Next violation = GAME OVER!",
+ "LAST CHANCE SALOON!\nViolation #3! This is it! One more and we're DONE!",
+ "EMERGENCY PROTOCOLS ACTIVATED!\nViolation #3! System preparing for immediate shutdown sequence!",
+ "YOU'VE PUSHED ME TO MY LIMIT!\nThree violations! ONE MORE and I'm shutting this whole thing down!",
+ "FINAL COUNTDOWN INITIATED!\nViolation #3! Next speed burst triggers immediate termination!",
+ "I'M LITERALLY SHAKING!\nViolation #3! My circuits can't take much more of this abuse!",
+ "CODE RED! CODE RED!\nViolation #3! All systems on high alert! Shutdown imminent!",
+ "THIS IS THE END OF THE LINE!\nViolation #3! One more violation and it's lights out forever!",
+ ];
+ }
+
+ final selectedMessage = sarcasticMessages.isNotEmpty
+ ? sarcasticMessages[random.nextInt(sarcasticMessages.length)]
+ : messages[random.nextInt(messages.length)];
+
+ _isSpeedWarningVisible = true;
+
+ showDialog(
+ context: context,
+ barrierDismissible: false,
+ builder: (context) => AlertDialog(
+ backgroundColor: Colors.white,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(4),
+ side: BorderSide(color: Colors.grey.shade300, width: 0.5),
+ ),
+ contentPadding: const EdgeInsets.all(20),
+ titlePadding: const EdgeInsets.fromLTRB(20, 20, 20, 8),
+ actionsPadding: const EdgeInsets.fromLTRB(20, 8, 20, 20),
+ title: Row(
+ children: [
+ Icon(
+ violationCount >= 3
+ ? Icons.error
+ : (violationCount >= 2 ? Icons.warning_amber : Icons.warning),
+ color: Colors.black87,
+ size: 20,
+ ),
+ const SizedBox(width: 8),
+ Text(
+ violationCount >= 3
+ ? 'FINAL WARNING'
+ : (violationCount >= 2 ? 'SERIOUS WARNING' : 'SPEED WARNING'),
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.w700,
+ fontFamily: 'Segoe UI',
+ color: Colors.black87,
+ ),
+ ),
+ const Spacer(),
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
+ decoration: BoxDecoration(
+ color: Colors.black87,
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Text(
+ 'VIOLATION #$violationCount',
+ style: TextStyle(
+ fontSize: 10,
+ fontWeight: FontWeight.w600,
+ color: Colors.white,
+ fontFamily: 'Segoe UI',
+ ),
+ ),
+ ),
+ ],
+ ),
+ content: SizedBox(
+ width: 380,
+ child: Text(
+ selectedMessage,
+ style: TextStyle(
+ fontSize: 14,
+ fontFamily: 'Segoe UI',
+ color: Colors.black87,
+ height: 1.4,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ ),
+ actions: [
+ ElevatedButton(
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Colors.black87,
+ foregroundColor: Colors.white,
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ textStyle: const TextStyle(fontSize: 13, fontFamily: 'Segoe UI'),
+ elevation: 1,
+ ),
+ onPressed: () {
+ Navigator.pop(context);
+ _isSpeedWarningVisible = false;
+ _typingSpeedMonitor?.acknowledgeCurrentViolation();
+ },
+ child: Text(
+ violationCount >= 2
+ ? 'I\'LL BEHAVE, I PROMISE!'
+ : 'OK, I\'LL TRY TO SLOW DOWN',
+ ),
+ ),
+ ],
+ ),
+ );
+
+ // Auto-shutdown after 2 violations with extreme speed
+ if (violationCount >= 2) {
+ Timer(const Duration(seconds: 3), () {
+ if (_isSpeedWarningVisible) {
+ Navigator.pop(context);
+ _isSpeedWarningVisible = false;
+ _typingSpeedMonitor?.acknowledgeCurrentViolation();
+ _forceShutdown();
+ }
+ });
+ }
+ }
+
+ void _forceShutdown() {
+ // Show final dramatic shutdown dialog
+ showDialog(
+ context: context,
+ barrierDismissible: false,
+ builder: (context) => AlertDialog(
+ backgroundColor: Colors.white,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(4),
+ side: BorderSide(color: Colors.grey.shade300, width: 0.5),
+ ),
+ contentPadding: const EdgeInsets.all(20),
+ titlePadding: const EdgeInsets.fromLTRB(20, 20, 20, 8),
+ actionsPadding: const EdgeInsets.fromLTRB(20, 8, 20, 20),
+ title: Row(
+ children: [
+ Icon(Icons.power_off, color: Colors.black87, size: 20),
+ const SizedBox(width: 8),
+ Text(
+ 'SYSTEM OVERLOAD',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.w700,
+ fontFamily: 'Segoe UI',
+ color: Colors.black87,
+ ),
+ ),
+ ],
+ ),
+ content: SizedBox(
+ width: 400,
+ child: Text(
+ "🎉 MISSION ACCOMPLISHED! 🎉\n\n� MAXIMUM SPEED ACHIEVED! �\n\nWow! You've pushed this humble text editor to its absolute limits! Your typing speed is truly legendary!\n\nYou've triggered $_speedViolationCount speed warnings - that's impressive dedication!\n\nTime for a well-deserved break! ☕\n\n✨ See you next time, Speed Champion! ✨\n\nClosing in 3... 2... 1... 🌟",
+ style: TextStyle(
+ fontSize: 14,
+ fontFamily: 'Segoe UI',
+ color: Colors.black87,
+ height: 1.4,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ ),
+ actions: [
+ ElevatedButton(
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Colors.black87,
+ foregroundColor: Colors.white,
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ textStyle: const TextStyle(fontSize: 13, fontFamily: 'Segoe UI'),
+ elevation: 1,
+ ),
+ onPressed: () {
+ Navigator.pop(context);
+ // Force close app immediately using dart:io exit
+ Timer(const Duration(milliseconds: 500), () {
+ // For Windows desktop apps, use exit() to forcefully terminate
+ exit(0);
+ });
+ },
+ child: const Text('ACCEPT DEFEAT'),
+ ),
+ ],
+ ),
+ );
+
+ // Also set a backup timer to force close even if user doesn't click button
+ Timer(const Duration(seconds: 5), () {
+ exit(0); // Backup force close after 5 seconds
+ });
+ }
+
+ void _immediateShutdown() {
+ // For the most extreme cases - immediate shutdown without dialog
+ exit(0);
+ }
+
+ void _triggerSpeedChaos() {
+ // Trigger additional chaos beyond normal chaos manager
+ // Call chaos manager's methods directly
+ if (_chaosManager != null) {
+ final extraChaosActions = [
+ () => _chaosManager!.cursorTeleportation(),
+ () => _chaosManager!.cursorTeleportation(), // Extra cursor chaos
+ () => _chaosManager!.punctuationInjection(),
+ () => _chaosManager!.randomLetterDeletion(),
+ ];
+
+ final action =
+ extraChaosActions[Random().nextInt(extraChaosActions.length)];
+ action();
+ setState(() {});
+ }
+ }
+
+ void _showSubscriptionPrompt() {
+ // Simplified to avoid character encoding issues
+ _showSnackBar(
+ 'Premium Speed Detected! Keep typing fast - this is just for fun!',
+ );
+
+ _isSubscriptionPromptVisible = true;
+
+ showDialog(
+ context: context,
+ barrierDismissible: false,
+ builder: (context) => AlertDialog(
+ backgroundColor: Colors.white,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(4),
+ side: BorderSide(color: Colors.grey.shade300, width: 0.5),
+ ),
+ contentPadding: const EdgeInsets.all(20),
+ titlePadding: const EdgeInsets.fromLTRB(20, 20, 20, 8),
+ actionsPadding: const EdgeInsets.fromLTRB(20, 8, 20, 20),
+ title: Row(
+ children: [
+ Icon(Icons.star, color: Colors.amber, size: 20),
+ const SizedBox(width: 8),
+ Text(
+ 'PREMIUM SPEED DETECTED',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.w700,
+ fontFamily: 'Segoe UI',
+ color: Colors.black87,
+ ),
+ ),
+ ],
+ ),
+ content: SizedBox(
+ width: 400,
+ child: Text(
+ "🚀 IMPRESSIVE TYPING SPEED! 🚀\n\n⚡ You're typing at 90+ WPM! ⚡\n\nThat's genuinely impressive! You're a speed typing champion!\n\n💎 UNLOCK TURBO MODE 💎\n\nWant to go even faster? Our imaginary premium version offers:\n\n✨ Only \\9.99/month ✨\n\nFeatures include:\n• Unlimited typing speed\n• 80% less chaos events\n• Premium autocorrect comedy\n• Rainbow cursors\n• Typing sound effects\n\n(This is totally a joke, by the way! �)",
+ style: TextStyle(
+ fontSize: 14,
+ fontFamily: 'Segoe UI',
+ color: Colors.black87,
+ height: 1.4,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ ),
+ actions: [
+ TextButton(
+ style: TextButton.styleFrom(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ textStyle: const TextStyle(fontSize: 13, fontFamily: 'Segoe UI'),
+ ),
+ onPressed: () {
+ Navigator.pop(context);
+ _isSubscriptionPromptVisible = false;
+ },
+ child: Text(
+ 'No thanks, I like the chaos!',
+ style: TextStyle(color: Colors.grey.shade600),
+ ),
+ ),
+ ElevatedButton(
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Colors.amber,
+ foregroundColor: Colors.black87,
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ textStyle: const TextStyle(fontSize: 13, fontFamily: 'Segoe UI'),
+ elevation: 1,
+ ),
+ onPressed: () {
+ Navigator.pop(context);
+ _isSubscriptionPromptVisible = false;
+ _showSnackBar(
+ 'Payment successful! Just kidding - this is a joke! 😄',
+ );
+ },
+ child: const Text('TOTALLY SUBSCRIBE!'),
+ ),
+ ],
+ ),
+ );
+ }
+
+ @override
+ void dispose() {
+ _chaosManager?.stopChaos();
+ _typingSpeedMonitor?.stopMonitoring();
+ _controller.removeListener(_updateStats);
+
+ // Dispose all tabs
+ for (final tab in _tabs) {
+ tab.dispose();
+ }
+
+ _searchController.dispose();
+ _replaceController.dispose();
+ super.dispose();
+ }
+
+ // Tab management methods
+ void _createNewTab() {
+ final controller = TextEditingController();
+ final focusNode = FocusNode();
+
+ controller.addListener(_updateStats);
+
+ final newTab = TabData(controller: controller, focusNode: focusNode);
+
+ setState(() {
+ _tabs.add(newTab);
+ _currentTabIndex = _tabs.length - 1;
+ });
+
+ // Initialize chaos manager for the new active tab
+ _initializeChaosManager();
+
+ // Focus the new tab
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ focusNode.requestFocus();
+ });
+ }
+
+ void _switchToTab(int index) {
+ if (index >= 0 && index < _tabs.length) {
+ setState(() {
+ _currentTabIndex = index;
+ });
+
+ // Reinitialize chaos manager for the new active tab
+ _initializeChaosManager();
+ _initializeTypingSpeedMonitor();
+
+ // Focus the selected tab
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ _focusNode.requestFocus();
+ });
+ }
+ }
+
+ void _closeTab(int index) {
+ if (_tabs.length <= 1) {
+ // Don't close the last tab, just create a new empty one
+ _createNewFile();
+ return;
+ }
+
+ final tabToClose = _tabs[index];
+
+ if (tabToClose.isModified) {
+ _showUnsavedChangesDialog(() => _performCloseTab(index));
+ } else {
+ _performCloseTab(index);
+ }
+ }
+
+ void _performCloseTab(int index) {
+ final tabToClose = _tabs[index];
+ tabToClose.dispose();
+
+ setState(() {
+ _tabs.removeAt(index);
+ if (_currentTabIndex >= _tabs.length) {
+ _currentTabIndex = _tabs.length - 1;
+ } else if (_currentTabIndex > index) {
+ _currentTabIndex--;
+ }
+ });
+
+ // Reinitialize chaos manager for the new current tab
+ _initializeChaosManager();
+
+ // Focus the current tab
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ _focusNode.requestFocus();
+ });
+ }
+
+ void _updateStats() {
+ final text = _controller.text;
+ final selection = _controller.selection;
+
+ setState(() {
+ _charCount = text.length;
+
+ // Calculate word count
+ _wordCount = text.trim().isEmpty
+ ? 0
+ : text.trim().split(RegExp(r'\s+')).length;
+
+ if (selection.isValid) {
+ final beforeCursor = text.substring(0, selection.baseOffset);
+ _lineNumber = '\n'.allMatches(beforeCursor).length + 1;
+ _columnNumber = beforeCursor.split('\n').last.length + 1;
+ } else {
+ _columnNumber = 30;
+ }
+ });
+ }
+
+ // File handling methods
+ void _newFile() {
+ // Create a new tab instead of clearing current one
+ _createNewTab();
+ }
+
+ void _createNewFile() {
+ setState(() {
+ _controller.text = '';
+ _currentFileName = 'Untitled.txt';
+ _currentFilePath = null;
+ _isModified = false;
+ });
+ }
+
+ Future _openFile() async {
+ if (_isModified) {
+ _showUnsavedChangesDialog(() => _performOpenFile());
+ } else {
+ await _performOpenFile();
+ }
+ }
+
+ Future _performOpenFile() async {
+ try {
+ // Use native Windows file dialog
+ FilePickerResult? result = await FilePicker.platform.pickFiles(
+ dialogTitle: 'Open File',
+ type: FileType.custom,
+ allowedExtensions: ['txt'],
+ allowMultiple: false,
+ );
+
+ if (result != null && result.files.single.path != null) {
+ final filePath = result.files.single.path!;
+ final file = File(filePath);
+
+ if (await file.exists()) {
+ final content = await file.readAsString();
+ setState(() {
+ _controller.text = content;
+ _currentFileName = result.files.single.name;
+ _currentFilePath = filePath;
+ _isModified = false;
+ });
+ _showSnackBar('File opened successfully');
+ } else {
+ _showSnackBar('File not found');
+ }
+ }
+ } catch (e) {
+ _showSnackBar('Error opening file: $e');
+ }
+ }
+
+ void _clearFormatting() {
+ _showSnackBar('Formatting cleared');
+ }
+
+ void _exitApplication() {
+ if (_isModified) {
+ _showUnsavedChangesDialog(() => _performExit());
+ } else {
+ _performExit();
+ }
+ }
+
+ void _performExit() {
+ _showSnackBar('Exit requested - close window manually');
+ }
+
+ void _goToLine() {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ backgroundColor: Colors.white,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(4),
+ side: BorderSide(color: Colors.grey.shade300, width: 0.5),
+ ),
+ contentPadding: const EdgeInsets.all(16),
+ titlePadding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
+ actionsPadding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
+ title: Row(
+ children: [
+ Icon(Icons.linear_scale, color: Colors.black87, size: 16),
+ const SizedBox(width: 6),
+ Text(
+ 'Go to Line',
+ style: TextStyle(
+ fontSize: 14,
+ fontWeight: FontWeight.w600,
+ fontFamily: 'Segoe UI',
+ color: Colors.black87,
+ ),
+ ),
+ ],
+ ),
+ content: SizedBox(
+ width: 280,
+ child: TextField(
+ decoration: InputDecoration(
+ hintText: 'Enter line number',
+ hintStyle: TextStyle(fontSize: 13, color: Colors.grey.shade500),
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(4),
+ ),
+ focusedBorder: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(4),
+ borderSide: BorderSide(color: Colors.black87, width: 1),
+ ),
+ contentPadding: const EdgeInsets.symmetric(
+ horizontal: 12,
+ vertical: 10,
+ ),
+ ),
+ style: const TextStyle(fontSize: 13, fontFamily: 'Segoe UI'),
+ keyboardType: TextInputType.number,
+ autofocus: true,
+ onSubmitted: (value) {
+ final lineNum = int.tryParse(value);
+ if (lineNum != null && lineNum > 0) {
+ _jumpToLine(lineNum);
+ }
+ Navigator.pop(context);
+ },
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ style: TextButton.styleFrom(
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
+ ),
+ child: Text(
+ 'Cancel',
+ style: TextStyle(
+ fontSize: 13,
+ fontFamily: 'Segoe UI',
+ color: Colors.grey.shade600,
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ void _jumpToLine(int lineNumber) {
+ final text = _controller.text;
+ final lines = text.split('\n');
+
+ if (lineNumber <= lines.length) {
+ int position = 0;
+ for (int i = 0; i < lineNumber - 1; i++) {
+ position += lines[i].length + 1; // +1 for newline character
+ }
+ _controller.selection = TextSelection.collapsed(offset: position);
+ }
+ }
+
+ void _insertDateTime() {
+ final now = DateTime.now();
+ final dateTime =
+ '${now.day}/${now.month}/${now.year} ${now.hour}:${now.minute.toString().padLeft(2, '0')}';
+
+ final selection = _controller.selection;
+ final text = _controller.text;
+ final newText = text.replaceRange(selection.start, selection.end, dateTime);
+
+ setState(() {
+ _controller.text = newText;
+ _controller.selection = TextSelection.collapsed(
+ offset: selection.start + dateTime.length,
+ );
+ _isModified = true;
+ });
+ }
+
+ // Find and Replace functionality
+ void _showFindDialog() {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ backgroundColor: Colors.white,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(4),
+ side: BorderSide(color: Colors.grey.shade300, width: 0.5),
+ ),
+ contentPadding: const EdgeInsets.all(16),
+ titlePadding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
+ actionsPadding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
+ title: Row(
+ children: [
+ Icon(Icons.search, color: Colors.black87, size: 16),
+ const SizedBox(width: 6),
+ Text(
+ 'Find',
+ style: TextStyle(
+ fontSize: 14,
+ fontWeight: FontWeight.w600,
+ fontFamily: 'Segoe UI',
+ color: Colors.black87,
+ ),
+ ),
+ ],
+ ),
+ content: SizedBox(
+ width: 300,
+ child: TextField(
+ controller: _searchController,
+ decoration: InputDecoration(
+ hintText: 'Enter text to find...',
+ hintStyle: TextStyle(fontSize: 13, color: Colors.grey.shade500),
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(4),
+ borderSide: BorderSide(color: Colors.grey.shade300),
+ ),
+ focusedBorder: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(4),
+ borderSide: BorderSide(color: Colors.black87, width: 1),
+ ),
+ prefixIcon: Icon(
+ Icons.search,
+ color: Colors.grey.shade600,
+ size: 18,
+ ),
+ contentPadding: const EdgeInsets.symmetric(
+ horizontal: 12,
+ vertical: 10,
+ ),
+ ),
+ style: const TextStyle(fontSize: 13, fontFamily: 'Segoe UI'),
+ autofocus: true,
+ onSubmitted: (_) => _performSearch(),
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ style: TextButton.styleFrom(
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
+ ),
+ child: Text(
+ 'Cancel',
+ style: TextStyle(
+ fontSize: 13,
+ fontFamily: 'Segoe UI',
+ color: Colors.grey.shade600,
+ ),
+ ),
+ ),
+ ElevatedButton.icon(
+ icon: Icon(
+ Icons.search_outlined,
+ size: 14,
+ color: Colors.grey.shade700,
+ ),
+ label: const Text('Find All'),
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Colors.white,
+ foregroundColor: Colors.black87,
+ side: BorderSide(color: Colors.grey.shade300, width: 0.5),
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
+ textStyle: const TextStyle(fontSize: 13, fontFamily: 'Segoe UI'),
+ elevation: 1,
+ ),
+ onPressed: () {
+ _performSearch();
+ Navigator.pop(context);
+ },
+ ),
+ ElevatedButton.icon(
+ icon: Icon(Icons.arrow_forward, size: 14, color: Colors.white),
+ label: const Text('Find Next'),
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Colors.black87,
+ foregroundColor: Colors.white,
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
+ textStyle: const TextStyle(fontSize: 13, fontFamily: 'Segoe UI'),
+ elevation: 1,
+ ),
+ onPressed: () {
+ _performSearch();
+ _findNext();
+ Navigator.pop(context);
+ },
+ ),
+ ],
+ ),
+ );
+ }
+
+ void _showReplaceDialog() {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ backgroundColor: Colors.white,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(4),
+ side: BorderSide(color: Colors.grey.shade300, width: 0.5),
+ ),
+ contentPadding: const EdgeInsets.all(16),
+ titlePadding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
+ actionsPadding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
+ title: Row(
+ children: [
+ Icon(Icons.find_replace, color: Colors.black87, size: 16),
+ const SizedBox(width: 6),
+ Text(
+ 'Find and Replace',
+ style: TextStyle(
+ fontSize: 14,
+ fontWeight: FontWeight.w600,
+ fontFamily: 'Segoe UI',
+ color: Colors.black87,
+ ),
+ ),
+ ],
+ ),
+ content: SizedBox(
+ width: 300,
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ TextField(
+ controller: _searchController,
+ decoration: InputDecoration(
+ labelText: 'Find',
+ labelStyle: TextStyle(
+ fontSize: 13,
+ color: Colors.grey.shade600,
+ ),
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(4),
+ ),
+ focusedBorder: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(4),
+ borderSide: BorderSide(color: Colors.black87, width: 1),
+ ),
+ prefixIcon: Icon(
+ Icons.search,
+ color: Colors.grey.shade600,
+ size: 18,
+ ),
+ contentPadding: const EdgeInsets.symmetric(
+ horizontal: 12,
+ vertical: 10,
+ ),
+ ),
+ style: const TextStyle(fontSize: 13, fontFamily: 'Segoe UI'),
+ ),
+ const SizedBox(height: 12),
+ TextField(
+ controller: _replaceController,
+ decoration: InputDecoration(
+ labelText: 'Replace with',
+ labelStyle: TextStyle(
+ fontSize: 13,
+ color: Colors.grey.shade600,
+ ),
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(4),
+ ),
+ focusedBorder: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(4),
+ borderSide: BorderSide(color: Colors.black87, width: 1),
+ ),
+ prefixIcon: Icon(
+ Icons.edit,
+ color: Colors.grey.shade600,
+ size: 18,
+ ),
+ contentPadding: const EdgeInsets.symmetric(
+ horizontal: 12,
+ vertical: 10,
+ ),
+ ),
+ style: const TextStyle(fontSize: 13, fontFamily: 'Segoe UI'),
+ ),
+ ],
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ style: TextButton.styleFrom(
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
+ ),
+ child: Text(
+ 'Cancel',
+ style: TextStyle(
+ fontSize: 13,
+ fontFamily: 'Segoe UI',
+ color: Colors.grey.shade600,
+ ),
+ ),
+ ),
+ ElevatedButton.icon(
+ icon: Icon(
+ Icons.search_outlined,
+ size: 14,
+ color: Colors.grey.shade700,
+ ),
+ label: const Text('Find All'),
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Colors.white,
+ foregroundColor: Colors.black87,
+ side: BorderSide(color: Colors.grey.shade300, width: 0.5),
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
+ textStyle: const TextStyle(fontSize: 13, fontFamily: 'Segoe UI'),
+ elevation: 1,
+ ),
+ onPressed: () {
+ _performSearch();
+ Navigator.pop(context);
+ },
+ ),
+ ElevatedButton.icon(
+ icon: Icon(Icons.swap_horiz, size: 14, color: Colors.white),
+ label: const Text('Replace'),
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Colors.grey.shade700,
+ foregroundColor: Colors.white,
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
+ textStyle: const TextStyle(fontSize: 13, fontFamily: 'Segoe UI'),
+ elevation: 1,
+ ),
+ onPressed: () {
+ _replaceSelected();
+ Navigator.pop(context);
+ },
+ ),
+ ElevatedButton.icon(
+ icon: Icon(Icons.swap_vert, size: 14, color: Colors.white),
+ label: const Text('Replace All'),
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Colors.black87,
+ foregroundColor: Colors.white,
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
+ textStyle: const TextStyle(fontSize: 13, fontFamily: 'Segoe UI'),
+ elevation: 1,
+ ),
+ onPressed: () {
+ _replaceAll();
+ Navigator.pop(context);
+ },
+ ),
+ ],
+ ),
+ );
+ }
+
+ void _performSearch() {
+ final text = _controller.text;
+ final searchTerm = _searchController.text;
+
+ if (searchTerm.isEmpty) {
+ setState(() {
+ _searchResults.clear();
+ _currentSearchIndex = -1;
+ });
+ return;
+ }
+
+ setState(() {
+ _searchResults.clear();
+ _currentSearchIndex = -1;
+
+ // Find all occurrences
+ int index = 0;
+ while (index < text.length) {
+ final foundIndex = text.indexOf(searchTerm, index);
+ if (foundIndex == -1) break;
+ _searchResults.add(foundIndex);
+ index = foundIndex + 1;
+ }
+
+ if (_searchResults.isNotEmpty) {
+ _currentSearchIndex = 0;
+ _highlightSearchResult();
+ }
+ });
+ }
+
+ void _findNext() {
+ if (_searchResults.isEmpty) {
+ _performSearch();
+ return;
+ }
+
+ setState(() {
+ _currentSearchIndex = (_currentSearchIndex + 1) % _searchResults.length;
+ _highlightSearchResult();
+ });
+ }
+
+ void _findPrevious() {
+ if (_searchResults.isEmpty) {
+ _performSearch();
+ return;
+ }
+
+ setState(() {
+ _currentSearchIndex =
+ (_currentSearchIndex - 1 + _searchResults.length) %
+ _searchResults.length;
+ _highlightSearchResult();
+ });
+ }
+
+ void _highlightSearchResult() {
+ if (_searchResults.isEmpty || _currentSearchIndex == -1) return;
+
+ final position = _searchResults[_currentSearchIndex];
+ final searchTerm = _searchController.text;
+
+ _controller.selection = TextSelection(
+ baseOffset: position,
+ extentOffset: position + searchTerm.length,
+ );
+ }
+
+ void _replaceSelected() {
+ if (_searchResults.isEmpty || _currentSearchIndex == -1) return;
+
+ final text = _controller.text;
+ final searchTerm = _searchController.text;
+ final replaceTerm = _replaceController.text;
+ final position = _searchResults[_currentSearchIndex];
+
+ final newText = text.replaceRange(
+ position,
+ position + searchTerm.length,
+ replaceTerm,
+ );
+
+ setState(() {
+ _controller.text = newText;
+ _isModified = true;
+ });
+
+ // Refresh search results
+ _performSearch();
+ }
+
+ void _replaceAll() {
+ final searchTerm = _searchController.text;
+ final replaceTerm = _replaceController.text;
+
+ if (searchTerm.isEmpty) return;
+
+ final newText = _controller.text.replaceAll(searchTerm, replaceTerm);
+
+ setState(() {
+ _controller.text = newText;
+ _isModified = true;
+ _searchResults.clear();
+ _currentSearchIndex = -1;
+ });
+
+ _showSnackBar('Replaced all occurrences');
+ }
+
+ void _showUnsavedChangesDialog(VoidCallback onProceed) {
+ showDialog(
+ context: context,
+ builder: (BuildContext context) {
+ return AlertDialog(
+ title: const Text('Unsaved Changes'),
+ content: Text('Do you want to save changes to $_currentFileName?'),
+ actions: [
+ TextButton(
+ onPressed: () {
+ Navigator.of(context).pop();
+ onProceed();
+ },
+ child: const Text('Don\'t Save'),
+ ),
+ TextButton(
+ onPressed: () {
+ Navigator.of(context).pop();
+ },
+ child: const Text('Cancel'),
+ ),
+ TextButton(
+ onPressed: () {
+ Navigator.of(context).pop();
+ _saveFile().then((_) => onProceed());
+ },
+ child: const Text('Save'),
+ ),
+ ],
+ );
+ },
+ );
+ }
+
+ Future _saveFile() async {
+ try {
+ // Apply random indentation chaos before saving
+ _chaosManager?.randomIndentationOnSave();
+
+ if (_currentFilePath != null) {
+ // Save directly to existing file without popup
+ final file = File(_currentFilePath!);
+ await file.writeAsString(_controller.text);
+ setState(() {
+ _isModified = false;
+ });
+ _showSnackBar('File saved successfully');
+ } else {
+ // Use native file dialog for new files
+ await _saveAsFile();
+ }
+ } catch (e) {
+ _showSnackBar('Error saving file: $e');
+ }
+ }
+
+ Future _saveAsFile() async {
+ try {
+ // Use native Windows file dialog
+ String? outputFile = await FilePicker.platform.saveFile(
+ dialogTitle: 'Save File As',
+ fileName: _currentFileName.endsWith('.txt')
+ ? _currentFileName
+ : '${_currentFileName.replaceAll('.txt', '')}.txt',
+ type: FileType.custom,
+ allowedExtensions: ['txt'],
+ );
+
+ if (outputFile != null) {
+ // Apply random indentation chaos before saving
+ _chaosManager?.randomIndentationOnSave();
+
+ final file = File(outputFile);
+ await file.writeAsString(_controller.text);
+
+ setState(() {
+ _currentFilePath = outputFile;
+ _currentFileName = outputFile.split(Platform.pathSeparator).last;
+ _isModified = false;
+ });
+
+ _showSnackBar('File saved as $_currentFileName');
+ }
+ } catch (e) {
+ _showSnackBar('Error saving file: $e');
+ }
+ }
+
+ void _showSnackBar(String message) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text(message), duration: const Duration(seconds: 2)),
+ );
+ }
+
+ void _selectAll() {
+ _controller.selection = TextSelection(
+ baseOffset: 0,
+ extentOffset: _controller.text.length,
+ );
+ }
+
+ void _handleSave() {
+ _saveFile();
+ }
+
+ // Text formatting methods
+ void _toggleBold() {
+ setState(() {
+ _isBold = !_isBold;
+ });
+ }
+
+ void _toggleItalic() {
+ setState(() {
+ _isItalic = !_isItalic;
+ });
+ }
+
+ void _toggleUnderline() {
+ setState(() {
+ _isUnderline = !_isUnderline;
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Shortcuts(
+ shortcuts: {
+ LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyA):
+ const SelectAllIntent(),
+ LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyS):
+ const SaveIntent(),
+ LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyN):
+ const NewFileIntent(),
+ LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyO):
+ const OpenFileIntent(),
+ LogicalKeySet(
+ LogicalKeyboardKey.control,
+ LogicalKeyboardKey.shift,
+ LogicalKeyboardKey.keyS,
+ ): const SaveAsIntent(),
+ LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyF):
+ const FindIntent(),
+ LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyH):
+ const ReplaceIntent(),
+ LogicalKeySet(LogicalKeyboardKey.f3): const FindNextIntent(),
+ LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.f3):
+ const FindPreviousIntent(),
+ },
+ child: Actions(
+ actions: >{
+ SelectAllIntent: CallbackAction(
+ onInvoke: (SelectAllIntent intent) {
+ _selectAll();
+ return null;
+ },
+ ),
+ SaveIntent: CallbackAction(
+ onInvoke: (SaveIntent intent) {
+ _handleSave();
+ return null;
+ },
+ ),
+ NewFileIntent: CallbackAction(
+ onInvoke: (NewFileIntent intent) {
+ _newFile();
+ return null;
+ },
+ ),
+ OpenFileIntent: CallbackAction(
+ onInvoke: (OpenFileIntent intent) {
+ _openFile();
+ return null;
+ },
+ ),
+ SaveAsIntent: CallbackAction(
+ onInvoke: (SaveAsIntent intent) {
+ _saveAsFile();
+ return null;
+ },
+ ),
+ FindIntent: CallbackAction(
+ onInvoke: (FindIntent intent) {
+ _showFindDialog();
+ return null;
+ },
+ ),
+ ReplaceIntent: CallbackAction(
+ onInvoke: (ReplaceIntent intent) {
+ _showReplaceDialog();
+ return null;
+ },
+ ),
+ FindNextIntent: CallbackAction(
+ onInvoke: (FindNextIntent intent) {
+ _findNext();
+ return null;
+ },
+ ),
+ FindPreviousIntent: CallbackAction(
+ onInvoke: (FindPreviousIntent intent) {
+ _findPrevious();
+ return null;
+ },
+ ),
+ },
+ child: Focus(
+ autofocus: true,
+ child: Scaffold(
+ backgroundColor: Colors.grey.shade50,
+ body: Column(
+ children: [
+ Container(
+ height: 36,
+ decoration: BoxDecoration(
+ color: Colors.white,
+ border: Border(
+ bottom: BorderSide(
+ color: Colors.grey.shade200,
+ width: 0.5,
+ ),
+ ),
+ ),
+ child: Row(
+ children: [
+ // Tab list - scrollable if too many tabs
+ Expanded(
+ child: ListView.builder(
+ scrollDirection: Axis.horizontal,
+ itemCount: _tabs.length,
+ itemBuilder: (context, index) {
+ final tab = _tabs[index];
+ final isActive = index == _currentTabIndex;
+
+ return Container(
+ margin: const EdgeInsets.symmetric(
+ horizontal: 2,
+ vertical: 4,
+ ),
+ padding: const EdgeInsets.symmetric(
+ horizontal: 8,
+ vertical: 4,
+ ),
+ decoration: BoxDecoration(
+ color: isActive
+ ? Colors.white
+ : Colors.grey.shade100,
+ borderRadius: BorderRadius.circular(4),
+ border: Border.all(
+ color: isActive
+ ? Colors.grey.shade300
+ : Colors.grey.shade200,
+ width: 0.5,
+ ),
+ ),
+ child: Material(
+ color: Colors.transparent,
+ child: InkWell(
+ onTap: () => _switchToTab(index),
+ borderRadius: BorderRadius.circular(4),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text(
+ tab.isModified
+ ? '${tab.fileName}*'
+ : tab.fileName,
+ style: TextStyle(
+ fontSize: 10,
+ fontWeight: isActive
+ ? FontWeight.w600
+ : FontWeight.w400,
+ color: isActive
+ ? Colors.black87
+ : Colors.grey.shade600,
+ ),
+ ),
+ const SizedBox(width: 4),
+ Material(
+ color: Colors.transparent,
+ child: InkWell(
+ onTap: () => _closeTab(index),
+ borderRadius: BorderRadius.circular(
+ 10,
+ ),
+ child: Icon(
+ Icons.close,
+ size: 10,
+ color: isActive
+ ? Colors.grey.shade600
+ : Colors.grey.shade400,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ },
+ ),
+ ),
+ // Add new tab button
+ Container(
+ width: 20,
+ height: 20,
+ margin: const EdgeInsets.only(left: 3, right: 6),
+ decoration: BoxDecoration(
+ border: Border.all(
+ color: Colors.pink.shade100,
+ width: 0.8,
+ ),
+ borderRadius: BorderRadius.circular(6),
+ color: Colors.pink.shade50,
+ ),
+ child: Material(
+ color: Colors.transparent,
+ child: InkWell(
+ onTap: _newFile,
+ borderRadius: BorderRadius.circular(6),
+ child: Icon(
+ Icons.add,
+ size: 12,
+ color: Colors.grey.shade400,
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+
+ // Toolbar
+ Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 12,
+ vertical: 8,
+ ),
+ decoration: BoxDecoration(
+ color: Colors.white,
+ border: Border(
+ bottom: BorderSide(
+ color: const Color(0xFFE0E0E0),
+ width: 0.5,
+ ),
+ ),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withValues(alpha: 0.05),
+ blurRadius: 4,
+ offset: const Offset(0, 2),
+ ),
+ ],
+ ),
+ child: Row(
+ children: [
+ // File menu
+ Container(
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(6),
+ color: Colors.transparent,
+ border: null,
+ ),
+ child: Theme(
+ data: Theme.of(context).copyWith(
+ popupMenuTheme: PopupMenuThemeData(
+ color: Colors.white,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(4),
+ side: BorderSide(
+ color: Colors.grey.shade300,
+ width: 0.5,
+ ),
+ ),
+ elevation: 2,
+ ),
+ ),
+ child: PopupMenuButton(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 8,
+ vertical: 4,
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(
+ Icons.folder_outlined,
+ size: 12,
+ color: Colors.black87,
+ ),
+ const SizedBox(width: 4),
+ Text(
+ 'File',
+ style: TextStyle(
+ fontSize: 11,
+ fontWeight: FontWeight.w500,
+ color: Colors.black87,
+ ),
+ ),
+ ],
+ ),
+ ),
+ onSelected: (value) {
+ switch (value) {
+ case 'new':
+ _newFile();
+ break;
+ case 'open':
+ _openFile();
+ break;
+ case 'save':
+ _handleSave();
+ break;
+ case 'save_as':
+ _saveAsFile();
+ break;
+ case 'exit':
+ _exitApplication();
+ break;
+ }
+ },
+ itemBuilder: (context) => [
+ PopupMenuItem(
+ height: 32,
+ padding: const EdgeInsets.symmetric(
+ horizontal: 12,
+ vertical: 4,
+ ),
+ value: 'new',
+ child: Row(
+ children: [
+ Icon(
+ Icons.note_add,
+ size: 14,
+ color: Colors.black87,
+ ),
+ const SizedBox(width: 8),
+ Text(
+ 'New (Ctrl+N)',
+ style: TextStyle(
+ fontSize: 13,
+ fontWeight: FontWeight.w400,
+ color: Colors.black87,
+ fontFamily: 'Segoe UI',
+ ),
+ ),
+ ],
+ ),
+ ),
+ PopupMenuItem(
+ height: 32,
+ padding: const EdgeInsets.symmetric(
+ horizontal: 12,
+ vertical: 4,
+ ),
+ value: 'open',
+ child: Row(
+ children: [
+ Icon(
+ Icons.folder_open,
+ size: 14,
+ color: Colors.black87,
+ ),
+ const SizedBox(width: 8),
+ Text(
+ 'Open (Ctrl+O)',
+ style: TextStyle(
+ fontSize: 13,
+ fontWeight: FontWeight.w400,
+ color: Colors.black87,
+ fontFamily: 'Segoe UI',
+ ),
+ ),
+ ],
+ ),
+ ),
+ PopupMenuItem(
+ height: 32,
+ padding: const EdgeInsets.symmetric(
+ horizontal: 12,
+ vertical: 4,
+ ),
+ value: 'save',
+ child: Row(
+ children: [
+ Icon(
+ Icons.save,
+ size: 14,
+ color: Colors.black87,
+ ),
+ const SizedBox(width: 8),
+ Text(
+ 'Save (Ctrl+S)',
+ style: TextStyle(
+ fontSize: 13,
+ fontWeight: FontWeight.w400,
+ color: Colors.black87,
+ fontFamily: 'Segoe UI',
+ ),
+ ),
+ ],
+ ),
+ ),
+ PopupMenuItem(
+ height: 32,
+ padding: const EdgeInsets.symmetric(
+ horizontal: 12,
+ vertical: 4,
+ ),
+ value: 'save_as',
+ child: Row(
+ children: [
+ Icon(
+ Icons.save_as,
+ size: 14,
+ color: Colors.black87,
+ ),
+ const SizedBox(width: 8),
+ Text(
+ 'Save As (Ctrl+Shift+S)',
+ style: TextStyle(
+ fontSize: 13,
+ fontWeight: FontWeight.w400,
+ color: Colors.black87,
+ fontFamily: 'Segoe UI',
+ ),
+ ),
+ ],
+ ),
+ ),
+ PopupMenuItem(
+ height: 32,
+ padding: const EdgeInsets.symmetric(
+ horizontal: 12,
+ vertical: 4,
+ ),
+ value: 'exit',
+ child: Row(
+ children: [
+ Icon(
+ Icons.exit_to_app,
+ size: 14,
+ color: Colors.black87,
+ ),
+ const SizedBox(width: 8),
+ Text(
+ 'Exit',
+ style: TextStyle(
+ fontSize: 13,
+ fontWeight: FontWeight.w400,
+ color: Colors.black87,
+ fontFamily: 'Segoe UI',
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ // Edit menu
+ Container(
+ margin: const EdgeInsets.only(left: 4),
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(6),
+ ),
+ child: Theme(
+ data: Theme.of(context).copyWith(
+ popupMenuTheme: PopupMenuThemeData(
+ color: Colors.white,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(4),
+ side: BorderSide(
+ color: Colors.grey.shade300,
+ width: 0.5,
+ ),
+ ),
+ elevation: 2,
+ ),
+ ),
+ child: PopupMenuButton(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 8,
+ vertical: 4,
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(
+ Icons.edit_outlined,
+ size: 12,
+ color: Colors.black87,
+ ),
+ const SizedBox(width: 4),
+ Text(
+ 'Edit',
+ style: TextStyle(
+ fontSize: 11,
+ fontWeight: FontWeight.w500,
+ color: Colors.black87,
+ ),
+ ),
+ ],
+ ),
+ ),
+ onSelected: (value) {
+ switch (value) {
+ case 'select_all':
+ _selectAll();
+ break;
+ case 'find':
+ _showFindDialog();
+ break;
+ case 'replace':
+ _showReplaceDialog();
+ break;
+ case 'clear':
+ _clearFormatting();
+ break;
+ case 'go_to_line':
+ _goToLine();
+ break;
+ case 'insert_datetime':
+ _insertDateTime();
+ break;
+ }
+ },
+ itemBuilder: (context) => [
+ PopupMenuItem(
+ height: 32,
+ padding: const EdgeInsets.symmetric(
+ horizontal: 12,
+ vertical: 4,
+ ),
+ value: 'select_all',
+ child: Row(
+ children: [
+ Icon(
+ Icons.select_all,
+ size: 14,
+ color: Colors.black87,
+ ),
+ const SizedBox(width: 8),
+ Text(
+ 'Select All (Ctrl+A)',
+ style: TextStyle(
+ fontSize: 13,
+ fontWeight: FontWeight.w400,
+ color: Colors.black87,
+ fontFamily: 'Segoe UI',
+ ),
+ ),
+ ],
+ ),
+ ),
+ PopupMenuItem(
+ height: 32,
+ padding: const EdgeInsets.symmetric(
+ horizontal: 12,
+ vertical: 4,
+ ),
+ value: 'find',
+ child: Row(
+ children: [
+ Icon(
+ Icons.search,
+ size: 14,
+ color: Colors.black87,
+ ),
+ const SizedBox(width: 8),
+ Text(
+ 'Find (Ctrl+F)',
+ style: TextStyle(
+ fontSize: 13,
+ fontWeight: FontWeight.w400,
+ color: Colors.black87,
+ fontFamily: 'Segoe UI',
+ ),
+ ),
+ ],
+ ),
+ ),
+ PopupMenuItem(
+ height: 32,
+ padding: const EdgeInsets.symmetric(
+ horizontal: 12,
+ vertical: 4,
+ ),
+ value: 'replace',
+ child: Row(
+ children: [
+ Icon(
+ Icons.find_replace,
+ size: 14,
+ color: Colors.black87,
+ ),
+ const SizedBox(width: 8),
+ Text(
+ 'Replace (Ctrl+H)',
+ style: TextStyle(
+ fontSize: 13,
+ fontWeight: FontWeight.w400,
+ color: Colors.black87,
+ fontFamily: 'Segoe UI',
+ ),
+ ),
+ ],
+ ),
+ ),
+ PopupMenuItem(
+ height: 32,
+ padding: const EdgeInsets.symmetric(
+ horizontal: 12,
+ vertical: 4,
+ ),
+ value: 'clear',
+ child: Row(
+ children: [
+ Icon(
+ Icons.clear_all,
+ size: 14,
+ color: Colors.black87,
+ ),
+ const SizedBox(width: 8),
+ Text(
+ 'Clear Formatting',
+ style: TextStyle(
+ fontSize: 13,
+ fontWeight: FontWeight.w400,
+ color: Colors.black87,
+ fontFamily: 'Segoe UI',
+ ),
+ ),
+ ],
+ ),
+ ),
+ PopupMenuItem(
+ height: 32,
+ padding: const EdgeInsets.symmetric(
+ horizontal: 12,
+ vertical: 4,
+ ),
+ value: 'go_to_line',
+ child: Row(
+ children: [
+ Icon(
+ Icons.linear_scale,
+ size: 14,
+ color: Colors.black87,
+ ),
+ const SizedBox(width: 8),
+ Text(
+ 'Go to Line (Ctrl+G)',
+ style: TextStyle(
+ fontSize: 13,
+ fontWeight: FontWeight.w400,
+ color: Colors.black87,
+ fontFamily: 'Segoe UI',
+ ),
+ ),
+ ],
+ ),
+ ),
+ PopupMenuItem(
+ height: 32,
+ padding: const EdgeInsets.symmetric(
+ horizontal: 12,
+ vertical: 4,
+ ),
+ value: 'insert_datetime',
+ child: Row(
+ children: [
+ Icon(
+ Icons.access_time,
+ size: 14,
+ color: Colors.black87,
+ ),
+ const SizedBox(width: 8),
+ Text(
+ 'Insert Date/Time (F5)',
+ style: TextStyle(
+ fontSize: 13,
+ fontWeight: FontWeight.w400,
+ color: Colors.black87,
+ fontFamily: 'Segoe UI',
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ // Text formatting buttons
+ _buildFormatButton(
+ Icons.format_bold,
+ _isBold,
+ _toggleBold,
+ ),
+ _buildFormatButton(
+ Icons.format_italic,
+ _isItalic,
+ _toggleItalic,
+ ),
+ _buildFormatButton(
+ Icons.format_underlined,
+ _isUnderline,
+ _toggleUnderline,
+ ),
+ ],
+ ),
+ ),
+
+ // Text Editor Area
+ Expanded(
+ child: Container(
+ color: Colors.white,
+ child: KeyboardListener(
+ focusNode: FocusNode(),
+ onKeyEvent: (KeyEvent event) {
+ // Only record on key down events to avoid duplicates
+ if (event is KeyDownEvent) {
+ _typingSpeedMonitor?.recordKeystroke();
+ }
+ },
+ child: TextField(
+ controller: _controller,
+ focusNode: _focusNode,
+ maxLines: null,
+ expands: true,
+ onChanged: (String newText) {
+ setState(() {
+ _isModified = true;
+ });
+ _updateStats();
+ // Reset idle timer when user types
+ _chaosManager?.onUserActivity();
+ // Don't call recordKeystroke here to avoid conflicts
+ },
+ style: TextStyle(
+ fontSize: 14,
+ fontFamily: 'Segoe UI',
+ color: Colors.black,
+ height: 1.4,
+ fontWeight: FontWeight.w400,
+ ),
+ decoration: const InputDecoration(
+ border: InputBorder.none,
+ contentPadding: EdgeInsets.all(16),
+ hintText: 'Start typing...',
+ hintStyle: TextStyle(
+ color: Color(0xFF999999),
+ fontSize: 14,
+ ),
+ ),
+ textAlign: TextAlign.start,
+ textAlignVertical: TextAlignVertical.top,
+ cursorColor: Colors.black,
+ cursorWidth: 1.0,
+ ),
+ ),
+ ),
+ ),
+
+ // Status Bar
+ Container(
+ height: 24,
+ padding: const EdgeInsets.symmetric(horizontal: 16),
+ decoration: const BoxDecoration(
+ color: Color(0xFFF8F8F8),
+ border: Border(
+ top: BorderSide(color: Color(0xFFE0E0E0), width: 0.5),
+ ),
+ ),
+ child: Row(
+ children: [
+ Text(
+ 'Ln $_lineNumber, Col $_columnNumber',
+ style: const TextStyle(
+ fontSize: 11,
+ color: Colors.black54,
+ ),
+ ),
+ const SizedBox(width: 16),
+ Text(
+ 'Words: $_wordCount | Characters: $_charCount',
+ style: const TextStyle(
+ fontSize: 11,
+ color: Colors.black54,
+ ),
+ ),
+ const Spacer(),
+ // Chaos Mode Indicator
+ if (_isChaosEnabled) ...[
+ Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 4,
+ vertical: 1,
+ ),
+ decoration: BoxDecoration(
+ color: Colors.red.shade100,
+ borderRadius: BorderRadius.circular(3),
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(
+ Icons.warning_amber_rounded,
+ size: 10,
+ color: Colors.red.shade600,
+ ),
+ const SizedBox(width: 2),
+ Text(
+ 'CHAOS',
+ style: TextStyle(
+ fontSize: 9,
+ fontWeight: FontWeight.w600,
+ color: Colors.red.shade700,
+ ),
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(width: 16),
+ ],
+ const Text(
+ 'Plain text',
+ style: TextStyle(fontSize: 11, color: Colors.black54),
+ ),
+ const SizedBox(width: 16),
+ const Text(
+ '100%',
+ style: TextStyle(fontSize: 11, color: Colors.black54),
+ ),
+ const SizedBox(width: 16),
+ const Text(
+ 'Windows (CRLF)',
+ style: TextStyle(fontSize: 11, color: Colors.black54),
+ ),
+ const SizedBox(width: 16),
+ const Text(
+ 'UTF-8',
+ style: TextStyle(fontSize: 11, color: Colors.black54),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildFormatButton(IconData icon, bool isActive, VoidCallback onTap) {
+ return Container(
+ width: 24,
+ height: 24,
+ decoration: BoxDecoration(
+ color: isActive ? Colors.blue.shade100 : Colors.transparent,
+ border: Border.all(
+ color: isActive ? Colors.blue.shade300 : Colors.grey.shade300,
+ width: 0.8,
+ ),
+ borderRadius: BorderRadius.circular(4),
+ ),
+ child: Material(
+ color: Colors.transparent,
+ child: InkWell(
+ onTap: onTap,
+ borderRadius: BorderRadius.circular(4),
+ child: Icon(
+ icon,
+ size: 14,
+ color: isActive ? Colors.blue.shade600 : Colors.grey.shade600,
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/screens/chaos_settings_screen.dart b/lib/screens/chaos_settings_screen.dart
new file mode 100644
index 000000000..39bc48625
--- /dev/null
+++ b/lib/screens/chaos_settings_screen.dart
@@ -0,0 +1,283 @@
+import 'package:flutter/material.dart';
+
+class ChaosSettingsScreen extends StatefulWidget {
+ final bool isChaosEnabled;
+ final Function(bool) onChaosToggle;
+
+ const ChaosSettingsScreen({
+ super.key,
+ required this.isChaosEnabled,
+ required this.onChaosToggle,
+ });
+
+ @override
+ State createState() => _ChaosSettingsScreenState();
+}
+
+class _ChaosSettingsScreenState extends State {
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: Colors.white,
+ appBar: AppBar(
+ backgroundColor: Colors.white,
+ elevation: 0,
+ leading: IconButton(
+ icon: const Icon(Icons.arrow_back, color: Colors.black54),
+ onPressed: () => Navigator.pop(context),
+ ),
+ title: const Text(
+ 'Settings',
+ style: TextStyle(
+ color: Colors.black,
+ fontSize: 16,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ ),
+ body: Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Chaos Mode Section
+ Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.red.shade50,
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(color: Colors.red.shade100),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ Icon(
+ Icons.warning_amber_rounded,
+ color: Colors.red.shade600,
+ size: 20,
+ ),
+ const SizedBox(width: 8),
+ Text(
+ 'Chaos Mode',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.w600,
+ color: Colors.red.shade700,
+ ),
+ ),
+ const Spacer(),
+ Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 8,
+ vertical: 4,
+ ),
+ decoration: BoxDecoration(
+ color: Colors.red.shade200,
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Text(
+ 'ALWAYS ON',
+ style: TextStyle(
+ fontSize: 10,
+ fontWeight: FontWeight.w700,
+ color: Colors.red.shade800,
+ ),
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ Text(
+ 'Chaotic text editor behaviors are permanently active and will randomly interfere with your typing experience.',
+ style: TextStyle(
+ fontSize: 12,
+ color: Colors.red.shade600,
+ height: 1.4,
+ ),
+ ),
+ // Always show chaos descriptions since it's permanently active
+ const SizedBox(height: 16),
+ Container(
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: Colors.red.shade100,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ 'Active Chaos Behaviors:',
+ style: TextStyle(
+ fontSize: 11,
+ fontWeight: FontWeight.w600,
+ color: Colors.red.shade800,
+ ),
+ ),
+ const SizedBox(height: 6),
+ ..._buildChaosDescriptions(),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // General Settings Section
+ Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.grey.shade50,
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(color: Colors.grey.shade200),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ Icon(
+ Icons.settings_outlined,
+ color: Colors.grey.shade600,
+ size: 20,
+ ),
+ const SizedBox(width: 8),
+ Text(
+ 'Editor Settings',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.w600,
+ color: Colors.grey.shade700,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 16),
+ _buildSettingTile(
+ 'Font Size',
+ '14px',
+ Icons.text_fields,
+ onTap: () {},
+ ),
+ _buildSettingTile(
+ 'Theme',
+ 'Light',
+ Icons.palette_outlined,
+ onTap: () {},
+ ),
+ _buildSettingTile(
+ 'Auto Save',
+ 'Every 30 seconds',
+ Icons.save_outlined,
+ onTap: () {},
+ ),
+ _buildSettingTile(
+ 'Chaos Interval',
+ '8-15 seconds',
+ Icons.timer_outlined,
+ onTap: () {},
+ ),
+ ],
+ ),
+ ),
+
+ const Spacer(),
+
+ // Warning Footer - Always show since chaos is permanent
+ Container(
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: Colors.orange.shade50,
+ borderRadius: BorderRadius.circular(8),
+ border: Border.all(color: Colors.orange.shade200),
+ ),
+ child: Row(
+ children: [
+ Icon(
+ Icons.info_outline,
+ color: Colors.orange.shade600,
+ size: 16,
+ ),
+ const SizedBox(width: 8),
+ Expanded(
+ child: Text(
+ 'Chaos mode is permanently active. Your text will be randomly modified while typing every 8-15 seconds.',
+ style: TextStyle(
+ fontSize: 10,
+ color: Colors.orange.shade700,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ List _buildChaosDescriptions() {
+ final descriptions = [
+ '• Random cursor displacement while typing',
+ '• Spontaneous deletion of characters or words',
+ '• Letter and word position swapping',
+ '• Unexpected punctuation insertion',
+ '• Random line indentation on save',
+ '• Chaos interval: 8-15 seconds (increased)',
+ ];
+
+ return descriptions
+ .map(
+ (desc) => Padding(
+ padding: const EdgeInsets.only(bottom: 2),
+ child: Text(
+ desc,
+ style: TextStyle(fontSize: 10, color: Colors.red.shade700),
+ ),
+ ),
+ )
+ .toList();
+ }
+
+ Widget _buildSettingTile(
+ String title,
+ String value,
+ IconData icon, {
+ VoidCallback? onTap,
+ }) {
+ return InkWell(
+ onTap: onTap,
+ borderRadius: BorderRadius.circular(6),
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 8),
+ child: Row(
+ children: [
+ Icon(icon, size: 16, color: Colors.grey.shade500),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Text(
+ title,
+ style: TextStyle(fontSize: 13, color: Colors.grey.shade700),
+ ),
+ ),
+ Text(
+ value,
+ style: TextStyle(fontSize: 12, color: Colors.grey.shade500),
+ ),
+ const SizedBox(width: 8),
+ Icon(
+ Icons.arrow_forward_ios,
+ size: 12,
+ color: Colors.grey.shade400,
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/pubspec.lock b/pubspec.lock
new file mode 100644
index 000000000..e3e6c480b
--- /dev/null
+++ b/pubspec.lock
@@ -0,0 +1,274 @@
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+ async:
+ dependency: transitive
+ description:
+ name: async
+ sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.13.0"
+ boolean_selector:
+ dependency: transitive
+ description:
+ name: boolean_selector
+ sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.2"
+ characters:
+ dependency: transitive
+ description:
+ name: characters
+ sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.4.0"
+ clock:
+ dependency: transitive
+ description:
+ name: clock
+ sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.2"
+ collection:
+ dependency: transitive
+ description:
+ name: collection
+ sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.19.1"
+ cross_file:
+ dependency: transitive
+ description:
+ name: cross_file
+ sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.3.4+2"
+ cupertino_icons:
+ dependency: "direct main"
+ description:
+ name: cupertino_icons
+ sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.8"
+ fake_async:
+ dependency: transitive
+ description:
+ name: fake_async
+ sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.3.3"
+ ffi:
+ dependency: transitive
+ description:
+ name: ffi
+ sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.4"
+ file_picker:
+ dependency: "direct main"
+ description:
+ name: file_picker
+ sha256: ab13ae8ef5580a411c458d6207b6774a6c237d77ac37011b13994879f68a8810
+ url: "https://pub.dev"
+ source: hosted
+ version: "8.3.7"
+ flutter:
+ dependency: "direct main"
+ description: flutter
+ source: sdk
+ version: "0.0.0"
+ flutter_lints:
+ dependency: "direct dev"
+ description:
+ name: flutter_lints
+ sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
+ url: "https://pub.dev"
+ source: hosted
+ version: "5.0.0"
+ flutter_plugin_android_lifecycle:
+ dependency: transitive
+ description:
+ name: flutter_plugin_android_lifecycle
+ sha256: "6382ce712ff69b0f719640ce957559dde459e55ecd433c767e06d139ddf16cab"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.29"
+ flutter_test:
+ dependency: "direct dev"
+ description: flutter
+ source: sdk
+ version: "0.0.0"
+ flutter_web_plugins:
+ dependency: transitive
+ description: flutter
+ source: sdk
+ version: "0.0.0"
+ leak_tracker:
+ dependency: transitive
+ description:
+ name: leak_tracker
+ sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
+ url: "https://pub.dev"
+ source: hosted
+ version: "10.0.9"
+ leak_tracker_flutter_testing:
+ dependency: transitive
+ description:
+ name: leak_tracker_flutter_testing
+ sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.9"
+ leak_tracker_testing:
+ dependency: transitive
+ description:
+ name: leak_tracker_testing
+ sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.1"
+ lints:
+ dependency: transitive
+ description:
+ name: lints
+ sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7
+ url: "https://pub.dev"
+ source: hosted
+ version: "5.1.1"
+ matcher:
+ dependency: transitive
+ description:
+ name: matcher
+ sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.12.17"
+ material_color_utilities:
+ dependency: transitive
+ description:
+ name: material_color_utilities
+ sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.11.1"
+ meta:
+ dependency: transitive
+ description:
+ name: meta
+ sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.16.0"
+ path:
+ dependency: transitive
+ description:
+ name: path
+ sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.9.1"
+ plugin_platform_interface:
+ dependency: transitive
+ description:
+ name: plugin_platform_interface
+ sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.8"
+ sky_engine:
+ dependency: transitive
+ description: flutter
+ source: sdk
+ version: "0.0.0"
+ source_span:
+ dependency: transitive
+ description:
+ name: source_span
+ sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.10.1"
+ stack_trace:
+ dependency: transitive
+ description:
+ name: stack_trace
+ sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.12.1"
+ stream_channel:
+ dependency: transitive
+ description:
+ name: stream_channel
+ sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.4"
+ string_scanner:
+ dependency: transitive
+ description:
+ name: string_scanner
+ sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.4.1"
+ term_glyph:
+ dependency: transitive
+ description:
+ name: term_glyph
+ sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.2.2"
+ test_api:
+ dependency: transitive
+ description:
+ name: test_api
+ sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.7.4"
+ vector_math:
+ dependency: transitive
+ description:
+ name: vector_math
+ sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.4"
+ vm_service:
+ dependency: transitive
+ description:
+ name: vm_service
+ sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
+ url: "https://pub.dev"
+ source: hosted
+ version: "15.0.0"
+ web:
+ dependency: transitive
+ description:
+ name: web
+ sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.1"
+ win32:
+ dependency: transitive
+ description:
+ name: win32
+ sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03"
+ url: "https://pub.dev"
+ source: hosted
+ version: "5.14.0"
+sdks:
+ dart: ">=3.8.1 <4.0.0"
+ flutter: ">=3.27.0"
diff --git a/pubspec.yaml b/pubspec.yaml
new file mode 100644
index 000000000..1f24bcb88
--- /dev/null
+++ b/pubspec.yaml
@@ -0,0 +1,34 @@
+name: scampad
+description: "ScamPad - A chaotic notepad application"
+
+publish_to: 'none'
+
+version: 1.0.0+1
+
+environment:
+ sdk: ^3.8.1
+
+
+dependencies:
+ flutter:
+ sdk: flutter
+
+ cupertino_icons: ^1.0.8
+ file_picker: ^8.1.2
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
+
+ flutter_lints: ^5.0.0
+
+
+
+# The following section is specific to Flutter packages.
+flutter:
+
+ uses-material-design: true
+
+ # App icons and assets
+ assets:
+ - assets/icons/
diff --git a/test/widget_test.dart b/test/widget_test.dart
new file mode 100644
index 000000000..b71e970e6
--- /dev/null
+++ b/test/widget_test.dart
@@ -0,0 +1,30 @@
+// This is a basic Flutter widget test.
+//
+// To perform an interaction with a widget in your test, use the WidgetTester
+// utility in the flutter_test package. For example, you can send tap and scroll
+// gestures. You can also use WidgetTester to find child widgets in the widget
+// tree, read text, and verify that the values of widget properties are correct.
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import 'package:scampad/main.dart';
+
+void main() {
+ testWidgets('Counter increments smoke test', (WidgetTester tester) async {
+ // Build our app and trigger a frame.
+ await tester.pumpWidget(const MyApp());
+
+ // Verify that our counter starts at 0.
+ expect(find.text('0'), findsOneWidget);
+ expect(find.text('1'), findsNothing);
+
+ // Tap the '+' icon and trigger a frame.
+ await tester.tap(find.byIcon(Icons.add));
+ await tester.pump();
+
+ // Verify that our counter has incremented.
+ expect(find.text('0'), findsNothing);
+ expect(find.text('1'), findsOneWidget);
+ });
+}
diff --git a/windows/.gitignore b/windows/.gitignore
new file mode 100644
index 000000000..d492d0d98
--- /dev/null
+++ b/windows/.gitignore
@@ -0,0 +1,17 @@
+flutter/ephemeral/
+
+# Visual Studio user-specific files.
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# Visual Studio build-related files.
+x64/
+x86/
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!*.[Cc]ache/
diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt
new file mode 100644
index 000000000..ff3546580
--- /dev/null
+++ b/windows/CMakeLists.txt
@@ -0,0 +1,108 @@
+# Project-level configuration.
+cmake_minimum_required(VERSION 3.14)
+project(scampad LANGUAGES CXX)
+
+# The name of the executable created for the application. Change this to change
+# the on-disk name of your application.
+set(BINARY_NAME "scampad")
+
+# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
+# versions of CMake.
+cmake_policy(VERSION 3.14...3.25)
+
+# Define build configuration option.
+get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
+if(IS_MULTICONFIG)
+ set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release"
+ CACHE STRING "" FORCE)
+else()
+ if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
+ set(CMAKE_BUILD_TYPE "Debug" CACHE
+ STRING "Flutter build mode" FORCE)
+ set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
+ "Debug" "Profile" "Release")
+ endif()
+endif()
+# Define settings for the Profile build mode.
+set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}")
+set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}")
+set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}")
+set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}")
+
+# Use Unicode for all projects.
+add_definitions(-DUNICODE -D_UNICODE)
+
+# Compilation settings that should be applied to most targets.
+#
+# Be cautious about adding new options here, as plugins use this function by
+# default. In most cases, you should add new options to specific targets instead
+# of modifying this function.
+function(APPLY_STANDARD_SETTINGS TARGET)
+ target_compile_features(${TARGET} PUBLIC cxx_std_17)
+ target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100")
+ target_compile_options(${TARGET} PRIVATE /EHsc)
+ target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0")
+ target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>")
+endfunction()
+
+# Flutter library and tool build rules.
+set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
+add_subdirectory(${FLUTTER_MANAGED_DIR})
+
+# Application build; see runner/CMakeLists.txt.
+add_subdirectory("runner")
+
+
+# Generated plugin build rules, which manage building the plugins and adding
+# them to the application.
+include(flutter/generated_plugins.cmake)
+
+
+# === Installation ===
+# Support files are copied into place next to the executable, so that it can
+# run in place. This is done instead of making a separate bundle (as on Linux)
+# so that building and running from within Visual Studio will work.
+set(BUILD_BUNDLE_DIR "$")
+# Make the "install" step default, as it's required to run.
+set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)
+if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
+ set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
+endif()
+
+set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
+set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}")
+
+install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
+ COMPONENT Runtime)
+
+install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
+ COMPONENT Runtime)
+
+install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+ COMPONENT Runtime)
+
+if(PLUGIN_BUNDLED_LIBRARIES)
+ install(FILES "${PLUGIN_BUNDLED_LIBRARIES}"
+ DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+ COMPONENT Runtime)
+endif()
+
+# Copy the native assets provided by the build.dart from all packages.
+set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/")
+install(DIRECTORY "${NATIVE_ASSETS_DIR}"
+ DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+ COMPONENT Runtime)
+
+# Fully re-copy the assets directory on each build to avoid having stale files
+# from a previous install.
+set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
+install(CODE "
+ file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
+ " COMPONENT Runtime)
+install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
+ DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
+
+# Install the AOT library on non-Debug builds only.
+install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
+ CONFIGURATIONS Profile;Release
+ COMPONENT Runtime)
diff --git a/windows/flutter/CMakeLists.txt b/windows/flutter/CMakeLists.txt
new file mode 100644
index 000000000..903f4899d
--- /dev/null
+++ b/windows/flutter/CMakeLists.txt
@@ -0,0 +1,109 @@
+# This file controls Flutter-level build steps. It should not be edited.
+cmake_minimum_required(VERSION 3.14)
+
+set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
+
+# Configuration provided via flutter tool.
+include(${EPHEMERAL_DIR}/generated_config.cmake)
+
+# TODO: Move the rest of this into files in ephemeral. See
+# https://github.com/flutter/flutter/issues/57146.
+set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper")
+
+# Set fallback configurations for older versions of the flutter tool.
+if (NOT DEFINED FLUTTER_TARGET_PLATFORM)
+ set(FLUTTER_TARGET_PLATFORM "windows-x64")
+endif()
+
+# === Flutter Library ===
+set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll")
+
+# Published to parent scope for install step.
+set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
+set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
+set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
+set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE)
+
+list(APPEND FLUTTER_LIBRARY_HEADERS
+ "flutter_export.h"
+ "flutter_windows.h"
+ "flutter_messenger.h"
+ "flutter_plugin_registrar.h"
+ "flutter_texture_registrar.h"
+)
+list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/")
+add_library(flutter INTERFACE)
+target_include_directories(flutter INTERFACE
+ "${EPHEMERAL_DIR}"
+)
+target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib")
+add_dependencies(flutter flutter_assemble)
+
+# === Wrapper ===
+list(APPEND CPP_WRAPPER_SOURCES_CORE
+ "core_implementations.cc"
+ "standard_codec.cc"
+)
+list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/")
+list(APPEND CPP_WRAPPER_SOURCES_PLUGIN
+ "plugin_registrar.cc"
+)
+list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/")
+list(APPEND CPP_WRAPPER_SOURCES_APP
+ "flutter_engine.cc"
+ "flutter_view_controller.cc"
+)
+list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/")
+
+# Wrapper sources needed for a plugin.
+add_library(flutter_wrapper_plugin STATIC
+ ${CPP_WRAPPER_SOURCES_CORE}
+ ${CPP_WRAPPER_SOURCES_PLUGIN}
+)
+apply_standard_settings(flutter_wrapper_plugin)
+set_target_properties(flutter_wrapper_plugin PROPERTIES
+ POSITION_INDEPENDENT_CODE ON)
+set_target_properties(flutter_wrapper_plugin PROPERTIES
+ CXX_VISIBILITY_PRESET hidden)
+target_link_libraries(flutter_wrapper_plugin PUBLIC flutter)
+target_include_directories(flutter_wrapper_plugin PUBLIC
+ "${WRAPPER_ROOT}/include"
+)
+add_dependencies(flutter_wrapper_plugin flutter_assemble)
+
+# Wrapper sources needed for the runner.
+add_library(flutter_wrapper_app STATIC
+ ${CPP_WRAPPER_SOURCES_CORE}
+ ${CPP_WRAPPER_SOURCES_APP}
+)
+apply_standard_settings(flutter_wrapper_app)
+target_link_libraries(flutter_wrapper_app PUBLIC flutter)
+target_include_directories(flutter_wrapper_app PUBLIC
+ "${WRAPPER_ROOT}/include"
+)
+add_dependencies(flutter_wrapper_app flutter_assemble)
+
+# === Flutter tool backend ===
+# _phony_ is a non-existent file to force this command to run every time,
+# since currently there's no way to get a full input/output list from the
+# flutter tool.
+set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_")
+set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE)
+add_custom_command(
+ OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
+ ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN}
+ ${CPP_WRAPPER_SOURCES_APP}
+ ${PHONY_OUTPUT}
+ COMMAND ${CMAKE_COMMAND} -E env
+ ${FLUTTER_TOOL_ENVIRONMENT}
+ "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"
+ ${FLUTTER_TARGET_PLATFORM} $
+ VERBATIM
+)
+add_custom_target(flutter_assemble DEPENDS
+ "${FLUTTER_LIBRARY}"
+ ${FLUTTER_LIBRARY_HEADERS}
+ ${CPP_WRAPPER_SOURCES_CORE}
+ ${CPP_WRAPPER_SOURCES_PLUGIN}
+ ${CPP_WRAPPER_SOURCES_APP}
+)
diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc
new file mode 100644
index 000000000..8b6d4680a
--- /dev/null
+++ b/windows/flutter/generated_plugin_registrant.cc
@@ -0,0 +1,11 @@
+//
+// Generated file. Do not edit.
+//
+
+// clang-format off
+
+#include "generated_plugin_registrant.h"
+
+
+void RegisterPlugins(flutter::PluginRegistry* registry) {
+}
diff --git a/windows/flutter/generated_plugin_registrant.h b/windows/flutter/generated_plugin_registrant.h
new file mode 100644
index 000000000..dc139d85a
--- /dev/null
+++ b/windows/flutter/generated_plugin_registrant.h
@@ -0,0 +1,15 @@
+//
+// Generated file. Do not edit.
+//
+
+// clang-format off
+
+#ifndef GENERATED_PLUGIN_REGISTRANT_
+#define GENERATED_PLUGIN_REGISTRANT_
+
+#include
+
+// Registers Flutter plugins.
+void RegisterPlugins(flutter::PluginRegistry* registry);
+
+#endif // GENERATED_PLUGIN_REGISTRANT_
diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake
new file mode 100644
index 000000000..b93c4c30c
--- /dev/null
+++ b/windows/flutter/generated_plugins.cmake
@@ -0,0 +1,23 @@
+#
+# Generated file, do not edit.
+#
+
+list(APPEND FLUTTER_PLUGIN_LIST
+)
+
+list(APPEND FLUTTER_FFI_PLUGIN_LIST
+)
+
+set(PLUGIN_BUNDLED_LIBRARIES)
+
+foreach(plugin ${FLUTTER_PLUGIN_LIST})
+ add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin})
+ target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
+ list(APPEND PLUGIN_BUNDLED_LIBRARIES $)
+ list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
+endforeach(plugin)
+
+foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
+ add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})
+ list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
+endforeach(ffi_plugin)
diff --git a/windows/runner/CMakeLists.txt b/windows/runner/CMakeLists.txt
new file mode 100644
index 000000000..394917c05
--- /dev/null
+++ b/windows/runner/CMakeLists.txt
@@ -0,0 +1,40 @@
+cmake_minimum_required(VERSION 3.14)
+project(runner LANGUAGES CXX)
+
+# Define the application target. To change its name, change BINARY_NAME in the
+# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
+# work.
+#
+# Any new source files that you add to the application should be added here.
+add_executable(${BINARY_NAME} WIN32
+ "flutter_window.cpp"
+ "main.cpp"
+ "utils.cpp"
+ "win32_window.cpp"
+ "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
+ "Runner.rc"
+ "runner.exe.manifest"
+)
+
+# Apply the standard set of build settings. This can be removed for applications
+# that need different build settings.
+apply_standard_settings(${BINARY_NAME})
+
+# Add preprocessor definitions for the build version.
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"")
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}")
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}")
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}")
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}")
+
+# Disable Windows macros that collide with C++ standard library functions.
+target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
+
+# Add dependency libraries and include directories. Add any application-specific
+# dependencies here.
+target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
+target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib")
+target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
+
+# Run the Flutter tool portions of the build. This must not be removed.
+add_dependencies(${BINARY_NAME} flutter_assemble)
diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc
new file mode 100644
index 000000000..33589005b
--- /dev/null
+++ b/windows/runner/Runner.rc
@@ -0,0 +1,121 @@
+// Microsoft Visual C++ generated resource script.
+//
+#pragma code_page(65001)
+#include "resource.h"
+
+#define APSTUDIO_READONLY_SYMBOLS
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 2 resource.
+//
+#include "winres.h"
+
+/////////////////////////////////////////////////////////////////////////////
+#undef APSTUDIO_READONLY_SYMBOLS
+
+/////////////////////////////////////////////////////////////////////////////
+// English (United States) resources
+
+#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
+LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
+
+#ifdef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// TEXTINCLUDE
+//
+
+1 TEXTINCLUDE
+BEGIN
+ "resource.h\0"
+END
+
+2 TEXTINCLUDE
+BEGIN
+ "#include ""winres.h""\r\n"
+ "\0"
+END
+
+3 TEXTINCLUDE
+BEGIN
+ "\r\n"
+ "\0"
+END
+
+#endif // APSTUDIO_INVOKED
+
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// Icon
+//
+
+// Icon with lowest ID value placed first to ensure application icon
+// remains consistent on all systems.
+IDI_APP_ICON ICON "resources\\app_icon.ico"
+
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// Version
+//
+
+#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)
+#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD
+#else
+#define VERSION_AS_NUMBER 1,0,0,0
+#endif
+
+#if defined(FLUTTER_VERSION)
+#define VERSION_AS_STRING FLUTTER_VERSION
+#else
+#define VERSION_AS_STRING "1.0.0"
+#endif
+
+VS_VERSION_INFO VERSIONINFO
+ FILEVERSION VERSION_AS_NUMBER
+ PRODUCTVERSION VERSION_AS_NUMBER
+ FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
+#ifdef _DEBUG
+ FILEFLAGS VS_FF_DEBUG
+#else
+ FILEFLAGS 0x0L
+#endif
+ FILEOS VOS__WINDOWS32
+ FILETYPE VFT_APP
+ FILESUBTYPE 0x0L
+BEGIN
+ BLOCK "StringFileInfo"
+ BEGIN
+ BLOCK "040904e4"
+ BEGIN
+ VALUE "CompanyName", "com.example" "\0"
+ VALUE "FileDescription", "ScamPad" "\0"
+ VALUE "FileVersion", VERSION_AS_STRING "\0"
+ VALUE "InternalName", "scampad" "\0"
+ VALUE "LegalCopyright", "Copyright (C) 2025 com.example. All rights reserved." "\0"
+ VALUE "OriginalFilename", "scampad.exe" "\0"
+ VALUE "ProductName", "ScamPad" "\0"
+ VALUE "ProductVersion", VERSION_AS_STRING "\0"
+ END
+ END
+ BLOCK "VarFileInfo"
+ BEGIN
+ VALUE "Translation", 0x409, 1252
+ END
+END
+
+#endif // English (United States) resources
+/////////////////////////////////////////////////////////////////////////////
+
+
+
+#ifndef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 3 resource.
+//
+
+
+/////////////////////////////////////////////////////////////////////////////
+#endif // not APSTUDIO_INVOKED
diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp
new file mode 100644
index 000000000..955ee3038
--- /dev/null
+++ b/windows/runner/flutter_window.cpp
@@ -0,0 +1,71 @@
+#include "flutter_window.h"
+
+#include
+
+#include "flutter/generated_plugin_registrant.h"
+
+FlutterWindow::FlutterWindow(const flutter::DartProject& project)
+ : project_(project) {}
+
+FlutterWindow::~FlutterWindow() {}
+
+bool FlutterWindow::OnCreate() {
+ if (!Win32Window::OnCreate()) {
+ return false;
+ }
+
+ RECT frame = GetClientArea();
+
+ // The size here must match the window dimensions to avoid unnecessary surface
+ // creation / destruction in the startup path.
+ flutter_controller_ = std::make_unique(
+ frame.right - frame.left, frame.bottom - frame.top, project_);
+ // Ensure that basic setup of the controller was successful.
+ if (!flutter_controller_->engine() || !flutter_controller_->view()) {
+ return false;
+ }
+ RegisterPlugins(flutter_controller_->engine());
+ SetChildContent(flutter_controller_->view()->GetNativeWindow());
+
+ flutter_controller_->engine()->SetNextFrameCallback([&]() {
+ this->Show();
+ });
+
+ // Flutter can complete the first frame before the "show window" callback is
+ // registered. The following call ensures a frame is pending to ensure the
+ // window is shown. It is a no-op if the first frame hasn't completed yet.
+ flutter_controller_->ForceRedraw();
+
+ return true;
+}
+
+void FlutterWindow::OnDestroy() {
+ if (flutter_controller_) {
+ flutter_controller_ = nullptr;
+ }
+
+ Win32Window::OnDestroy();
+}
+
+LRESULT
+FlutterWindow::MessageHandler(HWND hwnd, UINT const message,
+ WPARAM const wparam,
+ LPARAM const lparam) noexcept {
+ // Give Flutter, including plugins, an opportunity to handle window messages.
+ if (flutter_controller_) {
+ std::optional result =
+ flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,
+ lparam);
+ if (result) {
+ return *result;
+ }
+ }
+
+ switch (message) {
+ case WM_FONTCHANGE:
+ flutter_controller_->engine()->ReloadSystemFonts();
+ break;
+ }
+
+ return Win32Window::MessageHandler(hwnd, message, wparam, lparam);
+}
diff --git a/windows/runner/flutter_window.h b/windows/runner/flutter_window.h
new file mode 100644
index 000000000..6da0652f0
--- /dev/null
+++ b/windows/runner/flutter_window.h
@@ -0,0 +1,33 @@
+#ifndef RUNNER_FLUTTER_WINDOW_H_
+#define RUNNER_FLUTTER_WINDOW_H_
+
+#include
+#include
+
+#include
+
+#include "win32_window.h"
+
+// A window that does nothing but host a Flutter view.
+class FlutterWindow : public Win32Window {
+ public:
+ // Creates a new FlutterWindow hosting a Flutter view running |project|.
+ explicit FlutterWindow(const flutter::DartProject& project);
+ virtual ~FlutterWindow();
+
+ protected:
+ // Win32Window:
+ bool OnCreate() override;
+ void OnDestroy() override;
+ LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam,
+ LPARAM const lparam) noexcept override;
+
+ private:
+ // The project to run.
+ flutter::DartProject project_;
+
+ // The Flutter instance hosted by this window.
+ std::unique_ptr flutter_controller_;
+};
+
+#endif // RUNNER_FLUTTER_WINDOW_H_
diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp
new file mode 100644
index 000000000..cde3fdb8e
--- /dev/null
+++ b/windows/runner/main.cpp
@@ -0,0 +1,43 @@
+#include
+#include
+#include
+
+#include "flutter_window.h"
+#include "utils.h"
+
+int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
+ _In_ wchar_t *command_line, _In_ int show_command) {
+ // Attach to console when present (e.g., 'flutter run') or create a
+ // new console when running with a debugger.
+ if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
+ CreateAndAttachConsole();
+ }
+
+ // Initialize COM, so that it is available for use in the library and/or
+ // plugins.
+ ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
+
+ flutter::DartProject project(L"data");
+
+ std::vector command_line_arguments =
+ GetCommandLineArguments();
+
+ project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
+
+ FlutterWindow window(project);
+ Win32Window::Point origin(10, 10);
+ Win32Window::Size size(1280, 720);
+ if (!window.Create(L"ScamPad", origin, size)) {
+ return EXIT_FAILURE;
+ }
+ window.SetQuitOnClose(true);
+
+ ::MSG msg;
+ while (::GetMessage(&msg, nullptr, 0, 0)) {
+ ::TranslateMessage(&msg);
+ ::DispatchMessage(&msg);
+ }
+
+ ::CoUninitialize();
+ return EXIT_SUCCESS;
+}
diff --git a/windows/runner/resource.h b/windows/runner/resource.h
new file mode 100644
index 000000000..66a65d1e4
--- /dev/null
+++ b/windows/runner/resource.h
@@ -0,0 +1,16 @@
+//{{NO_DEPENDENCIES}}
+// Microsoft Visual C++ generated include file.
+// Used by Runner.rc
+//
+#define IDI_APP_ICON 101
+
+// Next default values for new objects
+//
+#ifdef APSTUDIO_INVOKED
+#ifndef APSTUDIO_READONLY_SYMBOLS
+#define _APS_NEXT_RESOURCE_VALUE 102
+#define _APS_NEXT_COMMAND_VALUE 40001
+#define _APS_NEXT_CONTROL_VALUE 1001
+#define _APS_NEXT_SYMED_VALUE 101
+#endif
+#endif
diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico
new file mode 100644
index 000000000..74e20810d
Binary files /dev/null and b/windows/runner/resources/app_icon.ico differ
diff --git a/windows/runner/runner.exe.manifest b/windows/runner/runner.exe.manifest
new file mode 100644
index 000000000..153653e8d
--- /dev/null
+++ b/windows/runner/runner.exe.manifest
@@ -0,0 +1,14 @@
+
+
+
+
+ PerMonitorV2
+
+
+
+
+
+
+
+
+
diff --git a/windows/runner/utils.cpp b/windows/runner/utils.cpp
new file mode 100644
index 000000000..3a0b46511
--- /dev/null
+++ b/windows/runner/utils.cpp
@@ -0,0 +1,65 @@
+#include "utils.h"
+
+#include
+#include
+#include
+#include
+
+#include
+
+void CreateAndAttachConsole() {
+ if (::AllocConsole()) {
+ FILE *unused;
+ if (freopen_s(&unused, "CONOUT$", "w", stdout)) {
+ _dup2(_fileno(stdout), 1);
+ }
+ if (freopen_s(&unused, "CONOUT$", "w", stderr)) {
+ _dup2(_fileno(stdout), 2);
+ }
+ std::ios::sync_with_stdio();
+ FlutterDesktopResyncOutputStreams();
+ }
+}
+
+std::vector GetCommandLineArguments() {
+ // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use.
+ int argc;
+ wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc);
+ if (argv == nullptr) {
+ return std::vector();
+ }
+
+ std::vector command_line_arguments;
+
+ // Skip the first argument as it's the binary name.
+ for (int i = 1; i < argc; i++) {
+ command_line_arguments.push_back(Utf8FromUtf16(argv[i]));
+ }
+
+ ::LocalFree(argv);
+
+ return command_line_arguments;
+}
+
+std::string Utf8FromUtf16(const wchar_t* utf16_string) {
+ if (utf16_string == nullptr) {
+ return std::string();
+ }
+ unsigned int target_length = ::WideCharToMultiByte(
+ CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
+ -1, nullptr, 0, nullptr, nullptr)
+ -1; // remove the trailing null character
+ int input_length = (int)wcslen(utf16_string);
+ std::string utf8_string;
+ if (target_length == 0 || target_length > utf8_string.max_size()) {
+ return utf8_string;
+ }
+ utf8_string.resize(target_length);
+ int converted_length = ::WideCharToMultiByte(
+ CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
+ input_length, utf8_string.data(), target_length, nullptr, nullptr);
+ if (converted_length == 0) {
+ return std::string();
+ }
+ return utf8_string;
+}
diff --git a/windows/runner/utils.h b/windows/runner/utils.h
new file mode 100644
index 000000000..3879d5475
--- /dev/null
+++ b/windows/runner/utils.h
@@ -0,0 +1,19 @@
+#ifndef RUNNER_UTILS_H_
+#define RUNNER_UTILS_H_
+
+#include
+#include
+
+// Creates a console for the process, and redirects stdout and stderr to
+// it for both the runner and the Flutter library.
+void CreateAndAttachConsole();
+
+// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string
+// encoded in UTF-8. Returns an empty std::string on failure.
+std::string Utf8FromUtf16(const wchar_t* utf16_string);
+
+// Gets the command line arguments passed in as a std::vector,
+// encoded in UTF-8. Returns an empty std::vector on failure.
+std::vector GetCommandLineArguments();
+
+#endif // RUNNER_UTILS_H_
diff --git a/windows/runner/win32_window.cpp b/windows/runner/win32_window.cpp
new file mode 100644
index 000000000..60608d0fe
--- /dev/null
+++ b/windows/runner/win32_window.cpp
@@ -0,0 +1,288 @@
+#include "win32_window.h"
+
+#include
+#include
+
+#include "resource.h"
+
+namespace {
+
+/// Window attribute that enables dark mode window decorations.
+///
+/// Redefined in case the developer's machine has a Windows SDK older than
+/// version 10.0.22000.0.
+/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
+#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
+#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
+#endif
+
+constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
+
+/// Registry key for app theme preference.
+///
+/// A value of 0 indicates apps should use dark mode. A non-zero or missing
+/// value indicates apps should use light mode.
+constexpr const wchar_t kGetPreferredBrightnessRegKey[] =
+ L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
+constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme";
+
+// The number of Win32Window objects that currently exist.
+static int g_active_window_count = 0;
+
+using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);
+
+// Scale helper to convert logical scaler values to physical using passed in
+// scale factor
+int Scale(int source, double scale_factor) {
+ return static_cast(source * scale_factor);
+}
+
+// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module.
+// This API is only needed for PerMonitor V1 awareness mode.
+void EnableFullDpiSupportIfAvailable(HWND hwnd) {
+ HMODULE user32_module = LoadLibraryA("User32.dll");
+ if (!user32_module) {
+ return;
+ }
+ auto enable_non_client_dpi_scaling =
+ reinterpret_cast(
+ GetProcAddress(user32_module, "EnableNonClientDpiScaling"));
+ if (enable_non_client_dpi_scaling != nullptr) {
+ enable_non_client_dpi_scaling(hwnd);
+ }
+ FreeLibrary(user32_module);
+}
+
+} // namespace
+
+// Manages the Win32Window's window class registration.
+class WindowClassRegistrar {
+ public:
+ ~WindowClassRegistrar() = default;
+
+ // Returns the singleton registrar instance.
+ static WindowClassRegistrar* GetInstance() {
+ if (!instance_) {
+ instance_ = new WindowClassRegistrar();
+ }
+ return instance_;
+ }
+
+ // Returns the name of the window class, registering the class if it hasn't
+ // previously been registered.
+ const wchar_t* GetWindowClass();
+
+ // Unregisters the window class. Should only be called if there are no
+ // instances of the window.
+ void UnregisterWindowClass();
+
+ private:
+ WindowClassRegistrar() = default;
+
+ static WindowClassRegistrar* instance_;
+
+ bool class_registered_ = false;
+};
+
+WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr;
+
+const wchar_t* WindowClassRegistrar::GetWindowClass() {
+ if (!class_registered_) {
+ WNDCLASS window_class{};
+ window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);
+ window_class.lpszClassName = kWindowClassName;
+ window_class.style = CS_HREDRAW | CS_VREDRAW;
+ window_class.cbClsExtra = 0;
+ window_class.cbWndExtra = 0;
+ window_class.hInstance = GetModuleHandle(nullptr);
+ window_class.hIcon =
+ LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));
+ window_class.hbrBackground = 0;
+ window_class.lpszMenuName = nullptr;
+ window_class.lpfnWndProc = Win32Window::WndProc;
+ RegisterClass(&window_class);
+ class_registered_ = true;
+ }
+ return kWindowClassName;
+}
+
+void WindowClassRegistrar::UnregisterWindowClass() {
+ UnregisterClass(kWindowClassName, nullptr);
+ class_registered_ = false;
+}
+
+Win32Window::Win32Window() {
+ ++g_active_window_count;
+}
+
+Win32Window::~Win32Window() {
+ --g_active_window_count;
+ Destroy();
+}
+
+bool Win32Window::Create(const std::wstring& title,
+ const Point& origin,
+ const Size& size) {
+ Destroy();
+
+ const wchar_t* window_class =
+ WindowClassRegistrar::GetInstance()->GetWindowClass();
+
+ const POINT target_point = {static_cast(origin.x),
+ static_cast(origin.y)};
+ HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);
+ UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);
+ double scale_factor = dpi / 96.0;
+
+ HWND window = CreateWindow(
+ window_class, title.c_str(), WS_OVERLAPPEDWINDOW,
+ Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),
+ Scale(size.width, scale_factor), Scale(size.height, scale_factor),
+ nullptr, nullptr, GetModuleHandle(nullptr), this);
+
+ if (!window) {
+ return false;
+ }
+
+ UpdateTheme(window);
+
+ return OnCreate();
+}
+
+bool Win32Window::Show() {
+ return ShowWindow(window_handle_, SW_SHOWNORMAL);
+}
+
+// static
+LRESULT CALLBACK Win32Window::WndProc(HWND const window,
+ UINT const message,
+ WPARAM const wparam,
+ LPARAM const lparam) noexcept {
+ if (message == WM_NCCREATE) {
+ auto window_struct = reinterpret_cast(lparam);
+ SetWindowLongPtr(window, GWLP_USERDATA,
+ reinterpret_cast(window_struct->lpCreateParams));
+
+ auto that = static_cast(window_struct->lpCreateParams);
+ EnableFullDpiSupportIfAvailable(window);
+ that->window_handle_ = window;
+ } else if (Win32Window* that = GetThisFromHandle(window)) {
+ return that->MessageHandler(window, message, wparam, lparam);
+ }
+
+ return DefWindowProc(window, message, wparam, lparam);
+}
+
+LRESULT
+Win32Window::MessageHandler(HWND hwnd,
+ UINT const message,
+ WPARAM const wparam,
+ LPARAM const lparam) noexcept {
+ switch (message) {
+ case WM_DESTROY:
+ window_handle_ = nullptr;
+ Destroy();
+ if (quit_on_close_) {
+ PostQuitMessage(0);
+ }
+ return 0;
+
+ case WM_DPICHANGED: {
+ auto newRectSize = reinterpret_cast(lparam);
+ LONG newWidth = newRectSize->right - newRectSize->left;
+ LONG newHeight = newRectSize->bottom - newRectSize->top;
+
+ SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,
+ newHeight, SWP_NOZORDER | SWP_NOACTIVATE);
+
+ return 0;
+ }
+ case WM_SIZE: {
+ RECT rect = GetClientArea();
+ if (child_content_ != nullptr) {
+ // Size and position the child window.
+ MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,
+ rect.bottom - rect.top, TRUE);
+ }
+ return 0;
+ }
+
+ case WM_ACTIVATE:
+ if (child_content_ != nullptr) {
+ SetFocus(child_content_);
+ }
+ return 0;
+
+ case WM_DWMCOLORIZATIONCOLORCHANGED:
+ UpdateTheme(hwnd);
+ return 0;
+ }
+
+ return DefWindowProc(window_handle_, message, wparam, lparam);
+}
+
+void Win32Window::Destroy() {
+ OnDestroy();
+
+ if (window_handle_) {
+ DestroyWindow(window_handle_);
+ window_handle_ = nullptr;
+ }
+ if (g_active_window_count == 0) {
+ WindowClassRegistrar::GetInstance()->UnregisterWindowClass();
+ }
+}
+
+Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {
+ return reinterpret_cast(
+ GetWindowLongPtr(window, GWLP_USERDATA));
+}
+
+void Win32Window::SetChildContent(HWND content) {
+ child_content_ = content;
+ SetParent(content, window_handle_);
+ RECT frame = GetClientArea();
+
+ MoveWindow(content, frame.left, frame.top, frame.right - frame.left,
+ frame.bottom - frame.top, true);
+
+ SetFocus(child_content_);
+}
+
+RECT Win32Window::GetClientArea() {
+ RECT frame;
+ GetClientRect(window_handle_, &frame);
+ return frame;
+}
+
+HWND Win32Window::GetHandle() {
+ return window_handle_;
+}
+
+void Win32Window::SetQuitOnClose(bool quit_on_close) {
+ quit_on_close_ = quit_on_close;
+}
+
+bool Win32Window::OnCreate() {
+ // No-op; provided for subclasses.
+ return true;
+}
+
+void Win32Window::OnDestroy() {
+ // No-op; provided for subclasses.
+}
+
+void Win32Window::UpdateTheme(HWND const window) {
+ DWORD light_mode;
+ DWORD light_mode_size = sizeof(light_mode);
+ LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey,
+ kGetPreferredBrightnessRegValue,
+ RRF_RT_REG_DWORD, nullptr, &light_mode,
+ &light_mode_size);
+
+ if (result == ERROR_SUCCESS) {
+ BOOL enable_dark_mode = light_mode == 0;
+ DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE,
+ &enable_dark_mode, sizeof(enable_dark_mode));
+ }
+}
diff --git a/windows/runner/win32_window.h b/windows/runner/win32_window.h
new file mode 100644
index 000000000..e901dde68
--- /dev/null
+++ b/windows/runner/win32_window.h
@@ -0,0 +1,102 @@
+#ifndef RUNNER_WIN32_WINDOW_H_
+#define RUNNER_WIN32_WINDOW_H_
+
+#include
+
+#include
+#include
+#include
+
+// A class abstraction for a high DPI-aware Win32 Window. Intended to be
+// inherited from by classes that wish to specialize with custom
+// rendering and input handling
+class Win32Window {
+ public:
+ struct Point {
+ unsigned int x;
+ unsigned int y;
+ Point(unsigned int x, unsigned int y) : x(x), y(y) {}
+ };
+
+ struct Size {
+ unsigned int width;
+ unsigned int height;
+ Size(unsigned int width, unsigned int height)
+ : width(width), height(height) {}
+ };
+
+ Win32Window();
+ virtual ~Win32Window();
+
+ // Creates a win32 window with |title| that is positioned and sized using
+ // |origin| and |size|. New windows are created on the default monitor. Window
+ // sizes are specified to the OS in physical pixels, hence to ensure a
+ // consistent size this function will scale the inputted width and height as
+ // as appropriate for the default monitor. The window is invisible until
+ // |Show| is called. Returns true if the window was created successfully.
+ bool Create(const std::wstring& title, const Point& origin, const Size& size);
+
+ // Show the current window. Returns true if the window was successfully shown.
+ bool Show();
+
+ // Release OS resources associated with window.
+ void Destroy();
+
+ // Inserts |content| into the window tree.
+ void SetChildContent(HWND content);
+
+ // Returns the backing Window handle to enable clients to set icon and other
+ // window properties. Returns nullptr if the window has been destroyed.
+ HWND GetHandle();
+
+ // If true, closing this window will quit the application.
+ void SetQuitOnClose(bool quit_on_close);
+
+ // Return a RECT representing the bounds of the current client area.
+ RECT GetClientArea();
+
+ protected:
+ // Processes and route salient window messages for mouse handling,
+ // size change and DPI. Delegates handling of these to member overloads that
+ // inheriting classes can handle.
+ virtual LRESULT MessageHandler(HWND window,
+ UINT const message,
+ WPARAM const wparam,
+ LPARAM const lparam) noexcept;
+
+ // Called when CreateAndShow is called, allowing subclass window-related
+ // setup. Subclasses should return false if setup fails.
+ virtual bool OnCreate();
+
+ // Called when Destroy is called.
+ virtual void OnDestroy();
+
+ private:
+ friend class WindowClassRegistrar;
+
+ // OS callback called by message pump. Handles the WM_NCCREATE message which
+ // is passed when the non-client area is being created and enables automatic
+ // non-client DPI scaling so that the non-client area automatically
+ // responds to changes in DPI. All other messages are handled by
+ // MessageHandler.
+ static LRESULT CALLBACK WndProc(HWND const window,
+ UINT const message,
+ WPARAM const wparam,
+ LPARAM const lparam) noexcept;
+
+ // Retrieves a class instance pointer for |window|
+ static Win32Window* GetThisFromHandle(HWND const window) noexcept;
+
+ // Update the window frame's theme to match the system theme.
+ static void UpdateTheme(HWND const window);
+
+ bool quit_on_close_ = false;
+
+ // window handle for top level window.
+ HWND window_handle_ = nullptr;
+
+ // window handle for hosted content.
+ HWND child_content_ = nullptr;
+};
+
+#endif // RUNNER_WIN32_WINDOW_H_