feat(pen): extensible brush model (4 brushes)
All checks were successful
CI / Windows build (push) Successful in 14m54s
All checks were successful
CI / Windows build (push) Successful in 14m54s
Replace the 2-tool ink system with a data-driven, Krita-style BrushProfile (lib/editor/engine/brush.dart). Adding a brush is a const map entry, not render-path branching. Four presets from the rnote/krita spec: - fountain pen: quadratic (p^2) pressure, wide dynamic width - ballpoint: near-constant width (thinning 0.15) - highlighter: flat width, square caps - pencil: sqrt(p) pressure, moderate width Pressure is pre-warped per brush via PressureCurve(gamma) before perfect_freehand; geometry fields (thinning/streamline/smoothing/ caps) flow through the shared stroke recipe so the PDF overlay and the note/slide PenCanvas both honor the brush. Brush kind is now persisted on the stroke model. Picker added to all three toolbars. Opacity/multiply and pencil grain are carried as data but not yet composited (TODO brush-opacity / brush-texture); this increment is width + pressure-curve differentiation. analyze clean, 283 tests.
This commit is contained in:
@@ -6,6 +6,119 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import '../engine/brush.dart';
|
||||
|
||||
/// Localized display name for a brush (single source so all three editors agree).
|
||||
String brushLabel(BrushKind kind, AppLocalizations l) => switch (kind) {
|
||||
BrushKind.fountainPen => l.brushFountainPen,
|
||||
BrushKind.ballpoint => l.brushBallpoint,
|
||||
BrushKind.pencil => l.brushPencil,
|
||||
BrushKind.highlighter => l.brushHighlighter,
|
||||
};
|
||||
|
||||
/// English fallback brush name, for the note/slide editors which (like their
|
||||
/// other chrome) use hardcoded English strings rather than [AppLocalizations]
|
||||
/// (their test harness mounts a MaterialApp without localization delegates).
|
||||
/// TODO(brush-l10n-noteslide): localize the note/slide toolbars wholesale.
|
||||
String brushLabelEn(BrushKind kind) => switch (kind) {
|
||||
BrushKind.fountainPen => 'Fountain pen',
|
||||
BrushKind.ballpoint => 'Ballpoint',
|
||||
BrushKind.pencil => 'Pencil',
|
||||
BrushKind.highlighter => 'Highlighter',
|
||||
};
|
||||
|
||||
/// The brushes selectable as the PEN tool. The highlighter is its own tool, so
|
||||
/// it is NOT offered here (eraser is also a separate tool).
|
||||
const List<BrushKind> kPenToolBrushes = [
|
||||
BrushKind.fountainPen,
|
||||
BrushKind.ballpoint,
|
||||
BrushKind.pencil,
|
||||
];
|
||||
|
||||
/// Material icon for a brush (used in the brush picker + the pen tool button).
|
||||
IconData brushIcon(BrushKind kind) => switch (kind) {
|
||||
BrushKind.fountainPen => Icons.edit_outlined, // nib pen
|
||||
BrushKind.ballpoint => Icons.create_outlined, // ballpoint
|
||||
BrushKind.pencil => Icons.draw_outlined, // pencil
|
||||
BrushKind.highlighter => Icons.brush_outlined, // marker
|
||||
};
|
||||
|
||||
/// A dropdown that selects the active PEN brush (fountain / ballpoint / pencil).
|
||||
///
|
||||
/// Highlighter and eraser remain separate tools. Tapping the button opens a
|
||||
/// menu of [kPenToolBrushes]; the chosen brush is reported via [onSelected].
|
||||
/// [labelFor] localizes each brush name so the menu honors the app locale.
|
||||
class BrushPickerButton extends StatelessWidget {
|
||||
const BrushPickerButton({
|
||||
super.key,
|
||||
required this.selected,
|
||||
required this.active,
|
||||
required this.onSelected,
|
||||
required this.labelFor,
|
||||
required this.tooltip,
|
||||
});
|
||||
|
||||
/// The currently selected pen brush.
|
||||
final BrushKind selected;
|
||||
|
||||
/// True when the pen tool (this brush) is the active tool — drives highlight.
|
||||
final bool active;
|
||||
|
||||
final ValueChanged<BrushKind> onSelected;
|
||||
|
||||
/// Localized display name for a brush.
|
||||
final String Function(BrushKind) labelFor;
|
||||
|
||||
final String tooltip;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final iconColor =
|
||||
active ? cs.onSecondaryContainer : cs.onSurfaceVariant;
|
||||
return PopupMenuButton<BrushKind>(
|
||||
tooltip: tooltip,
|
||||
initialValue: selected,
|
||||
onSelected: onSelected,
|
||||
itemBuilder: (context) => [
|
||||
for (final b in kPenToolBrushes)
|
||||
PopupMenuItem<BrushKind>(
|
||||
value: b,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(brushIcon(b), size: 20),
|
||||
const SizedBox(width: 10),
|
||||
Text(labelFor(b)),
|
||||
if (b == selected) ...[
|
||||
const SizedBox(width: 8),
|
||||
Icon(Icons.check, size: 18, color: cs.primary),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 2),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: active ? cs.secondaryContainer : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(brushIcon(selected), size: 22, color: iconColor),
|
||||
Icon(Icons.arrow_drop_down, size: 18, color: iconColor),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A Material 3 toggle-style icon button for the floating tool palette.
|
||||
class ToolButton extends StatelessWidget {
|
||||
const ToolButton({
|
||||
|
||||
Reference in New Issue
Block a user