hand_drawn_toolkit 0.2.0 copy "hand_drawn_toolkit: ^0.2.0" to clipboard
hand_drawn_toolkit: ^0.2.0 copied to clipboard

A lightweight Flutter package for rendering hand-drawn, sketchy lines, borders, and containers.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:hand_drawn_toolkit/hand_drawn_toolkit.dart';

// ── Palette ───────────────────────────────────────────────────────────────
const _ink = Color(0xFF2C2C2C);
const _inkLight = Color(0xFF6B6B6B);
const _accent = Color(0xFF4A7C6F);
const _cardFill = Color(0xFFFAF7F2);

// ── Notebook grid ─────────────────────────────────────────────────────────
const _notebookFontSize = 15.0;
const _notebookLineHeight = 28.0;

void main() => runApp(const MyJournalApp());

const _months = [
  'January',
  'February',
  'March',
  'April',
  'May',
  'June',
  'July',
  'August',
  'September',
  'October',
  'November',
  'December',
];

String _formatDate(DateTime date) =>
    '${_months[date.month - 1]} ${date.day}, ${date.year}';

class MyJournalApp extends StatelessWidget {
  const MyJournalApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Hand Drawn Toolkit Example',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        scaffoldBackgroundColor: const Color(0xFFF5F0E8),
        textTheme: const TextTheme(
          bodyMedium: TextStyle(fontFamily: 'Georgia'),
        ),
      ),
      home: const JournalPage(),
    );
  }
}

class JournalPage extends StatefulWidget {
  const JournalPage({super.key});

  @override
  State<JournalPage> createState() => _JournalPageState();
}

class _JournalPageState extends State<JournalPage> {
  // ── Status square demo state ───────────────────────────────────────────
  final List<_TaskItem> _tasks = [
    const _TaskItem('Read a chapter of a good book'),
    const _TaskItem('Sketch something from observation'),
    const _TaskItem('Take a 20-minute walk', status: _TaskStatus.completed),
    const _TaskItem('Write morning pages', status: _TaskStatus.skipped),
  ];

  void _cycleStatus(int index) {
    setState(() {
      _tasks[index] = _tasks[index].cycled();
    });
  }

