inital commit

This commit is contained in:
HAMZA ALSARSOUR 2026-02-19 05:44:08 +03:00
parent dad1330013
commit 8ad9065059
215 changed files with 26736 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

BIN
NeonFramework-2/.DS_Store vendored Normal file

Binary file not shown.

27
NeonFramework-2/.gitignore vendored Normal file
View file

@ -0,0 +1,27 @@
# See https://www.dartlang.org/guides/libraries/private-files
# Files and directories created by pub
.dart_tool/
.packages
build/
# If you're building an application, you may want to check-in your pubspec.lock
pubspec.lock
# Directory created by dartdoc
# If you don't generate documentation locally you can remove this line.
doc/api/
# dotenv environment variables file
.env*
# Avoid committing generated Javascript files:
*.dart.js
*.info.json # Produced by the --dump-info flag.
*.js # When generated by dart2js. Don't specify *.js if your
# project includes source files written in JavaScript.
*.js_
*.js.deps
*.js.map
.flutter-plugins
.flutter-plugins-dependencies

BIN
NeonFramework-2/.local/.DS_Store vendored Normal file

Binary file not shown.

View file

@ -0,0 +1 @@
{"latest": "main"}

35
NeonFramework-2/.replit Normal file
View file

@ -0,0 +1,35 @@
entrypoint = "main.dart"
modules = ["dart-3.3", "dart-3.10"]
[nix]
channel="stable-24_05"
[deployment]
run = ["dart", "main.dart"]
deploymentTarget = "cloudrun"
[agent]
expertMode = true
[workflows]
runButton = "Project"
[[workflows.workflow]]
name = "Project"
mode = "parallel"
author = "agent"
[[workflows.workflow.tasks]]
task = "workflow.run"
args = "Neon Example"
[[workflows.workflow]]
name = "Neon Example"
author = "agent"
[[workflows.workflow.tasks]]
task = "shell.exec"
args = "cd neon_framework && dart run example/main.dart"
[workflows.workflow.metadata]
outputType = "console"

View file

@ -0,0 +1,3 @@
void main() {
print('Hello World!');
}

BIN
NeonFramework-2/neon_framework/.DS_Store vendored Normal file

Binary file not shown.

View file

@ -0,0 +1,9 @@
.dart_tool/
.packages
build/
pubspec.lock
doc/api/
*.js_
*.js.deps
*.js.map
.pub/

Binary file not shown.

View file

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,388 @@
<component name="libraryTable">
<library name="Dart Packages" type="DartPackagesLibraryType">
<properties>
<option name="packageNameToDirsMap">
<entry key="_fe_analyzer_shared">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/_fe_analyzer_shared-67.0.0/lib" />
</list>
</value>
</entry>
<entry key="analyzer">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/analyzer-6.4.1/lib" />
</list>
</value>
</entry>
<entry key="args">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/args-2.7.0/lib" />
</list>
</value>
</entry>
<entry key="async">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/async-2.11.0/lib" />
</list>
</value>
</entry>
<entry key="boolean_selector">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib" />
</list>
</value>
</entry>
<entry key="collection">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/collection-1.18.0/lib" />
</list>
</value>
</entry>
<entry key="convert">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/convert-3.1.1/lib" />
</list>
</value>
</entry>
<entry key="coverage">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/coverage-1.8.0/lib" />
</list>
</value>
</entry>
<entry key="crypto">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/crypto-3.0.3/lib" />
</list>
</value>
</entry>
<entry key="file">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/file-7.0.1/lib" />
</list>
</value>
</entry>
<entry key="frontend_server_client">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/frontend_server_client-4.0.0/lib" />
</list>
</value>
</entry>
<entry key="glob">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/glob-2.1.3/lib" />
</list>
</value>
</entry>
<entry key="http_multi_server">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/http_multi_server-3.2.2/lib" />
</list>
</value>
</entry>
<entry key="http_parser">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/http_parser-4.0.2/lib" />
</list>
</value>
</entry>
<entry key="io">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/io-1.0.4/lib" />
</list>
</value>
</entry>
<entry key="js">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/js-0.7.1/lib" />
</list>
</value>
</entry>
<entry key="logging">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/logging-1.2.0/lib" />
</list>
</value>
</entry>
<entry key="matcher">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/matcher-0.12.16+1/lib" />
</list>
</value>
</entry>
<entry key="meta">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/meta-1.16.0/lib" />
</list>
</value>
</entry>
<entry key="mime">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/mime-2.0.0/lib" />
</list>
</value>
</entry>
<entry key="node_preamble">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/node_preamble-2.0.2/lib" />
</list>
</value>
</entry>
<entry key="package_config">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/package_config-2.1.0/lib" />
</list>
</value>
</entry>
<entry key="path">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/path-1.9.0/lib" />
</list>
</value>
</entry>
<entry key="pool">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/pool-1.5.1/lib" />
</list>
</value>
</entry>
<entry key="pub_semver">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/pub_semver-2.1.4/lib" />
</list>
</value>
</entry>
<entry key="shelf">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/shelf-1.4.1/lib" />
</list>
</value>
</entry>
<entry key="shelf_packages_handler">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/shelf_packages_handler-3.0.2/lib" />
</list>
</value>
</entry>
<entry key="shelf_static">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/shelf_static-1.1.3/lib" />
</list>
</value>
</entry>
<entry key="shelf_web_socket">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/shelf_web_socket-2.0.1/lib" />
</list>
</value>
</entry>
<entry key="source_map_stack_trace">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/source_map_stack_trace-2.1.2/lib" />
</list>
</value>
</entry>
<entry key="source_maps">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/source_maps-0.10.13/lib" />
</list>
</value>
</entry>
<entry key="source_span">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/source_span-1.10.2/lib" />
</list>
</value>
</entry>
<entry key="stack_trace">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/stack_trace-1.11.1/lib" />
</list>
</value>
</entry>
<entry key="stream_channel">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/stream_channel-2.1.4/lib" />
</list>
</value>
</entry>
<entry key="string_scanner">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib" />
</list>
</value>
</entry>
<entry key="term_glyph">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib" />
</list>
</value>
</entry>
<entry key="test">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/test-1.25.7/lib" />
</list>
</value>
</entry>
<entry key="test_api">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/test_api-0.7.2/lib" />
</list>
</value>
</entry>
<entry key="test_core">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/test_core-0.6.4/lib" />
</list>
</value>
</entry>
<entry key="typed_data">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/typed_data-1.3.2/lib" />
</list>
</value>
</entry>
<entry key="vm_service">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/vm_service-14.3.1/lib" />
</list>
</value>
</entry>
<entry key="watcher">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/watcher-1.1.4/lib" />
</list>
</value>
</entry>
<entry key="web">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/web-0.5.1/lib" />
</list>
</value>
</entry>
<entry key="web_socket">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/web_socket-0.1.6/lib" />
</list>
</value>
</entry>
<entry key="web_socket_channel">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/web_socket_channel-3.0.3/lib" />
</list>
</value>
</entry>
<entry key="webkit_inspection_protocol">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/webkit_inspection_protocol-1.2.1/lib" />
</list>
</value>
</entry>
<entry key="yaml">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dev/yaml-3.1.2/lib" />
</list>
</value>
</entry>
</option>
</properties>
<CLASSES>
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/_fe_analyzer_shared-67.0.0/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/analyzer-6.4.1/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/args-2.7.0/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/async-2.11.0/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/collection-1.18.0/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/convert-3.1.1/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/coverage-1.8.0/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/crypto-3.0.3/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/file-7.0.1/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/frontend_server_client-4.0.0/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/glob-2.1.3/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/http_multi_server-3.2.2/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/http_parser-4.0.2/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/io-1.0.4/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/js-0.7.1/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/logging-1.2.0/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/matcher-0.12.16+1/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/meta-1.16.0/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/mime-2.0.0/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/node_preamble-2.0.2/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/package_config-2.1.0/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/path-1.9.0/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/pool-1.5.1/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/pub_semver-2.1.4/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/shelf-1.4.1/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/shelf_packages_handler-3.0.2/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/shelf_static-1.1.3/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/shelf_web_socket-2.0.1/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/source_map_stack_trace-2.1.2/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/source_maps-0.10.13/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/source_span-1.10.2/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/stack_trace-1.11.1/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/stream_channel-2.1.4/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/test-1.25.7/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/test_api-0.7.2/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/test_core-0.6.4/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/typed_data-1.3.2/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/vm_service-14.3.1/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/watcher-1.1.4/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/web-0.5.1/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/web_socket-0.1.6/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/web_socket_channel-3.0.3/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/webkit_inspection_protocol-1.2.1/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/yaml-3.1.2/lib" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>

View file

@ -0,0 +1,31 @@
<component name="libraryTable">
<library name="Dart SDK">
<CLASSES>
<root url="file:///Volumes/extendedT2nvme/flutter/bin/cache/dart-sdk/lib/_internal" />
<root url="file:///Volumes/extendedT2nvme/flutter/bin/cache/dart-sdk/lib/async" />
<root url="file:///Volumes/extendedT2nvme/flutter/bin/cache/dart-sdk/lib/cli" />
<root url="file:///Volumes/extendedT2nvme/flutter/bin/cache/dart-sdk/lib/collection" />
<root url="file:///Volumes/extendedT2nvme/flutter/bin/cache/dart-sdk/lib/concurrent" />
<root url="file:///Volumes/extendedT2nvme/flutter/bin/cache/dart-sdk/lib/convert" />
<root url="file:///Volumes/extendedT2nvme/flutter/bin/cache/dart-sdk/lib/core" />
<root url="file:///Volumes/extendedT2nvme/flutter/bin/cache/dart-sdk/lib/developer" />
<root url="file:///Volumes/extendedT2nvme/flutter/bin/cache/dart-sdk/lib/ffi" />
<root url="file:///Volumes/extendedT2nvme/flutter/bin/cache/dart-sdk/lib/html" />
<root url="file:///Volumes/extendedT2nvme/flutter/bin/cache/dart-sdk/lib/indexed_db" />
<root url="file:///Volumes/extendedT2nvme/flutter/bin/cache/dart-sdk/lib/io" />
<root url="file:///Volumes/extendedT2nvme/flutter/bin/cache/dart-sdk/lib/isolate" />
<root url="file:///Volumes/extendedT2nvme/flutter/bin/cache/dart-sdk/lib/js" />
<root url="file:///Volumes/extendedT2nvme/flutter/bin/cache/dart-sdk/lib/js_interop" />
<root url="file:///Volumes/extendedT2nvme/flutter/bin/cache/dart-sdk/lib/js_interop_unsafe" />
<root url="file:///Volumes/extendedT2nvme/flutter/bin/cache/dart-sdk/lib/js_util" />
<root url="file:///Volumes/extendedT2nvme/flutter/bin/cache/dart-sdk/lib/math" />
<root url="file:///Volumes/extendedT2nvme/flutter/bin/cache/dart-sdk/lib/mirrors" />
<root url="file:///Volumes/extendedT2nvme/flutter/bin/cache/dart-sdk/lib/svg" />
<root url="file:///Volumes/extendedT2nvme/flutter/bin/cache/dart-sdk/lib/typed_data" />
<root url="file:///Volumes/extendedT2nvme/flutter/bin/cache/dart-sdk/lib/web_audio" />
<root url="file:///Volumes/extendedT2nvme/flutter/bin/cache/dart-sdk/lib/web_gl" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MaterialThemeProjectNewConfig">
<option name="metadata">
<MTProjectMetadataState>
<option name="migrated" value="true" />
<option name="pristineConfig" value="false" />
<option name="userId" value="6e970a7:193f7a5a2d4:-7ffe" />
</MTProjectMetadataState>
</option>
</component>
</project>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/neon_framework.iml" filepath="$PROJECT_DIR$/.idea/neon_framework.iml" />
</modules>
</component>
</project>

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.dart_tool" />
<excludeFolder url="file://$MODULE_DIR$/.pub" />
<excludeFolder url="file://$MODULE_DIR$/build" />
<excludeFolder url="file://$MODULE_DIR$/my_2nd_test_app/.dart_tool" />
<excludeFolder url="file://$MODULE_DIR$/my_2nd_test_app/.pub" />
<excludeFolder url="file://$MODULE_DIR$/my_2nd_test_app/build" />
<excludeFolder url="file://$MODULE_DIR$/my_app/.dart_tool" />
<excludeFolder url="file://$MODULE_DIR$/my_app/.pub" />
<excludeFolder url="file://$MODULE_DIR$/my_app/build" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Dart SDK" level="project" />
<orderEntry type="library" name="Dart Packages" level="project" />
</component>
</module>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

View file

@ -0,0 +1,319 @@
# Android & iOS Widget Rendering Fix
## Problem
The showcase widgets (Switch, Checkbox, Radio, Slider, Chips, Badge) were **not appearing** in Android or iOS apps.
## Root Cause
Both native apps were **missing rendering support** for Material 3 widgets. They only supported:
- ✅ Text, Button, Container
- ✅ Column, Row, AppBar, NavigationBar
- ❌ Switch, Checkbox, Radio, Slider, RangeSlider
- ❌ Chips (ActionChip, FilterChip, ChoiceChip, InputChip)
- ❌ Badge
## Solution
### Android (MainActivity.kt)
Added rendering support for all M3 widgets in the `renderWidget()` method:
#### Switch
```kotlin
type == "Switch" -> {
val switch = android.widget.Switch(this)
switch.isChecked = node.optBoolean("value", false)
val switchId = node.optString("id")
switch.setOnCheckedChangeListener { _, isChecked ->
if (switchId.isNotEmpty()) sendAction(switchId, value = isChecked)
}
switch
}
```
#### Checkbox
```kotlin
type == "Checkbox" -> {
val checkbox = android.widget.CheckBox(this)
checkbox.isChecked = node.optBoolean("value", false)
val checkboxId = node.optString("id")
checkbox.setOnCheckedChangeListener { _, isChecked ->
if (checkboxId.isNotEmpty()) sendAction(checkboxId, value = isChecked)
}
checkbox
}
```
#### Radio
```kotlin
type == "Radio" -> {
val radio = android.widget.RadioButton(this)
radio.isChecked = node.optBoolean("selected", false)
val radioId = node.optString("id")
radio.setOnClickListener {
if (radioId.isNotEmpty()) sendAction(radioId)
}
radio
}
```
#### Slider
```kotlin
type == "Slider" -> {
val slider = android.widget.SeekBar(this)
val min = node.optDouble("min", 0.0)
val max = node.optDouble("max", 1.0)
val value = node.optDouble("value", 0.5)
slider.max = 100 // Use 0-100 range
slider.progress = ((value - min) / (max - min) * 100).toInt()
val sliderId = node.optString("id")
slider.setOnSeekBarChangeListener(object : android.widget.SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: android.widget.SeekBar?, progress: Int, fromUser: Boolean) {
if (fromUser && sliderId.isNotEmpty()) {
val normalizedValue = min + (progress / 100.0) * (max - min)
sendAction(sliderId, value = normalizedValue)
}
}
override fun onStartTrackingTouch(seekBar: android.widget.SeekBar?) {}
override fun onStopTrackingTouch(seekBar: android.widget.SeekBar?) {}
})
slider
}
```
#### Chips
```kotlin
type == "ActionChip" || type == "FilterChip" || type == "ChoiceChip" || type == "InputChip" -> {
val chip = TextView(this)
val density = resources.displayMetrics.density
chip.setPadding((16 * density).toInt(), (8 * density).toInt(), (16 * density).toInt(), (8 * density).toInt())
// Get label from children
val children = node.optJSONArray("children")
if (children != null && children.length() > 0) {
val firstChild = children.getJSONObject(0)
if (firstChild.optString("type").contains("Text")) {
chip.text = firstChild.optString("text")
}
}
// Style based on type
val background = android.graphics.drawable.GradientDrawable()
background.cornerRadius = 16 * density
if (type == "FilterChip") {
val isSelected = node.optBoolean("selected", false)
if (isSelected) {
background.setColor(Color.BLUE)
chip.setTextColor(Color.WHITE)
} else {
background.setColor(0xFFEEEEEE.toInt())
chip.setTextColor(Color.BLUE)
}
} else {
background.setColor(0xFFEEEEEE.toInt())
chip.setTextColor(Color.BLACK)
}
chip.background = background
val chipId = node.optString("id")
chip.setOnClickListener {
if (chipId.isNotEmpty()) sendAction(chipId)
}
chip
}
```
#### Badge
```kotlin
type == "Badge" -> {
val badge = TextView(this)
badge.text = node.optString("label", "")
badge.setBackgroundColor(Color.RED)
badge.setTextColor(Color.WHITE)
badge.textSize = 12f
badge.gravity = Gravity.CENTER
val density = resources.displayMetrics.density
badge.setPadding((8 * density).toInt(), (4 * density).toInt(), (8 * density).toInt(), (4 * density).toInt())
val background = android.graphics.drawable.GradientDrawable()
background.cornerRadius = 10 * density
background.setColor(Color.RED)
badge.background = background
badge.layoutParams = ViewGroup.LayoutParams((20 * density).toInt(), (20 * density).toInt())
badge
}
```
#### Updated sendAction Method
```kotlin
private fun sendAction(id: String, index: Int? = null, value: Any? = null) {
thread {
try {
val url = URL("http://127.0.0.1:8080/action")
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "POST"
connection.doOutput = true
connection.setRequestProperty("Content-Type", "application/json")
val json = JSONObject()
json.put("id", id)
if (index != null) {
json.put("index", index)
}
if (value != null) { // ← NEW
json.put("value", value)
}
val os = connection.outputStream
os.write(json.toString().toByteArray())
os.close()
val responseCode = connection.responseCode
if (responseCode == 200) {
// Read the response JSON (contains updated widget tree)
val reader = BufferedReader(InputStreamReader(connection.inputStream))
val response = reader.readText()
reader.close()
// Parse and re-render the updated tree
val rootNode = JSONObject(response)
runOnUiThread {
val rootView = renderWidget(rootNode)
val scrollView = ScrollView(this)
scrollView.addView(rootView)
setContentView(scrollView)
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
```
### iOS (ViewController.swift)
Same widgets were added to iOS (already documented in `IOS_ACTION_FIX.md`).
## Testing
### 1. Setup ADB Reverse (Android Only)
```bash
adb reverse tcp:8080 tcp:8080
```
### 2. Start Dart Backend
```bash
cd /Users/hamzaibrahim/Downloads/NeonFramework-latest-v1-2026-2/NeonFramework-2/neon_framework
dart run my_2nd_test_app/lib/main.dart
```
### 3. Rebuild and Run Apps
**Android:**
```bash
cd my_2nd_test_app/android
./gradlew installDebug
```
**iOS:**
- Open `NeonApp.xcodeproj` in Xcode
- Product > Clean Build Folder (Cmd+Shift+K)
- Product > Run (Cmd+R)
### 4. Verify Widgets Appear
You should now see all widgets in the showcase app:
- ✅ Buttons (Filled, Elevated, Outlined, Text)
- ✅ Switches
- ✅ Checkboxes
- ✅ Radio buttons
- ✅ Sliders
- ✅ Chips (Action, Filter, Choice, Input)
- ✅ Badges
- ✅ Navigation components
### 5. Test Interactions
- **Tap buttons** → Should trigger actions
- **Toggle switches** → Should update state
- **Move sliders** → Should send new values
- **Tap chips** → Should trigger actions
- **Check checkboxes** → Should update state
## Supported Widgets
### ✅ Now Working on Android & iOS:
- Text, Container, Column, Row
- Button (all variants)
- Switch
- Checkbox
- Radio
- Slider
- ActionChip, FilterChip, ChoiceChip, InputChip
- Badge
- SegmentedButton
- NavigationBar, TabBar
- AppBar
- NavigationDrawer, NavigationRail
### ⚠️ Not Yet Implemented:
- RangeSlider (needs custom two-thumb implementation)
- Complex animations
- Custom gestures
## Common Issues
### Issue: Widgets still not showing
**Cause**: Old app version cached
**Fix**:
- Android: Uninstall and reinstall app
- iOS: Product > Clean Build Folder, then rebuild
### Issue: "Unknown Widget" error
**Cause**: Widget type not recognized
**Fix**: Check that the widget type in JSON matches exactly (case-sensitive)
### Issue: Actions not working
**Cause**: Widget doesn't have an ID
**Fix**: Add `key: 'widget_id'` to the widget in Dart
### Issue: UI doesn't update
**Cause**: Response not being parsed
**Fix**: Check logcat (Android) or Xcode console (iOS) for errors
## Debugging
### Android Logcat
```bash
adb logcat | grep -i neon
```
### iOS Console
Check Xcode console for:
```
👆 Switch toggled to true
🚀 Sending POST request to Dart engine...
✅ Received response from Dart.
🎨 Response is a widget tree. Redrawing screen!
```
## Files Modified
- **Android**: `android/app/src/main/java/com/neon/myapp/MainActivity.kt`
- **iOS**: `NeonApp/NeonApp/ViewController.swift`
## Summary
Both Android and iOS apps now have **complete support for all Material 3 widgets**! Every widget can:
- ✅ Render correctly from JSON
- ✅ Display with proper styling
- ✅ Handle user interactions
- ✅ Send actions to Dart backend
- ✅ Update UI when state changes
The showcase app should now display all widgets correctly on both platforms! 🎉

View file

