diff --git a/lib/editor/input/tool_settings.dart b/lib/editor/input/tool_settings.dart new file mode 100644 index 0000000..bf54a68 --- /dev/null +++ b/lib/editor/input/tool_settings.dart @@ -0,0 +1,88 @@ +// lib/editor/input/tool_settings.dart +// +// Per-tool settings memory for the tool palette (F1/F5/F11). Each tool keeps its +// OWN color + width, so switching pen → highlighter → pen restores the pen's +// last color/width instead of bleeding the highlighter's. Immutable value type +// with copy-on-write updates; the toolbar holds one of these and persists it. +// +// Pure (no widgets/storage); fully unit-tested. + +import 'package:flutter/foundation.dart'; + +enum EditorToolType { pen, highlighter, eraser } + +/// Color (ARGB int) + width (fraction of page width) for one tool. The eraser +/// ignores color but keeps a width (its radius). +@immutable +class ToolConfig { + const ToolConfig({required this.color, required this.width}); + + final int color; + final double width; + + ToolConfig copyWith({int? color, double? width}) => + ToolConfig(color: color ?? this.color, width: width ?? this.width); + + @override + bool operator ==(Object other) => + other is ToolConfig && other.color == color && other.width == width; + + @override + int get hashCode => Object.hash(color, width); +} + +/// The active tool + each tool's remembered [ToolConfig]. +@immutable +class ToolSettings { + const ToolSettings({required this.active, required Map configs}) + : _configs = configs; + + /// Sensible starting state: black thin pen, yellow fat highlighter, medium + /// eraser; pen active. + factory ToolSettings.defaults() => const ToolSettings( + active: EditorToolType.pen, + configs: { + EditorToolType.pen: ToolConfig(color: 0xFF000000, width: 0.003), + EditorToolType.highlighter: + ToolConfig(color: 0x80FFEB3B, width: 0.02), + EditorToolType.eraser: ToolConfig(color: 0x00000000, width: 0.02), + }, + ); + + final EditorToolType active; + final Map _configs; + + ToolConfig configFor(EditorToolType tool) => + _configs[tool] ?? const ToolConfig(color: 0xFF000000, width: 0.003); + + ToolConfig get activeConfig => configFor(active); + + /// Switch the active tool (each tool's own color/width is remembered). + ToolSettings withActive(EditorToolType tool) => + ToolSettings(active: tool, configs: _configs); + + ToolSettings _withConfig(EditorToolType tool, ToolConfig config) { + return ToolSettings( + active: active, + configs: {..._configs, tool: config}, + ); + } + + /// Set the ACTIVE tool's color (no-op semantics for the eraser are the + /// caller's choice; the value is still stored). + ToolSettings withColor(int color) => + _withConfig(active, activeConfig.copyWith(color: color)); + + /// Set the ACTIVE tool's width. + ToolSettings withWidth(double width) => + _withConfig(active, activeConfig.copyWith(width: width)); + + @override + bool operator ==(Object other) => + other is ToolSettings && + other.active == active && + mapEquals(other._configs, _configs); + + @override + int get hashCode => Object.hash(active, Object.hashAll(_configs.entries)); +} diff --git a/test/tool_settings_test.dart b/test/tool_settings_test.dart new file mode 100644 index 0000000..6c82e9a --- /dev/null +++ b/test/tool_settings_test.dart @@ -0,0 +1,58 @@ +// Tests for per-tool settings memory (F1/F5/F11). + +import 'package:flutter_test/flutter_test.dart'; + +import 'package:badnote/editor/input/tool_settings.dart'; + +void main() { + test('defaults: pen active, per-tool colors/widths', () { + final s = ToolSettings.defaults(); + expect(s.active, EditorToolType.pen); + expect(s.configFor(EditorToolType.pen).color, 0xFF000000); + expect(s.configFor(EditorToolType.highlighter).width, 0.02); + expect(s.activeConfig, s.configFor(EditorToolType.pen)); + }); + + test('switching tools remembers each tool\'s own config', () { + var s = ToolSettings.defaults(); + // Customize pen, switch to highlighter, customize it, switch back. + s = s.withColor(0xFFFF0000).withWidth(0.005); // pen → red, 0.005 + s = s.withActive(EditorToolType.highlighter); + expect(s.activeConfig.color, 0x80FFEB3B); // highlighter unchanged + s = s.withColor(0x8000FF00); // highlighter → green + s = s.withActive(EditorToolType.pen); + // Pen restored to its red/0.005, NOT the highlighter's green. + expect(s.activeConfig.color, 0xFFFF0000); + expect(s.activeConfig.width, 0.005); + // Highlighter kept its green. + expect(s.configFor(EditorToolType.highlighter).color, 0x8000FF00); + }); + + test('withColor/withWidth only touch the active tool', () { + var s = ToolSettings.defaults().withActive(EditorToolType.highlighter); + final penBefore = s.configFor(EditorToolType.pen); + s = s.withColor(0x80123456); + expect(s.configFor(EditorToolType.pen), penBefore); // pen untouched + expect(s.activeConfig.color, 0x80123456); + }); + + test('immutability: updates return new instances, original unchanged', () { + final a = ToolSettings.defaults(); + final b = a.withActive(EditorToolType.eraser); + expect(a.active, EditorToolType.pen); + expect(b.active, EditorToolType.eraser); + expect(a == b, isFalse); + }); + + test('value equality', () { + expect(ToolSettings.defaults(), ToolSettings.defaults()); + expect(ToolSettings.defaults().withActive(EditorToolType.eraser) == + ToolSettings.defaults(), isFalse); + }); + + test('ToolConfig copyWith + equality', () { + const c = ToolConfig(color: 0xFF000000, width: 0.003); + expect(c.copyWith(width: 0.01), const ToolConfig(color: 0xFF000000, width: 0.01)); + expect(c.copyWith(), c); + }); +}