diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96486fd --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..6c08927 --- /dev/null +++ b/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + channel: stable + +project_type: package diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..286759e --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,45 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "pixels", + "request": "launch", + "type": "dart" + }, + { + "name": "pixels (profile mode)", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "pixels (release mode)", + "request": "launch", + "type": "dart", + "flutterMode": "release" + }, + { + "name": "example", + "cwd": "example", + "request": "launch", + "type": "dart" + }, + { + "name": "example (profile mode)", + "cwd": "example", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "example (release mode)", + "cwd": "example", + "request": "launch", + "type": "dart", + "flutterMode": "release" + } + ] +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..41cc7d8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ba75c69 --- /dev/null +++ b/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/README.md b/README.md new file mode 100644 index 0000000..061fc21 --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# Pixels + +Pixels is a minimalistic pixel editor for Flutter. It also comes with a couple +of handy widgets for displaying and manipulating pixel images. + + +## Usage + +```dart +class MyHomePage extends StatefulWidget { + const MyHomePage({ + super.key, + }); + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + final _controller = PixelImageController( + palette: const PixelPalette.rPlace(), + width: 64, + height: 64, + ); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + body: Center( + child: PixelEditor( + controller: _controller, + ), + ), + ); + } +} +``` + +## Additional information + +This project is sponsored by [Serverpod](https://serverpod.dev) - the missing +server for Flutter. diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..834db83 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,6 @@ +include: package:flutter_lints/flutter.yaml + +linter: + rules: + package_api_docs: true + public_member_api_docs: true \ No newline at end of file diff --git a/example/.gitignore b/example/.gitignore new file mode 100644 index 0000000..24476c5 --- /dev/null +++ b/example/.gitignore @@ -0,0 +1,44 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/example/.metadata b/example/.metadata new file mode 100644 index 0000000..e27f5cc --- /dev/null +++ b/example/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled. + +version: + revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + channel: stable + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + - platform: android + create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + - platform: ios + create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + - platform: linux + create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + - platform: macos + create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + - platform: web + create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + - platform: windows + create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/example/README.md b/example/README.md new file mode 100644 index 0000000..190cc32 --- /dev/null +++ b/example/README.md @@ -0,0 +1,8 @@ +# Pixels Example + +This is an example of how to use the pixels package. + +## Getting Started + +Run by changing directory into the `example` folder. Then run `flutter create .` +followed by `flutter run`. \ No newline at end of file diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml new file mode 100644 index 0000000..61b6c4d --- /dev/null +++ b/example/analysis_options.yaml @@ -0,0 +1,29 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/example/lib/main.dart b/example/lib/main.dart new file mode 100644 index 0000000..0b2cb30 --- /dev/null +++ b/example/lib/main.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:pixels/pixels.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + // This widget is the root of your application. + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Pixels Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + home: const MyHomePage(), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({ + super.key, + }); + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + final _controller = PixelImageController( + palette: const PixelPalette.rPlace(), + width: 64, + height: 64, + ); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + body: Center( + child: PixelEditor( + controller: _controller, + ), + ), + ); + } +} diff --git a/example/linux/.gitignore b/example/linux/.gitignore new file mode 100644 index 0000000..d3896c9 --- /dev/null +++ b/example/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/example/linux/CMakeLists.txt b/example/linux/CMakeLists.txt new file mode 100644 index 0000000..74c66dd --- /dev/null +++ b/example/linux/CMakeLists.txt @@ -0,0 +1,138 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "example") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.example.example") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Define the application target. To change its name, change BINARY_NAME above, +# not the value here, or `flutter run` will no longer work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/example/linux/flutter/CMakeLists.txt b/example/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/example/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/example/linux/flutter/generated_plugin_registrant.cc b/example/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..e71a16d --- /dev/null +++ b/example/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void fl_register_plugins(FlPluginRegistry* registry) { +} diff --git a/example/linux/flutter/generated_plugin_registrant.h b/example/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..e0f0a47 --- /dev/null +++ b/example/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/example/linux/flutter/generated_plugins.cmake b/example/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..2e1de87 --- /dev/null +++ b/example/linux/flutter/generated_plugins.cmake @@ -0,0 +1,23 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/example/linux/main.cc b/example/linux/main.cc new file mode 100644 index 0000000..e7c5c54 --- /dev/null +++ b/example/linux/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/example/linux/my_application.cc b/example/linux/my_application.cc new file mode 100644 index 0000000..0ba8f43 --- /dev/null +++ b/example/linux/my_application.cc @@ -0,0 +1,104 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "example"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "example"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/example/linux/my_application.h b/example/linux/my_application.h new file mode 100644 index 0000000..72271d5 --- /dev/null +++ b/example/linux/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/example/pubspec.lock b/example/pubspec.lock new file mode 100644 index 0000000..9e21ca4 --- /dev/null +++ b/example/pubspec.lock @@ -0,0 +1,168 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.9.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.1" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.16.0" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + lints: + dependency: transitive + description: + name: lints + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.12" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.5" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.0" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.2" + pixels: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "1.0.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.9.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.10.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.12" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" +sdks: + dart: ">=2.18.1 <3.0.0" + flutter: ">=3.0.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml new file mode 100644 index 0000000..c147e5d --- /dev/null +++ b/example/pubspec.yaml @@ -0,0 +1,93 @@ +name: example +description: A new Flutter project. + +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: '>=2.18.1 <3.0.0' + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + pixels: + path: .. + + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.2 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^2.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart new file mode 100644 index 0000000..092d222 --- /dev/null +++ b/example/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:example/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +} diff --git a/example/web/favicon.png b/example/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/example/web/favicon.png differ diff --git a/example/web/icons/Icon-192.png b/example/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/example/web/icons/Icon-192.png differ diff --git a/example/web/icons/Icon-512.png b/example/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/example/web/icons/Icon-512.png differ diff --git a/example/web/icons/Icon-maskable-192.png b/example/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/example/web/icons/Icon-maskable-192.png differ diff --git a/example/web/icons/Icon-maskable-512.png b/example/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/example/web/icons/Icon-maskable-512.png differ diff --git a/example/web/index.html b/example/web/index.html new file mode 100644 index 0000000..41b3bc3 --- /dev/null +++ b/example/web/index.html @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + example + + + + + + + + + + diff --git a/example/web/manifest.json b/example/web/manifest.json new file mode 100644 index 0000000..096edf8 --- /dev/null +++ b/example/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "example", + "short_name": "example", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/lib/pixels.dart b/lib/pixels.dart new file mode 100644 index 0000000..46e319b --- /dev/null +++ b/lib/pixels.dart @@ -0,0 +1,7 @@ +library pixels; + +export 'src/editable_pixel_image.dart'; +export 'src/pixel_color_picker.dart'; +export 'src/pixel_editor.dart'; +export 'src/pixel_image.dart'; +export 'src/pixel_palette.dart'; diff --git a/lib/src/editable_pixel_image.dart b/lib/src/editable_pixel_image.dart new file mode 100644 index 0000000..8a74f9c --- /dev/null +++ b/lib/src/editable_pixel_image.dart @@ -0,0 +1,182 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:pixels/src/pixel_image.dart'; +import 'package:pixels/src/pixel_palette.dart'; + +/// A [PixelImage] that can be manipulated using the [PixelImageController]. +class EditablePixelImage extends StatefulWidget { + /// The controller controlling this image. + final PixelImageController controller; + + /// Callback for when a pixel is tapped on the image. + final void Function(PixelTapDetails details)? onTappedPixel; + + /// Creates a new [EditablePixelImage]. + const EditablePixelImage({ + required this.controller, + this.onTappedPixel, + super.key, + }); + + @override + State createState() => _EditablePixelImageState(); +} + +class _EditablePixelImageState extends State { + @override + void initState() { + super.initState(); + widget.controller.addListener(_pixelValueChanged); + } + + @override + void dispose() { + super.dispose(); + widget.controller.removeListener(_pixelValueChanged); + } + + void _pixelValueChanged() { + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return AspectRatio( + aspectRatio: widget.controller.width / widget.controller.height, + child: LayoutBuilder(builder: (context, constraints) { + return GestureDetector( + onTapDown: (details) { + var xLocal = details.localPosition.dx; + var yLocal = details.localPosition.dy; + + var x = widget.controller.width * xLocal ~/ constraints.maxWidth; + var y = widget.controller.height * yLocal ~/ constraints.maxHeight; + + if (widget.onTappedPixel != null) { + widget.onTappedPixel!( + PixelTapDetails._( + x: x, + y: y, + index: y * widget.controller.width + x, + localPosition: details.localPosition, + ), + ); + } + }, + child: PixelImage( + width: widget.controller.value.width, + height: widget.controller.value.height, + palette: widget.controller.value.palette, + pixels: widget.controller.value.pixels, + ), + ); + }), + ); + } +} + +/// Provides details about a tapped pixel on an [EditablePixelImage]. +class PixelTapDetails { + /// The x location of the pixel. + final int x; + + /// The y location of the pixel. + final int y; + + /// The index of the pixel in the [ByteData] of the image. + final int index; + + /// Position in coordinates local to the Widget itself. + final Offset localPosition; + + const PixelTapDetails._({ + required this.x, + required this.y, + required this.index, + required this.localPosition, + }); +} + +class _PixelImageValue { + final ByteData pixels; + final PixelPalette palette; + final int width; + final int height; + + const _PixelImageValue({ + required this.pixels, + required this.palette, + required this.width, + required this.height, + }); +} + +/// Controller for an [EditablePixelImage]. Use it to listen to taps on the +/// image or to set or replace pixels in the image. +class PixelImageController extends ValueNotifier<_PixelImageValue> { + late Uint8List _pixelBytes; + + /// The palette of the [EditablePixelImage] controlled by the controller. + final PixelPalette palette; + + /// Height in pixels of the [EditablePixelImage] controlled by the controller. + final int height; + + /// Width in pixels of the [EditablePixelImage] controlled by the controller. + final int width; + + /// Callback when a pixel is tapped on the [EditablePixelImage] controlled by + /// the controller. + final void Function(PixelTapDetails details)? onTappedPixel; + + /// Creates a new [PixelImageController]. + PixelImageController({ + ByteData? pixels, + required this.palette, + required this.width, + required this.height, + this.onTappedPixel, + }) : super(_PixelImageValue( + pixels: pixels ?? _emptyPixels(), + palette: palette, + width: width, + height: height, + )) { + _pixelBytes = value.pixels.buffer.asUint8List(); + assert(_pixelBytes.length == width * height); + } + + static ByteData _emptyPixels() { + var bytes = Uint8List(64 * 64); + return bytes.buffer.asByteData(); + } + + /// Gets or sets the [ByteData] of the [EditablePixelImage] controlled by the + /// controller. + ByteData get pixels => _pixelBytes.buffer.asByteData(); + + set pixels(ByteData pixels) { + assert(pixels.lengthInBytes == width * height); + _update(); + } + + /// Sets a specific pixel in teh [EditablePixelImage] controlled by the + /// controller. + void setPixel({ + required int pixel, + required int x, + required int y, + }) { + _pixelBytes[y * width + x] = pixel; + _update(); + } + + void _update() { + value = _PixelImageValue( + pixels: pixels, + palette: palette, + width: width, + height: height, + ); + } +} diff --git a/lib/src/pixel_color_picker.dart b/lib/src/pixel_color_picker.dart new file mode 100644 index 0000000..dd0b1da --- /dev/null +++ b/lib/src/pixel_color_picker.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import 'package:pixels/src/pixel_palette.dart'; + +/// A [PixelPalette] color picker. It can be displayed vertically or +/// horizontally depending on the [direction]. +class PixelColorPicker extends StatelessWidget { + /// The palette used by the color picker. + final PixelPalette palette; + + /// Defines if the color picker should be displayed horizontally or + /// vertically. + final Axis direction; + + /// The currently selected index. + final int selectedIndex; + + /// A callback for when the user picks another color index. + final void Function(int index) onChanged; + + /// The width or height (depending on it's direction) of the color picker. + final double crossAxisWidth; + + /// Creates a new [PixelColorPicker]. + const PixelColorPicker({ + required this.selectedIndex, + required this.onChanged, + required this.palette, + this.direction = Axis.horizontal, + this.crossAxisWidth = 32.0, + super.key, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: direction == Axis.vertical ? crossAxisWidth : null, + height: direction == Axis.horizontal ? crossAxisWidth : null, + child: Flex( + direction: direction, + mainAxisSize: MainAxisSize.max, + children: [ + for (var i = 0; i < palette.colors.length; i++) + Expanded( + flex: 1, + child: _PixelColorPickerWell( + index: i, + palette: palette, + selected: selectedIndex == i, + onTap: () { + onChanged(i); + }, + ), + ) + ], + ), + ); + } +} + +class _PixelColorPickerWell extends StatelessWidget { + final int index; + final PixelPalette palette; + final bool selected; + final VoidCallback onTap; + + const _PixelColorPickerWell({ + required this.index, + required this.palette, + required this.selected, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + var color = palette.colors[index]; + + return GestureDetector( + onTap: onTap, + child: Container( + decoration: BoxDecoration( + color: color, + border: selected + ? Border.all( + width: 3, + color: Colors.white, + ) + : null, + boxShadow: selected + ? [ + const BoxShadow( + color: Colors.black54, + offset: Offset(0, 1), + blurRadius: 2, + ) + ] + : null, + ), + ), + ); + } +} diff --git a/lib/src/pixel_editor.dart b/lib/src/pixel_editor.dart new file mode 100644 index 0000000..458301e --- /dev/null +++ b/lib/src/pixel_editor.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:pixels/src/editable_pixel_image.dart'; +import 'package:pixels/src/pixel_color_picker.dart'; + +/// A pixel editor widget where the colors and dimensions are specified in the +/// [controller]. Whenver a pixel is set, [onSetPixel] is called. The pixel +/// editor displays a pixel editing area and a palette where colors can be +/// choosen. +class PixelEditor extends StatefulWidget { + /// The controller specifying the drawing area and palette of the editor. + final PixelImageController controller; + + /// A callback for when a new pixel is set. + final void Function(SetPixelDetails)? onSetPixel; + + /// Creates a new [PixelEditor]. + const PixelEditor({ + required this.controller, + this.onSetPixel, + super.key, + }); + + @override + State createState() => _PixelEditorState(); +} + +class _PixelEditorState extends State { + int _selectedColor = 0; + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (context, constraints) { + var isHorizontal = constraints.maxWidth > constraints.maxHeight; + + return Flex( + direction: isHorizontal ? Axis.horizontal : Axis.vertical, + mainAxisSize: MainAxisSize.min, + children: [ + EditablePixelImage( + controller: widget.controller, + onTappedPixel: (details) { + widget.controller.setPixel( + pixel: _selectedColor, + x: details.x, + y: details.y, + ); + if (widget.onSetPixel != null) { + widget.onSetPixel!( + SetPixelDetails._( + tapDetails: details, + colorIndex: _selectedColor, + ), + ); + } + }, + ), + PixelColorPicker( + direction: isHorizontal ? Axis.vertical : Axis.horizontal, + palette: widget.controller.palette, + selectedIndex: _selectedColor, + onChanged: (index) { + setState(() { + _selectedColor = index; + }); + }, + ), + ], + ); + }); + } +} + +/// Details of a newly set pixel. +class SetPixelDetails { + /// Information about where the pixel is located. + final PixelTapDetails tapDetails; + + /// The newly set color index of the pixel. + final int colorIndex; + + SetPixelDetails._({required this.tapDetails, required this.colorIndex}); +} diff --git a/lib/src/pixel_image.dart b/lib/src/pixel_image.dart new file mode 100644 index 0000000..471654e --- /dev/null +++ b/lib/src/pixel_image.dart @@ -0,0 +1,111 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:pixels/src/pixel_palette.dart'; +import 'dart:ui' as ui; + +/// Displays a pixellated image using the data provided in [pixels], where each +/// byte is mapped to a color in the [palette]. The numer of horizontal and +/// vertical pixels are defined by [width] and [height]. +class PixelImage extends StatefulWidget { + /// Width in pixels. + final int width; + + /// Height in pixels. + final int height; + + /// The palette used by this image. + final PixelPalette palette; + + /// The [ByteData] representing the pixels in the image. Each byte corresponds + /// to one pixel. + final ByteData pixels; + + /// Creates a new [PixelImage]. + const PixelImage({ + required this.width, + required this.height, + required this.palette, + required this.pixels, + super.key, + }); + + @override + State createState() => _PixelImageState(); +} + +class _PixelImageState extends State { + ui.Image? _uiImage; + + @override + void initState() { + super.initState(); + _updateUIImage(); + } + + Future _updateUIImage() async { + assert(widget.pixels.lengthInBytes == widget.width * widget.height); + + var dstImageBytes = Uint8List(widget.width * widget.height * 4); + + var srcPixels = widget.pixels.buffer.asUint8List(); + + // Iterate over all pixels. + for (var i = 0; i < widget.width * widget.height; i++) { + var color = widget.palette.colors[srcPixels[i]]; + var r = color.red; + var g = color.green; + var b = color.blue; + var a = color.alpha; + + dstImageBytes[i * 4 + 0] = r; + dstImageBytes[i * 4 + 1] = g; + dstImageBytes[i * 4 + 2] = b; + dstImageBytes[i * 4 + 3] = a; + } + + var immutableBuffer = await ui.ImmutableBuffer.fromUint8List(dstImageBytes); + var imageDescriptor = ui.ImageDescriptor.raw( + immutableBuffer, + width: widget.width, + height: widget.height, + pixelFormat: ui.PixelFormat.rgba8888, + ); + var codec = await imageDescriptor.instantiateCodec( + targetWidth: widget.width, + targetHeight: widget.height, + ); + + var frameInfo = await codec.getNextFrame(); + codec.dispose(); + immutableBuffer.dispose(); + imageDescriptor.dispose(); + + _uiImage = frameInfo.image; + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return AspectRatio( + aspectRatio: widget.width / widget.height, + child: _uiImage == null + ? null + : RawImage( + image: _uiImage, + fit: BoxFit.fill, + filterQuality: FilterQuality.none, + ), + ); + } + + @override + void didUpdateWidget(covariant PixelImage oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.width != widget.width || oldWidget.height != widget.height) { + // If the image changes dimension, we don't want to risk showing the old + // image until the new one is prepared. + _uiImage = null; + } + _updateUIImage(); + } +} diff --git a/lib/src/pixel_palette.dart b/lib/src/pixel_palette.dart new file mode 100644 index 0000000..a6952f4 --- /dev/null +++ b/lib/src/pixel_palette.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; + +/// An indexed palette where each index of the [colors] list represents a color. +class PixelPalette { + /// List of colors in the palette. + final List colors; + + /// Creates a new [PixelPalette] with the provided colors. + const PixelPalette({required this.colors}); + + /// A [PixelPalette] with the colors used on Commodore 64. + const PixelPalette.c64() + : colors = const [ + Color(0xFF000000), + Color(0xFFFFFFFF), + Color(0xFF880000), + Color(0xFFAAFFEE), + Color(0xFFCC44CC), + Color(0xFF00CC55), + Color(0xFF0000AA), + Color(0xFFEEEE77), + Color(0xFFDD8855), + Color(0xFF664400), + Color(0xFFFF7777), + Color(0xFF333333), + Color(0xFF777777), + Color(0xFFAAFF66), + Color(0xFF0088FF), + Color(0xFFBBBBBB), + ]; + + /// A [PixelPalette] with the colors used by r/place. + const PixelPalette.rPlace() + : colors = const [ + Color(0xFFFFFFFF), + Color(0xFFE4E4E4), + Color(0xFF888888), + Color(0xFF222222), + Color(0xFFFFA7D1), + Color(0xFFE50000), + Color(0xFFE59500), + Color(0xFFA06A42), + Color(0xFFE5D900), + Color(0xFF94E044), + Color(0xFF02BE01), + Color(0xFF00D3DD), + Color(0xFF0083C7), + Color(0xFF0000EA), + Color(0xFFCF6EE4), + Color(0xFF820080), + ]; +} diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..268808f --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,19 @@ +name: pixels +description: Minimalistic pixel editor for Flutter. +version: 1.0.0 +homepage: + +environment: + sdk: '>=2.18.1 <3.0.0' + flutter: ">=3.0.0" + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + +flutter: diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000..53e7dcc Binary files /dev/null and b/screenshot.png differ