  // ── Interactive chart state ────────────────────────────────────────────
  String? _barHitLabel;
  String? _lineHitLabel;
  String? _scatterHitLabel;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: SingleChildScrollView(
          padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              // ── Page title with hand-drawn underline ────────────────
              const Text(
                'Hand Drawn Toolkit',
                style: TextStyle(
                  fontSize: 32,
                  fontWeight: FontWeight.bold,
                  color: _ink,
                  letterSpacing: 0.5,
                ),
              ),
              const SizedBox(height: 2),
              SizedBox(
                width: 170,
                height: 8,
                child: CustomPaint(
                  painter: HandDrawnLinePainter(
                    color: _ink,
                    strokeWidth: 2.0,
                    irregularity: 2.0,
                    seed: 42,
                    buildPath: (size, h) => h.lineHorizontal(size),
                  ),
                ),
              ),

              const SizedBox(height: 12),

              // ── Date ───────────────────────────────────────────────
              Text(
                _formatDate(DateTime.now()),
                style: const TextStyle(
                  fontSize: 14,
                  color: _inkLight,
                  fontStyle: FontStyle.italic,
                ),
              ),

              const SizedBox(height: 8),
              const HandDrawnDivider(color: _inkLight, seed: 42),
              const SizedBox(height: 24),

              // ── Journal entry card ─────────────────────────────────
              const HandDrawnContainer(
                backgroundColor: _cardFill,
                strokeColor: _ink,
                strokeWidth: 1.8,
                irregularity: 3.0,
                seed: 6,
                padding: EdgeInsets.all(20),
                child: Text(
                  'Hand Drawn Toolkit is a lightweight Flutter package for '
                  'rendering sketchy, organic lines, borders, and containers. '
                  'It generates random perpendicular offsets along a path and '
                  'smooths them with a three-point moving average to produce '
                  'natural-looking wobble.\n\n'
                  'The package has zero external dependencies and relies '
                  'entirely on the Flutter SDK. All randomness is seed-based, '
                  'so identical parameters always produce identical strokes. '
                  'Paths are cached internally and only recomputed when the '
                  'widget size or generation parameters change.',
                  style: TextStyle(fontSize: 15, height: 1.6, color: _ink),
                ),
              ),

              const SizedBox(height: 28),

              // ── "Key Components" heading ──────────────────────────
              const Text(
                'Key Components',
                style: TextStyle(
                  fontSize: 20,
                  fontWeight: FontWeight.w600,
                  color: _ink,
                ),
              ),
              const SizedBox(height: 14),

              // ── Component items with unique seeds ──────────────────
              const _GoalItem(
                seed: 9,
                text:
                    'HandDrawnContainer wraps any child widget with a '
                    'sketchy rectangular border and solid background fill.',
              ),
              const SizedBox(height: 10),
              const _GoalItem(
                seed: 20,
                text:
                    'HandDrawnDivider is a drop-in replacement for '
                    "Flutter's Divider, supporting both horizontal and "
                    'vertical orientations.',
              ),
              const SizedBox(height: 10),
              const _GoalItem(
                seed: 49,
                text:
                    'HandDrawnLinePainter provides full control via a '
                    'buildPath callback for custom jittered shapes.',
              ),

              const SizedBox(height: 28),

              // ── Indented divider ───────────────────────────────────
              const HandDrawnDivider(
                color: _inkLight,
                indent: 32,
                endIndent: 32,
                seed: 40,
              ),
              const SizedBox(height: 28),

              // ── Status square demo ─────────────────────────────────
              const Text(
                'Status Square',
                style: TextStyle(
                  fontSize: 20,
                  fontWeight: FontWeight.w600,
                  color: _ink,
                ),
              ),
              const SizedBox(height: 8),
              const Text(
                'HandDrawnStatusSquare is a tappable indicator with a '
                'hand-drawn border. Tap each square below to cycle '
                'through empty, checked, and dashed states.',
                style: TextStyle(fontSize: 14, height: 1.55, color: _inkLight),
              ),
              const SizedBox(height: 16),

              HandDrawnContainer(
                backgroundColor: _cardFill,
                strokeColor: _ink,
                strokeWidth: 1.4,
                irregularity: 2.5,
                seed: 77,
                padding: const EdgeInsets.symmetric(horizontal: 16),
                child: HandDrawnNotebook(
                  lineHeight: _notebookLineHeight,
                  lineColor: const Color(0xFFB0AAA0),
                  irregularity: 2.0,
                  uniformLines: false,
                  seed: 50,
                  child: Column(
                    children: [
                      for (var i = 0; i < _tasks.length; i++)
                        _TaskRow(
                          task: _tasks[i],
                          seed: i * 13 + 5,
                          onTap: () => _cycleStatus(i),
                        ),
                    ],
                  ),
                ),
              ),

              const SizedBox(height: 28),

              const HandDrawnDivider(
                color: _inkLight,
                indent: 32,
                endIndent: 32,
                seed: 55,
              ),
              const SizedBox(height: 28),

              // ── Text field demo ────────────────────────────────────
              const Text(
                'Text Field',
                style: TextStyle(
                  fontSize: 20,
                  fontWeight: FontWeight.w600,
                  color: _ink,
                ),
              ),
              const SizedBox(height: 8),
              const Text(
                'HandDrawnTextField pairs a standard TextField with a '
                'hand-drawn divider underline.',
                style: TextStyle(fontSize: 14, height: 1.55, color: _inkLight),
              ),
              const SizedBox(height: 16),

              const HandDrawnTextField(
                hintText: 'Title your entry…',
                backgroundColor: _cardFill,
                textColor: _ink,
                hintColor: _inkLight,
                dividerColor: Color(0xFFD8D3CB),
                fontSize: 16,
                seed: 33,
              ),
              const SizedBox(height: 12),
              const HandDrawnTextField(
                hintText: 'Write your thoughts…',
                maxLines: 4,
                backgroundColor: _cardFill,
                textColor: _ink,
                hintColor: _inkLight,
                dividerColor: Color(0xFFD8D3CB),
                fontSize: 14,
                seed: 34,
              ),

              const SizedBox(height: 28),
              const HandDrawnDivider(
                color: _inkLight,
                indent: 32,
                endIndent: 32,
                seed: 65,
              ),
              const SizedBox(height: 28),

              // ── Notebook demo ──────────────────────────────────────
              const Text(
                'Notebook',
                style: TextStyle(
                  fontSize: 20,
                  fontWeight: FontWeight.w600,
                  color: _ink,
                ),
              ),
              const SizedBox(height: 8),
              const Text(
                'HandDrawnNotebook draws ruled lines behind content. '
                'NotebookRow snaps children to the line grid.',
                style: TextStyle(fontSize: 14, height: 1.55, color: _inkLight),
              ),
              const SizedBox(height: 16),

              const Text(
                'Uniform lines',
                style: TextStyle(
                  fontSize: 14,
                  fontWeight: FontWeight.w600,
                  color: _inkLight,
                ),
              ),
              const SizedBox(height: 8),
              HandDrawnContainer(
                backgroundColor: _cardFill,
                strokeColor: _ink,
                strokeWidth: 1.4,
                irregularity: 2.5,
                seed: 88,
                padding: const EdgeInsets.symmetric(horizontal: 16),
                child: HandDrawnNotebook(
                  lineHeight: _notebookLineHeight,
                  lineColor: const Color(0xFFB0AAA0),
                  irregularity: 2.5,
                  seed: 10,
                  child: Column(
                    children: [
                      for (final text in [
                        'First line on the grid',
                        'Second line sits neatly',
                        'Third line, same wobble',
                      ])
                        NotebookRow(
                          lineHeight: _notebookLineHeight,
                          child: Text(
                            text,
                            style: const TextStyle(
                              fontSize: _notebookFontSize,
                              height: _notebookLineHeight / _notebookFontSize,
                              color: _ink,
                            ),
                          ),
                        ),
                    ],
                  ),
                ),
              ),

              const SizedBox(height: 28),
              const HandDrawnDivider(
                color: _inkLight,
                indent: 32,
                endIndent: 32,
                seed: 70,
              ),
              const SizedBox(height: 28),

              // ════════════════════════════════════════════════════════
              // CHARTS
              // ════════════════════════════════════════════════════════
              const Text(
                'Charts',
                style: TextStyle(
                  fontSize: 24,
                  fontWeight: FontWeight.bold,
                  color: _ink,
                ),
              ),
              const SizedBox(height: 8),
              const Text(
                'Bar, line, and scatter charts with wobbly axes, grid '
                'lines, auto-generated legends, and smart label thinning.',
                style: TextStyle(fontSize: 14, height: 1.55, color: _inkLight),
              ),
              const SizedBox(height: 20),

              // ── Bar chart ──────────────────────────────────────────
              _sectionHeading('Stacked Bar Chart'),
              const SizedBox(height: 12),
              HandDrawnBarChart(data: _sampleBarData(), height: 260, seed: 10),

              const SizedBox(height: 28),

              // ── Line chart ─────────────────────────────────────────
              _sectionHeading('Multi-Series Line Chart'),
              const SizedBox(height: 12),
              HandDrawnLineChart(
                data: _sampleLineData(),
                height: 260,
                seed: 20,
              ),

              const SizedBox(height: 28),

              // ── Scatter plot ───────────────────────────────────────
              _sectionHeading('Scatter Plot'),
              const SizedBox(height: 12),
              HandDrawnScatterPlot(
                data: _sampleScatterData(),
                height: 260,
                seed: 30,
              ),

              const SizedBox(height: 28),
              const HandDrawnDivider(
                color: _inkLight,
                indent: 32,
                endIndent: 32,
                seed: 80,
              ),
              const SizedBox(height: 28),

              // ════════════════════════════════════════════════════════
              // TABLE
              // ════════════════════════════════════════════════════════
              const Text(
                'Table',
                style: TextStyle(
                  fontSize: 24,
                  fontWeight: FontWeight.bold,
                  color: _ink,
                ),
              ),
              const SizedBox(height: 8),
              const Text(
                'HandDrawnTable renders column-aligned data inside a '
                'hand-drawn container with divider separators.',
                style: TextStyle(fontSize: 14, height: 1.55, color: _inkLight),
              ),
              const SizedBox(height: 16),

              const HandDrawnTable(
                title: 'Reading Log',
                columns: [
                  HandDrawnTableColumn(header: 'TITLE', flex: 3),
                  HandDrawnTableColumn(
                    header: 'PAGES',
                    width: 100,
                    alignment: Alignment.centerRight,
                  ),
                  HandDrawnTableColumn(
                    header: 'RATING',
                    width: 100,
                    alignment: Alignment.center,
                  ),
                ],
                rows: [
                  HandDrawnTableRow(
                    cells: ['Dune', '412', '★★★★★'],
                    highlight: true,
                  ),
                  HandDrawnTableRow(cells: ['Neuromancer', '271', '★★★★']),
                  HandDrawnTableRow(cells: ['Foundation', '244', '★★★★']),
                  HandDrawnTableRow(cells: ['Snow Crash', '480', '★★★']),
                ],
                rowDividers: TableDividerStyle(
                  seed: 60,
                  irregularity: 2,
                  uniform: false,
                ),
                columnDividers: TableDividerStyle(seed: 70, irregularity: 2),
              ),

              const SizedBox(height: 20),

              _sectionHeading('Resizable Columns'),
              const SizedBox(height: 4),
              const Text(
                'Drag the column edges to resize. Built entirely with '
                'the public API — explicit widths, column dividers, '
                'and consumer-owned gesture handling.',
                style: TextStyle(fontSize: 13, height: 1.55, color: _inkLight),
              ),
              const SizedBox(height: 12),
              const _ResizableTableDemo(),

              const SizedBox(height: 28),
              const HandDrawnDivider(
                color: _inkLight,
                indent: 32,
                endIndent: 32,
                seed: 85,
              ),
              const SizedBox(height: 28),

              // ════════════════════════════════════════════════════════
              // INTERACTIVE CHARTS
              // ════════════════════════════════════════════════════════
              const Text(
                'Interactive Charts',
                style: TextStyle(
                  fontSize: 24,
                  fontWeight: FontWeight.bold,
                  color: _ink,
                ),
              ),
              const SizedBox(height: 8),
              const Text(
                'The package provides computeLayout() and hitTest() so '
                'consumers can build their own tap, hover, and drag '
                'behaviors. Tap the charts below to see hit-test results.',
                style: TextStyle(fontSize: 14, height: 1.55, color: _inkLight),
              ),
              const SizedBox(height: 20),

              // ── Interactive bar chart ──────────────────────────────
              _sectionHeading('Tap a Bar Segment'),
              const SizedBox(height: 4),
              _hitLabel(_barHitLabel),
              const SizedBox(height: 8),
              _InteractiveBarChart(
                data: _sampleBarData(),
                onHit: (label) => setState(() => _barHitLabel = label),
              ),

              const SizedBox(height: 28),

              // ── Interactive line chart ─────────────────────────────
              _sectionHeading('Tap the Line Chart'),
              const SizedBox(height: 4),
              _hitLabel(_lineHitLabel),
              const SizedBox(height: 8),
              _InteractiveLineChart(
                data: _sampleLineData(),
                onHit: (label) => setState(() => _lineHitLabel = label),
              ),

              const SizedBox(height: 28),

              // ── Interactive scatter plot ───────────────────────────
              _sectionHeading('Tap a Scatter Point'),
              const SizedBox(height: 4),
              _hitLabel(_scatterHitLabel),
              const SizedBox(height: 8),
              _InteractiveScatterPlot(
                data: _sampleScatterData(),
                onHit: (label) => setState(() => _scatterHitLabel = label),
              ),

              const SizedBox(height: 28),

              // ── "Tip" callout ──────────────────────────────────────
              const HandDrawnContainer(
                backgroundColor: Color(0xFFF0F6F4),
                strokeColor: _accent,
                strokeWidth: 2.0,
                irregularity: 4.5,
                seed: 5,
                padding: EdgeInsets.all(18),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      'Tip',
                      style: TextStyle(
                        fontSize: 16,
                        fontWeight: FontWeight.w600,
                        color: _accent,
                      ),
                    ),
                    SizedBox(height: 8),
                    Text(
                      'Use a unique seed for each adjacent element to avoid '
                      'identical wobble patterns lining up. The irregularity '
                      'parameter controls roughness — values around 2.0 to '
                      '4.0 work well for borders, while 0.5 to 1.5 suit '
                      'dividers and chart grid lines.',
                      style: TextStyle(fontSize: 14, height: 1.55, color: _ink),
                    ),
                  ],
                ),
              ),

              const SizedBox(height: 28),

              // ── Custom path section ────────────────────────────────
              const Text(
                'Custom Paths',
                style: TextStyle(
                  fontSize: 20,
                  fontWeight: FontWeight.w600,
                  color: _ink,
                ),
              ),
              const SizedBox(height: 8),
              const Text(
                'HandDrawnLinePainter also accepts a buildPath callback '
                'for arbitrary shapes.',
                style: TextStyle(fontSize: 14, height: 1.55, color: _inkLight),
              ),
              const SizedBox(height: 16),
              SizedBox(
                width: double.infinity,
                height: 40,
                child: CustomPaint(
                  painter: HandDrawnLinePainter(
                    color: _ink,
                    strokeWidth: 3.0,
                    irregularity: 2.0,
                    seed: 100,
                    segments: 100,
                    buildPath: (size, h) {
                      final offsets = h.smoothedOffsets();
                      final dx = size.width / h.segments;
                      final path = Path()..moveTo(0, size.height);
                      for (int i = 1; i <= h.segments; i++) {
                        final t = i / h.segments;
                        final y = size.height * (1 - t) + offsets[i];
                        path.lineTo(dx * i, y);
                      }
                      return path;
                    },
                  ),
                ),
              ),

              const SizedBox(height: 16),
            ],
          ),
        ),
      ),
    );
  }
}

