feat(f1): per-tool settings memory (color/width per tool) pure model
Some checks failed
CI / Windows build (push) Has been cancelled
Some checks failed
CI / Windows build (push) Has been cancelled
ToolSettings: the active tool + each tool's OWN remembered ToolConfig (color/width), so pen→highlighter→pen restores the pen's last color/width instead of bleeding the highlighter's. Immutable copy-on-write (withActive/ withColor/withWidth only touch the active tool); sensible defaults (black thin pen, yellow fat highlighter, medium eraser). The toolbar holds + persists one. Pure model; fully unit-tested (per-tool isolation, immutability, equality). flutter analyze lib/editor clean; 204/204 tests (+6). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
88
lib/editor/input/tool_settings.dart
Normal file
88
lib/editor/input/tool_settings.dart
Normal file
@@ -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<EditorToolType, ToolConfig> 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<EditorToolType, ToolConfig> _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));
|
||||
}
|
||||
58
test/tool_settings_test.dart
Normal file
58
test/tool_settings_test.dart
Normal file
@@ -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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user