// lib/editor/canvas/pen_palette_widgets.dart // // Shared Material 3 chrome for the pen-first editors (PDF, note, slide) so the // floating tool palette looks and behaves identically everywhere — one source // of truth for the inking UI. import 'package:flutter/material.dart'; import '../../l10n/app_localizations.dart'; import '../engine/brush.dart'; import 'editor_tool.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', }; /// Localized display name for a shape kind. String shapeLabel(ShapeKind kind, AppLocalizations l) => switch (kind) { ShapeKind.line => l.shapeLine, ShapeKind.rectangle => l.shapeRectangle, ShapeKind.ellipse => l.shapeEllipse, ShapeKind.arrow => l.shapeArrow, }; /// English fallback shape name (for the note/slide editors which use hardcoded /// English strings — see [brushLabelEn]). /// TODO(brush-l10n-noteslide): localize the note/slide toolbars wholesale. String shapeLabelEn(ShapeKind kind) => switch (kind) { ShapeKind.line => 'Line', ShapeKind.rectangle => 'Rectangle', ShapeKind.ellipse => 'Ellipse', ShapeKind.arrow => 'Arrow', }; /// 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 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 }; /// Material icon for a [ShapeKind] (used in the shape picker + tool button). IconData shapeIcon(ShapeKind kind) => switch (kind) { ShapeKind.line => Icons.show_chart, // straight line ShapeKind.rectangle => Icons.crop_square, ShapeKind.ellipse => Icons.circle_outlined, ShapeKind.arrow => Icons.arrow_outward, }; /// 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. /// [colorFor] returns each brush's REMEMBERED color (rnote-style per-brush color /// memory): the active brush's color is shown as an underline on the button and /// as a dot beside each menu item, so the toolbar makes each brush's color /// visible at a glance. class BrushPickerButton extends StatelessWidget { const BrushPickerButton({ super.key, required this.selected, required this.active, required this.onSelected, required this.labelFor, required this.colorFor, 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 onSelected; /// Localized display name for a brush. final String Function(BrushKind) labelFor; /// The remembered color for a brush (drives the underline + menu dots). final Color Function(BrushKind) colorFor; final String tooltip; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final iconColor = active ? cs.onSecondaryContainer : cs.onSurfaceVariant; return PopupMenuButton( tooltip: tooltip, initialValue: selected, onSelected: onSelected, itemBuilder: (context) => [ for (final b in kPenToolBrushes) PopupMenuItem( value: b, child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(brushIcon(b), size: 20), const SizedBox(width: 10), Text(labelFor(b)), const SizedBox(width: 8), // The brush's remembered color (rnote per-brush color memory). Container( width: 12, height: 12, decoration: BoxDecoration( color: colorFor(b), shape: BoxShape.circle, border: Border.all(color: cs.outlineVariant), ), ), 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: Column( mainAxisSize: MainAxisSize.min, children: [ Row( mainAxisSize: MainAxisSize.min, children: [ Icon(brushIcon(selected), size: 22, color: iconColor), Icon(Icons.arrow_drop_down, size: 18, color: iconColor), ], ), // Per-brush color underline: shows the active brush's remembered // color so switching brushes visibly switches the color. Container( height: 3, width: 24, decoration: BoxDecoration( color: colorFor(selected), borderRadius: BorderRadius.circular(2), ), ), ], ), ), ); } } /// A toggle-style tool button that doubles as a [ShapeKind] picker: a short tap /// activates the shape tool with the current shape; a long-press (or the dropdown /// caret) opens the line / rectangle / ellipse / arrow submenu. class ShapePickerButton extends StatelessWidget { const ShapePickerButton({ super.key, required this.selected, required this.active, required this.onActivate, required this.onSelected, required this.labelFor, required this.tooltip, }); /// The currently selected shape kind. final ShapeKind selected; /// True when the shape tool is the active tool — drives highlight. final bool active; /// Called when the button body is tapped (activate the shape tool). final VoidCallback onActivate; /// Called when a shape kind is picked from the submenu (also activates). final ValueChanged onSelected; /// Localized display name for a shape kind. final String Function(ShapeKind) labelFor; final String tooltip; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final iconColor = active ? cs.onSecondaryContainer : cs.onSurfaceVariant; return PopupMenuButton( tooltip: tooltip, initialValue: selected, onSelected: onSelected, itemBuilder: (context) => [ for (final s in ShapeKind.values) PopupMenuItem( value: s, child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(shapeIcon(s), size: 20), const SizedBox(width: 10), Text(labelFor(s)), if (s == selected) ...[ const SizedBox(width: 8), Icon(Icons.check, size: 18, color: cs.primary), ], ], ), ), ], child: InkWell( borderRadius: BorderRadius.circular(20), onTap: onActivate, 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(shapeIcon(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({ super.key, required this.icon, required this.selected, required this.tooltip, required this.onPressed, }); final IconData icon; final bool selected; final String tooltip; /// Tap handler. When null the button renders disabled (dimmed, no ripple). final VoidCallback? onPressed; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final enabled = onPressed != null; final iconColor = !enabled ? cs.onSurfaceVariant.withValues(alpha: 0.38) : selected ? cs.onSecondaryContainer : cs.onSurfaceVariant; return Tooltip( message: tooltip, child: InkWell( borderRadius: BorderRadius.circular(20), onTap: onPressed, child: AnimatedContainer( duration: const Duration(milliseconds: 150), margin: const EdgeInsets.symmetric(horizontal: 2), padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: selected ? cs.secondaryContainer : Colors.transparent, borderRadius: BorderRadius.circular(20), ), child: Icon(icon, size: 22, color: iconColor), ), ), ); } } /// A thin vertical divider between palette groups. class PaletteDivider extends StatelessWidget { const PaletteDivider({super.key, required this.cs}); final ColorScheme cs; @override Widget build(BuildContext context) => Container( width: 1, height: 24, margin: const EdgeInsets.symmetric(horizontal: 6), color: cs.outlineVariant, ); } /// A round, tonal icon button (used for the floating back button). class RoundIconButton extends StatelessWidget { const RoundIconButton({ super.key, required this.icon, required this.tooltip, required this.onPressed, }); final IconData icon; final String tooltip; final VoidCallback onPressed; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; return Material( color: cs.surfaceContainerHigh, elevation: 3, shape: const CircleBorder(), child: IconButton( tooltip: tooltip, icon: Icon(icon), color: cs.onSurfaceVariant, onPressed: onPressed, ), ); } }