// ══════════════════════════════════════════════════════════════════════════
// SAMPLE DATA
// ══════════════════════════════════════════════════════════════════════════

BarChartData _sampleBarData() {
  return const BarChartData(
    title: 'Weekly Activity',
    yAxisLabel: 'Minutes',
    bars: [
      BarGroup(
        label: 'Mon',
        segments: [
          BarSegment(
            category: 'Exercise',
            value: 30,
            color: Color(0xFF6BAF7A),
            fillColor: Color.fromARGB(255, 169, 168, 168),
            fillAlpha: 0.6,
          ),
          BarSegment(category: 'Reading', value: 25, color: Color(0xFF6B9BD2)),
          BarSegment(
            category: 'Creative',
            value: 15,
            color: Color(0xFFE8943A),
            fillAlpha: 0.0,
          ),
        ],
      ),
      BarGroup(
        label: 'Tue',
        segments: [
          BarSegment(
            category: 'Exercise',
            value: 50,
            color: Color(0xFF6BAF7A),
            fillColor: Color.fromARGB(255, 169, 168, 168),
            fillAlpha: 0.6,
          ),
          BarSegment(category: 'Reading', value: 20, color: Color(0xFF6B9BD2)),
          BarSegment(
            category: 'Creative',
            value: 25,
            color: Color(0xFFE8943A),
            fillAlpha: 0.0,
          ),
        ],
      ),
      BarGroup(
        label: 'Wed',
        segments: [
          BarSegment(
            category: 'Exercise',
            value: 25,
            color: Color(0xFF6BAF7A),
            fillColor: Color.fromARGB(255, 169, 168, 168),
            fillAlpha: 0.6,
          ),
          BarSegment(category: 'Reading', value: 40, color: Color(0xFF6B9BD2)),
          BarSegment(
            category: 'Creative',
            value: 20,
            color: Color(0xFFE8943A),
            fillAlpha: 0.0,
          ),
        ],
      ),
      BarGroup(
        label: 'Thu',
        segments: [
          BarSegment(
            category: 'Exercise',
            value: 40,
            color: Color(0xFF6BAF7A),
            fillColor: Color.fromARGB(255, 169, 168, 168),
            fillAlpha: 0.6,
          ),
          BarSegment(category: 'Reading', value: 15, color: Color(0xFF6B9BD2)),
          BarSegment(
            category: 'Creative',
            value: 30,
            color: Color(0xFFE8943A),
            fillAlpha: 0.0,
          ),
        ],
      ),
      BarGroup(
        label: 'Fri',
        segments: [
          BarSegment(
            category: 'Exercise',
            value: 45,
            color: Color(0xFF6BAF7A),
            fillColor: Color.fromARGB(255, 169, 168, 168),
            fillAlpha: 0.6,
          ),
          BarSegment(category: 'Reading', value: 30, color: Color(0xFF6B9BD2)),
          BarSegment(
            category: 'Creative',
            value: 10,
            color: Color(0xFFE8943A),
            fillAlpha: 0.0,
          ),
        ],
      ),
    ],
    legend: [
      LegendEntry(label: 'Exercise', color: Color(0xFF6BAF7A)),
      LegendEntry(label: 'Reading', color: Color(0xFF6B9BD2)),
      LegendEntry(label: 'Creative', color: Color(0xFFE8943A)),
    ],
  );
}