@ -0,0 +1,336 @@
# Neon Framework - Build Guide
How to build and run the Neon Framework test app on Android and iOS devices.
---
## How the Neon Framework Works
The Neon Framework has two parts that work together:
1. **Dart Backend** - Runs a local HTTP server (on port 8080) that serves the UI as JSON
2. **Native Shell** - An Android or iOS app that fetches the JSON and renders real native UI components
The Dart backend must be running on the same machine (or reachable network) for the native app to work.
---
## Prerequisites
### For Android (APK)
- [Android Studio](https://developer.android.com/studio) (latest stable)
- Android SDK (API 34) - installed via Android Studio SDK Manager
- Java 17 (bundled with Android Studio)
- [Dart SDK](https://dart.dev/get-dart) (3.0 or higher)
### For iOS (IPA)
- A Mac computer (required - iOS apps can only be built on macOS)
- [Xcode](https://developer.apple.com/xcode/) (15.0 or higher)
- An Apple Developer Account (free for simulator testing, paid for device/distribution)
- [Dart SDK](https://dart.dev/get-dart) (3.0 or higher)
---
## Step 1: Download the Project
Download the entire `neon_framework/` folder from Replit. This contains:
```
neon_framework/
lib/ <-- Framework source code
my_2nd_test_app/ <-- Test app with native shells
lib/ <-- Dart app code (screens, widgets)
android/ <-- Android native project
NeonApp/ <-- iOS native project (Xcode)
pubspec.yaml <-- Dart dependencies
pubspec.yaml <-- Framework package definition
```
---
## Step 2: Install Dart Dependencies
Open a terminal and navigate to the test app:
```bash
cd neon_framework/my_2nd_test_app
dart pub get
```
This resolves the framework dependency (which points to `..`, the parent `neon_framework/` folder).
---
## Step 3: Start the Dart Backend
The Dart backend must be running before you launch the native app.
```bash
cd neon_framework/my_2nd_test_app
dart run lib/main.dart
```
You should see output like:
```
Neon app server running on http://localhost:8080
```
Keep this terminal open while using the native app.
---
## Building the Android APK
### Open the Project
1. Open **Android Studio**
2. Click **File > Open**
3. Navigate to `neon_framework/my_2nd_test_app/android/`
4. Click **Open** and wait for Gradle to sync
### Project Details
| Setting | Value |
|------------------|--------------------|
| Package name | `com.neon.myapp` |
| Min SDK | 21 (Android 5.0) |
| Target SDK | 34 (Android 14) |
| Kotlin | 1.9.0 |
| Gradle | 8.2 |
| AGP | 8.2.0 |
| Java | 17 |
### Run on Emulator or Device
1. Make sure the Dart backend is running (Step 3)
2. In Android Studio, select a device/emulator from the toolbar
3. Click the **Run** button (green play icon)
4. The app will connect to `http://10.0.2.2:8080` (Android emulator's alias for host localhost)
### Build a Debug APK
From the terminal:
```bash
cd neon_framework/my_2nd_test_app/android
./gradlew assembleDebug
```
The APK will be at:
```
android/app/build/outputs/apk/debug/app-debug.apk
```
### Build a Release APK
For a release build, you need to set up signing. Create a keystore first:
```bash
keytool -genkey -v -keystore my-release-key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias my-key
```
Then add signing config to `android/app/build.gradle`:
```gradle
android {
signingConfigs {
release {
storeFile file('my-release-key.jks')
storePassword 'your-password'
keyAlias 'my-key'
keyPassword 'your-password'
}
}
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')
}
}
}
```
Then build:
```bash
./gradlew assembleRelease
```
The signed APK will be at:
```
android/app/build/outputs/apk/release/app-release.apk
```
### Important: Server URL for Physical Devices
The Android app connects to `http://10.0.2.2:8080` by default, which only works on the Android Emulator. For a physical device on the same Wi-Fi network:
1. Open `android/app/src/main/kotlin/com/neon/myapp/MainActivity.kt`
2. Find the server URL (look for `10.0.2.2` or `127.0.0.1`)
3. Replace it with your computer's local IP address (e.g., `http://192.168.1.100:8080`)
4. Your computer's firewall must allow connections on port 8080
---
## Building the iOS App (IPA)
### Open the Project
1. Open **Xcode**
2. Click **File > Open**
3. Navigate to `neon_framework/my_2nd_test_app/NeonApp/NeonApp.xcodeproj`
4. Click **Open**
### Project Details
| Setting | Value |
|----------------------|----------------------------|
| Bundle Identifier | Set in Xcode project |
| Deployment Target | iOS 15.0+ |
| Language | Swift (UIKit) |
| Storyboard | Main.storyboard |
### Run on Simulator
1. Make sure the Dart backend is running (Step 3)
2. In Xcode, select a simulator from the device dropdown (e.g., "iPhone 15")
3. Click the **Run** button (play icon) or press Cmd+R
4. The app connects to `http://127.0.0.1:8080` - this works on the simulator because it shares the Mac's network
### Run on Physical Device
For a physical iPhone/iPad:
1. In Xcode, go to **Signing & Capabilities**
2. Select your **Team** (Apple Developer account)
3. Xcode will automatically create a provisioning profile
4. Connect your device via USB
5. Update the server URL in `ViewController.swift`:
- Find `http://127.0.0.1:8080`
- Replace with your Mac's local IP (e.g., `http://192.168.1.100:8080`)
6. Click **Run**
### Build an Archive (for IPA distribution)
1. In Xcode, select **Product > Archive**
2. Once the archive completes, the Organizer window opens
3. Click **Distribute App**
4. Choose your distribution method:
- **App Store Connect** - for App Store submission
- **Ad Hoc** - for testing on specific devices
- **Development** - for development testing
5. Follow the prompts to export the IPA file
### App Transport Security
The iOS app is configured to allow local networking (HTTP to localhost). This is set in `Info.plist`:
```xml
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
```
For production, you should use HTTPS and update this setting accordingly.
---
## Project Structure Reference
### Dart App Code (`my_2nd_test_app/lib/`)
```
lib/
main.dart <-- Entry point, AppLauncher widget
app.dart <-- Sample app with transpiled widgets
state_test.dart <-- State management tests
screens/
showcase.dart <-- ShowcaseApp with 8-tab navigation
home_screen.dart <-- Dashboard screen
navigation_screen.dart <-- Interaction & navigation demos
containment_screen.dart <-- Cards, dialogs, bottom sheets
input_screen.dart <-- Text fields, search bars
```
### Android Native (`my_2nd_test_app/android/`)
```
android/
app/
build.gradle <-- App-level build config
src/main/
AndroidManifest.xml <-- App permissions & config
kotlin/com/neon/myapp/
MainActivity.kt <-- Native widget renderer
build.gradle <-- Project-level build config
settings.gradle <-- Project settings
gradle/wrapper/ <-- Gradle wrapper (v8.2)
gradlew <-- Gradle wrapper script
```
### iOS Native (`my_2nd_test_app/NeonApp/`)
```
NeonApp/
NeonApp.xcodeproj/ <-- Xcode project file
NeonApp/
AppDelegate.swift <-- App lifecycle
SceneDelegate.swift <-- Scene lifecycle
ViewController.swift <-- Native widget renderer (1400+ lines)
Info.plist <-- App configuration
Assets.xcassets/ <-- App icons & colors
Base.lproj/
Main.storyboard <-- UI storyboard
LaunchScreen.storyboard <-- Launch screen
```
---
## Troubleshooting
### "Connection refused" or blank screen
- Make sure the Dart backend is running (`dart run lib/main.dart`)
- Check the server URL matches your setup (localhost for emulator/simulator, IP for physical devices)
### Android: Gradle sync fails
- Make sure Android SDK 34 is installed (Android Studio > SDK Manager)
- Make sure Java 17 is configured (Android Studio > Settings > Build Tools > Gradle > JDK)
### iOS: Signing errors
- Go to Signing & Capabilities in Xcode and select your development team
- For free accounts, you may need to change the bundle identifier to something unique
### Android: cleartext HTTP blocked
- The AndroidManifest.xml already includes `android:usesCleartextTraffic="true"` to allow HTTP connections to the Dart backend
### iOS: Network request fails
- The Info.plist already includes `NSAllowsLocalNetworking` to allow local HTTP connections
- For physical devices, make sure both the phone and computer are on the same Wi-Fi network
---
## Quick Reference Commands
```bash
# Install Dart dependencies
cd neon_framework/my_2nd_test_app && dart pub get
# Start the Dart backend
cd neon_framework/my_2nd_test_app && dart run lib/main.dart
# Build Android debug APK
cd neon_framework/my_2nd_test_app/android && ./gradlew assembleDebug
# Build Android release APK
cd neon_framework/my_2nd_test_app/android && ./gradlew assembleRelease
# Clean Android build
cd neon_framework/my_2nd_test_app/android && ./gradlew clean
# iOS: Open in Xcode
open neon_framework/my_2nd_test_app/NeonApp/NeonApp.xcodeproj
```

View file

@ -0,0 +1,131 @@
# Button Action Listener Fix
## Problem
The `onPressed` action listeners were not working for buttons in the showcase app.
## Root Causes
### 1. Missing Keys
Buttons didn't have stable keys, making them hard to target from native apps.
### 2. Empty Callbacks
Many buttons had empty callbacks like `onPressed: () {}` which did nothing.
### 3. StatelessWidget in NavigationScreen
The `NavigationScreen` was a `StatelessWidget`, so even when callbacks were triggered, there was no state to update and no rebuild would occur.
## Fixes Applied
### 1. Added Keys to All Buttons (showcase.dart)
```dart
// BEFORE:
FilledButton(onPressed: () {}, child: const Text('Filled'))
// AFTER:
FilledButton(
key: 'btn_filled',
onPressed: () => print('Filled button pressed'),
child: const Text('Filled'),
)
```
All buttons now have keys:
- `'btn_filled'`, `'btn_elevated'`, `'btn_outlined'`, `'btn_text'`
### 2. Made NavigationScreen Stateful (navigation_screen.dart)
```dart
// BEFORE:
class NavigationScreen extends NeonWidget { ... }
// AFTER:
class NavigationScreen extends StatefulWidget { ... }
class NavigationScreenState extends NeonState<NavigationScreen> {
int tabIndex = 0;
int navIndex = 0;
// Now callbacks can call setState() to trigger rebuilds
}
```
Added keys:
- `'tab_bar'` for TabBar
- `'nav_bar'` for NavigationBar
### 3. Functional Callbacks
All interactive widgets now have proper callbacks that:
- Print debug messages (for testing)
- Call `setState()` to update state (for stateful widgets)
- Trigger visual updates
## How to Test from Native Apps
### Test Button Click
```bash
curl -X POST http://localhost:8080/action \
-H "Content-Type: application/json" \
-d '{"id": "btn_filled"}'
```
Expected console output:
```
📥 RECEIVED ACTION 📥
Target ID: btn_filled
Index: null
Value: null
✅ Button found! Executing tap()...
Filled button pressed
🔄 Rebuilding tree after action...
```
### Test NavigationBar
```bash
curl -X POST http://localhost:8080/action \
-H "Content-Type: application/json" \
-d '{"id": "nav_bar", "index": 1}'
```
Expected: Navigation bar updates to index 1, content area changes.
### Test TabBar
```bash
curl -X POST http://localhost:8080/action \
-H "Content-Type: application/json" \
-d '{"id": "tab_bar", "index": 2}'
```
Expected: Tab bar updates to index 2, content changes to "Profile Content".
## Widget Keys Reference
### Showcase App (showcase.dart)
- Navigation: `'main_nav'`
- Buttons: `'btn_filled'`, `'btn_elevated'`, `'btn_outlined'`, `'btn_text'`
- Chips: `'action_chip'`, `'filter_chip'`
- Segmented: `'segmented_btn'`
- Controls: `'switch_notifications'`, `'checkbox_terms'`, `'radio_light'`, `'radio_dark'`
- Sliders: `'slider_volume'`, `'range_slider_price'`
### Navigation Screen (navigation_screen.dart)
- TabBar: `'tab_bar'`
- NavigationBar: `'nav_bar'`
## Debugging Tips
1. **Check Console Output**: When you send an action, you should see the "📥 RECEIVED ACTION 📥" message
2. **Verify Widget ID**: The framework logs the target ID - make sure it matches your key
3. **Look for Callback Execution**: You should see "✅ Button found!" or similar messages
4. **Check Rebuild**: After the action, you should see "🔄 Rebuilding tree after action..."
## Common Issues
### "Widget not found"
- The key doesn't match any widget in the tree
- Check the widget tree output to see actual IDs
### "Callback not triggered"
- Widget might not have an `onPressed` callback
- Check that the widget is interactive (not disabled)
### "No visual update"
- Widget might be in a StatelessWidget (no state to update)
- Convert to StatefulWidget if you need state changes

View file

@ -0,0 +1,301 @@
# iOS Action Listener Fix
## Problem
The iOS app was showing the UI but:
1. ❌ No action listeners were working (buttons, switches, etc.)
2. ❌ UI wasn't updating when interactions occurred
## Root Cause
The iOS `ViewController.swift` was **only handling Button widgets**. It was missing support for:
- Switch
- Checkbox
- Radio
- Slider
- RangeSlider
- Chips (ActionChip, FilterChip, ChoiceChip, InputChip)
- Badge
## Fixes Applied
### 1. Added Widget Rendering Support
Added rendering logic for all M3 interactive widgets in `renderWidget()`:
#### Switch
```swift
else if type == "Switch" {
let toggle = UISwitch()
toggle.isOn = node["value"] as? Bool ?? false
toggle.accessibilityIdentifier = node["id"] as? String
toggle.addTarget(self, action: #selector(handleSwitchChange(_:)), for: .valueChanged)
return toggle
}
```
#### Checkbox
```swift
else if type == "Checkbox" {
let checkbox = UIButton(type: .system)
let isChecked = node["value"] as? Bool ?? false
checkbox.setImage(UIImage(systemName: isChecked ? "checkmark.square.fill" : "square"), for: .normal)
checkbox.accessibilityIdentifier = node["id"] as? String
checkbox.addTarget(self, action: #selector(handleCheckboxTap(_:)), for: .touchUpInside)
return checkbox
}
```
#### Radio
```swift
else if type == "Radio" {
let radio = UIButton(type: .system)
let isSelected = node["selected"] as? Bool ?? false
radio.setImage(UIImage(systemName: isSelected ? "circle.fill" : "circle"), for: .normal)
radio.accessibilityIdentifier = node["id"] as? String
radio.addTarget(self, action: #selector(handleRadioTap(_:)), for: .touchUpInside)
return radio
}
```
#### Slider
```swift
else if type == "Slider" {
let slider = UISlider()
slider.value = Float(node["value"] as? Double ?? 0.5)
slider.minimumValue = Float(node["min"] as? Double ?? 0.0)
slider.maximumValue = Float(node["max"] as? Double ?? 1.0)
slider.accessibilityIdentifier = node["id"] as? String
slider.addTarget(self, action: #selector(handleSliderChange(_:)), for: .valueChanged)
return slider
}
```
#### Chips (ActionChip, FilterChip, etc.)
```swift
else if type == "ActionChip" || type == "FilterChip" || type == "ChoiceChip" || type == "InputChip" {
let chip = UIButton(type: .system)
chip.backgroundColor = .systemGray5
chip.layer.cornerRadius = 16
chip.contentEdgeInsets = UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 16)
chip.accessibilityIdentifier = node["id"] as? String
// Set label from children
if let children = node["children"] as? [[String: Any]], let firstChild = children.first {
let childView = renderWidget(node: firstChild)
if let label = childView as? UILabel {
chip.setTitle(label.text, for: .normal)
}
}
// FilterChip shows selected state
if type == "FilterChip" {
let isSelected = node["selected"] as? Bool ?? false
chip.backgroundColor = isSelected ? .systemBlue : .systemGray5
chip.setTitleColor(isSelected ? .white : .systemBlue, for: .normal)
}
chip.addTarget(self, action: #selector(handleChipTap(_:)), for: .touchUpInside)
return chip
}
```
### 2. Added Action Handlers
Added handler methods for each widget type:
```swift
@objc private func handleSwitchChange(_ sender: UISwitch) {
guard let switchId = sender.accessibilityIdentifier else { return }
print("👆 Switch toggled to \(sender.isOn)")
sendActionToDart(buttonId: switchId, value: sender.isOn)
}
@objc private func handleCheckboxTap(_ sender: UIButton) {
guard let checkboxId = sender.accessibilityIdentifier else { return }
let currentState = sender.currentImage == UIImage(systemName: "checkmark.square.fill")
let newState = !currentState
sender.setImage(UIImage(systemName: newState ? "checkmark.square.fill" : "square"), for: .normal)
print("👆 Checkbox toggled to \(newState)")
sendActionToDart(buttonId: checkboxId, value: newState)
}
@objc private func handleRadioTap(_ sender: UIButton) {
guard let radioId = sender.accessibilityIdentifier else { return }
print("👆 Radio selected")
sendActionToDart(buttonId: radioId)
}
@objc private func handleSliderChange(_ sender: UISlider) {
guard let sliderId = sender.accessibilityIdentifier else { return }
print("👆 Slider changed to \(sender.value)")
sendActionToDart(buttonId: sliderId, value: Double(sender.value))
}
@objc private func handleChipTap(_ sender: UIButton) {
guard let chipId = sender.accessibilityIdentifier else { return }
print("👆 Chip tapped")
sendActionToDart(buttonId: chipId)
}
```
### 3. Updated sendActionToDart Method
Added `value` parameter to support sending widget values:
```swift
private func sendActionToDart(buttonId: String, index: Int? = nil, value: Any? = nil) {
guard let url = URL(string: "http://127.0.0.1:8080/action") else { return }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
var body: [String: Any] = ["id": buttonId]
if let index = index {
body["index"] = index
}
if let value = value { // ← NEW
body["value"] = value
}
request.httpBody = try? JSONSerialization.data(withJSONObject: body)
// ... rest of the method (already handles response and UI update)
}
```
## How It Works Now
### Complete Flow Example (Switch Toggle)
```
1. User toggles Switch in iOS app
2. handleSwitchChange() is called
3. Sends: POST /action {"id": "switch_notifications", "value": true}
4. Dart backend receives action
5. Backend calls widget.onChanged(true)
6. onChanged calls setState(() => isToggled = true)
7. Backend waits for setState to complete
8. Backend rebuilds widget tree
9. Backend sends JSON response with updated state
10. iOS app receives response in sendActionToDart
11. iOS app calls renderWidget() with new JSON
12. iOS app removes old views and renders new tree
13. User sees updated UI with Switch ON ✅
```
## Testing
### 1. Rebuild the iOS App
```bash
# In Xcode, clean and rebuild
Product > Clean Build Folder (Cmd+Shift+K)
Product > Build (Cmd+B)
Product > Run (Cmd+R)
```
### 2. Start Dart Backend
```bash
cd /Users/hamzaibrahim/Downloads/NeonFramework-latest-v1-2026-2/NeonFramework-2/neon_framework
dart run my_2nd_test_app/lib/main.dart
```
### 3. Test Interactions
In the iOS Simulator:
- **Tap buttons** → Should see console: "👆 Button Tapped! Triggering action for ID: btn_filled"
- **Toggle switches** → Should see: "👆 Switch toggled to true"
- **Move sliders** → Should see: "👆 Slider changed to 0.75"
- **Tap chips** → Should see: "👆 Chip tapped"
### 4. Verify UI Updates
After each interaction:
- Check Xcode console for: "✅ Received response from Dart."
- Check for: "🎨 Response is a widget tree. Redrawing screen!"
- UI should update to reflect new state
## Supported Widgets
### ✅ Now Working:
- Button (FilledButton, ElevatedButton, OutlinedButton, TextButton, IconButton)
- Switch
- Checkbox
- Radio
- Slider
- ActionChip, FilterChip, ChoiceChip, InputChip
- SegmentedButton
- NavigationBar (TabBar)
- TabBar (SegmentedControl)
- Badge
### ⚠️ Not Yet Implemented:
- RangeSlider (needs two-thumb slider implementation)
- Custom animations
- Gesture recognizers beyond tap
## Debugging
### Check Console Output
**When you tap a button:**
```
🔗 Attaching tap gesture to Button ID: btn_filled
👆 Button Tapped! Triggering action for ID: btn_filled
🚀 Sending POST request to Dart engine...
✅ Received response from Dart.
🎨 Response is a widget tree. Redrawing screen!
```
**When you toggle a switch:**
```
👆 Switch toggled to true
🚀 Sending POST request to Dart engine...
✅ Received response from Dart.
🎨 Response is a widget tree. Redrawing screen!
```
### Common Issues
#### Issue: "Widget not found"
**Cause**: Widget doesn't have a key or ID
**Fix**: Add `key: 'widget_id'` to the widget in Dart
#### Issue: "Tap not registered"
**Cause**: Child view is blocking touches
**Fix**: Already handled - child views have `isUserInteractionEnabled = false`
#### Issue: "UI doesn't update"
**Cause**: Response parsing failed
**Fix**: Check Xcode console for JSON errors
#### Issue: "Wrong state shown"
**Cause**: Widget ID changed between renders
**Fix**: Ensure stable keys on all stateful widgets
## Files Modified
- `NeonApp/NeonApp/ViewController.swift` - Added M3 widget support and action handlers
## Next Steps
1. **Test all widgets** in the showcase app
2. **Implement RangeSlider** if needed (requires custom two-thumb slider)
3. **Add animations** for smoother transitions
4. **Optimize rendering** to only update changed widgets (diff algorithm)
## Summary
The iOS app now has **full support for all Material 3 interactive widgets**! Every widget type can:
- ✅ Render correctly from JSON
- ✅ Handle user interactions
- ✅ Send actions to Dart backend
- ✅ Receive updated state
- ✅ Update UI to reflect new state
The complete interaction loop is working end-to-end! 🎉

View file

@ -0,0 +1,166 @@
# Native State & Action Event Fix
## Problem Summary
The states and action events were not functional in Android and iOS apps due to:
1. **Unstable Widget IDs**: Widget IDs were being generated with `.u` suffixes during unwrapping, making them unpredictable
2. **Missing Keys**: Interactive widgets in the showcase app lacked stable keys
3. **Type Conversion Issues**: Native apps send values as strings/numbers, but the framework expected specific types
4. **JSON Serialization Crash**: `double.infinity` values caused JSON encoding to fail
## Solutions Implemented
### 1. Fixed Widget ID Generation (`widget_tree.dart`)
**Problem**: The ID generation logic was appending `.u` to every unwrapped layer, creating IDs like `root.0.u.u` instead of using the widget's key.
**Solution**: Modified the unwrapping logic to preserve keys:
```dart
// BEFORE:
currentId = "${currentContext.id!}.u";
// AFTER:
if (builtResult.key == null || builtResult.key == currentWidget.key) {
// Keep ID but update context for the new widget type
currentContext = currentContext.spawn(builtResult, currentId);
} else {
currentId = "${currentContext.id!}.${builtResult.key}";
currentContext = currentContext.spawn(builtResult, currentId);
}
```
### 2. Added Stable Keys to Showcase App (`showcase.dart`)
Added explicit keys to all interactive widgets:
- `'action_chip'`, `'filter_chip'`, `'segmented_btn'`
- `'switch_notifications'`, `'checkbox_terms'`
- `'radio_light'`, `'radio_dark'`
- `'slider_volume'`, `'range_slider_price'`
### 3. Enhanced Action Handler (`neon_app.dart`)
**Added robust logging**:
```dart
print('\n📥 RECEIVED ACTION 📥');
print(' Target ID: $targetId');
print(' Index: $index');
print(' Value: $value');
```
**Improved type conversion**:
```dart
// Switch/Checkbox: Handle both bool and string/int values
final newVal = value is bool ? value : (value == 'true' || value == 1);
widget.onChanged?.call(newVal);
// Slider: Handle both num and string values
final newVal = value is num ? value.toDouble() : double.tryParse(value.toString()) ?? 0.0;
widget.onChanged?.call(newVal);
```
### 4. Fixed JSON Serialization (`widget_tree.dart`)
**Problem**: `double.infinity` (used in `Container(width: double.infinity)`) cannot be JSON-encoded.
**Solution**: Added `_sanitizeDouble` helper:
```dart
dynamic _sanitizeDouble(double? value) {
if (value == null) return null;
if (value.isInfinite || value.isNaN) {
return -1.0; // Sentinel value for native apps
}
return value;
}
```
Applied to all double properties: `width`, `height`, `left`, `top`, `right`, `bottom`, `value`, `min`, `max`, `start`, `end`.
### 5. Propagated Keys in M3 Widgets
Updated Material 3 button wrappers to pass keys down:
```dart
// FilledButton, ElevatedButton, OutlinedButton, TextButton, IconButton
return Button(
key: key, // ← Added this
onPressed: onPressed,
color: color,
child: child,
);
```
## Testing
### Unit Tests
- ✅ `test/json_fix_test.dart`: Verifies `double.infinity` handling
- ✅ `test/m3_widgets_test.dart`: All M3 widgets (11 tests passing)
- ✅ `test/core_test.dart`: Full framework integration (343 tests passing)
### Integration Test
- `test/native_action_test.dart`: End-to-end test of native action flow
## How Native Apps Should Send Actions
### Button/Chip Click
```json
POST /action
{
"id": "action_chip"
}
```
### Switch/Checkbox Toggle
```json
POST /action
{
"id": "switch_notifications",
"value": true
}
```
### Slider Change
```json
POST /action
{
"id": "slider_volume",
"value": 0.75
}
```
### RangeSlider Change
```json
POST /action
{
"id": "range_slider_price",
"start": 20.0,
"end": 80.0
}
```
### Radio/SegmentedButton Selection
```json
POST /action
{
"id": "segmented_btn",
"index": 1
}
```
## Expected Response
After any action, the server responds with the updated widget tree JSON, including the new state values.
## Debugging Tips
1. **Check Widget IDs**: Look at the JSON response from `GET /` to see the actual IDs assigned
2. **Enable Logging**: The framework now logs all received actions with full details
3. **Verify Keys**: Ensure all interactive widgets have explicit `key` parameters
4. **Type Matching**: Native apps should send values in the correct type (bool for Switch, double for Slider, etc.)
## Next Steps for Native Apps
1. **Parse Initial State**: On app start, fetch `GET /` and parse the widget tree
2. **Render UI**: Use the widget types and properties to render native components
3. **Handle Interactions**: When user interacts, send POST to `/action` with the widget ID and new value
4. **Update UI**: Parse the response and update the native UI with the new state
## Files Modified
- `lib/src/widgets/widget_tree.dart`: ID generation, JSON serialization
- `lib/src/core/neon_app.dart`: Action handling, logging
- `lib/src/widgets/m3_buttons.dart`: Key propagation
- `lib/src/widgets/icon_button.dart`: Key propagation
- `my_2nd_test_app/lib/screens/showcase.dart`: Added stable keys
- `test/json_fix_test.dart`: New test file
- `test/native_action_test.dart`: New integration test

View file

@ -0,0 +1,40 @@
# Neon Framework — Phase 0: Vision & Constraints (LOCKED)
## Framework Identity
- **Mobile-first** (Android & iOS)
- Web (WASM/Canvas) is a future possibility, not a current goal
- VS Code is the primary development environment
## Dart Usage
- **Pure Dart + custom engine**
- No Flutter dependency — Neon owns its runtime
- Dart handles the API surface; a lightweight custom engine handles rendering via platform bridges
## Rendering Strategy
- **Hybrid** (custom layout + native paint)
- Neon defines its own layout system
- Delegates painting to platform canvas (Android Canvas / iOS Core Graphics)
- Custom renderer can be swapped in later
## Design Philosophy
- **Custom hybrid** combining the best of:
- Flutter: widget tree structure, composition over inheritance
- Compose: functions as UI building blocks, scoped state
- SwiftUI: declarative syntax, diffing for efficient updates
## Non-Goals
- No web-first design
- No no-code / visual builder tooling
- No Flutter dependency or compatibility layer
- No desktop support in initial release
- No server-side rendering
- No backward compatibility with Flutter widgets/plugins
## Target Platforms
- Android (via Dart + platform bridge)
- iOS (via Dart + platform bridge)
## SDK Distribution
- Downloadable Dart package (Neon SDK)
- Usable in VS Code as a standard Dart project
- Future: publishable to pub.dev

View file

@ -0,0 +1,76 @@
# Neon Framework — Phase 6: Tooling (CLI & Hot Reload)
## Status: Complete
## What Was Built
### CLI Tool (`neon`)
A full command-line interface for Neon Framework development:
| Command | Description |
|---------|-------------|
| `neon create <name>` | Scaffold a new Neon project (app or minimal template) |
| `neon run` | Compile and launch with hot reload active |
| `neon build` | Produce release builds (Android APK / iOS IPA) |
| `neon doctor` | Check development environment setup |
| `neon clean` | Wipe build artifacts and caches |
| `neon --version` | Print CLI version |
| `neon --help` | Show available commands |
### Project Scaffolding
Two project templates:
- **app** — Full starter app with screens directory, StatefulWidget example, counter demo
- **minimal** — Single-file starter with StatelessWidget
Generated files include:
- `pubspec.yaml` with neon_framework dependency
- `neon.yaml` project configuration (platforms, build, hot reload settings)
- `analysis_options.yaml`
- `.gitignore`
- Entry point and widget files
### Hot Reload Engine
Three-tier reload strategy:
1. **UI-only reload** — Re-runs build methods, fastest option
2. **State-preserving reload** (default) — Keeps signal values and widget state intact
3. **Full reload fallback** — Triggered on structural changes (new imports, class hierarchy changes)
Features:
- File watcher with 300ms debounce
- Automatic structural change detection
- Reload history tracking
- Session statistics (uptime, reload count)
- Filters out test files, build artifacts, .dart_tool
### Architecture
```
neon_framework/
bin/
neon.dart # CLI entry point (executable)
lib/src/tooling/
cli.dart # NeonCli runner, NeonLogger
command_create.dart # neon create implementation
command_run.dart # neon run implementation
command_build.dart # neon build implementation
command_doctor.dart # neon doctor implementation
command_clean.dart # neon clean implementation
project_template.dart # NeonProjectTemplate scaffolding
neon_project.dart # NeonProject loader (pubspec + neon.yaml)
hot_reload.dart # HotReloadEngine, HotReloadSession
```
### Dependencies Added
- `args` — Command-line argument parsing
- `path` — Cross-platform path manipulation
- `watcher` — File system change monitoring
## Tests
38 new tests added (169 total passing):
- NeonCli instantiation, version flag, unknown command handling
- NeonLogger output methods
- NeonProjectTemplate: app and minimal generation, file contents, project name injection
- HotReloadMode parsing
- HotReloadEvent creation and tracking
- HotReloadEngine lifecycle (start/stop/mode)
- HotReloadSession statistics
- NeonProject loading and error handling

View file

@ -0,0 +1,69 @@
# Neon Framework — Phase 7: Storage & Networking
## Status: Complete
## What Was Built
### Key-Value Store (`NeonKeyValueStore`)
Simple preferences and settings storage:
- **Type-safe accessors**: getString, setString, getInt, setInt, getDouble, setDouble, getBool, setBool
- **Complex types**: setStringList/getStringList, setJson/getJson
- **Backend abstraction**: `NeonKVBackend` interface with `NeonMemoryKVBackend` for testing
- **Key prefixing**: Namespace isolation via prefix parameter
- **CRUD**: containsKey, remove, clear, keys
### SQLite Database (`NeonDatabase`)
Structured data persistence:
- **Table schemas**: `NeonTableSchema` with `NeonColumn` definitions (INTEGER, REAL, TEXT, BLOB)
- **Column constraints**: PRIMARY KEY, AUTOINCREMENT, NOT NULL, UNIQUE, DEFAULT
- **Query builder**: insert, queryTable (with WHERE, ORDER BY, LIMIT, OFFSET), update, delete
- **Raw SQL**: rawQuery and rawExecute for custom operations
- **Migrations**: Ordered `NeonMigration` system with version tracking and up/down statements
- **Backend abstraction**: `NeonDatabaseBackend` interface with `NeonMemoryDatabaseBackend` for testing
### HTTP Client (`NeonHttpClient`)
REST API access:
- **All HTTP methods**: GET, POST, PUT, PATCH, DELETE, HEAD
- **Base URL**: Automatic path resolution against configurable base URL
- **Headers**: Case-insensitive `NeonHttpHeaders` with merge support
- **Request/Response interceptors**: Composable middleware pipeline for auth, logging, etc.
- **Response parsing**: `.json`, `.jsonList`, status code helpers (isSuccess, isClientError, etc.)
- **Backend abstraction**: `NeonDartHttpBackend` (real dart:io) and `NeonMockHttpBackend` for testing
- **Mock support**: URL pattern matching, default handlers, request tracking
### WebSocket Client (`NeonWebSocket`)
Real-time communication:
- **Connection lifecycle**: connect, close, reconnect with state tracking
- **Send**: Raw string and JSON serialization
- **Receive**: Message listeners with JSON parsing support
- **Auto-reconnect**: Configurable delay and max attempts
- **Event hooks**: onConnect, onDisconnect, onMessage
- **Backend abstraction**: `NeonMockWebSocketBackend` with message simulation
### Cache Policy System (`NeonCachePolicy`)
Offline-first strategies:
- **Cache-first**: Read from cache, fall back to network (default for offline-first)
- **Network-first**: Try network, fall back to cache (for data freshness)
- **Cache-only**: Strict offline mode, throws if no cache
- **Network-only**: Always fetch, caches result for later
- **Stale-while-revalidate**: Return stale immediately, refresh in background
- **Cache store**: TTL-based expiry, eviction, statistics
- **Background refresh**: Callback notification when stale data is refreshed
### Architecture
```
neon_framework/lib/src/data/
kv_store.dart # NeonKeyValueStore, NeonKVBackend, NeonMemoryKVBackend
database.dart # NeonDatabase, NeonTableSchema, NeonColumn, NeonMigration
http_client.dart # NeonHttpClient, NeonHttpRequest/Response, backends
web_socket.dart # NeonWebSocket, NeonWebSocketMessage, backends
cache_policy.dart # NeonCachePolicy, NeonCacheStore, NeonCacheEntry
```
## Tests
81 new tests added (250 total passing):
- Key-Value Store: string/int/double/bool/list/json, CRUD, prefix isolation
- Database: open/close, schema creation, insert/query/delete, migrations, raw SQL
- HTTP Client: mock backend, URL resolution, interceptors, all methods, response parsing
- WebSocket: connect/send/receive/close, reconnect, JSON messages, listeners
- Cache Policy: all 5 strategies with edge cases (miss, hit, fallback, background refresh)

View file

@ -0,0 +1,83 @@
# Neon Framework — Phase 8: Plugins & Extensibility
## Status: Complete
## What Was Built
### Plugin Model (`NeonPlugin`)
Dual-mode plugin architecture supporting both Dart-only and native plugins:
- **NeonPlugin**: Abstract base class with `onRegister`/`onDispose` lifecycle hooks
- **NeonDartPlugin**: Simplified base for Dart-only plugins (no native channel needed)
- **NeonNativePlugin**: Base for native plugins that auto-creates a `NeonPlatformChannel` and registers it with the platform dispatcher
- **NeonPluginManifest**: Declares plugin identity (id, name, version), metadata (description, author, homepage, license), SDK constraints, capabilities, dependencies, and type
### Semantic Versioning (`NeonSemanticVersion`)
Full semver implementation:
- Parses `major.minor.patch[-preRelease]` with optional `v` prefix
- Comparison operators (`<`, `>`, `<=`, `>=`, `==`)
- Pre-release ordering (pre-release < release at same version)
### SDK Constraints (`NeonSdkConstraint`)
Version compatibility checking:
- **any**: Accepts all SDK versions
- **exact**: Matches one specific version
- **range**: Min/max with inclusive/exclusive boundaries
- Validated at registration time — incompatible plugins are rejected
### Plugin Registry (`NeonPluginRegistry`)
Singleton registry managing the full plugin lifecycle:
- **Register/Unregister**: With validation for duplicates, SDK compatibility, dependency ordering, and capability conflicts
- **Dependency Resolution**: Plugins can declare dependencies; missing deps are rejected, and dependents prevent unregistration
- **Capability Conflict Detection**: Two plugins cannot declare the same capability
- **Hook System**: Plugins can register named hooks; any code can execute hooks across all registered plugins
- **Lifecycle Management**: Dispose runs in reverse registration order
- **Plugin Context**: Shared data store accessible by all plugins for inter-plugin communication
- **Stats**: Registry statistics (total, dart-only, native, hooks, registration order)
### Example Plugins
**NeonLoggerPlugin** (Dart-only):
- Structured logging with 5 levels (debug, info, warning, error, fatal)
- Configurable minimum log level
- Tag-based filtering and level-based filtering
- Log callback for custom handlers
- Stats reporting and log clearing
- Exposes itself via plugin context as `logger`
**NeonDeviceInfoPlugin** (Native):
- Auto-creates `neon/plugin/neon_device_info` platform channel
- Handles native messages: `updateDeviceInfo`, `updateBattery`
- Can invoke native methods: `getDeviceInfo`, `getBatteryInfo`
- Tracks device model, OS version, app version, battery level, charging state
- Exposes itself via plugin context as `device_info`
### Architecture
```
neon_framework/lib/src/plugins/
plugin.dart # NeonPlugin, NeonDartPlugin, NeonNativePlugin, NeonPluginManifest,
# NeonSdkConstraint, NeonSemanticVersion, NeonPluginContext
plugin_registry.dart # NeonPluginRegistry, NeonPluginHook, NeonPluginException
example_plugins.dart # NeonLoggerPlugin, NeonDeviceInfoPlugin
```
## Design Decisions
- **Dual plugin types**: Dart-only for logic/utility plugins, Native for platform access — unified by common base class
- **Capability uniqueness**: Prevents two plugins from providing the same capability (e.g., two loggers)
- **Dependency ordering**: Registration order matters — dependencies must be registered first
- **Reverse disposal**: Plugins are disposed in reverse registration order to respect dependency chains
- **Hook system**: Loose coupling — plugins register hooks, any code can trigger them
- **Plugin context**: Shared mutable data store enables inter-plugin communication without tight coupling
- **Distribution**: Git-based (primary) + manual import; registry deferred to future phase
## Tests
57 new tests added (307 total passing):
- Semantic version parsing, comparison, pre-release ordering
- SDK constraint validation (any, exact, range)
- Plugin manifest fields and JSON serialization
- Plugin context data storage and plugin tracking
- Registry: register, unregister, duplicate rejection, SDK incompatibility, dependency validation, capability conflicts
- Hook registration, execution, and edge cases
- Logger plugin: levels, filtering, callbacks, stats, context access
- Device info plugin: native channel, message handling, lifecycle, context access
- Plugin exception formatting

View file

@ -0,0 +1,196 @@
# State Update Fix Summary
## Problem
UI doesn't update on Android/iOS apps when buttons are clicked or state changes occur.
## Root Cause Analysis
### Backend (Dart) Side ✅ FIXED
1. **Timing Issue**: The widget tree was being rebuilt BEFORE `setState()` completed
- **Fix**: Added `await Future.delayed(Duration.zero)` to wait for microtask queue
2. **Missing Error Handling**: No feedback when widget wasn't found
- **Fix**: Added logging for unrecognized widgets and missing IDs
3. **State Persistence**: State is correctly stored in `NeonApp.globalStateStore`
- No changes needed - this was already working correctly
### Frontend (Native Apps) Side ⚠️ NEEDS IMPLEMENTATION
The **critical missing piece** is that native Android/iOS apps must:
1. **Wait for the response** from `POST /action`
2. **Parse the JSON response** containing the updated widget tree
3. **Update the entire UI** based on the new tree
## Changes Made
### 1. Fixed Action Handler (`neon_app.dart`)
```dart
// Added async delay to wait for setState
await Future.delayed(Duration.zero);
// Rebuild tree after state updates
_widgetTree.buildTree(root, _rootContext);
// Send updated tree back to native app
final newJsonTree = _widgetTree.root?.toJson() ?? {};
request.response
..headers.contentType = ContentType.json
..write(jsonEncode(newJsonTree))
..close();
```
### 2. Added Better Logging
- `⚠️ Widget type not recognized` for unknown widget types
- `❌ Widget not found for ID` when target widget doesn't exist
- `⏳ Wait for setState to complete` to show timing
### 3. Created Test App (`state_test.dart`)
Simple app to test state updates with:
- Counter (increment/decrement buttons)
- Toggle switch
- Slider
### 4. Created Implementation Guides
- `STATE_UPDATE_GUIDE.md` - Complete guide for native app developers
- `BUTTON_ACTION_FIX.md` - Button-specific fixes
- `NATIVE_STATE_FIX.md` - General native integration guide
## How It Works Now
### Complete Flow
```
1. User clicks button in native app (e.g., "Increment")
2. Native app sends: POST /action {"id": "btn_increment"}
3. Dart backend receives action
4. Backend finds widget by ID
5. Backend calls widget.onPressed()
6. onPressed calls setState(() => counter++)
7. Backend waits for setState to complete (microtask queue)
8. Backend rebuilds entire widget tree
9. Backend serializes tree to JSON
10. Backend sends JSON response with updated counter value
11. Native app receives response
12. Native app parses JSON and finds counter Text widget
13. Native app updates TextView to show new counter value
14. User sees updated UI ✅
```
## Testing
### Test with curl
```bash
# Start the app
dart run my_2nd_test_app/lib/state_test.dart
# In another terminal, test increment
curl -X POST http://localhost:8080/action \
-H "Content-Type: application/json" \
-d '{"id": "btn_increment"}'
# Response should contain: "text": "Counter: 1"
```
### Expected Console Output
```
📥 RECEIVED ACTION 📥
Target ID: btn_increment
Index: null
Value: null
✅ Button found! Executing tap()...
⏳ Wait for setState to complete (microtask queue)
🔄 Rebuilding tree after action...
═════════════ 🔄 NEON REBUILD FRAME ═══════════════
📦 StateTestApp [id: root]
│ ├── AppBar [id: root.0]
│ ├── Container [id: root.1]
│ │ ├── Column [id: root.1.0]
│ │ │ ├── Text [id: root.1.0.0.0]: "Counter: 1" ← UPDATED!
...
═══════════════════════════════════════════════════════
```
## Native App Implementation Checklist
### Android (Kotlin)
- [ ] Create `NeonRenderer` class
- [ ] Implement `sendAction(widgetId, value?, index?)` method
- [ ] Parse JSON response in `sendAction`
- [ ] Call `renderUI(jsonTree)` after receiving response
- [ ] Implement `parseWidget(json)` to create native views
- [ ] Set up click listeners to call `sendAction`
### iOS (Swift)
- [ ] Create `NeonRenderer` class
- [ ] Implement `sendAction(widgetId:value:index:)` async method
- [ ] Parse JSON response in `sendAction`
- [ ] Call `renderUI(tree)` after receiving response
- [ ] Implement `parseWidget(_:)` to create native views
- [ ] Set up action handlers to call `sendAction`
## Key Files
### Framework Files (Modified)
- `lib/src/core/neon_app.dart` - Added async delay and better logging
- `lib/src/widgets/widget_tree.dart` - ID generation fixes (from previous work)
### Test/Example Files (New)
- `my_2nd_test_app/lib/state_test.dart` - Simple test app
- `my_2nd_test_app/lib/screens/showcase.dart` - Full showcase with all widgets
- `my_2nd_test_app/lib/screens/navigation_screen.dart` - Navigation examples
### Documentation (New)
- `STATE_UPDATE_GUIDE.md` - Complete implementation guide
- `BUTTON_ACTION_FIX.md` - Button-specific guide
- `NATIVE_STATE_FIX.md` - General native integration
## Next Steps for Native Developers
1. **Read `STATE_UPDATE_GUIDE.md`** - Contains complete Android/iOS code examples
2. **Test with curl** - Verify the backend is working correctly
3. **Implement response parsing** - This is the critical missing piece
4. **Test with simple app** - Use `state_test.dart` for initial testing
5. **Integrate with showcase** - Move to full showcase app once basics work
## Verification
Run tests to verify everything works:
```bash
# Unit tests
dart test test/m3_widgets_test.dart
# Integration test (if port 8080 is free)
dart run my_2nd_test_app/lib/state_test.dart
```
All tests should pass ✅
## Summary
The **Dart backend is now fully functional** and correctly:
- ✅ Receives actions
- ✅ Updates state
- ✅ Rebuilds widget tree
- ✅ Sends updated JSON back
The **native apps need to**:
- ⚠️ Parse the JSON response from `/action`
- ⚠️ Update their UI based on the new widget tree
See `STATE_UPDATE_GUIDE.md` for complete implementation examples in Kotlin and Swift.

View file

@ -0,0 +1,335 @@
# State Update Implementation Guide for Native Apps
## Problem
The UI doesn't update on Android/iOS when buttons are clicked or state changes occur.
## Root Cause
The native apps need to:
1. Send the action to the Dart backend
2. **Wait for the response** containing the updated widget tree
3. **Parse the new JSON** and update the native UI
## How State Updates Work
### Flow Diagram
```
User clicks button in native app
Native app sends POST /action with widget ID
Dart backend finds widget and calls callback
Callback calls setState() to update state
Backend waits for microtask queue (setState completes)
Backend rebuilds entire widget tree
Backend serializes tree to JSON
Backend sends JSON response back to native app
Native app receives updated JSON
Native app parses JSON and updates UI
```
## Testing State Updates
### 1. Test Counter Increment
```bash
# Get initial state
curl http://localhost:8080/
# Click increment button
curl -X POST http://localhost:8080/action \
-H "Content-Type: application/json" \
-d '{"id": "btn_increment"}'
```
**Expected Response:**
The JSON should contain updated counter value. Look for the Text widget showing the counter:
```json
{
"id": "root.1.0.0.0",
"type": "Text",
"text": "Counter: 1"
}
```
### 2. Test Switch Toggle
```bash
curl -X POST http://localhost:8080/action \
-H "Content-Type: application/json" \
-d '{"id": "switch_test", "value": true}'
```
**Expected Response:**
```json
{
"id": "switch_test",
"type": "Switch",
"value": true,
"enabled": true
}
```
### 3. Test Slider Change
```bash
curl -X POST http://localhost:8080/action \
-H "Content-Type: application/json" \
-d '{"id": "slider_test", "value": 0.75}'
```
**Expected Response:**
```json
{
"id": "slider_test",
"type": "Slider",
"value": 0.75,
"min": 0.0,
"max": 1.0
}
```
## Native App Implementation
### Android (Kotlin) Example
```kotlin
class NeonRenderer(private val context: Context) {
private val client = OkHttpClient()
private val baseUrl = "http://localhost:8080"
// Store current widget tree
private var widgetTree: JsonObject? = null
// Fetch initial state
suspend fun initialize() {
val request = Request.Builder()
.url(baseUrl)
.get()
.build()
client.newCall(request).execute().use { response ->
widgetTree = Json.parseToJsonElement(response.body!!.string()).jsonObject
renderUI(widgetTree!!)
}
}
// Send action and update UI
suspend fun sendAction(widgetId: String, value: Any? = null, index: Int? = null) {
val json = buildJsonObject {
put("id", widgetId)
value?.let { put("value", it) }
index?.let { put("index", it) }
}
val request = Request.Builder()
.url("$baseUrl/action")
.post(json.toString().toRequestBody("application/json".toMediaType()))
.build()
client.newCall(request).execute().use { response ->
// CRITICAL: Parse the response and update UI
widgetTree = Json.parseToJsonElement(response.body!!.string()).jsonObject
renderUI(widgetTree!!)
}
}
// Render the UI from widget tree
private fun renderUI(tree: JsonObject) {
// Parse tree and create/update native views
val rootView = parseWidget(tree)
// Update your activity/fragment view
activity.setContentView(rootView)
}
// Parse individual widget
private fun parseWidget(widget: JsonObject): View {
val type = widget["type"]?.jsonPrimitive?.content
val id = widget["id"]?.jsonPrimitive?.content
return when (type) {
"Button" -> {
Button(context).apply {
text = widget["text"]?.jsonPrimitive?.content
setOnClickListener {
// Send action when clicked
lifecycleScope.launch {
sendAction(id!!)
}
}
}
}
"Switch" -> {
SwitchCompat(context).apply {
isChecked = widget["value"]?.jsonPrimitive?.boolean ?: false
setOnCheckedChangeListener { _, isChecked ->
lifecycleScope.launch {
sendAction(id!!, value = isChecked)
}
}
}
}
"Text" -> {
TextView(context).apply {
text = widget["text"]?.jsonPrimitive?.content
}
}
// ... handle other widget types
else -> View(context)
}
}
}
```
### iOS (Swift) Example
```swift
class NeonRenderer {
private let baseURL = "http://localhost:8080"
private var widgetTree: [String: Any]?
// Fetch initial state
func initialize() async throws {
let url = URL(string: baseURL)!
let (data, _) = try await URLSession.shared.data(from: url)
widgetTree = try JSONSerialization.jsonObject(with: data) as? [String: Any]
renderUI(widgetTree!)
}
// Send action and update UI
func sendAction(widgetId: String, value: Any? = nil, index: Int? = nil) async throws {
var json: [String: Any] = ["id": widgetId]
if let value = value { json["value"] = value }
if let index = index { json["index"] = index }
let url = URL(string: "\(baseURL)/action")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONSerialization.data(withJSONObject: json)
let (data, _) = try await URLSession.shared.data(for: request)
// CRITICAL: Parse response and update UI
widgetTree = try JSONSerialization.jsonObject(with: data) as? [String: Any]
renderUI(widgetTree!)
}
// Render UI from widget tree
private func renderUI(_ tree: [String: Any]) {
let rootView = parseWidget(tree)
// Update your view controller
viewController.view = rootView
}
// Parse individual widget
private func parseWidget(_ widget: [String: Any]) -> UIView {
let type = widget["type"] as? String
let id = widget["id"] as? String
switch type {
case "Button":
let button = UIButton()
button.setTitle(widget["text"] as? String, for: .normal)
button.addAction(UIAction { _ in
Task {
try? await self.sendAction(widgetId: id!)
}
}, for: .touchUpInside)
return button
case "Switch":
let toggle = UISwitch()
toggle.isOn = widget["value"] as? Bool ?? false
toggle.addAction(UIAction { _ in
Task {
try? await self.sendAction(widgetId: id!, value: toggle.isOn)
}
}, for: .valueChanged)
return toggle
case "Text":
let label = UILabel()
label.text = widget["text"] as? String
return label
default:
return UIView()
}
}
}
```
## Key Points for Native Apps
### ✅ DO:
1. **Always wait for the response** from `/action` endpoint
2. **Parse the entire response** - it contains the updated widget tree
3. **Update your entire UI** based on the new tree (or diff and update changed widgets)
4. **Store the widget tree** so you can reference it later
5. **Handle async operations** properly (use coroutines/async-await)
### ❌ DON'T:
1. Don't ignore the response from `/action`
2. Don't only update the clicked widget - the entire tree might have changed
3. Don't cache widget state locally - the Dart backend is the source of truth
4. Don't send actions without waiting for the response
## Debugging
### Check Dart Console
When you send an action, you should see:
```
📥 RECEIVED ACTION 📥
Target ID: btn_increment
Value: null
✅ Button found! Executing tap()...
⏳ Waiting for setState to complete...
🔄 Rebuilding tree after action...
═════════════ 🔄 NEON REBUILD FRAME ═══════════════
📦 StateTestApp [id: root]
│ ├── Text [id: root.1.0.0.0]: "Counter: 1"
...
═══════════════════════════════════════════════════════
```
### Verify JSON Response
The response should contain the updated values:
```bash
curl -X POST http://localhost:8080/action \
-H "Content-Type: application/json" \
-d '{"id": "btn_increment"}' | jq '.children[] | select(.type=="Text" and (.text | contains("Counter")))'
```
Should show: `"text": "Counter: 1"` (or whatever the new value is)
## Common Issues
### Issue: UI doesn't update
**Cause**: Native app isn't parsing the response
**Fix**: Make sure you're reading and parsing the response body from `/action`
### Issue: State resets on every action
**Cause**: Widget IDs are changing between rebuilds
**Fix**: Ensure all stateful widgets have stable `key` parameters
### Issue: Wrong state shown
**Cause**: Native app is caching old state
**Fix**: Always use the state from the latest response, don't cache locally
### Issue: Delayed updates
**Cause**: Network latency or slow JSON parsing
**Fix**: Show loading indicator while waiting for response
## Test App
A simple test app is available at `my_2nd_test_app/lib/state_test.dart`. Run it with:
```bash
dart run my_2nd_test_app/lib/state_test.dart
```
Then test with curl commands above to verify state updates work correctly.

View file

@ -0,0 +1,16 @@
analyzer:
strong-mode:
implicit-casts: false
implicit-dynamic: false
errors:
missing_return: error
dead_code: warning
linter:
rules:
- prefer_const_constructors
- prefer_final_locals
- avoid_print
- annotate_overrides
- prefer_single_quotes
- sort_constructors_first

View file

@ -0,0 +1,8 @@
import 'dart:io';
import '../tool/tooling/cli.dart';
Future<void> main(List<String> args) async {
final cli = NeonCli();
final exitCode = await cli.run(args);
exit(exitCode);
}

View file

@ -0,0 +1,407 @@
import 'package:neon_framework/neon.dart';
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
NeonWidget build(NeonBuildContext context) {
return Column(
children: [
const Text(
'Welcome to Neon!',
style: NeonTextStyle(
fontSize: 24.0,
fontWeight: NeonFontWeight.bold,
color: NeonColor.blue,
),
),
const Text('A Dart-based mobile framework'),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Button(
onPressed: () {},
color: NeonColor.blue,
child: const Text(
'Get Started',
style: NeonTextStyle(color: NeonColor.white),
),
),
Button(
onPressed: () {},
color: NeonColor.grey,
child: const Text(
'Learn More',
style: NeonTextStyle(color: NeonColor.white),
),
),
],
),
const Container(
width: 300,
height: 100,
color: NeonColor.white,
padding: NeonEdgeInsets.all(16.0),
child: Text('Content area'),
),
],
);
}
}
Future<void> main() async {
final config = const NeonConfig(
environment: NeonEnvironment.development,
buildFlavor: NeonBuildFlavor(name: 'free'),
featureFlags: NeonFeatureFlags({'dark_mode': true}),
);
NeonApp.run(const MyApp(), config: config);
final app = NeonApp.instance;
// ignore: avoid_print
print('--- Widget Tree ---');
app.widgetTree.printTree();
// ignore: avoid_print
print('--- Render Pipeline ---');
final deviceInfo = const NeonDeviceInfo(
devicePixelRatio: 3.0,
screenSize: NeonSize(360, 800),
physicalSize: NeonSize(1080, 2400),
);
final pipeline = NeonRenderPipeline(deviceInfo: deviceInfo);
final rootElement = app.widgetTree.root!;
pipeline.buildRenderTree(rootElement);
final screenConstraints = NeonConstraints.tight(deviceInfo.screenSize);
final canvas = pipeline.runPipeline(screenConstraints);
// ignore: avoid_print
print('Render tree:');
// ignore: avoid_print
print(pipeline.debugDump());
// ignore: avoid_print
print('Paint commands: ${canvas.commandCount}');
for (final cmd in canvas.commands) {
// ignore: avoid_print
print(' ${cmd.type}: ${cmd.params}');
}
// ignore: avoid_print
print('\n--- Animation Foundation ---');
const anim = NeonAnimationValue<double>(begin: 0.0, end: 1.0);
// ignore: avoid_print
print('Animation: $anim');
final mid = anim.withProgress(0.5);
// ignore: avoid_print
print('At 50%: $mid');
// ignore: avoid_print
print('Curves:');
for (final t in [0.0, 0.25, 0.5, 0.75, 1.0]) {
// ignore: avoid_print
print(
' t=$t -> easeIn=${NeonCurves.easeIn.transform(t).toStringAsFixed(3)}, easeOut=${NeonCurves.easeOut.transform(t).toStringAsFixed(3)}');
}
// ignore: avoid_print
print('\n--- State Management (Signals) ---');
final counter = NeonSignal<int>(0);
// ignore: avoid_print
print('Counter: ${counter.value}');
counter.listen(() {
// ignore: avoid_print
print('Counter changed to: ${counter.peek()}');
});
counter.value = 1;
counter.value = 2;
counter.update((c) => c + 1);
// ignore: avoid_print
print('Final counter: ${counter.value}');
final doubled = NeonComputed<int>(() => counter.value * 2);
// ignore: avoid_print
print('Doubled: ${doubled.value}');
counter.value = 10;
// ignore: avoid_print
print('Doubled after update: ${doubled.value}');
final effectLog = <String>[];
final effect = NeonEffect(() {
effectLog.add('counter is ${counter.value}');
});
counter.value = 42;
// ignore: avoid_print
print('Effect log: $effectLog');
// ignore: avoid_print
print('\n--- Async Values ---');
const loading = NeonAsyncValue<String>.loading();
// ignore: avoid_print
print(loading);
const success = NeonAsyncValue<String>.success('Hello');
// ignore: avoid_print
print(success);
// ignore: avoid_print
print('Data: ${success.data}');
final errorVal = NeonAsyncValue<String>.error('Network failed');
// ignore: avoid_print
print(errorVal);
final result = success.when(
loading: () => 'Loading...',
success: (data) => 'Got: $data',
error: (e, st) => 'Error: $e',
);
// ignore: avoid_print
print('When result: $result');
effect.dispose();
doubled.dispose();
counter.dispose();
// ignore: avoid_print
print('\n--- Platform Integration ---');
final bridge = NeonPlatformBridge.instance;
bridge.initialize(platform: NeonTargetPlatform.android);
// ignore: avoid_print
print('Platform: ${bridge.platform?.name}');
// ignore: avoid_print
print('Initialized: ${bridge.isInitialized}');
bridge.platformInfo.setMockValues(
osVersion: 'Android 14',
deviceModel: 'Pixel 8 Pro',
devicePixelRatio: 2.75,
screenSize: const NeonSize(412, 892),
safeArea: const NeonEdgeInsetsData(top: 48, bottom: 24),
isDarkMode: true,
);
// ignore: avoid_print
print('Device: ${bridge.platformInfo}');
// ignore: avoid_print
print('Safe area: ${bridge.platformInfo.safeArea}');
final renderer = bridge.nativeRenderer;
await renderer.initialize(const NeonSize(412, 892), 2.75);
// ignore: avoid_print
print('Renderer ready: ${renderer.isReady}');
final demoCanvas = NeonCanvas();
demoCanvas.drawRect(const NeonRect(0, 0, 412, 892), color: NeonColor.white);
demoCanvas.drawText('Hello from Neon!', const NeonOffset(50, 100));
await renderer.submitFrame(demoCanvas);
// ignore: avoid_print
print(
'Frame submitted: ${renderer.frameCount} frames, avg ${renderer.averageFrameTimeMs.toStringAsFixed(2)}ms');
final fs = bridge.fileSystem;
fs.enableMock();
await fs.writeFile('settings.json', '{"theme": "dark"}');
final settings = await fs.readFile('settings.json');
// ignore: avoid_print
print('File content: $settings');
final lifecycleBridge = bridge.platformLifecycle;
lifecycleBridge.simulateNativeEvent(NeonNativeLifecycleEvent.onResume);
// ignore: avoid_print
print('Lifecycle events: ${lifecycleBridge.eventLog}');
NeonPlatformBridge.reset();
// ignore: avoid_print
print('\n--- Tooling (CLI & Hot Reload) ---');
NeonCli();
// ignore: avoid_print
print('CLI version: ${NeonCli.version}');
final template = const NeonProjectTemplate(
projectName: 'demo_app',
template: 'app',
);
final files = template.generate();
// ignore: avoid_print
print('App template files: ${files.keys.toList()}');
final minimalTemplate = const NeonProjectTemplate(
projectName: 'mini_app',
template: 'minimal',
);
final minimalFiles = minimalTemplate.generate();
// ignore: avoid_print
print('Minimal template files: ${minimalFiles.keys.toList()}');
final engine = HotReloadEngine(
projectPath: '.',
watchPaths: ['lib/'],
mode: HotReloadMode.statePreserving,
onReload: (event) {
// ignore: avoid_print
print(' Reload: ${event.description}');
},
);
// ignore: avoid_print
print('Hot reload mode: ${engine.mode.name}');
// ignore: avoid_print
print('Hot reload running: ${engine.isRunning}');
// ignore: avoid_print
print('\n--- Storage & Networking ---');
final kv = NeonKeyValueStore();
await kv.setString('theme', 'dark');
await kv.setInt('launch_count', 7);
await kv.setBool('onboarded', true);
await kv.setJson('user', {'name': 'Alice', 'level': 5});
// ignore: avoid_print
print('KV theme: ${await kv.getString("theme")}');
// ignore: avoid_print
print('KV launch_count: ${await kv.getInt("launch_count")}');
// ignore: avoid_print
print('KV onboarded: ${await kv.getBool("onboarded")}');
// ignore: avoid_print
print('KV user: ${await kv.getJson("user")}');
final db = NeonDatabase(path: 'app.db');
db.defineTable(const NeonTableSchema(
name: 'todos',
columns: [
NeonColumn(
name: 'id',
type: NeonColumnType.integer,
primaryKey: true,
autoIncrement: true),
NeonColumn(name: 'title', type: NeonColumnType.text, nullable: false),
NeonColumn(name: 'done', type: NeonColumnType.integer, defaultValue: 0),
],
));
await db.open();
await db.insert('todos', {'id': 1, 'title': 'Build Neon app', 'done': 0});
await db.insert('todos', {'id': 2, 'title': 'Write tests', 'done': 1});
final todos = await db.queryTable('todos');
// ignore: avoid_print
print('Todos: ${todos.all}');
await db.close();
final mockHttp = NeonMockHttpBackend();
mockHttp.onRequest(
'/users',
(req) => mockHttp.mockResponse(
statusCode: 200,
body: '{"users": [{"name": "Alice"}, {"name": "Bob"}]}',
request: req,
));
final httpClient = NeonHttpClient(
backend: mockHttp,
baseUrl: 'https://api.example.com',
defaultHeaders: NeonHttpHeaders({'x-api-key': 'neon-secret'}),
);
final usersResponse = await httpClient.get('/users');
// ignore: avoid_print
print('HTTP GET /users: ${usersResponse.statusCode}${usersResponse.body}');
final mockWs = NeonMockWebSocketBackend();
final ws = NeonWebSocket(backend: mockWs);
await ws.connect('ws://chat.example.com');
ws.onMessage((msg) {
// ignore: avoid_print
print('WS received: ${msg.data}');
});
ws.sendJson({'type': 'join', 'room': 'neon-dev'});
mockWs.simulateMessage('{"type": "welcome", "msg": "Hello!"}');
await Future.delayed(const Duration(milliseconds: 10));
// ignore: avoid_print
print('WS sent: ${mockWs.sentMessages}');
await ws.close();
final cacheStore = NeonCacheStore(defaultMaxAge: const Duration(minutes: 5));
final cachePolicy = NeonCachePolicy<String>.cacheFirst(
store: cacheStore,
fetcher: (key) async => 'fetched_$key',
);
final cached1 = await cachePolicy.execute('greeting');
// ignore: avoid_print
print('Cache policy (miss): $cached1');
final cached2 = await cachePolicy.execute('greeting');
// ignore: avoid_print
print('Cache policy (hit): $cached2');
// ignore: avoid_print
print('Cache stats: ${cacheStore.stats}');
// ignore: avoid_print
print('\n--- Plugins & Extensibility ---');
NeonPluginRegistry.reset();
final pluginRegistry = NeonPluginRegistry.instance;
pluginRegistry.initialize(sdkVersion: '0.8.0');
final logger = NeonLoggerPlugin();
pluginRegistry.register(logger);
// ignore: avoid_print
print('Logger registered: ${logger.isRegistered}');
logger.debug('App starting', tag: 'app');
logger.info('User logged in', tag: 'auth');
logger.warning('Low memory', tag: 'system');
logger.error('Failed to fetch', tag: 'http');
// ignore: avoid_print
print('Log entries: ${logger.entries.length}');
// ignore: avoid_print
print('Log stats: ${logger.stats}');
// ignore: avoid_print
print(
'HTTP logs: ${logger.getByTag("http").map((e) => e.toString()).toList()}');
NeonPlatformDispatcher.reset();
final deviceInfoPlugin = NeonDeviceInfoPlugin();
pluginRegistry.register(deviceInfoPlugin);
// ignore: avoid_print
print('DeviceInfo registered: ${deviceInfoPlugin.isRegistered}');
// ignore: avoid_print
print('DeviceInfo channel: ${deviceInfoPlugin.nativeChannel.name}');
await deviceInfoPlugin.nativeChannel.handlePlatformMessage(
'updateDeviceInfo',
{
'model': 'Pixel 8 Pro',
'os_version': 'Android 15',
'app_version': '2.0.0'
},
);
await deviceInfoPlugin.nativeChannel.handlePlatformMessage(
'updateBattery',
{'level': 0.92, 'charging': false},
);
// ignore: avoid_print
print('Device: ${deviceInfoPlugin.toJson()}');
pluginRegistry.registerHook('neon_logger', 'onAppPause', (arg) async {
logger.info('App pausing — flushing logs');
return logger.entries.length;
});
final hookResults = await pluginRegistry.executeHook('onAppPause');
// ignore: avoid_print
print('Hook results: $hookResults');
// ignore: avoid_print
print('Registry stats: ${pluginRegistry.getRegistryStats()}');
pluginRegistry.dispose();
app.dispose();
// ignore: avoid_print
print('\n--- Done ---');
}

View file

@ -0,0 +1,67 @@
/// The Neon Framework: a mobile-first, Dart-powered UI framework.
library neon;
export 'src/core/neon_app.dart';
export 'src/core/lifecycle.dart';
export 'src/core/error_handler.dart';
export 'src/core/environment.dart';
export 'src/widgets/widget.dart';
export 'src/widgets/build_context.dart';
export 'src/widgets/primitives.dart';
export 'src/widgets/text.dart';
export 'src/widgets/button.dart';
export 'src/widgets/m3_buttons.dart';
export 'src/widgets/icon_button.dart';
export 'src/widgets/floating_action_button.dart';
export 'src/widgets/segmented_button.dart';
export 'package:neon_framework/src/widgets/badge.dart';
export 'package:neon_framework/src/widgets/chip.dart';
export 'package:neon_framework/src/widgets/selection_controls.dart';
export 'package:neon_framework/src/widgets/slider.dart';
export 'src/widgets/remote_widget.dart';
export 'src/widgets/navigation.dart';
export 'src/widgets/app_bar.dart';
export 'src/widgets/container.dart';
export 'src/widgets/layout.dart';
export 'src/widgets/widget_tree.dart';
export 'src/widgets/card.dart';
export 'src/widgets/dialog.dart';
export 'src/widgets/bottom_sheet.dart';
export 'src/widgets/search_bar.dart';
export 'src/widgets/text_field.dart';
export 'src/widgets/menu.dart';
export 'src/widgets/list_tile.dart';
export 'src/widgets/progress_indicator.dart';
export 'src/widgets/tooltip.dart';
export 'src/widgets/snack_bar.dart';
export 'src/widgets/scroll_view.dart';
export 'src/rendering/constraints.dart';
export 'src/rendering/render_object.dart';
export 'src/rendering/canvas.dart';
export 'src/rendering/render_widgets.dart';
export 'src/rendering/render_pipeline.dart';
export 'src/rendering/animation.dart';
export 'src/state/signal.dart';
export 'src/state/async_value.dart';
export 'src/state/state_widget.dart';
export 'src/platform/platform_channel.dart';
export 'src/platform/platform_bridge.dart';
export 'src/platform/native_renderer.dart';
export 'src/platform/platform_info.dart';
export 'src/platform/file_system.dart';
export 'src/platform/platform_lifecycle.dart';
export 'src/data/kv_store.dart';
export 'src/data/database.dart';
export 'src/data/http_client.dart';
export 'src/data/web_socket.dart';
export 'src/data/cache_policy.dart';
export 'src/plugins/plugin.dart';
export 'src/plugins/plugin_registry.dart';
export 'src/plugins/example_plugins.dart';

View file

@ -0,0 +1,108 @@
/// Represents the deployment environment for the Neon application.
enum NeonEnvironment {
development,
staging,
production,
}
/// Defines a build flavor with a name and associated configuration.
class NeonBuildFlavor {
/// The name of this build flavor.
final String name;
/// Key-value configuration associated with this flavor.
final Map<String, dynamic> config;
/// Creates a build flavor with the given [name] and optional [config].
const NeonBuildFlavor({
required this.name,
this.config = const {},
});
/// Returns the config value for the given [key].
dynamic operator [](String key) => config[key];
@override
String toString() => 'NeonBuildFlavor($name)';
}
/// A set of named boolean feature flags.
class NeonFeatureFlags {
final Map<String, bool> _flags;
/// Creates feature flags from an optional map of flag names to values.
const NeonFeatureFlags([this._flags = const {}]);
/// Returns whether the given [flag] is enabled, defaulting to false.
bool isEnabled(String flag) => _flags[flag] ?? false;
/// Returns a copy with the given [overrides] applied.
NeonFeatureFlags copyWith(Map<String, bool> overrides) {
return NeonFeatureFlags({..._flags, ...overrides});
}
/// Iterates over all flag entries.
void forEach(void Function(String key, bool value) fn) {
_flags.forEach(fn);
}
@override
String toString() => 'NeonFeatureFlags($_flags)';
}
/// Configuration for a Neon application including environment, flavor, and flags.
class NeonConfig {
/// The target environment for this configuration.
final NeonEnvironment environment;
/// An optional build flavor for this configuration.
final NeonBuildFlavor? buildFlavor;
/// Feature flags for this configuration.
final NeonFeatureFlags featureFlags;
/// The port for the UI server.
final int port;
/// Custom key-value configuration data.
final Map<String, dynamic> custom;
/// Creates a Neon configuration with the given parameters.
const NeonConfig({
this.environment = NeonEnvironment.development,
this.buildFlavor,
this.featureFlags = const NeonFeatureFlags(),
this.port = 8080,
this.custom = const {},
});
/// Whether the environment is development.
bool get isDev => environment == NeonEnvironment.development;
/// Whether the environment is staging.
bool get isStaging => environment == NeonEnvironment.staging;
/// Whether the environment is production.
bool get isProd => environment == NeonEnvironment.production;
/// Returns a copy of this config with the given fields replaced.
NeonConfig copyWith({
NeonEnvironment? environment,
NeonBuildFlavor? buildFlavor,
NeonFeatureFlags? featureFlags,
int? port,
Map<String, dynamic>? custom,
}) {
return NeonConfig(
environment: environment ?? this.environment,
buildFlavor: buildFlavor ?? this.buildFlavor,
featureFlags: featureFlags ?? this.featureFlags,
port: port ?? this.port,
custom: custom ?? this.custom,
);
}
@override
String toString() =>
'NeonConfig(env: $environment, flavor: $buildFlavor, flags: $featureFlags)';
}

View file

@ -0,0 +1,119 @@
import 'environment.dart';
/// Callback type for custom error handling.
typedef NeonErrorCallback = void Function(
Object error, StackTrace stackTrace);
/// Defines how errors are handled based on the environment.
enum NeonErrorStrategy {
crash,
recoverAndLog,
configurable,
}
/// Handles errors in the Neon framework with environment-aware strategies.
class NeonErrorHandler {
/// The current environment used to determine error strategy.
final NeonEnvironment environment;
NeonErrorCallback? _customHandler;
final List<NeonErrorRecord> _errorLog = [];
/// Creates an error handler for the given [environment].
NeonErrorHandler({
this.environment = NeonEnvironment.development,
});
/// Returns an unmodifiable list of recorded errors.
List<NeonErrorRecord> get errorLog => List.unmodifiable(_errorLog);
/// Sets a custom error handler callback.
void setCustomHandler(NeonErrorCallback handler) {
_customHandler = handler;
}
/// Returns the error strategy based on the current environment.
NeonErrorStrategy get strategy {
switch (environment) {
case NeonEnvironment.development:
return NeonErrorStrategy.crash;
case NeonEnvironment.staging:
return NeonErrorStrategy.recoverAndLog;
case NeonEnvironment.production:
return NeonErrorStrategy.recoverAndLog;
}
}
/// Handles an error by logging it and applying the current strategy.
void handleError(Object error, StackTrace stackTrace) {
final record = NeonErrorRecord(
error: error,
stackTrace: stackTrace,
timestamp: DateTime.now(),
environment: environment,
);
_errorLog.add(record);
if (_customHandler != null) {
_customHandler!(error, stackTrace);
return;
}
switch (strategy) {
case NeonErrorStrategy.crash:
_logError(record);
throw error;
case NeonErrorStrategy.recoverAndLog:
_logError(record);
break;
case NeonErrorStrategy.configurable:
_logError(record);
break;
}
}
void _logError(NeonErrorRecord record) {
final buffer = StringBuffer()
..writeln('[Neon Error] ${record.timestamp}')
..writeln('Environment: ${record.environment.name}')
..writeln('Error: ${record.error}')
..writeln('Stack Trace:')
..writeln(record.stackTrace);
if (environment == NeonEnvironment.development) {
// ignore: avoid_print
print(buffer.toString());
}
}
/// Clears all recorded error logs.
void clearLog() {
_errorLog.clear();
}
}
/// A record of a single error occurrence with metadata.
class NeonErrorRecord {
/// The error object that was caught.
final Object error;
/// The stack trace at the time of the error.
final StackTrace stackTrace;
/// The time the error occurred.
final DateTime timestamp;
/// The environment in which the error occurred.
final NeonEnvironment environment;
/// Creates an error record with the given details.
const NeonErrorRecord({
required this.error,
required this.stackTrace,
required this.timestamp,
required this.environment,
});
@override
String toString() =>
'NeonErrorRecord(${error.runtimeType}: $error at $timestamp)';
}

View file

@ -0,0 +1,96 @@
/// Represents the possible states of the Neon application lifecycle.
enum NeonLifecycleState {
created,
initialized,
running,
paused,
resumed,
disposed,
}
/// Interface for observing lifecycle events in a Neon application.
abstract class NeonLifecycleObserver {
/// Called when the application is initialized.
void onInit() {}
/// Called when the application is paused.
void onPause() {}
/// Called when the application is resumed.
void onResume() {}
/// Called when the application is disposed.
void onDispose() {}
/// Called when a hot reload is triggered.
void onHotReload() {}
}
/// Manages application lifecycle state and notifies registered observers.
class NeonLifecycleManager {
NeonLifecycleState _state = NeonLifecycleState.created;
final List<NeonLifecycleObserver> _observers = [];
/// The current lifecycle state.
NeonLifecycleState get state => _state;
/// Registers an observer to receive lifecycle callbacks.
void addObserver(NeonLifecycleObserver observer) {
_observers.add(observer);
}
/// Removes a previously registered observer.
void removeObserver(NeonLifecycleObserver observer) {
_observers.remove(observer);
}
/// Initializes the lifecycle and notifies all observers.
void init() {
_state = NeonLifecycleState.initialized;
for (final observer in _observers) {
observer.onInit();
}
}
/// Marks the lifecycle state as running.
void markRunning() {
_state = NeonLifecycleState.running;
}
/// Pauses the lifecycle if currently running or resumed.
void pause() {
if (_state == NeonLifecycleState.running ||
_state == NeonLifecycleState.resumed) {
_state = NeonLifecycleState.paused;
for (final observer in _observers) {
observer.onPause();
}
}
}
/// Resumes the lifecycle from a paused state.
void resume() {
if (_state == NeonLifecycleState.paused) {
_state = NeonLifecycleState.resumed;
for (final observer in _observers) {
observer.onResume();
}
}
}
/// Disposes the lifecycle manager and clears all observers.
void dispose() {
_state = NeonLifecycleState.disposed;
for (final observer in _observers) {
observer.onDispose();
}
_observers.clear();
}
/// Triggers a hot reload notification to all observers.
void hotReload() {
for (final observer in _observers) {
observer.onHotReload();
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,245 @@
import 'dart:async';
enum NeonCachePolicyType {
cacheFirst,
networkFirst,
cacheOnly,
networkOnly,
staleWhileRevalidate,
}
class NeonCacheEntry<T> {
final String key;
final T data;
final DateTime cachedAt;
final Duration? maxAge;
NeonCacheEntry({
required this.key,
required this.data,
DateTime? cachedAt,
this.maxAge,
}) : cachedAt = cachedAt ?? DateTime.now();
bool get isExpired {
if (maxAge == null) return false;
return DateTime.now().difference(cachedAt) > maxAge!;
}
bool get isFresh => !isExpired;
Duration get age => DateTime.now().difference(cachedAt);
@override
String toString() =>
'NeonCacheEntry($key, age: ${age.inSeconds}s, expired: $isExpired)';
}
class NeonCacheStore {
final Map<String, NeonCacheEntry<dynamic>> _entries = {};
final Duration defaultMaxAge;
NeonCacheStore({this.defaultMaxAge = const Duration(minutes: 5)});
void put<T>(String key, T data, {Duration? maxAge}) {
_entries[key] = NeonCacheEntry<T>(
key: key,
data: data,
maxAge: maxAge ?? defaultMaxAge,
);
}
NeonCacheEntry<T>? get<T>(String key) {
final entry = _entries[key];
if (entry == null) return null;
return NeonCacheEntry<T>(
key: entry.key,
data: entry.data as T,
cachedAt: entry.cachedAt,
maxAge: entry.maxAge,
);
}
bool has(String key) => _entries.containsKey(key);
bool hasFresh(String key) {
final entry = _entries[key];
return entry != null && entry.isFresh;
}
void remove(String key) => _entries.remove(key);
void clear() => _entries.clear();
void evictExpired() {
_entries.removeWhere((_, entry) => entry.isExpired);
}
int get size => _entries.length;
List<String> get keys => _entries.keys.toList();
Map<String, dynamic> get stats => {
'size': size,
'fresh': _entries.values.where((e) => e.isFresh).length,
'expired': _entries.values.where((e) => e.isExpired).length,
};
}
class NeonCachePolicy<T> {
final NeonCachePolicyType type;
final NeonCacheStore _store;
final Future<T> Function(String key) _fetcher;
final Duration? maxAge;
final void Function(T data)? _onBackgroundRefresh;
NeonCachePolicy({
required this.type,
required NeonCacheStore store,
required Future<T> Function(String key) fetcher,
this.maxAge,
void Function(T data)? onBackgroundRefresh,
}) : _store = store,
_fetcher = fetcher,
_onBackgroundRefresh = onBackgroundRefresh;
factory NeonCachePolicy.cacheFirst({
required NeonCacheStore store,
required Future<T> Function(String key) fetcher,
Duration? maxAge,
}) {
return NeonCachePolicy(
type: NeonCachePolicyType.cacheFirst,
store: store,
fetcher: fetcher,
maxAge: maxAge,
);
}
factory NeonCachePolicy.networkFirst({
required NeonCacheStore store,
required Future<T> Function(String key) fetcher,
Duration? maxAge,
}) {
return NeonCachePolicy(
type: NeonCachePolicyType.networkFirst,
store: store,
fetcher: fetcher,
maxAge: maxAge,
);
}
factory NeonCachePolicy.cacheOnly({
required NeonCacheStore store,
required Future<T> Function(String key) fetcher,
}) {
return NeonCachePolicy(
type: NeonCachePolicyType.cacheOnly,
store: store,
fetcher: fetcher,
);
}
factory NeonCachePolicy.networkOnly({
required NeonCacheStore store,
required Future<T> Function(String key) fetcher,
Duration? maxAge,
}) {
return NeonCachePolicy(
type: NeonCachePolicyType.networkOnly,
store: store,
fetcher: fetcher,
maxAge: maxAge,
);
}
factory NeonCachePolicy.staleWhileRevalidate({
required NeonCacheStore store,
required Future<T> Function(String key) fetcher,
Duration? maxAge,
void Function(T data)? onBackgroundRefresh,
}) {
return NeonCachePolicy(
type: NeonCachePolicyType.staleWhileRevalidate,
store: store,
fetcher: fetcher,
maxAge: maxAge,
onBackgroundRefresh: onBackgroundRefresh,
);
}
Future<T> execute(String key) async {
switch (type) {
case NeonCachePolicyType.cacheFirst:
return _executeCacheFirst(key);
case NeonCachePolicyType.networkFirst:
return _executeNetworkFirst(key);
case NeonCachePolicyType.cacheOnly:
return _executeCacheOnly(key);
case NeonCachePolicyType.networkOnly:
return _executeNetworkOnly(key);
case NeonCachePolicyType.staleWhileRevalidate:
return _executeStaleWhileRevalidate(key);
}
}
Future<T> _executeCacheFirst(String key) async {
final cached = _store.get<T>(key);
if (cached != null && cached.isFresh) {
return cached.data;
}
final data = await _fetcher(key);
_store.put<T>(key, data, maxAge: maxAge);
return data;
}
Future<T> _executeNetworkFirst(String key) async {
try {
final data = await _fetcher(key);
_store.put<T>(key, data, maxAge: maxAge);
return data;
} catch (_) {
final cached = _store.get<T>(key);
if (cached != null) {
return cached.data;
}
rethrow;
}
}
Future<T> _executeCacheOnly(String key) async {
final cached = _store.get<T>(key);
if (cached != null) {
return cached.data;
}
throw StateError('No cached data for key "$key".');
}
Future<T> _executeNetworkOnly(String key) async {
final data = await _fetcher(key);
_store.put<T>(key, data, maxAge: maxAge);
return data;
}
Future<T> _executeStaleWhileRevalidate(String key) async {
final cached = _store.get<T>(key);
if (cached != null) {
if (cached.isExpired) {
_backgroundRefresh(key);
}
return cached.data;
}
final data = await _fetcher(key);
_store.put<T>(key, data, maxAge: maxAge);
return data;
}
void _backgroundRefresh(String key) {
_fetcher(key).then((data) {
_store.put<T>(key, data, maxAge: maxAge);
_onBackgroundRefresh?.call(data);
}).catchError((_) {});
}
}

View file

@ -0,0 +1,330 @@
import 'dart:async';
enum NeonColumnType { integer, real, text, blob }
class NeonColumn {
final String name;
final NeonColumnType type;
final bool primaryKey;
final bool autoIncrement;
final bool nullable;
final dynamic defaultValue;
final bool unique;
const NeonColumn({
required this.name,
required this.type,
this.primaryKey = false,
this.autoIncrement = false,
this.nullable = true,
this.defaultValue,
this.unique = false,
});
String toSql() {
final buf = StringBuffer();
buf.write('$name ');
switch (type) {
case NeonColumnType.integer:
buf.write('INTEGER');
break;
case NeonColumnType.real:
buf.write('REAL');
break;
case NeonColumnType.text:
buf.write('TEXT');
break;
case NeonColumnType.blob:
buf.write('BLOB');
break;
}
if (primaryKey) buf.write(' PRIMARY KEY');
if (autoIncrement) buf.write(' AUTOINCREMENT');
if (!nullable && !primaryKey) buf.write(' NOT NULL');
if (unique) buf.write(' UNIQUE');
if (defaultValue != null) buf.write(' DEFAULT $defaultValue');
return buf.toString();
}
}
class NeonTableSchema {
final String name;
final List<NeonColumn> columns;
const NeonTableSchema({required this.name, required this.columns});
String toCreateSql() {
final cols = columns.map((c) => c.toSql()).join(', ');
return 'CREATE TABLE IF NOT EXISTS $name ($cols)';
}
String toDropSql() => 'DROP TABLE IF EXISTS $name';
}
class NeonMigration {
final int version;
final String description;
final List<String> upStatements;
final List<String> downStatements;
const NeonMigration({
required this.version,
required this.description,
required this.upStatements,
this.downStatements = const [],
});
}
class NeonQueryResult {
final List<Map<String, dynamic>> rows;
final int rowsAffected;
final int? lastInsertId;
const NeonQueryResult({
this.rows = const [],
this.rowsAffected = 0,
this.lastInsertId,
});
bool get isEmpty => rows.isEmpty;
bool get isNotEmpty => rows.isNotEmpty;
int get length => rows.length;
Map<String, dynamic>? get first => rows.isNotEmpty ? rows.first : null;
List<Map<String, dynamic>> get all => rows;
}
abstract class NeonDatabaseBackend {
Future<void> open(String path);
Future<void> close();
Future<NeonQueryResult> execute(String sql, [List<dynamic>? params]);
Future<NeonQueryResult> query(String sql, [List<dynamic>? params]);
bool get isOpen;
}
class NeonMemoryDatabaseBackend implements NeonDatabaseBackend {
bool _isOpen = false;
final Map<String, List<Map<String, dynamic>>> _data = {};
final List<String> _executedSql = [];
@override
bool get isOpen => _isOpen;
List<String> get executedSql => List.unmodifiable(_executedSql);
@override
Future<void> open(String path) async {
_isOpen = true;
}
@override
Future<void> close() async {
_isOpen = false;
}
@override
Future<NeonQueryResult> execute(String sql, [List<dynamic>? params]) async {
_ensureOpen();
_executedSql.add(sql);
if (sql.toUpperCase().startsWith('CREATE TABLE')) {
final tableName = _extractTableName(sql);
if (tableName != null) {
_data.putIfAbsent(tableName, () => []);
}
return const NeonQueryResult(rowsAffected: 0);
}
if (sql.toUpperCase().startsWith('INSERT')) {
final tableName = _extractInsertTableName(sql);
if (tableName != null && _data.containsKey(tableName)) {
final row = _buildRowFromParams(sql, params ?? []);
_data[tableName]!.add(row);
return NeonQueryResult(
rowsAffected: 1,
lastInsertId: _data[tableName]!.length,
);
}
return const NeonQueryResult(rowsAffected: 1);
}
if (sql.toUpperCase().startsWith('DELETE')) {
final tableName = _extractFromTableName(sql);
if (tableName != null && _data.containsKey(tableName)) {
final before = _data[tableName]!.length;
_data[tableName]!.clear();
return NeonQueryResult(rowsAffected: before);
}
return const NeonQueryResult(rowsAffected: 0);
}
return const NeonQueryResult(rowsAffected: 0);
}
@override
Future<NeonQueryResult> query(String sql, [List<dynamic>? params]) async {
_ensureOpen();
_executedSql.add(sql);
final tableName = _extractFromTableName(sql);
if (tableName != null && _data.containsKey(tableName)) {
return NeonQueryResult(rows: List.from(_data[tableName]!));
}
return const NeonQueryResult();
}
void _ensureOpen() {
if (!_isOpen) {
throw StateError('Database is not open.');
}
}
String? _extractTableName(String sql) {
final match = RegExp(r'CREATE TABLE\s+(?:IF NOT EXISTS\s+)?(\w+)', caseSensitive: false)
.firstMatch(sql);
return match?.group(1);
}
String? _extractInsertTableName(String sql) {
final match = RegExp(r'INSERT\s+INTO\s+(\w+)', caseSensitive: false)
.firstMatch(sql);
return match?.group(1);
}
String? _extractFromTableName(String sql) {
final match = RegExp(r'FROM\s+(\w+)', caseSensitive: false)
.firstMatch(sql);
return match?.group(1);
}
Map<String, dynamic> _buildRowFromParams(String sql, List<dynamic> params) {
final match = RegExp(r'\(([^)]+)\)\s*VALUES', caseSensitive: false)
.firstMatch(sql);
if (match != null) {
final columns = match.group(1)!.split(',').map((c) => c.trim()).toList();
final row = <String, dynamic>{};
for (var i = 0; i < columns.length && i < params.length; i++) {
row[columns[i]] = params[i];
}
return row;
}
return {'_data': params};
}
Map<String, List<Map<String, dynamic>>> get dataSnapshot =>
Map.unmodifiable(_data);
}
class NeonDatabase {
final NeonDatabaseBackend _backend;
final String path;
final List<NeonTableSchema> _schemas = [];
final List<NeonMigration> _migrations = [];
int _version = 0;
NeonDatabase({
required this.path,
NeonDatabaseBackend? backend,
}) : _backend = backend ?? NeonMemoryDatabaseBackend();
bool get isOpen => _backend.isOpen;
int get version => _version;
NeonDatabaseBackend get backend => _backend;
void defineTable(NeonTableSchema schema) {
_schemas.add(schema);
}
void addMigration(NeonMigration migration) {
_migrations.add(migration);
_migrations.sort((a, b) => a.version.compareTo(b.version));
}
Future<void> open() async {
await _backend.open(path);
await _createTables();
await _runMigrations();
}
Future<void> close() async {
await _backend.close();
}
Future<void> _createTables() async {
for (final schema in _schemas) {
await _backend.execute(schema.toCreateSql());
}
}
Future<void> _runMigrations() async {
for (final migration in _migrations) {
if (migration.version > _version) {
for (final sql in migration.upStatements) {
await _backend.execute(sql);
}
_version = migration.version;
}
}
}
Future<NeonQueryResult> rawQuery(String sql, [List<dynamic>? params]) {
return _backend.query(sql, params);
}
Future<NeonQueryResult> rawExecute(String sql, [List<dynamic>? params]) {
return _backend.execute(sql, params);
}
Future<NeonQueryResult> insert(
String table,
Map<String, dynamic> values,
) {
final columns = values.keys.join(', ');
final placeholders = values.keys.map((_) => '?').join(', ');
final sql = 'INSERT INTO $table ($columns) VALUES ($placeholders)';
return _backend.execute(sql, values.values.toList());
}
Future<NeonQueryResult> queryTable(
String table, {
String? where,
List<dynamic>? whereArgs,
String? orderBy,
int? limit,
int? offset,
}) {
final buf = StringBuffer('SELECT * FROM $table');
if (where != null) buf.write(' WHERE $where');
if (orderBy != null) buf.write(' ORDER BY $orderBy');
if (limit != null) buf.write(' LIMIT $limit');
if (offset != null) buf.write(' OFFSET $offset');
return _backend.query(buf.toString(), whereArgs);
}
Future<NeonQueryResult> update(
String table,
Map<String, dynamic> values, {
String? where,
List<dynamic>? whereArgs,
}) {
final sets = values.keys.map((k) => '$k = ?').join(', ');
final buf = StringBuffer('UPDATE $table SET $sets');
if (where != null) buf.write(' WHERE $where');
final params = [...values.values, ...?whereArgs];
return _backend.execute(buf.toString(), params);
}
Future<NeonQueryResult> delete(
String table, {
String? where,
List<dynamic>? whereArgs,
}) {
final buf = StringBuffer('DELETE FROM $table');
if (where != null) buf.write(' WHERE $where');
return _backend.execute(buf.toString(), whereArgs);
}
}

View file

@ -0,0 +1,352 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
enum NeonHttpMethod { get, post, put, patch, delete, head }
class NeonHttpHeaders {
final Map<String, String> _headers;
NeonHttpHeaders([Map<String, String>? headers])
: _headers = Map.from(headers ?? {});
String? operator [](String key) => _headers[key.toLowerCase()];
void operator []=(String key, String value) {
_headers[key.toLowerCase()] = value;
}
void set(String key, String value) => _headers[key.toLowerCase()] = value;
void remove(String key) => _headers.remove(key.toLowerCase());
bool containsKey(String key) => _headers.containsKey(key.toLowerCase());
Map<String, String> toMap() => Map.unmodifiable(_headers);
NeonHttpHeaders merge(NeonHttpHeaders other) {
return NeonHttpHeaders({..._headers, ...other._headers});
}
}
class NeonHttpRequest {
final NeonHttpMethod method;
final Uri url;
final NeonHttpHeaders headers;
final dynamic body;
final Duration? timeout;
NeonHttpRequest({
required this.method,
required this.url,
NeonHttpHeaders? headers,
this.body,
this.timeout,
}) : headers = headers ?? NeonHttpHeaders();
NeonHttpRequest.get(String url, {NeonHttpHeaders? headers, Duration? timeout})
: this(
method: NeonHttpMethod.get,
url: Uri.parse(url),
headers: headers,
timeout: timeout,
);
NeonHttpRequest.post(String url, {dynamic body, NeonHttpHeaders? headers, Duration? timeout})
: this(
method: NeonHttpMethod.post,
url: Uri.parse(url),
headers: headers,
body: body,
timeout: timeout,
);
NeonHttpRequest.put(String url, {dynamic body, NeonHttpHeaders? headers, Duration? timeout})
: this(
method: NeonHttpMethod.put,
url: Uri.parse(url),
headers: headers,
body: body,
timeout: timeout,
);
NeonHttpRequest.delete(String url, {NeonHttpHeaders? headers, Duration? timeout})
: this(
method: NeonHttpMethod.delete,
url: Uri.parse(url),
headers: headers,
timeout: timeout,
);
}
class NeonHttpResponse {
final int statusCode;
final NeonHttpHeaders headers;
final String body;
final Duration duration;
final NeonHttpRequest request;
NeonHttpResponse({
required this.statusCode,
required this.headers,
required this.body,
required this.duration,
required this.request,
});
bool get isSuccess => statusCode >= 200 && statusCode < 300;
bool get isRedirect => statusCode >= 300 && statusCode < 400;
bool get isClientError => statusCode >= 400 && statusCode < 500;
bool get isServerError => statusCode >= 500;
Map<String, dynamic> get json {
try {
return jsonDecode(body) as Map<String, dynamic>;
} catch (e) {
throw FormatException('Response body is not valid JSON: $e');
}
}
List<dynamic> get jsonList {
try {
return jsonDecode(body) as List<dynamic>;
} catch (e) {
throw FormatException('Response body is not valid JSON array: $e');
}
}
@override
String toString() =>
'NeonHttpResponse($statusCode, ${body.length} bytes, ${duration.inMilliseconds}ms)';
}
typedef NeonHttpInterceptor = Future<NeonHttpRequest> Function(NeonHttpRequest request);
typedef NeonHttpResponseInterceptor = Future<NeonHttpResponse> Function(NeonHttpResponse response);
abstract class NeonHttpBackend {
Future<NeonHttpResponse> send(NeonHttpRequest request);
}
class NeonDartHttpBackend implements NeonHttpBackend {
@override
Future<NeonHttpResponse> send(NeonHttpRequest request) async {
final client = HttpClient();
final stopwatch = Stopwatch()..start();
try {
final ioRequest = await client.openUrl(
request.method.name.toUpperCase(),
request.url,
);
request.headers.toMap().forEach((key, value) {
ioRequest.headers.set(key, value);
});
if (request.body != null) {
String bodyStr;
if (request.body is Map || request.body is List) {
bodyStr = jsonEncode(request.body);
ioRequest.headers.set('content-type', 'application/json');
} else {
bodyStr = request.body.toString();
}
ioRequest.write(bodyStr);
}
final ioResponse = await ioRequest.close();
final responseBody = await ioResponse.transform(utf8.decoder).join();
stopwatch.stop();
final responseHeaders = NeonHttpHeaders();
ioResponse.headers.forEach((name, values) {
responseHeaders.set(name, values.join(', '));
});
return NeonHttpResponse(
statusCode: ioResponse.statusCode,
headers: responseHeaders,
body: responseBody,
duration: stopwatch.elapsed,
request: request,
);
} finally {
client.close();
}
}
}
class NeonMockHttpBackend implements NeonHttpBackend {
final List<NeonHttpRequest> sentRequests = [];
final Map<String, NeonHttpResponse Function(NeonHttpRequest)> _handlers = {};
NeonHttpResponse Function(NeonHttpRequest)? _defaultHandler;
void onRequest(String urlPattern, NeonHttpResponse Function(NeonHttpRequest) handler) {
_handlers[urlPattern] = handler;
}
void onAnyRequest(NeonHttpResponse Function(NeonHttpRequest) handler) {
_defaultHandler = handler;
}
NeonHttpResponse mockResponse({
int statusCode = 200,
String body = '',
Map<String, String>? headers,
required NeonHttpRequest request,
}) {
return NeonHttpResponse(
statusCode: statusCode,
headers: NeonHttpHeaders(headers ?? {}),
body: body,
duration: const Duration(milliseconds: 1),
request: request,
);
}
@override
Future<NeonHttpResponse> send(NeonHttpRequest request) async {
sentRequests.add(request);
final url = request.url.toString();
for (final entry in _handlers.entries) {
if (url.contains(entry.key)) {
return entry.value(request);
}
}
if (_defaultHandler != null) {
return _defaultHandler!(request);
}
return mockResponse(
statusCode: 200,
body: '{"mock": true}',
request: request,
);
}
}
class NeonHttpClient {
final NeonHttpBackend _backend;
final NeonHttpHeaders _defaultHeaders;
final String? _baseUrl;
final Duration _defaultTimeout;
final List<NeonHttpInterceptor> _requestInterceptors = [];
final List<NeonHttpResponseInterceptor> _responseInterceptors = [];
NeonHttpClient({
NeonHttpBackend? backend,
NeonHttpHeaders? defaultHeaders,
String? baseUrl,
Duration defaultTimeout = const Duration(seconds: 30),
}) : _backend = backend ?? NeonDartHttpBackend(),
_defaultHeaders = defaultHeaders ?? NeonHttpHeaders(),
_baseUrl = baseUrl,
_defaultTimeout = defaultTimeout;
NeonHttpBackend get backend => _backend;
void addRequestInterceptor(NeonHttpInterceptor interceptor) {
_requestInterceptors.add(interceptor);
}
void addResponseInterceptor(NeonHttpResponseInterceptor interceptor) {
_responseInterceptors.add(interceptor);
}
Uri _resolveUrl(String urlOrPath) {
if (urlOrPath.startsWith('http://') || urlOrPath.startsWith('https://')) {
return Uri.parse(urlOrPath);
}
if (_baseUrl != null) {
final base = _baseUrl!.endsWith('/') ? _baseUrl! : '$_baseUrl/';
final path = urlOrPath.startsWith('/') ? urlOrPath.substring(1) : urlOrPath;
return Uri.parse('$base$path');
}
return Uri.parse(urlOrPath);
}
Future<NeonHttpResponse> send(NeonHttpRequest request) async {
var req = request;
for (final interceptor in _requestInterceptors) {
req = await interceptor(req);
}
var response = await _backend.send(req);
for (final interceptor in _responseInterceptors) {
response = await interceptor(response);
}
return response;
}
Future<NeonHttpResponse> get(
String url, {
NeonHttpHeaders? headers,
Duration? timeout,
}) {
return send(NeonHttpRequest(
method: NeonHttpMethod.get,
url: _resolveUrl(url),
headers: _defaultHeaders.merge(headers ?? NeonHttpHeaders()),
timeout: timeout ?? _defaultTimeout,
));
}
Future<NeonHttpResponse> post(
String url, {
dynamic body,
NeonHttpHeaders? headers,
Duration? timeout,
}) {
return send(NeonHttpRequest(
method: NeonHttpMethod.post,
url: _resolveUrl(url),
headers: _defaultHeaders.merge(headers ?? NeonHttpHeaders()),
body: body,
timeout: timeout ?? _defaultTimeout,
));
}
Future<NeonHttpResponse> put(
String url, {
dynamic body,
NeonHttpHeaders? headers,
Duration? timeout,
}) {
return send(NeonHttpRequest(
method: NeonHttpMethod.put,
url: _resolveUrl(url),
headers: _defaultHeaders.merge(headers ?? NeonHttpHeaders()),
body: body,
timeout: timeout ?? _defaultTimeout,
));
}
Future<NeonHttpResponse> patch(
String url, {
dynamic body,
NeonHttpHeaders? headers,
Duration? timeout,
}) {
return send(NeonHttpRequest(
method: NeonHttpMethod.patch,
url: _resolveUrl(url),
headers: _defaultHeaders.merge(headers ?? NeonHttpHeaders()),
body: body,
timeout: timeout ?? _defaultTimeout,
));
}
Future<NeonHttpResponse> delete(
String url, {
NeonHttpHeaders? headers,
Duration? timeout,
}) {
return send(NeonHttpRequest(
method: NeonHttpMethod.delete,
url: _resolveUrl(url),
headers: _defaultHeaders.merge(headers ?? NeonHttpHeaders()),
timeout: timeout ?? _defaultTimeout,
));
}
}

View file

@ -0,0 +1,140 @@
import 'dart:async';
import 'dart:convert';
abstract class NeonKVBackend {
Future<String?> read(String key);
Future<void> write(String key, String value);
Future<void> remove(String key);
Future<void> clear();
Future<List<String>> keys();
Future<bool> containsKey(String key);
}
class NeonMemoryKVBackend implements NeonKVBackend {
final Map<String, String> _store = {};
@override
Future<String?> read(String key) async => _store[key];
@override
Future<void> write(String key, String value) async {
_store[key] = value;
}
@override
Future<void> remove(String key) async {
_store.remove(key);
}
@override
Future<void> clear() async {
_store.clear();
}
@override
Future<List<String>> keys() async => _store.keys.toList();
@override
Future<bool> containsKey(String key) async => _store.containsKey(key);
Map<String, String> get snapshot => Map.unmodifiable(_store);
}
class NeonKeyValueStore {
final NeonKVBackend _backend;
final String _prefix;
NeonKeyValueStore({
NeonKVBackend? backend,
String prefix = '',
}) : _backend = backend ?? NeonMemoryKVBackend(),
_prefix = prefix;
String _prefixedKey(String key) => _prefix.isEmpty ? key : '$_prefix.$key';
Future<String?> getString(String key) {
return _backend.read(_prefixedKey(key));
}
Future<void> setString(String key, String value) {
return _backend.write(_prefixedKey(key), value);
}
Future<int?> getInt(String key) async {
final raw = await _backend.read(_prefixedKey(key));
if (raw == null) return null;
return int.tryParse(raw);
}
Future<void> setInt(String key, int value) {
return _backend.write(_prefixedKey(key), value.toString());
}
Future<double?> getDouble(String key) async {
final raw = await _backend.read(_prefixedKey(key));
if (raw == null) return null;
return double.tryParse(raw);
}
Future<void> setDouble(String key, double value) {
return _backend.write(_prefixedKey(key), value.toString());
}
Future<bool?> getBool(String key) async {
final raw = await _backend.read(_prefixedKey(key));
if (raw == null) return null;
return raw == 'true';
}
Future<void> setBool(String key, bool value) {
return _backend.write(_prefixedKey(key), value.toString());
}
Future<List<String>?> getStringList(String key) async {
final raw = await _backend.read(_prefixedKey(key));
if (raw == null) return null;
try {
final decoded = jsonDecode(raw);
if (decoded is List) {
return decoded.cast<String>();
}
} catch (_) {}
return null;
}
Future<void> setStringList(String key, List<String> value) {
return _backend.write(_prefixedKey(key), jsonEncode(value));
}
Future<Map<String, dynamic>?> getJson(String key) async {
final raw = await _backend.read(_prefixedKey(key));
if (raw == null) return null;
try {
return jsonDecode(raw) as Map<String, dynamic>;
} catch (_) {
return null;
}
}
Future<void> setJson(String key, Map<String, dynamic> value) {
return _backend.write(_prefixedKey(key), jsonEncode(value));
}
Future<void> remove(String key) {
return _backend.remove(_prefixedKey(key));
}
Future<void> clear() {
return _backend.clear();
}
Future<List<String>> keys() {
return _backend.keys();
}
Future<bool> containsKey(String key) {
return _backend.containsKey(_prefixedKey(key));
}
NeonKVBackend get backend => _backend;
}

View file

@ -0,0 +1,177 @@
import 'dart:async';
import 'dart:convert';
enum NeonWebSocketState {
connecting,
connected,
disconnecting,
disconnected,
}
class NeonWebSocketMessage {
final String data;
final DateTime timestamp;
NeonWebSocketMessage(this.data, {DateTime? timestamp})
: timestamp = timestamp ?? DateTime.now();
Map<String, dynamic>? get json {
try {
return jsonDecode(data) as Map<String, dynamic>;
} catch (_) {
return null;
}
}
@override
String toString() => 'NeonWebSocketMessage($data)';
}
abstract class NeonWebSocketBackend {
Future<void> connect(String url, {Map<String, String>? headers});
void send(String data);
Future<void> close([int? code, String? reason]);
Stream<NeonWebSocketMessage> get messages;
NeonWebSocketState get state;
}
class NeonMockWebSocketBackend implements NeonWebSocketBackend {
NeonWebSocketState _state = NeonWebSocketState.disconnected;
final _controller = StreamController<NeonWebSocketMessage>.broadcast();
final List<String> sentMessages = [];
String? connectedUrl;
@override
NeonWebSocketState get state => _state;
@override
Stream<NeonWebSocketMessage> get messages => _controller.stream;
@override
Future<void> connect(String url, {Map<String, String>? headers}) async {
_state = NeonWebSocketState.connecting;
connectedUrl = url;
_state = NeonWebSocketState.connected;
}
@override
void send(String data) {
if (_state != NeonWebSocketState.connected) {
throw StateError('WebSocket is not connected.');
}
sentMessages.add(data);
}
@override
Future<void> close([int? code, String? reason]) async {
_state = NeonWebSocketState.disconnecting;
_state = NeonWebSocketState.disconnected;
}
void simulateMessage(String data) {
_controller.add(NeonWebSocketMessage(data));
}
void dispose() {
_controller.close();
}
}
class NeonWebSocket {
final NeonWebSocketBackend _backend;
final Duration _reconnectDelay;
final int _maxReconnectAttempts;
int _reconnectAttempts = 0;
String? _url;
Map<String, String>? _headers;
bool _shouldAutoReconnect;
final List<void Function(NeonWebSocketMessage)> _listeners = [];
final List<void Function()> _connectListeners = [];
final List<void Function(int?, String?)> _disconnectListeners = [];
StreamSubscription? _messageSub;
NeonWebSocket({
NeonWebSocketBackend? backend,
Duration reconnectDelay = const Duration(seconds: 3),
int maxReconnectAttempts = 5,
bool autoReconnect = true,
}) : _backend = backend ?? NeonMockWebSocketBackend(),
_reconnectDelay = reconnectDelay,
_maxReconnectAttempts = maxReconnectAttempts,
_shouldAutoReconnect = autoReconnect;
NeonWebSocketState get state => _backend.state;
bool get isConnected => _backend.state == NeonWebSocketState.connected;
bool get autoReconnect => _shouldAutoReconnect;
NeonWebSocketBackend get backend => _backend;
Future<void> connect(String url, {Map<String, String>? headers}) async {
_url = url;
_headers = headers;
_reconnectAttempts = 0;
await _backend.connect(url, headers: headers);
_messageSub = _backend.messages.listen((msg) {
for (final listener in _listeners) {
listener(msg);
}
});
for (final listener in _connectListeners) {
listener();
}
}
void send(String data) {
_backend.send(data);
}
void sendJson(Map<String, dynamic> data) {
_backend.send(jsonEncode(data));
}
Future<void> close([int? code, String? reason]) async {
_shouldAutoReconnect = false;
await _messageSub?.cancel();
await _backend.close(code, reason);
for (final listener in _disconnectListeners) {
listener(code, reason);
}
}
void onMessage(void Function(NeonWebSocketMessage) listener) {
_listeners.add(listener);
}
void onConnect(void Function() listener) {
_connectListeners.add(listener);
}
void onDisconnect(void Function(int?, String?) listener) {
_disconnectListeners.add(listener);
}
Future<void> reconnect() async {
if (_url == null) {
throw StateError('No previous connection to reconnect to.');
}
if (_reconnectAttempts >= _maxReconnectAttempts) {
throw StateError('Max reconnection attempts ($_maxReconnectAttempts) exceeded.');
}
_reconnectAttempts++;
await Future.delayed(_reconnectDelay);
await connect(_url!, headers: _headers);
}
void dispose() {
_messageSub?.cancel();
_listeners.clear();
_connectListeners.clear();
_disconnectListeners.clear();
}
}

View file

@ -0,0 +1,100 @@
import 'dart:async';
import 'package:neon_framework/src/platform/platform_channel.dart';
enum NeonStorageLocation { appDocuments, appCache, appSupport, temp }
class NeonFileSystem {
final NeonPlatformChannel channel;
final Map<String, String> _mockFiles = {};
bool _useMock = false;
NeonFileSystem({required this.channel});
void enableMock() {
_useMock = true;
}
void disableMock() {
_useMock = false;
_mockFiles.clear();
}
Future<String?> readFile(String path, {NeonStorageLocation location = NeonStorageLocation.appDocuments}) async {
if (_useMock) {
final key = '${location.name}:$path';
return _mockFiles[key];
}
return channel.invokeMethod<String>('file.read', {
'path': path,
'location': location.name,
});
}
Future<bool> writeFile(String path, String content, {NeonStorageLocation location = NeonStorageLocation.appDocuments}) async {
if (_useMock) {
final key = '${location.name}:$path';
_mockFiles[key] = content;
return true;
}
final result = await channel.invokeMethod<bool>('file.write', {
'path': path,
'content': content,
'location': location.name,
});
return result ?? false;
}
Future<bool> deleteFile(String path, {NeonStorageLocation location = NeonStorageLocation.appDocuments}) async {
if (_useMock) {
final key = '${location.name}:$path';
return _mockFiles.remove(key) != null;
}
final result = await channel.invokeMethod<bool>('file.delete', {
'path': path,
'location': location.name,
});
return result ?? false;
}
Future<bool> fileExists(String path, {NeonStorageLocation location = NeonStorageLocation.appDocuments}) async {
if (_useMock) {
final key = '${location.name}:$path';
return _mockFiles.containsKey(key);
}
final result = await channel.invokeMethod<bool>('file.exists', {
'path': path,
'location': location.name,
});
return result ?? false;
}
Future<List<String>> listFiles(String directory, {NeonStorageLocation location = NeonStorageLocation.appDocuments}) async {
if (_useMock) {
final prefix = '${location.name}:$directory';
return _mockFiles.keys
.where((k) => k.startsWith(prefix))
.map((k) => k.substring('${location.name}:'.length))
.toList();
}
final result = await channel.invokeMethod<List>('file.list', {
'directory': directory,
'location': location.name,
});
return result?.cast<String>() ?? [];
}
Future<String?> getStoragePath(NeonStorageLocation location) async {
if (_useMock) {
return '/mock/${location.name}';
}
return channel.invokeMethod<String>('file.getPath', {
'location': location.name,
});
}
}

View file

@ -0,0 +1,132 @@
import 'dart:async';
import 'package:neon_framework/src/platform/platform_channel.dart';
import 'package:neon_framework/src/rendering/canvas.dart';
import 'package:neon_framework/src/rendering/constraints.dart';
import 'package:neon_framework/src/widgets/primitives.dart';
abstract class NeonRenderSurface {
NeonSize get size;
double get devicePixelRatio;
bool get isReady;
}
class NeonNativeRenderer {
final NeonPlatformChannel channel;
bool _isReady = false;
int _frameCount = 0;
final List<NeonFrameRecord> _frameHistory = [];
static const int _maxFrameHistory = 60;
NeonNativeRenderer({required this.channel});
bool get isReady => _isReady;
int get frameCount => _frameCount;
List<NeonFrameRecord> get frameHistory => List.unmodifiable(_frameHistory);
Future<void> initialize(NeonSize surfaceSize, double devicePixelRatio) async {
await channel.invokeMethod('renderer.init', {
'width': surfaceSize.width,
'height': surfaceSize.height,
'devicePixelRatio': devicePixelRatio,
});
_isReady = true;
}
Future<void> submitFrame(NeonCanvas canvas) async {
if (!_isReady) {
throw StateError('Renderer is not initialized.');
}
final stopwatch = Stopwatch()..start();
final serialized = _serializeCommands(canvas.commands);
await channel.invokeMethod('renderer.submitFrame', {
'frameNumber': _frameCount,
'commands': serialized,
'commandCount': canvas.commandCount,
});
stopwatch.stop();
_frameCount++;
final record = NeonFrameRecord(
frameNumber: _frameCount - 1,
commandCount: canvas.commandCount,
renderTimeMs: stopwatch.elapsedMicroseconds / 1000.0,
timestamp: DateTime.now(),
);
_frameHistory.add(record);
if (_frameHistory.length > _maxFrameHistory) {
_frameHistory.removeAt(0);
}
}
Future<void> clear() async {
await channel.invokeMethod('renderer.clear');
}
List<Map<String, dynamic>> _serializeCommands(List<NeonPaintCommand> commands) {
return commands.map((cmd) {
final params = <String, dynamic>{};
for (final entry in cmd.params.entries) {
params[entry.key] = _serializeValue(entry.value);
}
return {
'type': cmd.type.name,
'params': params,
};
}).toList();
}
dynamic _serializeValue(dynamic value) {
if (value is NeonRect) {
return {'left': value.left, 'top': value.top, 'width': value.width, 'height': value.height};
}
if (value is NeonOffset) {
return {'dx': value.dx, 'dy': value.dy};
}
if (value is NeonSize) {
return {'width': value.width, 'height': value.height};
}
if (value is NeonColor) {
return value.value;
}
if (value is NeonTextStyle) {
return {
'fontSize': value.fontSize,
'fontWeight': value.fontWeight.name,
'color': value.color.value,
};
}
return value;
}
double get averageFrameTimeMs {
if (_frameHistory.isEmpty) return 0;
final total = _frameHistory.fold<double>(0, (sum, r) => sum + r.renderTimeMs);
return total / _frameHistory.length;
}
void dispose() {
_isReady = false;
_frameHistory.clear();
}
}
class NeonFrameRecord {
final int frameNumber;
final int commandCount;
final double renderTimeMs;
final DateTime timestamp;
const NeonFrameRecord({
required this.frameNumber,
required this.commandCount,
required this.renderTimeMs,
required this.timestamp,
});
@override
String toString() =>
'Frame #$frameNumber: $commandCount cmds in ${renderTimeMs.toStringAsFixed(2)}ms';
}

View file

@ -0,0 +1,112 @@
import 'dart:async';
import 'package:neon_framework/src/platform/platform_channel.dart';
import 'package:neon_framework/src/platform/platform_info.dart';
import 'package:neon_framework/src/platform/native_renderer.dart';
import 'package:neon_framework/src/platform/file_system.dart';
import 'package:neon_framework/src/platform/platform_lifecycle.dart';
enum NeonTargetPlatform { android, ios }
class NeonPlatformBridge {
static NeonPlatformBridge? _instance;
static NeonPlatformBridge get instance {
_instance ??= NeonPlatformBridge._();
return _instance!;
}
NeonPlatformBridge._();
NeonTargetPlatform? _platform;
bool _initialized = false;
late final NeonPlatformChannel _systemChannel;
late final NeonPlatformChannel _renderChannel;
late final NeonPlatformChannel _lifecycleChannel;
late final NeonPlatformChannel _fileChannel;
late final NeonPlatformInfo _platformInfo;
late final NeonNativeRenderer _nativeRenderer;
late final NeonFileSystem _fileSystem;
late final NeonPlatformLifecycle _platformLifecycle;
bool get isInitialized => _initialized;
NeonTargetPlatform? get platform => _platform;
NeonPlatformInfo get platformInfo {
_ensureInit();
return _platformInfo;
}
NeonNativeRenderer get nativeRenderer {
_ensureInit();
return _nativeRenderer;
}
NeonFileSystem get fileSystem {
_ensureInit();
return _fileSystem;
}
NeonPlatformLifecycle get platformLifecycle {
_ensureInit();
return _platformLifecycle;
}
NeonPlatformChannel get systemChannel {
_ensureInit();
return _systemChannel;
}
NeonPlatformChannel get renderChannel {
_ensureInit();
return _renderChannel;
}
void initialize({NeonTargetPlatform? platform}) {
if (_initialized) return;
_platform = platform;
_systemChannel = NeonPlatformChannel(name: 'neon/system');
_renderChannel = NeonPlatformChannel(name: 'neon/render', codec: NeonChannelCodec.binary);
_lifecycleChannel = NeonPlatformChannel(name: 'neon/lifecycle');
_fileChannel = NeonPlatformChannel(name: 'neon/file');
final dispatcher = NeonPlatformDispatcher.instance;
dispatcher.registerChannel(_systemChannel);
dispatcher.registerChannel(_renderChannel);
dispatcher.registerChannel(_lifecycleChannel);
dispatcher.registerChannel(_fileChannel);
_platformInfo = NeonPlatformInfo(channel: _systemChannel, platform: _platform);
_nativeRenderer = NeonNativeRenderer(channel: _renderChannel);
_fileSystem = NeonFileSystem(channel: _fileChannel);
_platformLifecycle = NeonPlatformLifecycle(channel: _lifecycleChannel);
_initialized = true;
}
void _ensureInit() {
if (!_initialized) {
throw StateError('NeonPlatformBridge has not been initialized. Call initialize() first.');
}
}
void dispose() {
if (!_initialized) return;
_platformLifecycle.dispose();
_nativeRenderer.dispose();
_systemChannel.close();
_renderChannel.close();
_lifecycleChannel.close();
_fileChannel.close();
NeonPlatformDispatcher.instance.dispose();
_initialized = false;
}
static void reset() {
_instance?.dispose();
_instance = null;
NeonPlatformDispatcher.reset();
}
}

View file

@ -0,0 +1,159 @@
import 'dart:async';
typedef MessageHandler = Future<dynamic> Function(String method, dynamic arguments);
enum NeonChannelCodec { json, binary, standard }
class NeonPlatformChannel {
final String name;
final NeonChannelCodec codec;
MessageHandler? _handler;
final StreamController<NeonChannelMessage> _incomingController =
StreamController<NeonChannelMessage>.broadcast();
final List<NeonChannelMessage> _messageLog = [];
bool _isOpen = true;
NeonPlatformChannel({
required this.name,
this.codec = NeonChannelCodec.standard,
});
bool get isOpen => _isOpen;
Stream<NeonChannelMessage> get incoming => _incomingController.stream;
List<NeonChannelMessage> get messageLog => List.unmodifiable(_messageLog);
void setMessageHandler(MessageHandler handler) {
_handler = handler;
}
Future<T?> invokeMethod<T>(String method, [dynamic arguments]) async {
_ensureOpen();
final message = NeonChannelMessage(
channel: name,
method: method,
arguments: arguments,
direction: NeonMessageDirection.dartToNative,
timestamp: DateTime.now(),
);
_messageLog.add(message);
return NeonPlatformDispatcher.instance.sendMessage<T>(this, message);
}
Future<dynamic> handlePlatformMessage(String method, dynamic arguments) async {
final message = NeonChannelMessage(
channel: name,
method: method,
arguments: arguments,
direction: NeonMessageDirection.nativeToDart,
timestamp: DateTime.now(),
);
_messageLog.add(message);
_incomingController.add(message);
if (_handler != null) {
return _handler!(method, arguments);
}
return null;
}
void close() {
_isOpen = false;
_incomingController.close();
}
void _ensureOpen() {
if (!_isOpen) {
throw StateError('Channel "$name" is closed.');
}
}
@override
String toString() => 'NeonPlatformChannel($name, codec: ${codec.name})';
}
enum NeonMessageDirection { dartToNative, nativeToDart }
class NeonChannelMessage {
final String channel;
final String method;
final dynamic arguments;
final NeonMessageDirection direction;
final DateTime timestamp;
const NeonChannelMessage({
required this.channel,
required this.method,
this.arguments,
required this.direction,
required this.timestamp,
});
@override
String toString() =>
'NeonChannelMessage($channel.$method, ${direction.name})';
}
class NeonPlatformDispatcher {
static NeonPlatformDispatcher? _instance;
static NeonPlatformDispatcher get instance {
_instance ??= NeonPlatformDispatcher._();
return _instance!;
}
NeonPlatformDispatcher._();
final Map<String, NeonPlatformChannel> _channels = {};
PlatformMessageCallback? _mockDispatcher;
void registerChannel(NeonPlatformChannel channel) {
_channels[channel.name] = channel;
}
void unregisterChannel(String name) {
_channels.remove(name);
}
NeonPlatformChannel? getChannel(String name) => _channels[name];
Future<T?> sendMessage<T>(
NeonPlatformChannel channel, NeonChannelMessage message) async {
if (_mockDispatcher != null) {
final result = await _mockDispatcher!(message);
return result as T?;
}
return null;
}
Future<void> receivePlatformMessage(
String channelName, String method, dynamic arguments) async {
final channel = _channels[channelName];
if (channel != null) {
await channel.handlePlatformMessage(method, arguments);
}
}
void setMockDispatcher(PlatformMessageCallback callback) {
_mockDispatcher = callback;
}
void clearMockDispatcher() {
_mockDispatcher = null;
}
void dispose() {
for (final channel in _channels.values) {
channel.close();
}
_channels.clear();
_mockDispatcher = null;
}
static void reset() {
_instance?.dispose();
_instance = null;
}
}
typedef PlatformMessageCallback = Future<dynamic> Function(NeonChannelMessage message);

View file

@ -0,0 +1,128 @@
import 'dart:async';
import 'package:neon_framework/src/platform/platform_channel.dart';
import 'package:neon_framework/src/platform/platform_bridge.dart';
import 'package:neon_framework/src/rendering/constraints.dart';
class NeonPlatformInfo {
final NeonPlatformChannel channel;
final NeonTargetPlatform? platform;
String _osVersion = 'unknown';
String _deviceModel = 'unknown';
double _devicePixelRatio = 1.0;
NeonSize _screenSize = const NeonSize(360, 800);
NeonEdgeInsetsData _safeArea = const NeonEdgeInsetsData();
String _locale = 'en_US';
bool _isDarkMode = false;
bool _isLoaded = false;
NeonPlatformInfo({
required this.channel,
this.platform,
});
bool get isLoaded => _isLoaded;
String get osVersion => _osVersion;
String get deviceModel => _deviceModel;
double get devicePixelRatio => _devicePixelRatio;
NeonSize get screenSize => _screenSize;
NeonEdgeInsetsData get safeArea => _safeArea;
String get locale => _locale;
bool get isDarkMode => _isDarkMode;
bool get isAndroid => platform == NeonTargetPlatform.android;
bool get isIOS => platform == NeonTargetPlatform.ios;
String get platformName {
switch (platform) {
case NeonTargetPlatform.android:
return 'Android';
case NeonTargetPlatform.ios:
return 'iOS';
case null:
return 'Unknown';
}
}
Future<void> load() async {
final result = await channel.invokeMethod<Map>('system.getDeviceInfo');
if (result != null) {
_osVersion = result['osVersion'] as String? ?? _osVersion;
_deviceModel = result['deviceModel'] as String? ?? _deviceModel;
_devicePixelRatio = (result['devicePixelRatio'] as num?)?.toDouble() ?? _devicePixelRatio;
_screenSize = NeonSize(
(result['screenWidth'] as num?)?.toDouble() ?? _screenSize.width,
(result['screenHeight'] as num?)?.toDouble() ?? _screenSize.height,
);
_locale = result['locale'] as String? ?? _locale;
_isDarkMode = result['isDarkMode'] as bool? ?? _isDarkMode;
final safeAreaMap = result['safeArea'] as Map?;
if (safeAreaMap != null) {
_safeArea = NeonEdgeInsetsData(
top: (safeAreaMap['top'] as num?)?.toDouble() ?? 0,
right: (safeAreaMap['right'] as num?)?.toDouble() ?? 0,
bottom: (safeAreaMap['bottom'] as num?)?.toDouble() ?? 0,
left: (safeAreaMap['left'] as num?)?.toDouble() ?? 0,
);
}
}
_isLoaded = true;
}
void setMockValues({
String? osVersion,
String? deviceModel,
double? devicePixelRatio,
NeonSize? screenSize,
NeonEdgeInsetsData? safeArea,
String? locale,
bool? isDarkMode,
}) {
_osVersion = osVersion ?? _osVersion;
_deviceModel = deviceModel ?? _deviceModel;
_devicePixelRatio = devicePixelRatio ?? _devicePixelRatio;
_screenSize = screenSize ?? _screenSize;
_safeArea = safeArea ?? _safeArea;
_locale = locale ?? _locale;
_isDarkMode = isDarkMode ?? _isDarkMode;
_isLoaded = true;
}
@override
String toString() =>
'NeonPlatformInfo($platformName, $deviceModel, $_osVersion, dpr: $_devicePixelRatio)';
}
class NeonEdgeInsetsData {
final double top;
final double right;
final double bottom;
final double left;
const NeonEdgeInsetsData({
this.top = 0,
this.right = 0,
this.bottom = 0,
this.left = 0,
});
double get horizontal => left + right;
double get vertical => top + bottom;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is NeonEdgeInsetsData &&
top == other.top &&
right == other.right &&
bottom == other.bottom &&
left == other.left;
@override
int get hashCode => Object.hash(top, right, bottom, left);
@override
String toString() =>
'NeonEdgeInsetsData(top: $top, right: $right, bottom: $bottom, left: $left)';
}

View file

@ -0,0 +1,100 @@
import 'dart:async';
import 'package:neon_framework/src/platform/platform_channel.dart';
import 'package:neon_framework/src/core/lifecycle.dart';
enum NeonNativeLifecycleEvent {
onCreate,
onStart,
onResume,
onPause,
onStop,
onDestroy,
onLowMemory,
onTrimMemory,
willEnterForeground,
didEnterBackground,
willResignActive,
didBecomeActive,
willTerminate,
}
class NeonPlatformLifecycle {
final NeonPlatformChannel channel;
NeonLifecycleManager? _lifecycleManager;
final StreamController<NeonNativeLifecycleEvent> _eventController =
StreamController<NeonNativeLifecycleEvent>.broadcast();
final List<NeonNativeLifecycleEvent> _eventLog = [];
NeonPlatformLifecycle({required this.channel}) {
channel.setMessageHandler(_handleLifecycleMessage);
}
Stream<NeonNativeLifecycleEvent> get events => _eventController.stream;
List<NeonNativeLifecycleEvent> get eventLog => List.unmodifiable(_eventLog);
void bindLifecycleManager(NeonLifecycleManager manager) {
_lifecycleManager = manager;
}
Future<dynamic> _handleLifecycleMessage(String method, dynamic arguments) async {
if (method == 'lifecycle.event') {
final eventName = arguments as String?;
if (eventName != null) {
final event = _parseEvent(eventName);
if (event != null) {
_onNativeEvent(event);
}
}
}
return null;
}
void _onNativeEvent(NeonNativeLifecycleEvent event) {
_eventLog.add(event);
_eventController.add(event);
_bridgeToNeonLifecycle(event);
}
void _bridgeToNeonLifecycle(NeonNativeLifecycleEvent event) {
if (_lifecycleManager == null) return;
switch (event) {
case NeonNativeLifecycleEvent.onResume:
case NeonNativeLifecycleEvent.didBecomeActive:
case NeonNativeLifecycleEvent.willEnterForeground:
_lifecycleManager!.resume();
break;
case NeonNativeLifecycleEvent.onPause:
case NeonNativeLifecycleEvent.willResignActive:
case NeonNativeLifecycleEvent.didEnterBackground:
_lifecycleManager!.pause();
break;
case NeonNativeLifecycleEvent.onDestroy:
case NeonNativeLifecycleEvent.willTerminate:
_lifecycleManager!.dispose();
break;
default:
break;
}
}
void simulateNativeEvent(NeonNativeLifecycleEvent event) {
_onNativeEvent(event);
}
NeonNativeLifecycleEvent? _parseEvent(String name) {
for (final event in NeonNativeLifecycleEvent.values) {
if (event.name == name) return event;
}
return null;
}
void dispose() {
_eventController.close();
_eventLog.clear();
_lifecycleManager = null;
}
}

View file

@ -0,0 +1,214 @@
import 'dart:async';
import 'package:neon_framework/src/plugins/plugin.dart';
import 'package:neon_framework/src/platform/platform_channel.dart';
class NeonLoggerPlugin extends NeonDartPlugin {
final List<NeonLogEntry> _entries = [];
NeonLogLevel _minLevel;
void Function(NeonLogEntry)? _onLog;
NeonLoggerPlugin({
NeonLogLevel minLevel = NeonLogLevel.debug,
void Function(NeonLogEntry)? onLog,
}) : _minLevel = minLevel,
_onLog = onLog;
@override
NeonPluginManifest get manifest => const NeonPluginManifest(
id: 'neon_logger',
name: 'Neon Logger',
version: '1.0.0',
description: 'Structured logging plugin for Neon apps',
author: 'Neon Team',
capabilities: {'logging'},
type: NeonPluginType.dartOnly,
);
List<NeonLogEntry> get entries => List.unmodifiable(_entries);
NeonLogLevel get minLevel => _minLevel;
set minLevel(NeonLogLevel level) => _minLevel = level;
@override
void onRegister(NeonPluginContext context) {
context.set<NeonLoggerPlugin>('logger', this);
log(NeonLogLevel.info, 'NeonLoggerPlugin registered');
}
@override
void onDispose() {
log(NeonLogLevel.info, 'NeonLoggerPlugin disposing');
_entries.clear();
_onLog = null;
}
void log(NeonLogLevel level, String message, {String? tag, Object? error, StackTrace? stackTrace}) {
if (level.index < _minLevel.index) return;
final entry = NeonLogEntry(
level: level,
message: message,
tag: tag,
error: error,
stackTrace: stackTrace,
timestamp: DateTime.now(),
);
_entries.add(entry);
_onLog?.call(entry);
}
void debug(String message, {String? tag}) =>
log(NeonLogLevel.debug, message, tag: tag);
void info(String message, {String? tag}) =>
log(NeonLogLevel.info, message, tag: tag);
void warning(String message, {String? tag}) =>
log(NeonLogLevel.warning, message, tag: tag);
void error(String message, {String? tag, Object? error, StackTrace? stackTrace}) =>
log(NeonLogLevel.error, message, tag: tag, error: error, stackTrace: stackTrace);
void fatal(String message, {String? tag, Object? error, StackTrace? stackTrace}) =>
log(NeonLogLevel.fatal, message, tag: tag, error: error, stackTrace: stackTrace);
List<NeonLogEntry> getByLevel(NeonLogLevel level) =>
_entries.where((e) => e.level == level).toList();
List<NeonLogEntry> getByTag(String tag) =>
_entries.where((e) => e.tag == tag).toList();
void clear() => _entries.clear();
Map<String, dynamic> get stats => {
'total': _entries.length,
'debug': getByLevel(NeonLogLevel.debug).length,
'info': getByLevel(NeonLogLevel.info).length,
'warning': getByLevel(NeonLogLevel.warning).length,
'error': getByLevel(NeonLogLevel.error).length,
'fatal': getByLevel(NeonLogLevel.fatal).length,
};
}
enum NeonLogLevel { debug, info, warning, error, fatal }
class NeonLogEntry {
final NeonLogLevel level;
final String message;
final String? tag;
final Object? error;
final StackTrace? stackTrace;
final DateTime timestamp;
const NeonLogEntry({
required this.level,
required this.message,
this.tag,
this.error,
this.stackTrace,
required this.timestamp,
});
@override
String toString() {
final prefix = '[${level.name.toUpperCase()}]';
final tagStr = tag != null ? '[$tag] ' : '';
return '$prefix $tagStr$message';
}
}
class NeonDeviceInfoPlugin extends NeonNativePlugin {
String _deviceModel = 'Unknown';
String _osVersion = 'Unknown';
String _appVersion = '0.0.0';
double _batteryLevel = -1;
bool _isCharging = false;
final Map<String, dynamic> _extraInfo = {};
String get deviceModel => _deviceModel;
String get osVersion => _osVersion;
String get appVersion => _appVersion;
double get batteryLevel => _batteryLevel;
bool get isCharging => _isCharging;
Map<String, dynamic> get extraInfo => Map.unmodifiable(_extraInfo);
@override
NeonPluginManifest get manifest => const NeonPluginManifest(
id: 'neon_device_info',
name: 'Neon Device Info',
version: '1.0.0',
description: 'Native device information plugin',
author: 'Neon Team',
capabilities: {'device_info'},
type: NeonPluginType.native,
);
@override
void onNativeRegister(NeonPluginContext context, NeonPlatformChannel channel) {
context.set<NeonDeviceInfoPlugin>('device_info', this);
}
@override
void registerNativeHandlers(NeonPlatformChannel channel) {
channel.setMessageHandler((method, arguments) async {
switch (method) {
case 'updateDeviceInfo':
final data = arguments as Map<String, dynamic>?;
if (data != null) {
_deviceModel = data['model'] as String? ?? _deviceModel;
_osVersion = data['os_version'] as String? ?? _osVersion;
_appVersion = data['app_version'] as String? ?? _appVersion;
}
return true;
case 'updateBattery':
final data = arguments as Map<String, dynamic>?;
if (data != null) {
_batteryLevel = (data['level'] as num?)?.toDouble() ?? _batteryLevel;
_isCharging = data['charging'] as bool? ?? _isCharging;
}
return true;
default:
return null;
}
});
}
Future<Map<String, dynamic>?> fetchDeviceInfo() async {
final result = await nativeChannel!.invokeMethod<Map<String, dynamic>>('getDeviceInfo');
if (result != null) {
_deviceModel = result['model'] as String? ?? _deviceModel;
_osVersion = result['os_version'] as String? ?? _osVersion;
_appVersion = result['app_version'] as String? ?? _appVersion;
_extraInfo.addAll(result);
}
return result;
}
Future<Map<String, dynamic>?> fetchBatteryInfo() async {
final result = await nativeChannel!.invokeMethod<Map<String, dynamic>>('getBatteryInfo');
if (result != null) {
_batteryLevel = (result['level'] as num?)?.toDouble() ?? _batteryLevel;
_isCharging = result['charging'] as bool? ?? _isCharging;
}
return result;
}
@override
void onNativeDispose() {
_extraInfo.clear();
}
Map<String, dynamic> toJson() => {
'device_model': _deviceModel,
'os_version': _osVersion,
'app_version': _appVersion,
'battery_level': _batteryLevel,
'is_charging': _isCharging,
'extra': _extraInfo,
};
@override
String toString() =>
'NeonDeviceInfoPlugin(model: $_deviceModel, os: $_osVersion)';
}

View file

@ -0,0 +1,256 @@
import 'package:neon_framework/src/platform/platform_channel.dart';
class NeonPluginManifest {
final String id;
final String name;
final String version;
final String? description;
final String? author;
final String? homepage;
final String? license;
final NeonSdkConstraint sdkConstraint;
final Set<String> capabilities;
final List<String> dependencies;
final NeonPluginType type;
const NeonPluginManifest({
required this.id,
required this.name,
required this.version,
this.description,
this.author,
this.homepage,
this.license,
this.sdkConstraint = const NeonSdkConstraint.any(),
this.capabilities = const {},
this.dependencies = const [],
this.type = NeonPluginType.dartOnly,
});
bool get isNativePlugin => type == NeonPluginType.native;
bool get isDartOnlyPlugin => type == NeonPluginType.dartOnly;
NeonSemanticVersion get parsedVersion => NeonSemanticVersion.parse(version);
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'version': version,
if (description != null) 'description': description,
if (author != null) 'author': author,
if (homepage != null) 'homepage': homepage,
if (license != null) 'license': license,
'sdk_constraint': sdkConstraint.toString(),
'capabilities': capabilities.toList(),
'dependencies': dependencies,
'type': type.name,
};
@override
String toString() => 'NeonPluginManifest($id@$version, ${type.name})';
}
enum NeonPluginType { dartOnly, native }
class NeonSdkConstraint {
final String? minVersion;
final String? maxVersion;
final bool minInclusive;
final bool maxInclusive;
const NeonSdkConstraint({
this.minVersion,
this.maxVersion,
this.minInclusive = true,
this.maxInclusive = false,
});
const NeonSdkConstraint.any()
: minVersion = null,
maxVersion = null,
minInclusive = true,
maxInclusive = false;
const NeonSdkConstraint.exact(String version)
: minVersion = version,
maxVersion = version,
minInclusive = true,
maxInclusive = true;
bool isSatisfiedBy(String sdkVersion) {
final sdk = NeonSemanticVersion.parse(sdkVersion);
if (minVersion != null) {
final min = NeonSemanticVersion.parse(minVersion!);
final cmp = sdk.compareTo(min);
if (minInclusive && cmp < 0) return false;
if (!minInclusive && cmp <= 0) return false;
}
if (maxVersion != null) {
final max = NeonSemanticVersion.parse(maxVersion!);
final cmp = sdk.compareTo(max);
if (maxInclusive && cmp > 0) return false;
if (!maxInclusive && cmp >= 0) return false;
}
return true;
}
@override
String toString() {
if (minVersion == null && maxVersion == null) return 'any';
if (minVersion != null && maxVersion != null && minVersion == maxVersion) {
return '=$minVersion';
}
final parts = <String>[];
if (minVersion != null) {
parts.add('${minInclusive ? ">=" : ">"}$minVersion');
}
if (maxVersion != null) {
parts.add('${maxInclusive ? "<=" : "<"}$maxVersion');
}
return parts.join(' ');
}
}
class NeonSemanticVersion implements Comparable<NeonSemanticVersion> {
final int major;
final int minor;
final int patch;
final String? preRelease;
const NeonSemanticVersion({
required this.major,
required this.minor,
required this.patch,
this.preRelease,
});
factory NeonSemanticVersion.parse(String version) {
final cleaned = version.replaceFirst(RegExp(r'^v'), '');
String? pre;
var core = cleaned;
final dashIndex = cleaned.indexOf('-');
if (dashIndex != -1) {
pre = cleaned.substring(dashIndex + 1);
core = cleaned.substring(0, dashIndex);
}
final parts = core.split('.');
return NeonSemanticVersion(
major: parts.isNotEmpty ? int.tryParse(parts[0]) ?? 0 : 0,
minor: parts.length > 1 ? int.tryParse(parts[1]) ?? 0 : 0,
patch: parts.length > 2 ? int.tryParse(parts[2]) ?? 0 : 0,
preRelease: pre,
);
}
@override
int compareTo(NeonSemanticVersion other) {
if (major != other.major) return major.compareTo(other.major);
if (minor != other.minor) return minor.compareTo(other.minor);
if (patch != other.patch) return patch.compareTo(other.patch);
if (preRelease == null && other.preRelease != null) return 1;
if (preRelease != null && other.preRelease == null) return -1;
if (preRelease != null && other.preRelease != null) {
return preRelease!.compareTo(other.preRelease!);
}
return 0;
}
bool operator >=(NeonSemanticVersion other) => compareTo(other) >= 0;
bool operator <=(NeonSemanticVersion other) => compareTo(other) <= 0;
bool operator >(NeonSemanticVersion other) => compareTo(other) > 0;
bool operator <(NeonSemanticVersion other) => compareTo(other) < 0;
@override
bool operator ==(Object other) =>
other is NeonSemanticVersion &&
major == other.major &&
minor == other.minor &&
patch == other.patch &&
preRelease == other.preRelease;
@override
int get hashCode => Object.hash(major, minor, patch, preRelease);
@override
String toString() {
final base = '$major.$minor.$patch';
return preRelease != null ? '$base-$preRelease' : base;
}
}
abstract class NeonPlugin {
NeonPluginManifest get manifest;
bool registeredInternal = false;
bool get isRegistered => registeredInternal;
void onRegister(NeonPluginContext context);
void onDispose();
NeonPlatformChannel? get nativeChannel => null;
void registerNativeHandlers(NeonPlatformChannel channel) {}
@override
String toString() => 'NeonPlugin(${manifest.id}@${manifest.version})';
}
abstract class NeonDartPlugin extends NeonPlugin {}
abstract class NeonNativePlugin extends NeonPlugin {
late NeonPlatformChannel _channel;
@override
NeonPlatformChannel get nativeChannel => _channel;
String get channelName => 'neon/plugin/${manifest.id}';
@override
void onRegister(NeonPluginContext context) {
_channel = NeonPlatformChannel(name: channelName);
NeonPlatformDispatcher.instance.registerChannel(_channel);
registerNativeHandlers(_channel);
onNativeRegister(context, _channel);
}
void onNativeRegister(NeonPluginContext context, NeonPlatformChannel channel);
@override
void onDispose() {
_channel.close();
NeonPlatformDispatcher.instance.unregisterChannel(channelName);
onNativeDispose();
}
void onNativeDispose() {}
}
class NeonPluginContext {
final String sdkVersion;
final Map<String, dynamic> _sharedData = {};
final List<String> _registeredPluginIds = [];
NeonPluginContext({required this.sdkVersion});
T? get<T>(String key) => _sharedData[key] as T?;
void set<T>(String key, T value) => _sharedData[key] = value;
bool has(String key) => _sharedData.containsKey(key);
void remove(String key) => _sharedData.remove(key);
List<String> get registeredPluginIds =>
List.unmodifiable(_registeredPluginIds);
bool isPluginRegistered(String pluginId) =>
_registeredPluginIds.contains(pluginId);
void trackRegistration(String pluginId) {
_registeredPluginIds.add(pluginId);
}
void trackUnregistration(String pluginId) {
_registeredPluginIds.remove(pluginId);
}
}

View file

@ -0,0 +1,269 @@
import 'package:neon_framework/src/plugins/plugin.dart';
class NeonPluginRegistry {
static NeonPluginRegistry? _instance;
static NeonPluginRegistry get instance {
_instance ??= NeonPluginRegistry._();
return _instance!;
}
NeonPluginRegistry._();
final Map<String, NeonPlugin> _plugins = {};
final Map<String, List<NeonPluginHook>> _hooks = {};
final List<String> _registrationOrder = [];
late NeonPluginContext _context;
bool _initialized = false;
String _sdkVersion = '0.8.0';
bool get isInitialized => _initialized;
String get sdkVersion => _sdkVersion;
int get pluginCount => _plugins.length;
List<String> get pluginIds => List.unmodifiable(_registrationOrder);
NeonPluginContext get context {
_ensureInit();
return _context;
}
void initialize({String sdkVersion = '0.8.0'}) {
if (_initialized) return;
_sdkVersion = sdkVersion;
_context = NeonPluginContext(sdkVersion: sdkVersion);
_initialized = true;
}
void register(NeonPlugin plugin) {
_ensureInit();
final id = plugin.manifest.id;
if (_plugins.containsKey(id)) {
throw NeonPluginException(
'Plugin "$id" is already registered.',
pluginId: id,
code: NeonPluginErrorCode.alreadyRegistered,
);
}
if (!plugin.manifest.sdkConstraint.isSatisfiedBy(_sdkVersion)) {
throw NeonPluginException(
'Plugin "$id" requires SDK ${plugin.manifest.sdkConstraint}, '
'but current SDK is $_sdkVersion.',
pluginId: id,
code: NeonPluginErrorCode.sdkIncompatible,
);
}
for (final dep in plugin.manifest.dependencies) {
if (!_plugins.containsKey(dep)) {
throw NeonPluginException(
'Plugin "$id" depends on "$dep", which is not registered. '
'Register "$dep" first.',
pluginId: id,
code: NeonPluginErrorCode.missingDependency,
);
}
}
for (final cap in plugin.manifest.capabilities) {
final conflict = _findCapabilityConflict(cap, id);
if (conflict != null) {
throw NeonPluginException(
'Plugin "$id" declares capability "$cap" which conflicts with '
'plugin "$conflict".',
pluginId: id,
code: NeonPluginErrorCode.capabilityConflict,
);
}
}
plugin.onRegister(_context);
plugin.registeredInternal = true;
_plugins[id] = plugin;
_registrationOrder.add(id);
_context.trackRegistration(id);
}
void unregister(String pluginId) {
_ensureInit();
final plugin = _plugins[pluginId];
if (plugin == null) {
throw NeonPluginException(
'Plugin "$pluginId" is not registered.',
pluginId: pluginId,
code: NeonPluginErrorCode.notRegistered,
);
}
final dependents = _findDependents(pluginId);
if (dependents.isNotEmpty) {
throw NeonPluginException(
'Cannot unregister "$pluginId" because these plugins depend on it: '
'${dependents.join(", ")}. Unregister them first.',
pluginId: pluginId,
code: NeonPluginErrorCode.hasDependents,
);
}
_hooks.remove(pluginId);
plugin.onDispose();
plugin.registeredInternal = false;
_plugins.remove(pluginId);
_registrationOrder.remove(pluginId);
_context.trackUnregistration(pluginId);
}
T? getPlugin<T extends NeonPlugin>(String pluginId) {
return _plugins[pluginId] as T?;
}
bool hasPlugin(String pluginId) => _plugins.containsKey(pluginId);
NeonPlugin? operator [](String pluginId) => _plugins[pluginId];
List<NeonPlugin> getPluginsWithCapability(String capability) {
return _plugins.values
.where((p) => p.manifest.capabilities.contains(capability))
.toList();
}
void registerHook(String pluginId, String hookName, NeonPluginHookCallback callback) {
_ensureInit();
if (!_plugins.containsKey(pluginId)) {
throw NeonPluginException(
'Plugin "$pluginId" is not registered.',
pluginId: pluginId,
code: NeonPluginErrorCode.notRegistered,
);
}
final hook = NeonPluginHook(
pluginId: pluginId,
name: hookName,
callback: callback,
);
_hooks.putIfAbsent(hookName, () => []).add(hook);
}
Future<List<dynamic>> executeHook(String hookName, [dynamic argument]) async {
final hooks = _hooks[hookName];
if (hooks == null || hooks.isEmpty) return [];
final results = <dynamic>[];
for (final hook in hooks) {
results.add(await hook.callback(argument));
}
return results;
}
List<NeonPluginHook> getHooks(String hookName) {
return List.unmodifiable(_hooks[hookName] ?? []);
}
Map<String, dynamic> getRegistryStats() {
final dartOnly = _plugins.values
.where((p) => p.manifest.type == NeonPluginType.dartOnly)
.length;
final native = _plugins.values
.where((p) => p.manifest.type == NeonPluginType.native)
.length;
return {
'total_plugins': _plugins.length,
'dart_only': dartOnly,
'native': native,
'sdk_version': _sdkVersion,
'hooks': _hooks.keys.toList(),
'registration_order': List.from(_registrationOrder),
};
}
String? _findCapabilityConflict(String capability, String excludeId) {
for (final entry in _plugins.entries) {
if (entry.key == excludeId) continue;
if (entry.value.manifest.capabilities.contains(capability)) {
return entry.key;
}
}
return null;
}
List<String> _findDependents(String pluginId) {
return _plugins.entries
.where((e) => e.value.manifest.dependencies.contains(pluginId))
.map((e) => e.key)
.toList();
}
void dispose() {
for (final id in _registrationOrder.reversed) {
final plugin = _plugins[id];
if (plugin != null) {
plugin.onDispose();
plugin.registeredInternal = false;
}
}
_plugins.clear();
_hooks.clear();
_registrationOrder.clear();
_initialized = false;
}
static void reset() {
_instance?.dispose();
_instance = null;
}
void _ensureInit() {
if (!_initialized) {
throw StateError(
'NeonPluginRegistry has not been initialized. Call initialize() first.',
);
}
}
}
typedef NeonPluginHookCallback = Future<dynamic> Function(dynamic argument);
class NeonPluginHook {
final String pluginId;
final String name;
final NeonPluginHookCallback callback;
const NeonPluginHook({
required this.pluginId,
required this.name,
required this.callback,
});
@override
String toString() => 'NeonPluginHook($pluginId:$name)';
}
enum NeonPluginErrorCode {
alreadyRegistered,
notRegistered,
sdkIncompatible,
missingDependency,
capabilityConflict,
hasDependents,
hookError,
}
class NeonPluginException implements Exception {
final String message;
final String? pluginId;
final NeonPluginErrorCode code;
const NeonPluginException(
this.message, {
this.pluginId,
required this.code,
});
@override
String toString() => 'NeonPluginException($code): $message';
}

View file

@ -0,0 +1,159 @@
/// Represents the current status of an animation.
enum NeonAnimationStatus {
idle,
running,
completed,
dismissed,
}
/// Abstract controller for managing animation playback.
abstract class NeonAnimationController {
/// The current status of the animation.
NeonAnimationStatus get status;
/// The current value of the animation, typically between 0.0 and 1.0.
double get value;
/// Starts playing the animation forward.
void forward();
/// Starts playing the animation in reverse.
void reverse();
/// Stops the animation at its current value.
void stop();
/// Resets the animation to its initial state.
void reset();
/// Releases resources used by the controller.
void dispose();
}
/// Holds the begin/end values and current progress of an animation.
class NeonAnimationValue<T> {
final T begin;
final T end;
final double progress;
const NeonAnimationValue({
required this.begin,
required this.end,
this.progress = 0.0,
});
/// Returns a new animation value with the given progress, clamped to [0.0, 1.0].
NeonAnimationValue<T> withProgress(double newProgress) {
return NeonAnimationValue<T>(
begin: begin,
end: end,
progress: newProgress.clamp(0.0, 1.0),
);
}
/// Whether the animation has reached its end.
bool get isComplete => progress >= 1.0;
/// Whether the animation is at its starting point.
bool get isAtStart => progress <= 0.0;
@override
String toString() =>
'NeonAnimationValue($begin -> $end, progress: $progress)';
}
/// Represents a duration of time in milliseconds.
class NeonDuration {
final int milliseconds;
const NeonDuration({this.milliseconds = 300});
/// Creates a duration from a millisecond value.
const NeonDuration.ms(this.milliseconds);
/// Zero duration.
static const NeonDuration zero = NeonDuration(milliseconds: 0);
/// A fast 150ms duration.
static const NeonDuration fast = NeonDuration(milliseconds: 150);
/// A normal 300ms duration.
static const NeonDuration normal = NeonDuration(milliseconds: 300);
/// A slow 500ms duration.
static const NeonDuration slow = NeonDuration(milliseconds: 500);
@override
String toString() => 'NeonDuration(${milliseconds}ms)';
}
/// An animation curve that maps input time [0.0, 1.0] to output progress.
abstract class NeonCurve {
/// Transforms a linear time value to a curved progress value.
double transform(double t);
}
/// A linear curve with no easing.
class NeonLinearCurve implements NeonCurve {
const NeonLinearCurve();
@override
double transform(double t) => t;
}
/// A curve that starts slowly and accelerates.
class NeonEaseInCurve implements NeonCurve {
const NeonEaseInCurve();
@override
double transform(double t) => t * t;
}
/// A curve that starts fast and decelerates.
class NeonEaseOutCurve implements NeonCurve {
const NeonEaseOutCurve();
@override
double transform(double t) => 1 - (1 - t) * (1 - t);
}
/// A curve that eases in and out with smooth acceleration and deceleration.
class NeonEaseInOutCurve implements NeonCurve {
const NeonEaseInOutCurve();
@override
double transform(double t) =>
t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) * (-2 * t + 2) / 2;
}
/// Provides predefined animation curve constants.
class NeonCurves {
/// Linear interpolation with no easing.
static const NeonCurve linear = NeonLinearCurve();
/// Ease-in curve (slow start).
static const NeonCurve easeIn = NeonEaseInCurve();
/// Ease-out curve (slow end).
static const NeonCurve easeOut = NeonEaseOutCurve();
/// Ease-in-out curve (slow start and end).
static const NeonCurve easeInOut = NeonEaseInOutCurve();
}
/// Simulates spring physics for animations.
class NeonSpringSimulation {
final double stiffness;
final double damping;
final double mass;
const NeonSpringSimulation({
this.stiffness = 100.0,
this.damping = 10.0,
this.mass = 1.0,
});
@override
String toString() =>
'NeonSpringSimulation(stiffness: $stiffness, damping: $damping, mass: $mass)';
}

View file

@ -0,0 +1,178 @@
import 'package:neon_framework/src/rendering/constraints.dart';
import 'package:neon_framework/src/widgets/primitives.dart';
/// Types of paint commands that can be recorded on a canvas.
enum NeonPaintCommandType {
drawRect,
drawText,
drawCircle,
drawLine,
clipRect,
transform,
save,
restore,
}
/// A recorded paint command with its type and parameters.
class NeonPaintCommand {
final NeonPaintCommandType type;
final Map<String, dynamic> params;
const NeonPaintCommand(this.type, this.params);
@override
String toString() => 'NeonPaintCommand($type, $params)';
}
/// A recording canvas that captures paint commands for later replay.
class NeonCanvas {
final List<NeonPaintCommand> _commands = [];
/// Device information used for physical pixel conversions.
final NeonDeviceInfo deviceInfo;
NeonCanvas({this.deviceInfo = const NeonDeviceInfo()});
/// Returns an unmodifiable list of recorded paint commands.
List<NeonPaintCommand> get commands => List.unmodifiable(_commands);
/// Draws a filled rectangle with the given color.
void drawRect(NeonRect rect, {NeonColor color = NeonColor.black}) {
_commands.add(NeonPaintCommand(
NeonPaintCommandType.drawRect,
{
'rect': rect,
'color': color,
'physicalRect': _toPhysical(rect),
},
));
}
/// Draws text at the given position with the specified style.
void drawText(
String text,
NeonOffset position, {
NeonTextStyle style = const NeonTextStyle(),
}) {
_commands.add(NeonPaintCommand(
NeonPaintCommandType.drawText,
{
'text': text,
'position': position,
'style': style,
'physicalPosition': _toPhysicalOffset(position),
},
));
}
/// Draws a circle at the given center with the specified radius and color.
void drawCircle(
NeonOffset center,
double radius, {
NeonColor color = NeonColor.black,
}) {
_commands.add(NeonPaintCommand(
NeonPaintCommandType.drawCircle,
{
'center': center,
'radius': radius,
'color': color,
'physicalRadius': radius * deviceInfo.devicePixelRatio,
},
));
}
/// Draws a line between two points with the given color and stroke width.
void drawLine(
NeonOffset start,
NeonOffset end, {
NeonColor color = NeonColor.black,
double strokeWidth = 1.0,
}) {
_commands.add(NeonPaintCommand(
NeonPaintCommandType.drawLine,
{
'start': start,
'end': end,
'color': color,
'strokeWidth': strokeWidth,
},
));
}
/// Clips subsequent drawing to the given rectangle.
void clipRect(NeonRect rect) {
_commands.add(NeonPaintCommand(
NeonPaintCommandType.clipRect,
{'rect': rect},
));
}
/// Saves the current canvas state onto the state stack.
void save() {
_commands.add(
const NeonPaintCommand(NeonPaintCommandType.save, {}));
}
/// Restores the most recently saved canvas state.
void restore() {
_commands.add(
const NeonPaintCommand(NeonPaintCommandType.restore, {}));
}
/// Translates the canvas origin by the given offsets.
void translate(double dx, double dy) {
_commands.add(NeonPaintCommand(
NeonPaintCommandType.transform,
{'dx': dx, 'dy': dy},
));
}
NeonRect _toPhysical(NeonRect rect) {
final ratio = deviceInfo.devicePixelRatio;
return NeonRect(
rect.left * ratio,
rect.top * ratio,
rect.width * ratio,
rect.height * ratio,
);
}
NeonOffset _toPhysicalOffset(NeonOffset offset) {
final ratio = deviceInfo.devicePixelRatio;
return NeonOffset(offset.dx * ratio, offset.dy * ratio);
}
/// Clears all recorded paint commands.
void clear() {
_commands.clear();
}
/// The number of recorded paint commands.
int get commandCount => _commands.length;
}
/// Describes the device's display properties for rendering calculations.
class NeonDeviceInfo {
/// The ratio of physical pixels to logical pixels.
final double devicePixelRatio;
/// The logical screen size.
final NeonSize screenSize;
/// The physical screen size in device pixels.
final NeonSize physicalSize;
const NeonDeviceInfo({
this.devicePixelRatio = 1.0,
this.screenSize = const NeonSize(360, 800),
this.physicalSize = const NeonSize(1080, 2400),
});
/// The logical size of the display (alias for screenSize).
NeonSize get logicalSize => screenSize;
@override
String toString() =>
'NeonDeviceInfo(dpr: $devicePixelRatio, screen: $screenSize)';
}

View file

@ -0,0 +1,229 @@
/// Represents a 2D size with width and height.
class NeonSize {
final double width;
final double height;
const NeonSize(this.width, this.height);
/// A size with zero width and height.
static const NeonSize zero = NeonSize(0, 0);
/// A size with infinite width and height.
static const NeonSize infinite = NeonSize(double.infinity, double.infinity);
/// Whether either dimension is zero or negative.
bool get isEmpty => width <= 0 || height <= 0;
/// Whether both dimensions are finite.
bool get isFinite => width.isFinite && height.isFinite;
/// Returns a new size clamped to the given constraints.
NeonSize constrain(NeonConstraints constraints) {
return NeonSize(
width.clamp(constraints.minWidth, constraints.maxWidth),
height.clamp(constraints.minHeight, constraints.maxHeight),
);
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is NeonSize && other.width == width && other.height == height;
@override
int get hashCode => Object.hash(width, height);
@override
String toString() => 'NeonSize($width, $height)';
}
/// Represents a 2D offset (displacement) with dx and dy components.
class NeonOffset {
final double dx;
final double dy;
const NeonOffset(this.dx, this.dy);
/// An offset with zero displacement.
static const NeonOffset zero = NeonOffset(0, 0);
/// Adds two offsets together.
NeonOffset operator +(NeonOffset other) =>
NeonOffset(dx + other.dx, dy + other.dy);
/// Subtracts another offset from this offset.
NeonOffset operator -(NeonOffset other) =>
NeonOffset(dx - other.dx, dy - other.dy);
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is NeonOffset && other.dx == dx && other.dy == dy;
@override
int get hashCode => Object.hash(dx, dy);
@override
String toString() => 'NeonOffset($dx, $dy)';
}
/// Represents an axis-aligned rectangle defined by position and size.
class NeonRect {
final double left;
final double top;
final double width;
final double height;
const NeonRect(this.left, this.top, this.width, this.height);
/// A zero-sized rectangle at the origin.
static const NeonRect zero = NeonRect(0, 0, 0, 0);
/// Creates a rectangle from an offset and size.
NeonRect.fromOffset(NeonOffset offset, NeonSize size)
: left = offset.dx,
top = offset.dy,
width = size.width,
height = size.height;
/// The right edge of the rectangle.
double get right => left + width;
/// The bottom edge of the rectangle.
double get bottom => top + height;
/// The top-left corner as an offset.
NeonOffset get topLeft => NeonOffset(left, top);
/// The size of the rectangle.
NeonSize get size => NeonSize(width, height);
/// Whether the given point lies within this rectangle.
bool contains(NeonOffset point) =>
point.dx >= left &&
point.dx <= right &&
point.dy >= top &&
point.dy <= bottom;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is NeonRect &&
other.left == left &&
other.top == top &&
other.width == width &&
other.height == height;
@override
int get hashCode => Object.hash(left, top, width, height);
@override
String toString() => 'NeonRect($left, $top, $width, $height)';
}
/// Immutable layout constraints with min/max width and height bounds.
class NeonConstraints {
final double minWidth;
final double maxWidth;
final double minHeight;
final double maxHeight;
const NeonConstraints({
this.minWidth = 0,
this.maxWidth = double.infinity,
this.minHeight = 0,
this.maxHeight = double.infinity,
});
/// Creates tight constraints that force an exact size.
NeonConstraints.tight(NeonSize size)
: minWidth = size.width,
maxWidth = size.width,
minHeight = size.height,
maxHeight = size.height;
/// Creates loose constraints with zero minimums and the given size as maximums.
NeonConstraints.loose(NeonSize size)
: minWidth = 0,
maxWidth = size.width,
minHeight = 0,
maxHeight = size.height;
/// Creates constraints that expand to fill, optionally with fixed dimensions.
const NeonConstraints.expand({
double? width,
double? height,
}) : minWidth = width ?? double.infinity,
maxWidth = width ?? double.infinity,
minHeight = height ?? double.infinity,
maxHeight = height ?? double.infinity;
/// Whether these constraints allow only a single size.
bool get isTight => minWidth == maxWidth && minHeight == maxHeight;
/// Whether the maximum width is finite.
bool get hasBoundedWidth => maxWidth < double.infinity;
/// Whether the maximum height is finite.
bool get hasBoundedHeight => maxHeight < double.infinity;
/// Returns a copy with optionally overridden constraint values.
NeonConstraints copyWith({
double? minWidth,
double? maxWidth,
double? minHeight,
double? maxHeight,
}) {
return NeonConstraints(
minWidth: minWidth ?? this.minWidth,
maxWidth: maxWidth ?? this.maxWidth,
minHeight: minHeight ?? this.minHeight,
maxHeight: maxHeight ?? this.maxHeight,
);
}
/// Returns new constraints with zero minimums, preserving maximums.
NeonConstraints loosen() {
return NeonConstraints(
minWidth: 0,
maxWidth: maxWidth,
minHeight: 0,
maxHeight: maxHeight,
);
}
/// Returns new constraints tightened to the given width and/or height.
NeonConstraints tighten({double? width, double? height}) {
return NeonConstraints(
minWidth: width ?? minWidth,
maxWidth: width ?? maxWidth,
minHeight: height ?? minHeight,
maxHeight: height ?? maxHeight,
);
}
/// Constrains the given size to fit within these constraints.
NeonSize constrain(NeonSize size) => size.constrain(this);
/// The smallest size that satisfies these constraints.
NeonSize get smallest => NeonSize(minWidth, minHeight);
/// The largest size that satisfies these constraints.
NeonSize get biggest => NeonSize(maxWidth, maxHeight);
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is NeonConstraints &&
other.minWidth == minWidth &&
other.maxWidth == maxWidth &&
other.minHeight == minHeight &&
other.maxHeight == maxHeight;
@override
int get hashCode => Object.hash(minWidth, maxWidth, minHeight, maxHeight);
@override
String toString() =>
'NeonConstraints($minWidth<=w<=$maxWidth, $minHeight<=h<=$maxHeight)';
}

View file

@ -0,0 +1,120 @@
import 'package:neon_framework/src/rendering/constraints.dart';
import 'package:neon_framework/src/rendering/canvas.dart';
/// Base class for all render objects in the Neon rendering tree.
abstract class NeonRenderObject {
/// The parent render object in the tree.
NeonRenderObject? parent;
/// The child render objects.
final List<NeonRenderObject> children = [];
NeonSize _size = NeonSize.zero;
NeonOffset _relativeOffset = NeonOffset.zero;
bool _needsLayout = true;
bool _needsPaint = true;
/// The size determined during layout.
NeonSize get size => _size;
/// The offset relative to this object's parent.
NeonOffset get relativeOffset => _relativeOffset;
/// Sets the offset relative to this object's parent.
set relativeOffset(NeonOffset value) {
_relativeOffset = value;
}
/// The absolute offset from the root of the render tree.
NeonOffset get absoluteOffset {
if (parent == null) return _relativeOffset;
return parent!.absoluteOffset + _relativeOffset;
}
/// The bounding rectangle in absolute coordinates.
NeonRect get bounds => NeonRect.fromOffset(absoluteOffset, _size);
/// Adds a child render object and marks layout as dirty.
void addChild(NeonRenderObject child) {
child.parent = this;
children.add(child);
markNeedsLayout();
}
/// Removes a child render object and marks layout as dirty.
void removeChild(NeonRenderObject child) {
children.remove(child);
child.parent = null;
markNeedsLayout();
}
/// Marks this object as needing layout and paint.
void markNeedsLayout() {
_needsLayout = true;
_needsPaint = true;
}
/// Marks this object as needing paint.
void markNeedsPaint() {
_needsPaint = true;
}
/// Lays out this object within the given constraints.
void layout(NeonConstraints constraints) {
if (!_needsLayout) return;
_size = performLayout(constraints);
_needsLayout = false;
_needsPaint = true;
}
/// Subclass hook to compute size given constraints.
NeonSize performLayout(NeonConstraints constraints);
/// Paints this object and its children onto the canvas.
void paint(NeonCanvas canvas) {
canvas.save();
canvas.translate(absoluteOffset.dx, absoluteOffset.dy);
performPaint(canvas);
canvas.restore();
_needsPaint = false;
}
/// Subclass hook to perform custom painting.
void performPaint(NeonCanvas canvas);
/// Tests whether the given point hits this object or any child.
bool hitTest(NeonOffset point) {
if (!bounds.contains(point)) return false;
for (var i = children.length - 1; i >= 0; i--) {
if (children[i].hitTest(point)) return true;
}
return true;
}
/// The depth of this object in the render tree (0 for root).
int get depth {
var d = 0;
var current = parent;
while (current != null) {
d++;
current = current.parent;
}
return d;
}
/// Returns a debug string representation of this object and its subtree.
String toDebugString({int indent = 0}) {
final buffer = StringBuffer();
final prefix = ' ' * indent;
buffer.writeln(
'$prefix$runtimeType(size: $_size, rel: $_relativeOffset, abs: $absoluteOffset)');
for (final child in children) {
buffer.write(child.toDebugString(indent: indent + 1));
}
return buffer.toString();
}
@override
String toString() => '$runtimeType(size: $_size, offset: $_relativeOffset)';
}

View file

@ -0,0 +1,121 @@
import 'package:neon_framework/src/rendering/render_object.dart';
import 'package:neon_framework/src/rendering/constraints.dart';
import 'package:neon_framework/src/rendering/canvas.dart';
import 'package:neon_framework/src/rendering/render_widgets.dart';
import 'package:neon_framework/src/widgets/widget.dart';
import 'package:neon_framework/src/widgets/text.dart' as w;
import 'package:neon_framework/src/widgets/container.dart' as w;
import 'package:neon_framework/src/widgets/button.dart' as w;
import 'package:neon_framework/src/widgets/layout.dart' as w;
import 'package:neon_framework/src/widgets/widget_tree.dart';
/// Manages the layout and paint pipeline for the Neon render tree.
class NeonRenderPipeline {
NeonRenderObject? _rootRenderObject;
/// Device information used for rendering calculations.
final NeonDeviceInfo deviceInfo;
NeonCanvas? _canvas;
NeonRenderPipeline({
this.deviceInfo = const NeonDeviceInfo(),
});
/// The root render object of the current render tree.
NeonRenderObject? get rootRenderObject => _rootRenderObject;
/// The canvas produced by the most recent paint pass.
NeonCanvas? get canvas => _canvas;
/// Builds a render tree from the given element tree.
NeonRenderObject buildRenderTree(NeonElement element) {
final renderObject = _createRenderObject(element);
for (final child in element.children) {
final childRender = buildRenderTree(child);
renderObject.addChild(childRender);
}
_rootRenderObject = renderObject;
return renderObject;
}
NeonRenderObject _createRenderObject(NeonElement element) {
final widget = element.widget;
if (widget is w.Text) {
return RenderText(
text: widget.data,
style: widget.style,
maxLines: widget.maxLines,
);
}
if (widget is w.Container) {
return RenderContainer(
fixedWidth: widget.width,
fixedHeight: widget.height,
color: widget.color,
padding: widget.padding,
margin: widget.margin,
);
}
if (widget is w.Column) {
return RenderColumn(
mainAxisAlignment: widget.mainAxisAlignment,
crossAxisAlignment: widget.crossAxisAlignment,
);
}
if (widget is w.Row) {
return RenderRow(
mainAxisAlignment: widget.mainAxisAlignment,
crossAxisAlignment: widget.crossAxisAlignment,
);
}
if (widget is w.Stack) {
return RenderStack(fit: widget.fit);
}
return _FallbackRenderObject();
}
/// Runs the layout pass on the render tree with the given constraints.
void layout(NeonConstraints constraints) {
if (_rootRenderObject == null) return;
_rootRenderObject!.layout(constraints);
}
/// Runs the paint pass and returns the resulting canvas.
NeonCanvas paint() {
_canvas = NeonCanvas(deviceInfo: deviceInfo);
if (_rootRenderObject != null) {
_rootRenderObject!.paint(_canvas!);
}
return _canvas!;
}
/// Runs the full layout and paint pipeline, returning the canvas.
NeonCanvas runPipeline(NeonConstraints constraints) {
layout(constraints);
return paint();
}
/// Returns a debug string representation of the render tree.
String debugDump() {
if (_rootRenderObject == null) return '<empty render tree>';
return _rootRenderObject!.toDebugString();
}
}
class _FallbackRenderObject extends NeonRenderObject {
@override
NeonSize performLayout(NeonConstraints constraints) {
return constraints.smallest;
}
@override
void performPaint(NeonCanvas canvas) {}
}

View file

@ -0,0 +1,403 @@
import 'package:neon_framework/src/rendering/render_object.dart';
import 'package:neon_framework/src/rendering/constraints.dart';
import 'package:neon_framework/src/rendering/canvas.dart';
import 'package:neon_framework/src/widgets/primitives.dart';
/// Render object that lays out and paints text content.
class RenderText extends NeonRenderObject {
final String text;
final NeonTextStyle style;
final int? maxLines;
/// Creates a text render object with the given text and style.
RenderText({
required this.text,
this.style = const NeonTextStyle(),
this.maxLines,
});
@override
NeonSize performLayout(NeonConstraints constraints) {
final charWidth = style.fontSize * 0.6;
final lineHeight = style.fontSize * 1.2;
final textWidth = text.length * charWidth;
final maxWidth =
constraints.hasBoundedWidth ? constraints.maxWidth : textWidth;
final lines = (textWidth / maxWidth).ceil().clamp(1, maxLines ?? 999);
final width = textWidth.clamp(constraints.minWidth, maxWidth);
final height =
(lines * lineHeight).clamp(constraints.minHeight, constraints.maxHeight);
return NeonSize(width, height);
}
@override
void performPaint(NeonCanvas canvas) {
canvas.drawText(text, NeonOffset.zero, style: style);
}
}
/// Render object for a box container with optional color, padding, and margin.
class RenderContainer extends NeonRenderObject {
final double? fixedWidth;
final double? fixedHeight;
final NeonColor? color;
final NeonEdgeInsets padding;
final NeonEdgeInsets margin;
/// Creates a container render object with optional fixed dimensions and styling.
RenderContainer({
this.fixedWidth,
this.fixedHeight,
this.color,
this.padding = NeonEdgeInsets.zero,
this.margin = NeonEdgeInsets.zero,
});
@override
NeonSize performLayout(NeonConstraints constraints) {
final horizontalPadding = padding.left + padding.right;
final verticalPadding = padding.top + padding.bottom;
final horizontalMargin = margin.left + margin.right;
final verticalMargin = margin.top + margin.bottom;
final innerMaxWidth =
(fixedWidth ?? constraints.maxWidth) - horizontalPadding - horizontalMargin;
final innerMaxHeight =
(fixedHeight ?? constraints.maxHeight) - verticalPadding - verticalMargin;
final childConstraints = NeonConstraints(
minWidth: 0,
maxWidth: innerMaxWidth > 0 ? innerMaxWidth : 0,
minHeight: 0,
maxHeight: innerMaxHeight > 0 ? innerMaxHeight : 0,
);
double contentWidth = 0;
double contentHeight = 0;
for (final child in children) {
child.layout(childConstraints);
child.relativeOffset = NeonOffset(
margin.left + padding.left,
margin.top + padding.top + contentHeight,
);
contentWidth =
contentWidth > child.size.width ? contentWidth : child.size.width;
contentHeight += child.size.height;
}
final width = fixedWidth ??
(contentWidth + horizontalPadding + horizontalMargin)
.clamp(constraints.minWidth, constraints.maxWidth);
final height = fixedHeight ??
(contentHeight + verticalPadding + verticalMargin)
.clamp(constraints.minHeight, constraints.maxHeight);
return NeonSize(width, height);
}
@override
void performPaint(NeonCanvas canvas) {
if (color != null) {
canvas.drawRect(
NeonRect(
margin.left,
margin.top,
size.width - margin.left - margin.right,
size.height - margin.top - margin.bottom,
),
color: color!,
);
}
for (final child in children) {
child.paint(canvas);
}
}
}
/// Render object that arranges children vertically in a column.
class RenderColumn extends NeonRenderObject {
final MainAxisAlignment mainAxisAlignment;
final CrossAxisAlignment crossAxisAlignment;
/// Creates a column render object with the given alignment settings.
RenderColumn({
this.mainAxisAlignment = MainAxisAlignment.start,
this.crossAxisAlignment = CrossAxisAlignment.center,
});
@override
NeonSize performLayout(NeonConstraints constraints) {
final childConstraints = constraints.loosen();
double totalHeight = 0;
double maxWidth = 0;
for (final child in children) {
if (crossAxisAlignment == CrossAxisAlignment.stretch &&
constraints.hasBoundedWidth) {
child.layout(childConstraints.copyWith(
minWidth: constraints.maxWidth,
maxWidth: constraints.maxWidth,
));
} else {
child.layout(childConstraints);
}
maxWidth = maxWidth > child.size.width ? maxWidth : child.size.width;
totalHeight += child.size.height;
}
final width = maxWidth.clamp(constraints.minWidth, constraints.maxWidth);
final height =
totalHeight.clamp(constraints.minHeight, constraints.maxHeight);
_positionChildren(width, height, totalHeight);
return NeonSize(width, height);
}
void _positionChildren(
double containerWidth, double containerHeight, double totalHeight) {
double currentY = 0;
switch (mainAxisAlignment) {
case MainAxisAlignment.start:
currentY = 0;
break;
case MainAxisAlignment.end:
currentY = containerHeight - totalHeight;
break;
case MainAxisAlignment.center:
currentY = (containerHeight - totalHeight) / 2;
break;
case MainAxisAlignment.spaceBetween:
case MainAxisAlignment.spaceAround:
case MainAxisAlignment.spaceEvenly:
currentY = 0;
break;
}
final spacing =
_calculateSpacing(containerHeight, totalHeight, children.length);
if (mainAxisAlignment == MainAxisAlignment.spaceEvenly) {
currentY += spacing;
} else if (mainAxisAlignment == MainAxisAlignment.spaceAround) {
currentY += spacing / 2;
}
for (final child in children) {
double childX;
switch (crossAxisAlignment) {
case CrossAxisAlignment.start:
childX = 0;
break;
case CrossAxisAlignment.end:
childX = containerWidth - child.size.width;
break;
case CrossAxisAlignment.center:
childX = (containerWidth - child.size.width) / 2;
break;
case CrossAxisAlignment.stretch:
childX = 0;
break;
}
child.relativeOffset = NeonOffset(childX, currentY);
currentY += child.size.height;
if (mainAxisAlignment == MainAxisAlignment.spaceBetween ||
mainAxisAlignment == MainAxisAlignment.spaceAround ||
mainAxisAlignment == MainAxisAlignment.spaceEvenly) {
currentY += spacing;
}
}
}
double _calculateSpacing(
double containerHeight, double totalHeight, int childCount) {
if (childCount <= 1) return 0;
final freeSpace = containerHeight - totalHeight;
if (freeSpace <= 0) return 0;
switch (mainAxisAlignment) {
case MainAxisAlignment.spaceBetween:
return freeSpace / (childCount - 1);
case MainAxisAlignment.spaceAround:
return freeSpace / childCount;
case MainAxisAlignment.spaceEvenly:
return freeSpace / (childCount + 1);
default:
return 0;
}
}
@override
void performPaint(NeonCanvas canvas) {
for (final child in children) {
child.paint(canvas);
}
}
}
/// Render object that arranges children horizontally in a row.
class RenderRow extends NeonRenderObject {
final MainAxisAlignment mainAxisAlignment;
final CrossAxisAlignment crossAxisAlignment;
/// Creates a row render object with the given alignment settings.
RenderRow({
this.mainAxisAlignment = MainAxisAlignment.start,
this.crossAxisAlignment = CrossAxisAlignment.center,
});
@override
NeonSize performLayout(NeonConstraints constraints) {
final childConstraints = constraints.loosen();
double totalWidth = 0;
double maxHeight = 0;
for (final child in children) {
if (crossAxisAlignment == CrossAxisAlignment.stretch &&
constraints.hasBoundedHeight) {
child.layout(childConstraints.copyWith(
minHeight: constraints.maxHeight,
maxHeight: constraints.maxHeight,
));
} else {
child.layout(childConstraints);
}
maxHeight =
maxHeight > child.size.height ? maxHeight : child.size.height;
totalWidth += child.size.width;
}
final width = totalWidth.clamp(constraints.minWidth, constraints.maxWidth);
final height =
maxHeight.clamp(constraints.minHeight, constraints.maxHeight);
_positionChildren(width, height, totalWidth);
return NeonSize(width, height);
}
void _positionChildren(
double containerWidth, double containerHeight, double totalWidth) {
double currentX = 0;
switch (mainAxisAlignment) {
case MainAxisAlignment.start:
currentX = 0;
break;
case MainAxisAlignment.end:
currentX = containerWidth - totalWidth;
break;
case MainAxisAlignment.center:
currentX = (containerWidth - totalWidth) / 2;
break;
case MainAxisAlignment.spaceBetween:
case MainAxisAlignment.spaceAround:
case MainAxisAlignment.spaceEvenly:
currentX = 0;
break;
}
final spacing =
_calculateSpacing(containerWidth, totalWidth, children.length);
if (mainAxisAlignment == MainAxisAlignment.spaceEvenly) {
currentX += spacing;
} else if (mainAxisAlignment == MainAxisAlignment.spaceAround) {
currentX += spacing / 2;
}
for (final child in children) {
double childY;
switch (crossAxisAlignment) {
case CrossAxisAlignment.start:
childY = 0;
break;
case CrossAxisAlignment.end:
childY = containerHeight - child.size.height;
break;
case CrossAxisAlignment.center:
childY = (containerHeight - child.size.height) / 2;
break;
case CrossAxisAlignment.stretch:
childY = 0;
break;
}
child.relativeOffset = NeonOffset(currentX, childY);
currentX += child.size.width;
if (mainAxisAlignment == MainAxisAlignment.spaceBetween ||
mainAxisAlignment == MainAxisAlignment.spaceAround ||
mainAxisAlignment == MainAxisAlignment.spaceEvenly) {
currentX += spacing;
}
}
}
double _calculateSpacing(
double containerWidth, double totalWidth, int childCount) {
if (childCount <= 1) return 0;
final freeSpace = containerWidth - totalWidth;
if (freeSpace <= 0) return 0;
switch (mainAxisAlignment) {
case MainAxisAlignment.spaceBetween:
return freeSpace / (childCount - 1);
case MainAxisAlignment.spaceAround:
return freeSpace / childCount;
case MainAxisAlignment.spaceEvenly:
return freeSpace / (childCount + 1);
default:
return 0;
}
}
@override
void performPaint(NeonCanvas canvas) {
for (final child in children) {
child.paint(canvas);
}
}
}
/// Render object that layers children on top of each other.
class RenderStack extends NeonRenderObject {
final StackFit fit;
/// Creates a stack render object with the given fit behavior.
RenderStack({this.fit = StackFit.loose});
@override
NeonSize performLayout(NeonConstraints constraints) {
double maxWidth = 0;
double maxHeight = 0;
final childConstraints = fit == StackFit.expand
? NeonConstraints.tight(constraints.biggest)
: constraints.loosen();
for (final child in children) {
child.layout(childConstraints);
child.relativeOffset = NeonOffset.zero;
maxWidth = maxWidth > child.size.width ? maxWidth : child.size.width;
maxHeight =
maxHeight > child.size.height ? maxHeight : child.size.height;
}
return NeonSize(
maxWidth.clamp(constraints.minWidth, constraints.maxWidth),
maxHeight.clamp(constraints.minHeight, constraints.maxHeight),
);
}
@override
void performPaint(NeonCanvas canvas) {
for (final child in children) {
child.paint(canvas);
}
}
}

View file

@ -0,0 +1,152 @@
/// Represents the status of an asynchronous operation.
enum NeonAsyncStatus { loading, success, error, cached }
/// A wrapper for asynchronous values that tracks loading, success, error, and cached states.
class NeonAsyncValue<T> {
final NeonAsyncStatus status;
final T? _data;
final Object? _error;
final StackTrace? _stackTrace;
/// Whether a refresh is currently in progress (for cached values).
final bool isRefreshing;
const NeonAsyncValue._({
required this.status,
T? data,
Object? error,
StackTrace? stackTrace,
this.isRefreshing = false,
}) : _data = data,
_error = error,
_stackTrace = stackTrace;
/// Creates an async value in the loading state.
const NeonAsyncValue.loading()
: this._(status: NeonAsyncStatus.loading);
/// Creates an async value in the success state with the given data.
const NeonAsyncValue.success(T data)
: this._(status: NeonAsyncStatus.success, data: data);
/// Creates an async value in the error state with the given error.
NeonAsyncValue.error(Object error, [StackTrace? stackTrace])
: this._(status: NeonAsyncStatus.error, error: error, stackTrace: stackTrace);
/// Creates an async value in the cached state with optional refresh indicator.
const NeonAsyncValue.cached(T data, {bool isRefreshing = false})
: this._(status: NeonAsyncStatus.cached, data: data, isRefreshing: isRefreshing);
/// Whether the value is currently loading.
bool get isLoading => status == NeonAsyncStatus.loading;
/// Whether the value completed successfully.
bool get isSuccess => status == NeonAsyncStatus.success;
/// Whether the value resulted in an error.
bool get isError => status == NeonAsyncStatus.error;
/// Whether the value is from cache.
bool get isCached => status == NeonAsyncStatus.cached;
/// Whether data is available.
bool get hasData => _data != null;
/// Whether an error is present.
bool get hasError => _error != null;
/// The data value; throws if no data is available.
T get data {
if (_data == null) {
throw StateError('No data available. Status: $status');
}
return _data as T;
}
/// The data value, or null if not available.
T? get dataOrNull => _data;
/// The error object; throws if no error is present.
Object get error {
if (_error == null) {
throw StateError('No error available. Status: $status');
}
return _error!;
}
/// The stack trace associated with the error, if any.
StackTrace? get stackTrace => _stackTrace;
/// Pattern-matches on the async status and returns the result of the matching callback.
R when<R>({
required R Function() loading,
required R Function(T data) success,
required R Function(Object error, StackTrace? stackTrace) error,
R Function(T data, bool isRefreshing)? cached,
}) {
switch (status) {
case NeonAsyncStatus.loading:
return loading();
case NeonAsyncStatus.success:
return success(_data as T);
case NeonAsyncStatus.error:
return error(_error!, _stackTrace);
case NeonAsyncStatus.cached:
if (cached != null) {
return cached(_data as T, isRefreshing);
}
return success(_data as T);
}
}
/// Pattern-matches with optional callbacks and a required fallback.
R maybeWhen<R>({
R Function()? loading,
R Function(T data)? success,
R Function(Object error, StackTrace? stackTrace)? error,
R Function(T data, bool isRefreshing)? cached,
required R Function() orElse,
}) {
switch (status) {
case NeonAsyncStatus.loading:
return loading != null ? loading() : orElse();
case NeonAsyncStatus.success:
return success != null ? success(_data as T) : orElse();
case NeonAsyncStatus.error:
return error != null ? error(_error!, _stackTrace) : orElse();
case NeonAsyncStatus.cached:
return cached != null ? cached(_data as T, isRefreshing) : orElse();
}
}
/// Returns a copy with optionally overridden fields.
NeonAsyncValue<T> copyWith({
NeonAsyncStatus? status,
T? data,
Object? error,
StackTrace? stackTrace,
bool? isRefreshing,
}) {
return NeonAsyncValue<T>._(
status: status ?? this.status,
data: data ?? _data,
error: error ?? _error,
stackTrace: stackTrace ?? _stackTrace,
isRefreshing: isRefreshing ?? this.isRefreshing,
);
}
@override
String toString() {
switch (status) {
case NeonAsyncStatus.loading:
return 'NeonAsyncValue<$T>.loading()';
case NeonAsyncStatus.success:
return 'NeonAsyncValue<$T>.success($_data)';
case NeonAsyncStatus.error:
return 'NeonAsyncValue<$T>.error($_error)';
case NeonAsyncStatus.cached:
return 'NeonAsyncValue<$T>.cached($_data, refreshing: $isRefreshing)';
}
}
}

View file

@ -0,0 +1,260 @@
typedef VoidCallback = void Function();
abstract class _Reactive {
final Set<NeonComputed> _dependentComputeds = {};
final Set<NeonEffect> _dependentEffects = {};
void _addDependentComputed(NeonComputed computed) {
_dependentComputeds.add(computed);
}
void _removeDependentComputed(NeonComputed computed) {
_dependentComputeds.remove(computed);
}
void _addDependentEffect(NeonEffect effect) {
_dependentEffects.add(effect);
}
void _removeDependentEffect(NeonEffect effect) {
_dependentEffects.remove(effect);
}
void _trackRead() {
final tracker = _NeonDependencyTracker.current;
if (tracker != null) {
tracker._trackedReactives.add(this);
}
}
void _notifyDependents() {
for (final computed in Set<NeonComputed>.from(_dependentComputeds)) {
computed._markDirty();
}
for (final effect in Set<NeonEffect>.from(_dependentEffects)) {
effect._run();
}
}
void _clearDependents() {
_dependentComputeds.clear();
_dependentEffects.clear();
}
}
/// A reactive signal that holds a mutable value and notifies listeners on change.
class NeonSignal<T> extends _Reactive {
T _value;
final List<VoidCallback> _listeners = [];
bool _disposed = false;
/// Creates a signal with the given initial value.
NeonSignal(this._value);
/// The current value; reading this tracks the signal as a dependency.
T get value {
_trackRead();
return _value;
}
/// Sets the signal's value and notifies listeners if changed.
set value(T newValue) {
if (_disposed) {
throw StateError('Cannot set value on a disposed signal.');
}
if (identical(_value, newValue)) return;
_value = newValue;
_notify();
}
/// Reads the current value without tracking as a dependency.
T peek() => _value;
/// Updates the value using a function that receives the current value.
void update(T Function(T current) updater) {
value = updater(_value);
}
/// Adds a listener that is called when the value changes.
void listen(VoidCallback listener) {
_listeners.add(listener);
}
/// Removes a previously added listener.
void removeListener(VoidCallback listener) {
_listeners.remove(listener);
}
void _notify() {
for (final listener in List<VoidCallback>.from(_listeners)) {
listener();
}
_notifyDependents();
}
/// Disposes the signal, clearing all listeners and dependencies.
void dispose() {
_disposed = true;
_listeners.clear();
_clearDependents();
}
/// Whether this signal has been disposed.
bool get isDisposed => _disposed;
/// The number of currently registered listeners.
int get listenerCount => _listeners.length;
@override
String toString() => 'NeonSignal<$T>($_value)';
}
/// A computed reactive value derived from other reactive sources.
class NeonComputed<T> extends _Reactive {
final T Function() _compute;
T? _cachedValue;
bool _dirty = true;
final Set<_Reactive> _dependencies = {};
final List<VoidCallback> _listeners = [];
bool _disposed = false;
/// Creates a computed value from the given computation function.
NeonComputed(this._compute) {
_recompute();
}
/// The current computed value; recomputes if dirty and tracks as a dependency.
T get value {
_trackRead();
if (_dirty) {
_recompute();
}
return _cachedValue as T;
}
/// Reads the current computed value without tracking as a dependency.
T peek() {
if (_dirty) {
_recompute();
}
return _cachedValue as T;
}
/// Adds a listener that is called when the computed value changes.
void listen(VoidCallback listener) {
_listeners.add(listener);
}
/// Removes a previously added listener.
void removeListener(VoidCallback listener) {
_listeners.remove(listener);
}
void _recompute() {
for (final dep in _dependencies) {
dep._removeDependentComputed(this);
}
_dependencies.clear();
final tracker = _NeonDependencyTracker();
_NeonDependencyTracker._stack.add(tracker);
try {
_cachedValue = _compute();
} finally {
_NeonDependencyTracker._stack.removeLast();
}
_dependencies.addAll(tracker._trackedReactives);
for (final dep in _dependencies) {
dep._addDependentComputed(this);
}
_dirty = false;
}
void _markDirty() {
if (_disposed) return;
_dirty = true;
final oldValue = _cachedValue;
_recompute();
if (!identical(oldValue, _cachedValue)) {
for (final listener in List<VoidCallback>.from(_listeners)) {
listener();
}
_notifyDependents();
}
}
/// Disposes the computed value, clearing dependencies and listeners.
void dispose() {
_disposed = true;
for (final dep in _dependencies) {
dep._removeDependentComputed(this);
}
_dependencies.clear();
_listeners.clear();
_clearDependents();
}
/// Whether this computed value has been disposed.
bool get isDisposed => _disposed;
@override
String toString() => 'NeonComputed<$T>(${peek()})';
}
/// A reactive side effect that re-runs when its dependencies change.
class NeonEffect {
final void Function() _fn;
final Set<_Reactive> _dependencies = {};
bool _disposed = false;
/// Creates an effect that immediately runs the given function and tracks dependencies.
NeonEffect(this._fn) {
_run();
}
void _run() {
if (_disposed) return;
for (final dep in _dependencies) {
dep._removeDependentEffect(this);
}
_dependencies.clear();
final tracker = _NeonDependencyTracker();
_NeonDependencyTracker._stack.add(tracker);
try {
_fn();
} finally {
_NeonDependencyTracker._stack.removeLast();
}
_dependencies.addAll(tracker._trackedReactives);
for (final dep in _dependencies) {
dep._addDependentEffect(this);
}
}
/// Disposes the effect, stopping future re-runs.
void dispose() {
_disposed = true;
for (final dep in _dependencies) {
dep._removeDependentEffect(this);
}
_dependencies.clear();
}
/// Whether this effect has been disposed.
bool get isDisposed => _disposed;
}
class _NeonDependencyTracker {
static final List<_NeonDependencyTracker> _stack = [];
final Set<_Reactive> _trackedReactives = {};
static _NeonDependencyTracker? get current =>
_stack.isEmpty ? null : _stack.last;
}

View file

@ -0,0 +1,84 @@
import 'package:neon_framework/src/state/signal.dart';
import 'package:neon_framework/src/widgets/widget.dart';
import 'package:neon_framework/src/widgets/build_context.dart';
/// Mixin that integrates reactive signals with stateful widgets.
mixin SignalMixin<T extends StatefulWidget> on NeonState<T> {
final Map<NeonSignal, VoidCallback> _activeSubscriptions = {};
final Set<NeonSignal> _currentBuildSignals = {};
/// Subscribes to a signal and returns its current value, triggering rebuilds on change.
S watch<S>(NeonSignal<S> signal) {
_currentBuildSignals.add(signal);
if (!_activeSubscriptions.containsKey(signal)) {
void listener() {
if (mounted) {
setState(() {});
}
}
signal.listen(listener);
_activeSubscriptions[signal] = listener;
}
return signal.value;
}
/// Reads a signal's value without subscribing to changes.
S read<S>(NeonSignal<S> signal) {
return signal.peek();
}
/// Removes subscriptions to signals no longer used in the current build.
void cleanupStaleSubscriptions() {
final stale = _activeSubscriptions.keys
.where((s) => !_currentBuildSignals.contains(s))
.toList();
for (final signal in stale) {
final listener = _activeSubscriptions.remove(signal);
if (listener != null) {
signal.removeListener(listener);
}
}
_currentBuildSignals.clear();
}
@override
NeonWidget build(NeonBuildContext context) {
_currentBuildSignals.clear();
final result = buildWithSignals(context);
cleanupStaleSubscriptions();
return result;
}
/// Builds the widget tree with access to watched signals.
NeonWidget buildWithSignals(NeonBuildContext context);
@override
void dispose() {
for (final entry in _activeSubscriptions.entries) {
entry.key.removeListener(entry.value);
}
_activeSubscriptions.clear();
_currentBuildSignals.clear();
super.dispose();
}
}
/// Provides a signal to descendant widgets via the build context.
class NeonSignalProvider<T> {
final NeonSignal<T> signal;
final String? key;
const NeonSignalProvider({required this.signal, this.key});
/// Injects this signal into the given build context.
void provideToContext(NeonBuildContext context) {
context.provide<NeonSignal<T>>(signal);
}
/// Retrieves a signal of the given type from the build context.
static NeonSignal<T>? of<T>(NeonBuildContext context) {
return context.findAncestor<NeonSignal<T>>();
}
}

View file

@ -0,0 +1,25 @@
import 'package:neon_framework/src/widgets/widget.dart';
import 'package:neon_framework/src/widgets/build_context.dart';
import 'package:neon_framework/src/widgets/primitives.dart';
enum AppBarVariant { centerAligned, small, medium, large }
class AppBar extends NeonWidget {
final NeonWidget title;
final List<NeonWidget>? actions;
final NeonWidget? leading;
final AppBarVariant variant;
const AppBar({
super.key,
required this.title,
this.actions,
this.leading,
this.variant = AppBarVariant.small,
});
@override
NeonWidget build(NeonBuildContext context) {
return LeafWidget(type: 'AppBar', key: key);
}
}

View file

@ -0,0 +1,61 @@
import 'package:neon_framework/src/widgets/widget.dart';
import 'package:neon_framework/src/widgets/build_context.dart';
import 'package:neon_framework/src/widgets/primitives.dart';
import 'package:neon_framework/src/widgets/layout.dart';
/// A Material 3 Badge widget.
///
/// Badges are used to decorate icons or buttons with supplemental information,
/// typically a count or a status dot.
class Badge extends StatelessWidget {
/// The widget that the badge will be attached to.
final NeonWidget child;
/// The widget to display within the badge (e.g., a [Text] widget for counts).
/// If null, a small "dot" badge is shown.
final NeonWidget? label;
/// The background color of the badge.
final NeonColor backgroundColor;
/// The color of the label text.
final NeonColor labelColor;
const Badge({
super.key,
required this.child,
this.label,
this.backgroundColor = NeonColor.red,
this.labelColor = NeonColor.white,
});
@override
NeonWidget build(NeonBuildContext context) {
// We return a Stack to position the badge over the child.
// However, since placement over an icon depends on the icon size,
// we use a specific primitive 'BadgeLabel' that the native renderer knows how to anchor.
return Stack(
children: [
child,
_BadgeLabel(
label: label,
backgroundColor: backgroundColor,
labelColor: labelColor,
),
],
);
}
}
/// Internal primitive for the badge label anchored to its parent.
class _BadgeLabel extends LeafWidget {
final NeonWidget? label;
final NeonColor backgroundColor;
final NeonColor labelColor;
const _BadgeLabel({
this.label,
required this.backgroundColor,
required this.labelColor,
}) : super(type: 'BadgeLabel');
}

View file

@ -0,0 +1,27 @@
import 'package:neon_framework/src/widgets/widget.dart';
import 'package:neon_framework/src/widgets/build_context.dart';
import 'package:neon_framework/src/widgets/primitives.dart';
class BottomSheet extends NeonWidget {
final NeonWidget child;
final bool showDragHandle;
final bool visible;
final double borderRadius;
final NeonColor? backgroundColor;
final void Function()? onDismiss;
const BottomSheet({
super.key,
required this.child,
this.showDragHandle = true,
this.visible = false,
this.borderRadius = 28.0,
this.backgroundColor,
this.onDismiss,
});
@override
NeonWidget build(NeonBuildContext context) {
return LeafWidget(type: 'BottomSheet', key: key);
}
}

View file

@ -0,0 +1,61 @@
import 'package:neon_framework/src/core/environment.dart';
import 'widget.dart'; // Import Widget so we can type 'NeonWidget'
/// Provides context information and inherited data during widget building.
class NeonBuildContext {
/// The application configuration available in this context.
final NeonConfig config;
/// The parent context, if any.
final NeonBuildContext? parent;
/// The specific widget this context is tied to.
/// (Added this so we know "where" we are in the tree).
final NeonWidget? widget;
/// The stable ID of the widget in the tree.
final String? id;
final Map<Type, dynamic> _inherited = {};
/// Creates a build context with the given [config] and optional [parent].
NeonBuildContext({
required this.config,
this.parent,
this.widget,
this.id,
});
/// THE MISSING METHOD
/// Creates a child context for a new widget, inheriting from this context.
NeonBuildContext spawn(NeonWidget childWidget, String childId) {
return NeonBuildContext(
config: config,
parent: this,
widget: childWidget,
id: childId,
);
}
/// Finds an inherited value of type [T] by searching up the context tree.
T? findAncestor<T>() {
if (_inherited.containsKey(T)) {
return _inherited[T] as T;
}
return parent?.findAncestor<T>();
}
/// Provides a [value] of type [T] to descendant contexts.
void provide<T>(T value) {
_inherited[T] = value;
}
/// (Optional) Kept for backward compatibility if you used it elsewhere.
NeonBuildContext createChild() {
return NeonBuildContext(config: config, parent: this);
}
@override
String toString() =>
'NeonBuildContext(env: ${config.environment.name}, widget: ${widget.runtimeType})';
}

View file

@ -0,0 +1,57 @@
import 'package:neon_framework/src/widgets/widget.dart';
import 'package:neon_framework/src/widgets/build_context.dart';
import 'package:neon_framework/src/widgets/container.dart';
import 'package:neon_framework/src/widgets/primitives.dart';
/// A callback with no arguments and no return value.
typedef NeonVoidCallback = void Function();
/// A tappable button widget that wraps a child widget.
class Button extends NeonWidget {
/// The widget displayed inside the button.
final NeonWidget child;
/// Called when the button is tapped, if enabled.
final NeonVoidCallback? onPressed;
/// The padding around the button content.
final NeonEdgeInsets padding;
/// The background color of the button.
final NeonColor? color;
/// Whether the button responds to taps.
final bool enabled;
/// Creates a button with a required [child] and optional tap handling.
const Button({
super.key,
required this.child,
this.onPressed,
this.padding = const NeonEdgeInsets.symmetric(
vertical: 8.0,
horizontal: 16.0,
),
this.color,
this.enabled = true,
});
@override
NeonWidget build(NeonBuildContext context) {
return Container(
padding: padding,
color: color ?? NeonColor.blue,
child: child,
);
}
/// Simulates a tap on the button, invoking [onPressed] if enabled.
void tap() {
if (enabled && onPressed != null) {
onPressed!();
}
}
@override
String toString() => 'Button(enabled: $enabled)';
}

View file

@ -0,0 +1,47 @@
import 'package:neon_framework/src/widgets/widget.dart';
import 'package:neon_framework/src/widgets/build_context.dart';
import 'package:neon_framework/src/widgets/primitives.dart';
enum CardVariant { elevated, filled, outlined }
class Card extends NeonWidget {
final NeonWidget child;
final CardVariant variant;
final NeonEdgeInsets padding;
final double borderRadius;
final NeonColor? color;
final void Function()? onTap;
const Card({
super.key,
required this.child,
this.variant = CardVariant.elevated,
this.padding = const NeonEdgeInsets.all(16.0),
this.borderRadius = 12.0,
this.color,
this.onTap,
});
const Card.filled({
super.key,
required this.child,
this.padding = const NeonEdgeInsets.all(16.0),
this.borderRadius = 12.0,
this.color,
this.onTap,
}) : variant = CardVariant.filled;
const Card.outlined({
super.key,
required this.child,
this.padding = const NeonEdgeInsets.all(16.0),
this.borderRadius = 12.0,
this.color,
this.onTap,
}) : variant = CardVariant.outlined;
@override
NeonWidget build(NeonBuildContext context) {
return LeafWidget(type: 'Card', key: key);
}
}

View file

@ -0,0 +1,136 @@
import 'package:neon_framework/src/widgets/widget.dart';
import 'package:neon_framework/src/widgets/build_context.dart';
import 'package:neon_framework/src/widgets/primitives.dart';
/// A Material 3 ActionChip widget.
class ActionChip extends StatelessWidget {
final NeonWidget label;
final NeonWidget? avatar;
final void Function()? onPressed;
final bool enabled;
const ActionChip({
super.key,
required this.label,
this.avatar,
this.onPressed,
this.enabled = true,
});
@override
NeonWidget build(NeonBuildContext context) {
return _ChipPrimitive(
type: 'ActionChip',
label: label,
avatar: avatar,
enabled: enabled,
onPressed: onPressed,
);
}
}
/// A Material 3 FilterChip widget.
class FilterChip extends StatelessWidget {
final NeonWidget label;
final bool selected;
final void Function(bool)? onSelected;
final NeonWidget? avatar;
const FilterChip({
super.key,
required this.label,
this.selected = false,
this.onSelected,
this.avatar,
});
@override
NeonWidget build(NeonBuildContext context) {
return _ChipPrimitive(
type: 'FilterChip',
label: label,
selected: selected,
avatar: avatar,
onSelected: onSelected != null ? () => onSelected!(!selected) : null,
);
}
}
/// A Material 3 ChoiceChip widget.
class ChoiceChip extends StatelessWidget {
final NeonWidget label;
final bool selected;
final void Function(bool)? onSelected;
final NeonWidget? avatar;
const ChoiceChip({
super.key,
required this.label,
this.selected = false,
this.onSelected,
this.avatar,
});
@override
NeonWidget build(NeonBuildContext context) {
return _ChipPrimitive(
type: 'ChoiceChip',
label: label,
selected: selected,
avatar: avatar,
onSelected: onSelected != null ? () => onSelected!(true) : null,
);
}
}
/// A Material 3 InputChip widget.
class InputChip extends StatelessWidget {
final NeonWidget label;
final NeonWidget? avatar;
final void Function()? onPressed;
final void Function()? onDeleted;
final bool selected;
const InputChip({
super.key,
required this.label,
this.avatar,
this.onPressed,
this.onDeleted,
this.selected = false,
});
@override
NeonWidget build(NeonBuildContext context) {
return _ChipPrimitive(
type: 'InputChip',
label: label,
avatar: avatar,
selected: selected,
onPressed: onPressed,
onDeleted: onDeleted,
);
}
}
/// Internal primitive for chips.
class _ChipPrimitive extends LeafWidget {
final NeonWidget label;
final NeonWidget? avatar;
final bool enabled;
final bool selected;
final void Function()? onPressed;
final void Function()? onSelected;
final void Function()? onDeleted;
const _ChipPrimitive({
required String type,
required this.label,
this.avatar,
this.enabled = true,
this.selected = false,
this.onPressed,
this.onSelected,
this.onDeleted,
}) : super(type: type);
}

View file

@ -0,0 +1,42 @@
import 'package:neon_framework/src/widgets/widget.dart';
import 'package:neon_framework/src/widgets/build_context.dart';
import 'package:neon_framework/src/widgets/primitives.dart';
/// A box widget with optional sizing, color, padding, margin, and a child.
class Container extends NeonWidget {
/// The child widget rendered inside this container.
final NeonWidget? child;
/// The fixed width of the container, or null for unconstrained.
final double? width;
/// The fixed height of the container, or null for unconstrained.
final double? height;
/// The background color of the container.
final NeonColor? color;
/// The padding inside the container around the child.
final NeonEdgeInsets padding;
/// The margin outside the container.
final NeonEdgeInsets margin;
/// Creates a container with optional sizing, color, padding, margin, and child.
const Container({
super.key,
this.child,
this.width,
this.height,
this.color,
this.padding = NeonEdgeInsets.zero,
this.margin = NeonEdgeInsets.zero,
});
@override
NeonWidget build(NeonBuildContext context) => this;
@override
String toString() =>
'Container(${width}x$height, color: $color)';
}

View file

@ -0,0 +1,41 @@
import 'package:neon_framework/src/widgets/widget.dart';
import 'package:neon_framework/src/widgets/build_context.dart';
import 'package:neon_framework/src/widgets/primitives.dart';
enum DialogVariant { standard, fullscreen }
class Dialog extends NeonWidget {
final NeonWidget? title;
final NeonWidget? content;
final List<NeonWidget>? actions;
final DialogVariant variant;
final double borderRadius;
final bool visible;
final void Function()? onDismiss;
const Dialog({
super.key,
this.title,
this.content,
this.actions,
this.variant = DialogVariant.standard,
this.borderRadius = 28.0,
this.visible = false,
this.onDismiss,
});
const Dialog.fullscreen({
super.key,
this.title,
this.content,
this.actions,
this.borderRadius = 28.0,
this.visible = false,
this.onDismiss,
}) : variant = DialogVariant.fullscreen;
@override
NeonWidget build(NeonBuildContext context) {
return LeafWidget(type: 'Dialog', key: key);
}
}

View file

@ -0,0 +1,33 @@
import 'package:neon_framework/src/widgets/widget.dart';
import 'package:neon_framework/src/widgets/build_context.dart';
import 'package:neon_framework/src/widgets/button.dart';
import 'package:neon_framework/src/widgets/primitives.dart';
enum FABVariant { small, regular, large, extended }
class FloatingActionButton extends NeonWidget {
final NeonWidget child;
final NeonVoidCallback? onPressed;
final FABVariant variant;
const FloatingActionButton({
super.key,
required this.child,
this.onPressed,
this.variant = FABVariant.regular,
});
@override
NeonWidget build(NeonBuildContext context) {
double paddingValue = 16.0;
if (variant == FABVariant.small) paddingValue = 8.0;
if (variant == FABVariant.large) paddingValue = 24.0;
return Button(
onPressed: onPressed,
color: const NeonColor(0xFFD0BCFF), // M3 FAB default color
padding: NeonEdgeInsets.all(paddingValue),
child: child,
);
}
}

View file

@ -0,0 +1,34 @@
import 'package:neon_framework/src/widgets/widget.dart';
import 'package:neon_framework/src/widgets/build_context.dart';
import 'package:neon_framework/src/widgets/button.dart';
import 'package:neon_framework/src/widgets/primitives.dart';
enum IconButtonVariant { standard, filled, tonal, outlined }
class IconButton extends NeonWidget {
final NeonWidget icon;
final NeonVoidCallback? onPressed;
final IconButtonVariant variant;
const IconButton({
super.key,
required this.icon,
this.onPressed,
this.variant = IconButtonVariant.standard,
});
@override
NeonWidget build(NeonBuildContext context) {
NeonColor color = NeonColor.transparent;
if (variant == IconButtonVariant.filled) color = NeonColor.blue;
if (variant == IconButtonVariant.tonal) color = const NeonColor(0xFFEADDFF);
return Button(
key: key,
onPressed: onPressed,
color: color,
padding: const NeonEdgeInsets.all(12.0),
child: icon,
);
}
}

View file

@ -0,0 +1,145 @@
import 'package:neon_framework/src/widgets/widget.dart';
import 'package:neon_framework/src/widgets/build_context.dart';
import 'package:neon_framework/src/widgets/primitives.dart';
/// A widget that arranges its children vertically.
class Column extends MultiChildWidget {
final MainAxisAlignment mainAxisAlignment;
final CrossAxisAlignment crossAxisAlignment;
const Column({
super.key,
required super.children, // MUST use super.children
this.mainAxisAlignment = MainAxisAlignment.start,
this.crossAxisAlignment = CrossAxisAlignment.center,
});
@override
NeonWidget? build(NeonBuildContext context) {
return null; // MUST return null
}
@override
String toString() =>
'Column(children: ${children.length}, main: $mainAxisAlignment, cross: $crossAxisAlignment)';
}
/// A widget that arranges its children horizontally.
class Row extends MultiChildWidget {
final MainAxisAlignment mainAxisAlignment;
final CrossAxisAlignment crossAxisAlignment;
const Row({
super.key,
required super.children,
this.mainAxisAlignment = MainAxisAlignment.start,
this.crossAxisAlignment = CrossAxisAlignment.center,
});
@override
NeonWidget? build(NeonBuildContext context) {
return null;
}
@override
String toString() =>
'Row(children: ${children.length}, main: $mainAxisAlignment, cross: $crossAxisAlignment)';
}
/// A widget that layers its children on top of each other.
class Stack extends MultiChildWidget {
final StackFit fit;
const Stack({
super.key,
required super.children,
this.fit = StackFit.loose,
});
@override
NeonWidget? build(NeonBuildContext context) {
return null;
}
@override
String toString() => 'Stack(children: ${children.length}, fit: $fit)';
}
/// A widget that expands a child of a [Row], [Column], or [Flex].
class Expanded extends NeonWidget {
final NeonWidget child;
final int flex;
const Expanded({
super.key,
required this.child,
this.flex = 1,
});
@override
NeonWidget? build(NeonBuildContext context) => null;
}
/// A widget that centers its child within itself.
class Center extends NeonWidget {
final NeonWidget child;
const Center({
super.key,
required this.child,
});
@override
NeonWidget? build(NeonBuildContext context) => null;
}
/// A box with a specified size.
class SizedBox extends NeonWidget {
final double? width;
final double? height;
final NeonWidget? child;
const SizedBox({
super.key,
this.width,
this.height,
this.child,
});
@override
NeonWidget? build(NeonBuildContext context) => null;
}
/// A flexible space that can be used in a [Row] or [Column].
class Spacer extends NeonWidget {
final int flex;
const Spacer({
super.key,
this.flex = 1,
});
@override
NeonWidget? build(NeonBuildContext context) => null;
}
/// A widget that controls where a child of a [Stack] is positioned.
class Positioned extends NeonWidget {
final double? left;
final double? top;
final double? right;
final double? bottom;
final NeonWidget child;
const Positioned({
super.key,
this.left,
this.top,
this.right,
this.bottom,
required this.child,
});
@override
NeonWidget? build(NeonBuildContext context) => null;
}

View file

@ -0,0 +1,33 @@
import 'package:neon_framework/src/widgets/widget.dart';
import 'package:neon_framework/src/widgets/build_context.dart';
import 'package:neon_framework/src/widgets/primitives.dart';
class ListTile extends NeonWidget {
final NeonWidget? leading;
final NeonWidget? title;
final NeonWidget? subtitle;
final NeonWidget? trailing;
final bool enabled;
final bool selected;
final bool dense;
final NeonEdgeInsets contentPadding;
final void Function()? onTap;
const ListTile({
super.key,
this.leading,
this.title,
this.subtitle,
this.trailing,
this.enabled = true,
this.selected = false,
this.dense = false,
this.contentPadding = const NeonEdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
this.onTap,
});
@override
NeonWidget build(NeonBuildContext context) {
return LeafWidget(type: 'ListTile', key: key);
}
}

View file

@ -0,0 +1,114 @@
import 'package:neon_framework/src/widgets/widget.dart';
import 'package:neon_framework/src/widgets/build_context.dart';
import 'package:neon_framework/src/widgets/button.dart';
import 'package:neon_framework/src/widgets/primitives.dart';
/// A Material 3 Filled Button.
class FilledButton extends NeonWidget {
final NeonWidget child;
final NeonVoidCallback? onPressed;
final NeonColor? color;
const FilledButton({
super.key,
required this.child,
this.onPressed,
this.color,
});
@override
NeonWidget build(NeonBuildContext context) {
return Button(
key: key,
onPressed: onPressed,
color: color ?? NeonColor.blue,
child: child,
);
}
}
/// A Material 3 Filled Tonal Button.
class FilledTonalButton extends NeonWidget {
final NeonWidget child;
final NeonVoidCallback? onPressed;
const FilledTonalButton({
super.key,
required this.child,
this.onPressed,
});
@override
NeonWidget build(NeonBuildContext context) {
return Button(
key: key,
onPressed: onPressed,
color: const NeonColor(0xFFEADDFF), // Tonal secondary container
child: child,
);
}
}
/// A Material 3 Elevated Button.
class ElevatedButton extends NeonWidget {
final NeonWidget child;
final NeonVoidCallback? onPressed;
const ElevatedButton({
super.key,
required this.child,
this.onPressed,
});
@override
NeonWidget build(NeonBuildContext context) {
return Button(
key: key,
onPressed: onPressed,
color: NeonColor.white,
child: child,
);
}
}
/// A Material 3 Outlined Button.
class OutlinedButton extends NeonWidget {
final NeonWidget child;
final NeonVoidCallback? onPressed;
const OutlinedButton({
super.key,
required this.child,
this.onPressed,
});
@override
NeonWidget build(NeonBuildContext context) {
return Button(
onPressed: onPressed,
color: NeonColor.transparent,
child: child,
);
}
}
/// A Material 3 Text Button.
class TextButton extends NeonWidget {
final NeonWidget child;
final NeonVoidCallback? onPressed;
const TextButton({
super.key,
required this.child,
this.onPressed,
});
@override
NeonWidget build(NeonBuildContext context) {
return Button(
onPressed: onPressed,
color: NeonColor.transparent,
child: child,
);
}
}

View file

@ -0,0 +1,61 @@
import 'package:neon_framework/src/widgets/widget.dart';
import 'package:neon_framework/src/widgets/build_context.dart';
import 'package:neon_framework/src/widgets/primitives.dart';
class MenuItem {
final String label;
final NeonWidget? leadingIcon;
final NeonWidget? trailingIcon;
final bool enabled;
final List<MenuItem>? subMenu;
final void Function()? onPressed;
const MenuItem({
required this.label,
this.leadingIcon,
this.trailingIcon,
this.enabled = true,
this.subMenu,
this.onPressed,
});
}
class MenuAnchor extends NeonWidget {
final NeonWidget child;
final List<MenuItem> menuItems;
final bool expanded;
final void Function()? onOpen;
final void Function()? onClose;
const MenuAnchor({
super.key,
required this.child,
required this.menuItems,
this.expanded = false,
this.onOpen,
this.onClose,
});
@override
NeonWidget build(NeonBuildContext context) {
return LeafWidget(type: 'MenuAnchor', key: key);
}
}
class MenuBar extends NeonWidget {
final List<MenuItem> items;
final int? selectedIndex;
final void Function(int)? onItemSelected;
const MenuBar({
super.key,
required this.items,
this.selectedIndex,
this.onItemSelected,
});
@override
NeonWidget build(NeonBuildContext context) {
return LeafWidget(type: 'MenuBar', key: key);
}
}

View file

@ -0,0 +1,127 @@
import 'package:neon_framework/src/widgets/widget.dart';
import 'package:neon_framework/src/widgets/build_context.dart';
import 'package:neon_framework/src/widgets/primitives.dart';
/// A Material 3 Navigation Bar destination.
class NavigationDestination {
final NeonWidget icon;
final String label;
const NavigationDestination({
required this.icon,
required this.label,
});
}
/// A Material 3 Navigation Bar.
class NavigationBar extends NeonWidget {
final List<NavigationDestination> destinations;
final int selectedIndex;
final Function(int)? onDestinationSelected;
const NavigationBar({
super.key,
required this.destinations,
this.selectedIndex = 0,
this.onDestinationSelected,
});
@override
NeonWidget build(NeonBuildContext context) {
return LeafWidget(type: 'NavigationBar', key: key);
}
}
/// A Material 3 Navigation Drawer destination.
class NavigationDrawerDestination {
final NeonWidget icon;
final NeonWidget label;
const NavigationDrawerDestination({
required this.icon,
required this.label,
});
}
/// A Material 3 Navigation Drawer.
class NavigationDrawer extends NeonWidget {
final List<NavigationDrawerDestination> children;
final int selectedIndex;
final Function(int)? onDestinationSelected;
const NavigationDrawer({
super.key,
required this.children,
this.selectedIndex = 0,
this.onDestinationSelected,
});
@override
NeonWidget build(NeonBuildContext context) {
return LeafWidget(type: 'NavigationDrawer', key: key);
}
}
/// A Material 3 Navigation Rail destination.
class NavigationRailDestination {
final NeonWidget icon;
final NeonWidget label;
const NavigationRailDestination({
required this.icon,
required this.label,
});
}
/// A Material 3 Navigation Rail.
class NavigationRail extends NeonWidget {
final List<NavigationRailDestination> destinations;
final int selectedIndex;
final Function(int)? onDestinationSelected;
const NavigationRail({
super.key,
required this.destinations,
this.selectedIndex = 0,
this.onDestinationSelected,
});
@override
NeonWidget build(NeonBuildContext context) {
return LeafWidget(type: 'NavigationRail', key: key);
}
}
/// A Material 3 Tab.
class Tab extends NeonWidget {
final String text;
final NeonWidget? icon;
const Tab({
super.key,
required this.text,
this.icon,
});
@override
NeonWidget build(NeonBuildContext context) => this;
}
/// A Material 3 Tab Bar.
class TabBar extends NeonWidget {
final List<Tab> tabs;
final int selectedIndex;
final Function(int)? onTap;
const TabBar({
super.key,
required this.tabs,
this.selectedIndex = 0,
this.onTap,
});
@override
NeonWidget build(NeonBuildContext context) {
return LeafWidget(type: 'TabBar', key: key);
}
}

View file

@ -0,0 +1,167 @@
import 'package:neon_framework/src/widgets/widget.dart';
import 'package:neon_framework/src/widgets/build_context.dart';
/// Represents an ARGB color value.
class NeonColor {
/// The 32-bit ARGB color value.
final int value;
/// Creates a color from a 32-bit ARGB [value].
const NeonColor(this.value);
/// Fully transparent color.
static const NeonColor transparent = NeonColor(0x00000000);
/// Opaque black.
static const NeonColor black = NeonColor(0xFF000000);
/// Opaque white.
static const NeonColor white = NeonColor(0xFFFFFFFF);
/// Opaque red.
static const NeonColor red = NeonColor(0xFFFF0000);
/// Opaque green.
static const NeonColor green = NeonColor(0xFF00FF00);
/// Opaque blue.
static const NeonColor blue = NeonColor(0xFF0000FF);
/// Opaque grey.
static const NeonColor grey = NeonColor(0xFF808080);
@override
bool operator ==(Object other) =>
identical(this, other) || other is NeonColor && other.value == value;
@override
int get hashCode => value.hashCode;
@override
String toString() =>
'NeonColor(0x${value.toRadixString(16).padLeft(8, '0').toUpperCase()})';
}
/// Represents font weight options for text rendering.
enum NeonFontWeight {
normal,
bold,
}
/// Defines the visual style for text rendering.
class NeonTextStyle {
/// The font size in logical pixels.
final double fontSize;
/// The text color.
final NeonColor color;
/// The font weight (normal or bold).
final NeonFontWeight fontWeight;
/// Creates a text style with optional font size, color, and weight.
const NeonTextStyle({
this.fontSize = 14.0,
this.color = NeonColor.black,
this.fontWeight = NeonFontWeight.normal,
});
@override
String toString() =>
'NeonTextStyle(size: $fontSize, color: $color, weight: $fontWeight)';
}
/// Represents padding or margin insets on all four sides.
class NeonEdgeInsets {
/// The top inset.
final double top;
/// The right inset.
final double right;
/// The bottom inset.
final double bottom;
/// The left inset.
final double left;
/// Creates edge insets with the same [value] on all sides.
const NeonEdgeInsets.all(double value)
: top = value,
right = value,
bottom = value,
left = value;
/// Creates edge insets with symmetric vertical and horizontal values.
const NeonEdgeInsets.symmetric({
double vertical = 0.0,
double horizontal = 0.0,
}) : top = vertical,
bottom = vertical,
left = horizontal,
right = horizontal;
/// Creates edge insets with individually specified sides.
const NeonEdgeInsets.only({
this.top = 0.0,
this.right = 0.0,
this.bottom = 0.0,
this.left = 0.0,
});
/// Zero insets on all sides.
static const NeonEdgeInsets zero = NeonEdgeInsets.all(0);
@override
bool operator ==(Object other) =>
other is NeonEdgeInsets &&
top == other.top &&
right == other.right &&
bottom == other.bottom &&
left == other.left;
@override
int get hashCode => Object.hash(top, right, bottom, left);
@override
String toString() => 'NeonEdgeInsets($top, $right, $bottom, $left)';
}
/// Defines how children are aligned along the main axis.
enum MainAxisAlignment {
start,
end,
center,
spaceBetween,
spaceAround,
spaceEvenly,
}
/// Defines how children are aligned along the cross axis.
enum CrossAxisAlignment {
start,
end,
center,
stretch,
}
/// Defines how a stack sizes its non-positioned children.
enum StackFit {
loose,
expand,
}
/// A terminal widget with no children, representing a primitive element.
class LeafWidget extends NeonWidget {
/// The type identifier for this leaf widget.
final String type;
/// Creates a leaf widget of the given [type].
const LeafWidget({super.key, required this.type});
@override
NeonWidget build(NeonBuildContext context) => this;
@override
String toString() => 'LeafWidget($type)';
}

View file

@ -0,0 +1,51 @@
import 'package:neon_framework/src/widgets/widget.dart';
import 'package:neon_framework/src/widgets/build_context.dart';
import 'package:neon_framework/src/widgets/primitives.dart';
class LinearProgressIndicator extends NeonWidget {
final double? value;
final NeonColor? color;
final NeonColor? backgroundColor;
final double minHeight;
final double borderRadius;
const LinearProgressIndicator({
super.key,
this.value,
this.color,
this.backgroundColor,
this.minHeight = 4.0,
this.borderRadius = 2.0,
});
bool get isIndeterminate => value == null;
@override
NeonWidget build(NeonBuildContext context) {
return LeafWidget(type: 'LinearProgressIndicator', key: key);
}
}
class CircularProgressIndicator extends NeonWidget {
final double? value;
final NeonColor? color;
final NeonColor? backgroundColor;
final double strokeWidth;
final double size;
const CircularProgressIndicator({
super.key,
this.value,
this.color,
this.backgroundColor,
this.strokeWidth = 4.0,
this.size = 36.0,
});
bool get isIndeterminate => value == null;
@override
NeonWidget build(NeonBuildContext context) {
return LeafWidget(type: 'CircularProgressIndicator', key: key);
}
}

View file

@ -0,0 +1,25 @@
import 'package:neon_framework/src/widgets/widget.dart';
import 'package:neon_framework/src/widgets/build_context.dart';
import 'package:neon_framework/src/widgets/primitives.dart';
/// A widget that fetches its content from a remote URL.
class RemoteWidget extends NeonWidget {
final String url;
final NeonWidget placeholder;
const RemoteWidget({
super.key,
required this.url,
this.placeholder = const LeafWidget(type: 'LoadingIndicator'),
});
@override
NeonWidget build(NeonBuildContext context) {
// The actual fetching happens on the client side (iOS/Android).
// This widget serves as a marker in the tree.
return LeafWidget(
type: 'RemoteWidget',
key: key,
);
}
}

Some files were not shown because too many files have changed in this diff Show more