LineChartData _sampleLineData() {
  return const LineChartData(
    title: 'Mood Tracker',
    xAxisLabel: 'Day',
    yAxisLabel: 'Score',
    minX: 0,
    maxX: 6,
    minY: 0,
    maxY: 10,
    xLabels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
    series: [
      LineSeriesData(
        name: 'Energy',
        color: Color(0xFFE8943A),
        points: [
          LinePoint(x: 0, y: 6),
          LinePoint(x: 1, y: 7),
          LinePoint(x: 2, y: 5),
          LinePoint(x: 3, y: 8),
          LinePoint(x: 4, y: 7),
          LinePoint(x: 5, y: 9),
          LinePoint(x: 6, y: 8),
        ],
      ),
      LineSeriesData(
        name: 'Focus',
        color: Color(0xFF7B68C4),
        points: [
          LinePoint(x: 0, y: 5),
          LinePoint(x: 1, y: 6),
          LinePoint(x: 2, y: 4),
          LinePoint(x: 3, y: 7),
          LinePoint(x: 4, y: 8),
          LinePoint(x: 5, y: 6),
          LinePoint(x: 6, y: 7),
        ],
      ),
    ],
  );
}

ScatterPlotData _sampleScatterData() {
  return const ScatterPlotData(
    title: 'Sleep vs Productivity',
    xAxisLabel: 'Hours of Sleep',
    yAxisLabel: 'Productivity Score',
    minX: 4,
    maxX: 10,
    minY: 0,
    maxY: 100,
    points: [
      ScatterPoint(x: 5.0, y: 35),
      ScatterPoint(x: 5.5, y: 42),
      ScatterPoint(x: 6.0, y: 55),
      ScatterPoint(x: 6.5, y: 50),
      ScatterPoint(x: 7.0, y: 68),
      ScatterPoint(x: 7.0, y: 72),
      ScatterPoint(x: 7.5, y: 78, size: 7),
      ScatterPoint(x: 8.0, y: 82, size: 7),
      ScatterPoint(x: 8.0, y: 75),
      ScatterPoint(x: 8.5, y: 88, size: 8),
      ScatterPoint(x: 9.0, y: 85),
      ScatterPoint(x: 9.5, y: 80),
    ],
  );
}

// ══════════════════════════════════════════════════════════════════════════
// INTERACTIVE CHART WIDGETS
// ══════════════════════════════════════════════════════════════════════════

/// Interactive bar chart using LayoutBuilder + GestureDetector + computeLayout.
class _InteractiveBarChart extends StatelessWidget {
  const _InteractiveBarChart({required this.data, required this.onHit});

  final BarChartData data;
  final ValueChanged<String?> onHit;

  @override
  Widget build(BuildContext context) {
    final painter = HandDrawnBarChartPainter(data: data, seed: 10);

    return LayoutBuilder(
      builder: (context, constraints) {
        final size = Size(constraints.maxWidth, 240);
        final layout = painter.computeLayout(size);

        return GestureDetector(
          onTapDown: (details) {
            final hit = layout.hitTest(details.localPosition);
            if (hit != null) {
              final seg = hit.segment;
              onHit(
                '${seg.barLabel} — ${seg.category}: '
                '${seg.value.toInt()} min',
              );
            } else {
              onHit(null);
            }
          },
          child: CustomPaint(size: size, painter: painter),
        );
      },
    );
  }
}

/// Interactive line chart demonstrating sealed hit-test results.
class _InteractiveLineChart extends StatelessWidget {
  const _InteractiveLineChart({required this.data, required this.onHit});

  final LineChartData data;
  final ValueChanged<String?> onHit;

  @override
  Widget build(BuildContext context) {
    final painter = HandDrawnLineChartPainter(data: data, seed: 20);

    return LayoutBuilder(
      builder: (context, constraints) {
        final size = Size(constraints.maxWidth, 240);
        final layout = painter.computeLayout(size);

        return GestureDetector(
          onTapDown: (details) {
            final hit = layout.hitTest(details.localPosition);
            if (hit != null) {
              final label = switch (hit) {
                LinePointHit(
                  :final seriesName,
                  :final pointIndex,
                  :final point,
                ) =>
                  '$seriesName point $pointIndex: ${point.y.toInt()}',
                LineSegmentHit(
                  :final seriesName,
                  :final interpolatedX,
                  :final interpolatedY,
                ) =>
                  '$seriesName segment at '
                      'x=${interpolatedX.toStringAsFixed(1)}, '
                      'y=${interpolatedY.toStringAsFixed(1)}',
              };
              onHit(label);
            } else {
              onHit(null);
            }
          },
          child: CustomPaint(size: size, painter: painter),
        );
      },
    );
  }
}

/// Interactive scatter plot with nearest-point hit-testing.
class _InteractiveScatterPlot extends StatelessWidget {
  const _InteractiveScatterPlot({required this.data, required this.onHit});

  final ScatterPlotData data;
  final ValueChanged<String?> onHit;

  @override
  Widget build(BuildContext context) {
    final painter = HandDrawnScatterPlotPainter(data: data, seed: 30);

    return LayoutBuilder(
      builder: (context, constraints) {
        final size = Size(constraints.maxWidth, 240);
        final layout = painter.computeLayout(size);

        return GestureDetector(
          onTapDown: (details) {
            final hit = layout.hitTest(details.localPosition);
            if (hit != null) {
              final p = hit.point.rawPoint;
              onHit(
                'Point ${hit.point.pointIndex}: '
                '(${p.x.toStringAsFixed(1)} hrs, '
                '${p.y.toInt()} score)',
              );
            } else {
              onHit(null);
            }
          },
          child: CustomPaint(size: size, painter: painter),
        );
      },
    );
  }
}

/// Resizable table demo — consumer-owned column drag using explicit widths,
/// column dividers, and GestureDetector strips at column boundaries.
class _ResizableTableDemo extends StatefulWidget {
  const _ResizableTableDemo();

  @override
  State<_ResizableTableDemo> createState() => _ResizableTableDemoState();
}

class _ResizableTableDemoState extends State<_ResizableTableDemo> {
  static const _minColWidth = 40.0;
  static const _tablePadding = 12.0;
  static const _handleWidth = 16.0;
  // Proportional ratios: 3:1:1 for Title, Pages, Rating.
  static const _initialRatios = [3.0, 1.0, 1.0];

  List<double>? _widths;

  final _rows = const [
    HandDrawnTableRow(cells: ['Dune', '412', '★★★★★'], highlight: true),
    HandDrawnTableRow(cells: ['Neuromancer', '271', '★★★★']),
    HandDrawnTableRow(cells: ['Foundation', '244', '★★★★']),
  ];

  List<double> _initWidths(double contentWidth) {
    final totalRatio = _initialRatios.fold(0.0, (s, r) => s + r);
    return [for (final r in _initialRatios) contentWidth * r / totalRatio];
  }

  void _onDrag(int boundary, double delta) {
    setState(() {
      final maxGrow = _widths![boundary + 1] - _minColWidth;
      final maxShrink = _widths![boundary] - _minColWidth;
      final clamped = delta.clamp(-maxShrink, maxGrow);
      _widths![boundary] += clamped;
      _widths![boundary + 1] -= clamped;
    });
  }

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        final contentWidth = constraints.maxWidth - _tablePadding * 2;
        _widths ??= _initWidths(contentWidth);

        return Stack(
          children: [
            HandDrawnTable(
              columns: [
                HandDrawnTableColumn(header: 'TITLE', width: _widths![0]),
                HandDrawnTableColumn(
                  header: 'PAGES',
                  width: _widths![1],
                  alignment: Alignment.centerRight,
                ),
                HandDrawnTableColumn(
                  header: 'RATING',
                  width: _widths![2],
                  alignment: Alignment.center,
                ),
              ],
              rows: _rows,
              rowDividers: const TableDividerStyle(irregularity: 3),
              columnDividers: const TableDividerStyle(irregularity: 3),
            ),
            for (int i = 0; i < _widths!.length - 1; i++)
              Positioned(
                left:
                    _tablePadding +
                    _widths!.take(i + 1).fold(0.0, (sum, w) => sum + w) -
                    _handleWidth / 2,
                top: 0,
                bottom: 0,
                width: _handleWidth,
                child: MouseRegion(
                  cursor: SystemMouseCursors.resizeColumn,
                  child: GestureDetector(
                    onHorizontalDragUpdate: (d) => _onDrag(i, d.delta.dx),
                  ),
                ),
              ),
          ],
        );
      },
    );
  }
}

// ══════════════════════════════════════════════════════════════════════════
// UI HELPERS
// ══════════════════════════════════════════════════════════════════════════

Widget _sectionHeading(String text) {
  return Text(
    text,
    style: const TextStyle(
      fontSize: 16,
      fontWeight: FontWeight.w600,
      color: _ink,
    ),
  );
}

Widget _hitLabel(String? label) {
  return Text(
    label ?? 'Tap a data element…',
    style: TextStyle(
      fontSize: 13,
      color: label != null ? _accent : _inkLight,
      fontStyle: label != null ? FontStyle.normal : FontStyle.italic,
    ),
  );
}

// ══════════════════════════════════════════════════════════════════════════
// STATUS SQUARE DEMO HELPERS
// ══════════════════════════════════════════════════════════════════════════

enum _TaskStatus { pending, completed, skipped }

class _TaskItem {
  const _TaskItem(this.label, {this.status = _TaskStatus.pending});

  final String label;
  final _TaskStatus status;

  _TaskItem cycled() {
    final next = switch (status) {
      _TaskStatus.pending => _TaskStatus.completed,
      _TaskStatus.completed => _TaskStatus.skipped,
      _TaskStatus.skipped => _TaskStatus.pending,
    };
    return _TaskItem(label, status: next);
  }

  Color get color => switch (status) {
    _TaskStatus.pending => _ink,
    _TaskStatus.completed => _accent,
    _TaskStatus.skipped => _inkLight,
  };

  bool get isFilled => status != _TaskStatus.pending;

  StatusIndicator get indicator => switch (status) {
    _TaskStatus.pending => StatusIndicator.none,
    _TaskStatus.completed => StatusIndicator.check,
    _TaskStatus.skipped => StatusIndicator.dash,
  };
}

class _TaskRow extends StatelessWidget {
  const _TaskRow({required this.task, required this.seed, required this.onTap});

  final _TaskItem task;
  final int seed;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) {
    return NotebookRow(
      lineHeight: _notebookLineHeight,
      child: Row(
        children: [
          HandDrawnStatusSquare(
            color: task.color,
            isFilled: task.isFilled,
            indicator: task.indicator,
            size: 18,
            seed: seed,
            onTap: onTap,
          ),
          const SizedBox(width: 12),
          Expanded(
            child: Text(
              task.label,
              style: TextStyle(
                fontSize: _notebookFontSize,
                height: _notebookLineHeight / _notebookFontSize,
                color: _ink,
                decoration: task.isFilled ? TextDecoration.lineThrough : null,
                decorationColor: _inkLight,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

/// A single component description wrapped in a hand-drawn container.
class _GoalItem extends StatelessWidget {
  const _GoalItem({required this.seed, required this.text});

  final int seed;
  final String text;

  @override
  Widget build(BuildContext context) {
    return HandDrawnContainer(
      backgroundColor: _cardFill,
      strokeColor: _ink,
      strokeWidth: 1.4,
      irregularity: 2.2,
      seed: seed,
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Text('•  ', style: TextStyle(fontSize: 15, color: _ink)),
          Expanded(
            child: Text(
              text,
              style: const TextStyle(fontSize: 14, height: 1.5, color: _ink),
            ),
          ),
        ],
      ),
    );
  }
}
1
likes
0
points
171
downloads

Publisher

verified publisherresengi.io

Weekly Downloads

A lightweight Flutter package for rendering hand-drawn, sketchy lines, borders, and containers.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter

More

Packages that depend on hand_drawn_toolkit